(no more) BGL in 2.8

There seems to be a bit of uncertainty in regards to the bgl module in 2.8, with parts of it not working, having been removed, and some talk of bgl being removed completely.

I*m starting this thread to collect what we know. Hopefully, we can get some official input as well.

Here’s some links, with bits of information but leaving a lot of uncertainty:

https://twitter.com/Heavypoly/status/1042127239439998976

For those unaware, bgl is an opengl wrapper allowing addon developers to essentially draw anything on top of the 3d view. It’s used heavily by various popular addons, such as retopoflow, boxcutter, speedflow, relast etc.

1 Like

Info from Dalai Felinto

If you really really want to, you can get it to work right now.

The API may change but you can actually draw things with 2.8 + OpenGL. For example:
https://developer.blender.org/diffusion/B/browse/blender2.8/doc/python_api/examples/gpu.types.GPUOffScreen.py

1 Like

Is this still relevant? https://developer.blender.org/T49043

1 Like

How to draw point in bgl 2.8 (based on - https://developer.blender.org/D3688 - draw lines by mano-wii)

import bpy
import bgl
import blf
import gpu
import numpy as np


g_vertSrc = '''
uniform mat4 viewproj;

in vec3 pos;

void main()
{
	gl_Position = viewproj * vec4(pos, 1.0);
}

'''

g_fragSrc = '''
uniform vec4 color;

out vec4 fragColor;

void main()
{
	fragColor = color;
}
'''
g_plane_vertices = np.array([ ([0.5, 0.5, 0]),], [('pos', 'f4', 3)])

class SnapDrawn():
    def __init__(self):
        self._format = gpu.types.GPUVertFormat()
        self._pos_id = self._format.attr_add(
                id = "pos",
                comp_type = "F32",
                len = 3,
                fetch_mode = "FLOAT")

        self.shader = gpu.types.GPUShader(g_vertSrc, g_fragSrc)
        self.unif_color = self.shader.uniform_from_name("color")
        self.color = np.array([1.0, 0.8, 0.0, 0.5], 'f')
        
        self.per_mat = self.shader.uniform_from_name("viewproj")


    def batch_line_strip_create(self, coords):
        global g_plane_vertices
        vbo = gpu.types.GPUVertBuf(len = len(g_plane_vertices), format = self._format)
        vbo.fill(id = self._pos_id, data = g_plane_vertices)

        batch_lines = gpu.types.GPUBatch(type = "POINTS", buf = vbo)
        #batch_lines.program_set_builtin(id = "2D_UNIFORM_COLOR")
        batch_lines.program_set(self.shader)

        return batch_lines


    def draw(self, list_verts_co, rv3d):
        

        batch = self.batch_line_strip_create(list_verts_co)

        #batch.uniform_f32("color", 1.0, 0.8, 0.0, 0.5)
        self.shader.uniform_vector_float(self.unif_color, self.color, 4)
        
        
       
        viewproj = np.array(rv3d.perspective_matrix.transposed(), 'f')
        self.shader.bind()
        self.shader.uniform_vector_float(self.per_mat, viewproj, 16)
        
        batch.draw()
        del batch


def draw_callback_px(self, context):
    print("mouse points", len(self.mouse_path))

    font_id = 0  # XXX, need to find out how best to get this.

    # draw some text
    blf.position(font_id, 15, 30, 0)
    blf.size(font_id, 20, 72)
    blf.draw(font_id, "Hello Word " + str(len(self.mouse_path)))

    # 50% alpha, 2 pixel width line
    bgl.glEnable(bgl.GL_BLEND)
    bgl.glLineWidth(2.0)

    self.snap_draw.draw(self.mouse_path, self.rv3d)

    #restore opengl defaults
    bgl.glLineWidth(1)
    bgl.glDisable(bgl.GL_BLEND)


class ModalDrawOperator(bpy.types.Operator):
    """Draw a line with the mouse"""
    bl_idname = "view3d.modal_operator"
    bl_label = "Simple Modal View3D Operator"
    
    global_shader = None
    unif_viewproj = -1
   
    def modal(self, context, event):
        context.area.tag_redraw()

        if event.type == 'MOUSEMOVE':
            self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))

        elif event.type == 'LEFTMOUSE':
            del self.snap_draw
            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            return {'FINISHED'}

        elif event.type in {'RIGHTMOUSE', 'ESC'}:
            del self.snap_draw
            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
            return {'CANCELLED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        if context.area.type == 'VIEW_3D':
            self.rv3d = context.region_data
           
            # the arguments we pass the the callback
            args = (self, context)
            # Add the region OpenGL drawing callback
            # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
            self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px, args, 'WINDOW', 'POST_PIXEL')

            self.mouse_path = []
            self.snap_draw = SnapDrawn()

            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
        else:
            self.report({'WARNING'}, "View3D not found, cannot run operator")
            return {'CANCELLED'}


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


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


if __name__ == "__main__":
    register()
1 Like

Heads up !
those examples will get outdated fast as long as we do not have a definite API

Very nice, thanks! I’ll take a closer look in a bit.

https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/Draw_API

Awesome , it’s finally here and this seems much more powerful than old bgl.

