(no more) BGL in 2.8

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)) 

Sounds like a state issue. Some draw functions force a state (blend mode, scissors, constants etc.) and might not necessarily clean up after themselves. It’s generally good practice to be explicit with states, like enabling alpha when you need it, and disable when you’re done. Some go as far as storing the previous state, set their own, draw stuff, and then restore back to the previous state. There aren’t concrete guidelines here, but generally if something must be turned on, or modified, it’s considered a state change which may affect drawing further down the gl pipeline.

Right. When doing a localized transform, it’s common to push a new matrix, make your transforms, draw stuff, then pop the matrix, restoring it back to before it was transformed. Blender offers a context manager which does the boilerplate code for you:

def draw():

    # Draw inside a new matrix layer
    with gpu.matrix.push_pop():

        gpu.matrix.translate(x, y)
        # (draw something)

    # Leaving the with-block reverts the matrix back
    # (draw something else)

3 Likes

aaah, ok. I can try that out, thanks!

Yeah, seems like that’s the case. Gotta cleanup or maybeis my fault, i might need to be doing more of that up the chain/in my code.

I have a question when it comes to registering a handler. Let’s say a user runs into an error and the drawn handler is frozen. How do I go about removing the handlers and clearing up the viewport?

Not sure if this is possible, especially if the draw handler is part of the operator.

I’ve installed blender 3.1.2? now, and had to fix up my code in a few spots. Although, now I noticed if I try to display the images as you have above / or what worked in 2.93, my blender instant crashes.

I tried to do a debug/log, but I’m not sure if it captures it properly or not.

I saw this, but… not entirely sure what it means/if this is the real issue or not.
I’ll have to keep debugging/testing some things… Unless it’s a change in openGL/Python in latest version.

ERROR (gpu.shader): pyGPUShader FragShader: 
      | 
   44 |     out vec4 gl_FragColor;
      | 
      | Error: C7528: OpenGL reserves names starting with 'gl_': gl_FragColor