How to properly update a glTexImage2D object with a new buffer?

I’ve already posted on BA about this but I have a feeling it’s a better-suited question for here.

I’m adding viewport rendering to my custom renderer add-on, and I’m running into a problem where the color rendered in my viewport isn’t changing when my pixel buffer is updated. Here’s what I know so far:

  • I’m basing my implementation on Blender’s example CustomDrawData class (RenderEngine(bpy_struct) — Blender Python API)
  • I’m initiating the class with a self.buffer variable that holds a list of pixel color values, just like the example (although the example calls it self.pixels). I start with a solid red color ( [1.0, 0.0, 0.0, 1.0] ), and it displays in the viewport perfectly fine.
  • After the red box appears I flood the buffer with 1’s to turn it white ( [1.0, 1.0, 1.0, 1.0] ). I can see that my self.buffer attribute is updated correctly, but the function I call to update the glTexImage2d object doesn’t seem to do anything, because the color stays red and never turns white.
  • I am calling FrameBuffer.build_gl_texture() (see below) and engine.tag_redraw() after updating my buffer values to refresh the texture in OpenGL.

Here is the code to my custom class so far. I’m not super familiar with OpenGL in general - is there something obvious that I’m missing that I need to do to get the glTexImage2d object to properly register the new pixel values? I’m not getting any errors in the console, so I have very little info to go on outside of the docs and the code I have.

class FrameBuffer:
    def __init__(self):
        # This is hard-coded for now, will change later
        self.width, self.height = 512, 512

        # For debugging purposes, I'm starting with a solid red color
        # so I KNOW when the color changes
        pixels = [1.0, 0.0, 0.0, 1.0] * self.width * self.height
        self.buffer = bgl.Buffer(bgl.GL_FLOAT, self.width * self.height * 4, pixels)

        self.init_opengl()

    def init_opengl(self):
        self.texture = bgl.Buffer(bgl.GL_INT, 1)
        bgl.glGenTextures(1, self.texture)

        self.build_gl_texture()

        shader_program = bgl.Buffer(bgl.GL_INT, 1)
        bgl.glGetIntegerv(bgl.GL_CURRENT_PROGRAM, shader_program)

        self.vertex_array = bgl.Buffer(bgl.GL_INT, 1)
        bgl.glGenVertexArrays(1, self.vertex_array)
        bgl.glBindVertexArray(self.vertex_array[0])

        texturecoord_location = bgl.glGetAttribLocation(shader_program[0], "texCoord")
        position_location = bgl.glGetAttribLocation(shader_program[0], "pos")

        bgl.glEnableVertexAttribArray(texturecoord_location)
        bgl.glEnableVertexAttribArray(position_location)

        position = [0.0, 0.0, self.width, 0.0, self.width, self.height, 0.0, self.height]
        position = bgl.Buffer(bgl.GL_FLOAT, len(position), position)
        texcoord = [0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0]
        texcoord = bgl.Buffer(bgl.GL_FLOAT, len(texcoord), texcoord)

        self.vertex_buffer = bgl.Buffer(bgl.GL_INT, 2)

        bgl.glGenBuffers(2, self.vertex_buffer)
        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, self.vertex_buffer[0])
        bgl.glBufferData(bgl.GL_ARRAY_BUFFER, 32, position, bgl.GL_STATIC_DRAW)
        bgl.glVertexAttribPointer(position_location, 2, bgl.GL_FLOAT, bgl.GL_FALSE, 0, None)

        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, self.vertex_buffer[1])
        bgl.glBufferData(bgl.GL_ARRAY_BUFFER, 32, texcoord, bgl.GL_STATIC_DRAW)
        bgl.glVertexAttribPointer(texturecoord_location, 2, bgl.GL_FLOAT, bgl.GL_FALSE, 0, None)

        bgl.glBindBuffer(bgl.GL_ARRAY_BUFFER, 0)
        bgl.glBindVertexArray(0)

    # This gets called every time I update self.buffer to refresh the OpenGL texture with the new values
    def build_gl_texture(self):
        bgl.glActiveTexture(bgl.GL_TEXTURE0)
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture[0])
        bgl.glTexImage2D(bgl.GL_TEXTURE_2D, 0, bgl.GL_RGBA16F, self.width, self.height, 0, bgl.GL_RGBA, bgl.GL_FLOAT, self.buffer)
        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR)
        bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR)
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)

    def __del__(self):
        bgl.glDeleteBuffers(2, self.vertex_buffer)
        bgl.glDeleteVertexArrays(1, self.vertex_array)
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)
        bgl.glDeleteTextures(1, self.texture)

    def draw(self):
        bgl.glActiveTexture(bgl.GL_TEXTURE0)
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, self.texture[0])
        bgl.glBindVertexArray(self.vertex_array[0])
        bgl.glDrawArrays(bgl.GL_TRIANGLE_FAN, 0, 4)
        bgl.glBindVertexArray(0)
        bgl.glBindTexture(bgl.GL_TEXTURE_2D, 0)

I think you need to add a

# Bind shader that converts from scene linear to display space
self.bind_display_space_shader(scene)
# call draw function here..
self.unbind_display_space_shader()

Cheers.

I’m doing that in the view_draw() function of the render engine. Does it need to be in the FrameBuffer class instead?

def view_draw(self, context, depsgraph):
        region = context.region
        scene = depsgraph.scene

        #dimensions = region.width, region.height

        bgl.glEnable(bgl.GL_BLEND)
        bgl.glBlendFunc(bgl.GL_ONE, bgl.GL_ONE_MINUS_SRC_ALPHA)
        self.bind_display_space_shader(scene)
        
        self.framebuffer.draw()

        self.unbind_display_space_shader()
        bgl.glDisable(bgl.GL_BLEND)