Just a question, let’s say I want to draw several lines, is it ok to make several :
batch = shader.new_batch(‘LINES’, {“pos” : coords}) ?

Or it’s better to put them all in the same batch ? if this the case does someone know how ?
I’ve read that it’s better to upload all at once to the GPU, but maybe this is where you’ll use :
shader.bind()
shader.uniform_float(“color”, (1, 1, 0, 1))
batch.draw(shader)

I know this is old, im trying to test this on OSX. Does this GPU type only work when a proper GPU is available?

I think it should work for all gpus (I only know it works on nvidia on Win and Linux)

1 Like

It does work on same platform / gpu than eevee.

1 Like

Trying to find yet, but no luck. I wonder how easy it is, to tint/color an image?

I was able to load a PNG with alpha/transparency, but I cannot figure out how to tint it a color…
Unless I need to use frag/etc.

in draw:

bgl.glEnable(bgl.GL_BLEND)

    bgl.glActiveTexture(bgl.GL_TEXTURE0)
    bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode)
    #gl.glColor4f(1.0, 0.0, 0.0, 0.5)
    
    shader.bind()
    shader.uniform_int("image", 0)
    batch.draw(shader)
    bgl.glDisable(bgl.GL_TEXTURE_2D) 

Edit: I guess for what I may use this for… i could just color it in photoshop/etc… but still would be good to know how to tint color in code.

If you’re using 2.93 and up, the gpu module should be used as bgl can be considered deprecated.

Tinting can be as simple as creating a 1x1 pixel texture filled with your desired color/alpha, then draw it on top of the other image with some blend operation.

This example creates an offscreen texture and draws it on top of another texture.
Assumes the image you’re drawing is called bpy.data.images["image"].

import bpy
import gpu
from gpu_extras.batch import batch_for_shader

image = bpy.data.images["image"]
tex = gpu.texture.from_image(image)
width, height = image.size

# Overlay texture for tinting, filled with green
off = gpu.types.GPUOffScreen(1, 1)
off.texture_color.clear(format='FLOAT', value=(0.0, 1.0, 0.0, 0.1))

content = {
    "pos": ((0, 0), (width, 0), (width, height), (0, height)),
    "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
}

shader = gpu.shader.from_builtin("2D_IMAGE")
batch = batch_for_shader(shader, 'TRI_FAN', content)

def draw():
    gpu.state.blend_set("ALPHA")
    shader.bind()

    # Draw un-tinted
    shader.uniform_sampler("image", tex)
    batch.draw(shader)

    # Draw again to the left, with tinted overlay
    gpu.matrix.translate((width, 0))
    batch.draw(shader)

    shader.uniform_sampler("image", off.texture_color)
    batch.draw(shader)

if __name__ == "__main__":
    bpy.context.space_data.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
    bpy.context.region.tag_redraw()
3 Likes

oh awesome!

I tried, but getting this error, but i could mess around with it some more. I’m in 2.93.0
Unless i need 2.93.3 or whatever it is?

image

Ah right. Looks like the associated GPUTexture object isn’t exposed in the offscreen yet in 2.93. The workaround is to replace the offscreen with a manually created texture.

# For 2.93 specifically
import bpy
import gpu
from gpu_extras.batch import batch_for_shader

image = bpy.data.images["image"]
tex = gpu.texture.from_image(image)
width, height = image.size

# Overlay texture for tinting, filled with green
buf = gpu.types.Buffer("FLOAT", 4, (0.0, 1.0, 0.0, 0.1))
overlay_tex = gpu.types.GPUTexture((1, 1), data=buf)

content = {
    "pos": ((0, 0), (width, 0), (width, height), (0, height)),
    "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
}

shader = gpu.shader.from_builtin("2D_IMAGE")
batch = batch_for_shader(shader, 'TRI_FAN', content)

def draw():
    gpu.state.blend_set("ALPHA")
    shader.bind()

    # Draw un-tinted
    shader.uniform_sampler("image", tex)
    batch.draw(shader)

    # Draw again to the right, with tinted overlay
    gpu.matrix.translate((width, 0))
    batch.draw(shader)

    shader.uniform_sampler("image", overlay_tex)
    batch.draw(shader)

if __name__ == "__main__":
    # For toggling on/off run script again
    try:
        bpy.context.space_data.draw_handler_remove(bpy.h, "WINDOW")
        del bpy.h
    except AttributeError:
        bpy.h = bpy.context.space_data.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
    bpy.context.region.tag_redraw()
2 Likes

Sweet, yeah that’s kinda working! Thank you again

Maybe this is getting too crazy, but can you tint, but then mask via Alpha?
Looks like right now it’s going over everything / ignoring alpha.

But again, if it’s getting too complicated, I would do something else as well.

image

As for reading the alpha off the texture, it’ll be easier to just write a tint shader and skip the whole extra tint texture thing :stuck_out_tongue:

This samples the image and applies a (configurable) tint based on the alpha of the source texture.

# Tint shader
tint_vert, tint_frag = gpu.shader.code_from_builtin("2D_IMAGE").values()

