GPU Module: Draw semi-transparent on top of viewport?

Good evening, everybody.

After struggling for days, I figured I need help with this: My goal is to use the GPU module’s draw_view3d() method to render stuff to an offscreen buffer, which I want to overlay on top of the 3dview again, but importantly not fully opaque, rather semi-transparent.

So most of it I got to work, but the transparency-part I can’t seem to figure out.
I thought I’d copy the offscreen buffer to a numpy-array and use numpy to alter the part of the array representing the alpha-channel, write that to a new GPU buffer object (I suppose you cannot overwrite an existing one) and display that in the shader used for viewport drawing.

Well it somehow doesn’t work as expected, while the viewport drawing part does work with transparency if drawing a constant color.

This is the code I’m using (excuse the mess):

import bpy, gpu
import numpy as np
from gpu_extras.batch import batch_for_shader

# prepare image datablock
if not 'Tex' in bpy.data.images:
    bpy.data.images.new( 'Tex', alpha=True, width=200, height=200 )
img = bpy.data.images['Tex']
img.source = 'GENERATED'
img.alpha_mode = 'STRAIGHT'
img.scale(200, 200)
img.pack( )
img.use_fake_user = True
img.colorspace_settings.name = 'Linear'

opacity = 0.3

# prepare verts (indices) and texturecoordinates for onscreen drawing
# -for uniform color
vertices = ( (100, 100), (300, 100),
             (100, 300), (300, 300))

indices = ((0, 1, 2), (2, 1, 3))
# -for texture
verts = (   (100, 100), (300, 100),
            (300, 300), (100, 300))

uvs = ((0, 0), (1, 0), (1, 1), (0, 1))

# prepare shaders, batches
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
shaderImg = gpu.shader.from_builtin('IMAGE')
batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
batchImg = batch_for_shader(shaderImg, 'TRI_FAN', {"pos": verts,
                            "texCoord": uvs})

offscreen = gpu.types.GPUOffScreen(200, 200)
gamma = 1.0 / 2.4


def sceneLinear_to_sRGB(self, gamma, np_buffer_linear):

    threshold = 0.0031308
    sRGB = np.copy(np_buffer_linear)
    condition = np.greater(np_buffer_linear, threshold)
    
    sRGB = np.power(sRGB, gamma)
    sRGB = np.multiply(sRGB, 1.055)
    sRGB = np.subtract(sRGB, 0.055)
    
    sRGB = np.where(condition, sRGB, np.multiply(np_buffer_linear, 12.92))
    
    return sRGB
    


def draw_callback_2d(self, context):
    
    scene = context.scene
    # get camera matrices
    view_matrix = scene.camera.matrix_world.inverted()
    projection_matrix = scene.camera.calc_matrix_camera(
        context.evaluated_depsgraph_get(), x=200, y=200)

    # draw to offscreen buffer
    offscreen.draw_view3d(
        scene,
        context.view_layer,
        context.space_data,
        context.region,
        view_matrix,
        projection_matrix,
        do_color_management=True)

    gpu.state.depth_mask_set(False)
    
    # make numpy image buffer
    np_buffer = np.asarray( offscreen.texture_color.read( ),
                            dtype = 'float32', order='C' )
    
    np_buffer[:, :, -1:4] = np.full_like(np_buffer[:, :, -1:4], opacity,
                                        dtype='float32', order='C')
#    np_buffer[:] = sceneLinear_to_sRGB(self, gamma, np_buffer)
    
    

    img.pixels.foreach_set(np_buffer.ravel())
    img.update()
#    print('img_pixels size: {a}'.format(a=len(img.pixels)))

    # make image buffer
    buffer = gpu.types.Buffer('FLOAT', [200, 200, 4], np_buffer.ravel())
    # make GPUTexture
    tex = gpu.types.GPUTexture((200, 200), layers=1, is_cubemap=False,
                                format='RGBA8', data=buffer)
    
    
    
    # draw uniform color rectangle for testing purposes
    if False:
        shader.bind()
        shader.uniform_float("color", (0, 0.5, 0.5, 0.3))
        if gpu.state.blend_get() != 'ALPHA':
            gpu.state.blend_set('ALPHA')
        batch.draw(shader)
        gpu.state.blend_set('NONE')
    else:
        shaderImg.bind()