tint_frag = """
in vec2 texCoord_interp;
out vec4 fragColor;

uniform vec4 tint = vec4(0.0, 1.0, 0.0, 0.5);  // Tint color
uniform sampler2D src_tex;  // Source texture to sample alpha off

void main()
{
    // Read the alpha from the image and apply it to the tint
    vec4 color = tint;

    color.a = min(tint.a, texture(src_tex, texCoord_interp).a);

    fragColor = color;
}
"""
tint_shader = gpu.types.GPUShader(tint_vert, tint_frag)

To draw it, use this in the draw function:


def draw():
    ...
    # Draw again to the right, with tinted overlay
    gpu.matrix.translate((width, 0))
    batch.draw(shader)

    # Tint
    tint_shader.bind()
    tint_shader.uniform_sampler("src_tex", tex)
    batch.draw(tint_shader)

image

2 Likes

Awesome, thank you again!! I really need to try to wrap my head about this shader programming…
This will be very useful. Thanks for all the examples!

Here’s your code + some other code I wrote, to load an image from a folder in addons directory, and also check scene for existing image, so not loading it 1000 times. Might be useful for someone. Also prob. a better way to write some of this…

import bpy
import gpu
import os
from gpu_extras.batch import batch_for_shader

imageName = "scissors"
addonDir = bpy.utils.user_resource('SCRIPTS', "addons")
pathTexture = os.path.join(os.path.dirname(addonDir), "addons/TT_Edge_Cutter/{}.png".format(imageName)) 
pathTexture = pathTexture.replace('\\', '/')

foundSceneImage = 0
loadedImage = None 


# Scan Scene for Existing/Loading Image and use that if we can , else load from disk.  So not loading the same texture X amount of times..
for image in bpy.data.images:
    if (image.name.find(imageName) != -1):
        foundSceneImage += 1
        loadedImage = image  #Found Checker Image / Set in Variable.

# If didn't find existing image in the scene, load from disk/create one.
if (foundSceneImage == 0):
    loadedImage = bpy.data.images.load(pathTexture)  #bpy.data.images[IMAGE_NAME]


width = 195
height = 256
scale = 0.5
x = 200
y = 200

width = width*scale
height = height*scale

# For 2.93 specifically

tex = gpu.texture.from_image(loadedImage)
#width, height = image.size


content = {
    "pos": ((0, 0), (width, 0), (width, height), (0, height)),
    "texCoord": ((0, 0), (1, 0), (1, 1), (0, 1)),
}

shader = gpu.shader.from_builtin("2D_IMAGE")
batch = batch_for_shader(shader, 'TRI_FAN', content)


# Tint shader
tint_vert, tint_frag = gpu.shader.code_from_builtin("2D_IMAGE").values()

tint_frag = """
in vec2 texCoord_interp;
out vec4 fragColor;

uniform vec4 tint = vec4(0.0, 1.0, 0.0, 0.5);  // Tint color
uniform sampler2D src_tex;  // Source texture to sample alpha off

void main()
{
    // Read the alpha from the image and apply it to the tint
    vec4 color = tint;

    color.a = min(tint.a, texture(src_tex, texCoord_interp).a);

    fragColor = color;
}
"""
tint_shader = gpu.types.GPUShader(tint_vert, tint_frag)


def draw():
    gpu.state.blend_set("ALPHA")
    shader.bind()

    # Draw un-tinted
    shader.uniform_sampler("image", tex)
    batch.draw(shader)
    
    # Draw again to the right, with tinted overlay
    gpu.matrix.translate((width, 0))
    batch.draw(shader)

    # Tint
    tint_shader.bind()
    tint_shader.uniform_sampler("src_tex", tex)
    batch.draw(tint_shader)


if __name__ == "__main__":
    # For toggling on/off run script again
    try:
        bpy.context.space_data.draw_handler_remove(bpy.h, "WINDOW")
        del bpy.h
    except AttributeError:
        bpy.h = bpy.context.space_data.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
    bpy.context.region.tag_redraw()

image

One more question.

I setup a 2nd drawing method, since when I tried to use my other one, sometimes the image went solid green/black and got messed up again, when i drew some text/toggled that on/off. Not sure why.

So when I moved it into a new draw def, it works! But… and I kinda expected this, if I try to use your gpu.matrix.translate, even in my seperate def, it moves ALL of my drawing stuff I’m doing. So other lines I drew/text/etc, gets shifted as well.

I’m guessing GPU is a global thing, or I’d have to be more specific, if that’s even possible.
I could figure out the “pos” for the image box/content, but the translate matrix is def. nice, if can use that. But, not a huge deal if not/not easily.

Edit: Oh… I realized I can just translate, do the rest, then reset it back and that seems to work? :slight_smile:

  # Draw again to the right, with tinted overlay
        gpu.matrix.translate((img_x, img_y))  #(img_width, 0)
        scissors_batch.draw(scissors_shader)
        

        # Tint
        tint_shader.bind()
        tint_shader.uniform_sampler("src_tex", tex)
        scissors_batch.draw(tint_shader)
        del scissors_shader
        del tint_shader
        del scissors_batch
        gpu.matrix.translate((-img_x, -img_y))