#        shaderImg.uniform_sampler("image", offscreen.texture_color)
        shaderImg.uniform_sampler("image", tex)
        if gpu.state.blend_get() != 'ALPHA_PREMULT':
            gpu.state.blend_set('ALPHA_PREMULT')
        batchImg.draw(shaderImg)
        gpu.state.blend_set('NONE')
    





class ModalDrawOperator(bpy.types.Operator):
    bl_idname = "view3d.modal_operator"
    bl_label = "Simple Modal View3D Operator"
    
    handle_2d = None

    def modal(self, context, event):
        context.area.tag_redraw()

        if event.type in {'RIGHTMOUSE', 'ESC'}:
            return self.finish(context)

        return {'PASS_THROUGH'}
    
    
    def execute( self, context ):
        self.report({'WARNING'}, 'Operator has no execution. Use as modal.')
        return{'CANCELLED'}


    def invoke(self, context, event):
        self.report( {'INFO'}, 'Start realtime update.' )
        # the arguments to pass to the callback
        args = (self, context)
        # Add the region OpenGL drawing callback
        self.handle_2d = bpy.types.SpaceView3D.draw_handler_add(
                                                            draw_callback_2d,
                                                            args,'WINDOW',
                                                            'POST_PIXEL')

        context.window_manager.modal_handler_add(self)
        # Force redraw on all 3D-view areas
        self.force_redraw(context)
        return {'RUNNING_MODAL'}

        
        
    def finish( self, context ):
        bpy.types.SpaceView3D.draw_handler_remove( self.handle_2d, 'WINDOW' )
        # Force redraw on all 3D-view areas
        self.force_redraw(context)
        self.report( {'INFO'}, 'Stopped realtime update.' )
        return{'FINISHED'}
    
    
    def force_redraw(self, context):
        # Force redraw on all 3D-view areas
        for area in context.screen.areas:
                if area.type == 'VIEW_3D':
                    area.tag_redraw()


def register():
    bpy.utils.register_class(ModalDrawOperator)


def unregister():
    bpy.utils.unregister_class(ModalDrawOperator)

if __name__ == "__main__":
    register()

Would be thankful for any insights, ideas, aproaches.

greetings, Kologe

There’s a cheaper way of adding transparency by using a custom texture shader that takes an additional float uniform, alpha.

Here’s a drop-in replacement for the built-in IMAGE shader.

tex_vert_shader = """
in vec2 texCoord;
in vec2 pos;
out vec2 uv;

uniform mat4 ModelViewProjectionMatrix;

void main() {
    uv = texCoord;
    gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0, 1.0);
}
"""

tex_frag_shader = """
in vec2 uv;
out vec4 fragColor;

uniform sampler2D image;
uniform float alpha = 1.0;

void main() {
    fragColor = mix(vec4(0.0), texture(image, uv), alpha);
}
"""
shaderImg = gpu.types.GPUShader(tex_vert_shader, tex_frag_shader)

To set a custom alpha, upload the uniform before drawing the batch:

        shaderImg.bind()
        shaderImg.uniform_sampler("image", tex)
        shaderImg.uniform_float("alpha", opacity)  # <-

Also, if you need to do linear-to-srgb transforms on the texture, Blender has a glsl function for this. The function name itself is misleading - it implies reverse transform.

vec4 color = mix(vec4(0.0), texture(image, uv), alpha);
fragColor = blender_srgb_to_framebuffer_space(color);
2 Likes

Great, that works like a charm! I already had this feeling there must be a more elegant way of doing this than bizzare numpy-roundtripping hackery. :laughing:
Thank you.

greetings, Kologe