Archive

Monthly Archives: October 2015

The sample Program from the previous post not only demonstrated some OpenGL calls and concepts, but also illustrates that code organization quickly becomes important as soon as the program grows beyond a couple of lines.

So I refactored some of the OpenGL Interface code into classes. I noted that for each draw call I need to setup the OpenGL context by binding some Objects like the shader program, the target framebuffer or the source texture.

I also learned that after you are done using any OpenGL object it is helpful to unbind it. The reason for this is not that they would use too much resources, but rather decoupling. It might be that your program just coincidentally has the right objects bound but as soon as you move any of the code, something will break. Explicitly unbinding helps avoid these dependencies of different parts of your program on what GL objects other parts might have bound last.

So my drawing operations started to look like this

# Bind needed objects 
framebuffer.bind()
render_program.use()
texture.bind()

gl.glDrawSomething(...)

# Unbind Objects
texture.unbind()
render_program.end_use()
framebuffer.unbind()

Just by renaming the matching method pairs .bind() and .unbind() to __enter__ and __exit__ the objects become context managers and the code above becomes much cleaner

with framebufer, render_program, texture:
    gl.glDrawSomething(...)

This style has the advantage of being able to specify the objects used for a draw call in a single with: line. On the other hand it also removes flexibility on the bind and unbind times of the objects. I think in most cases this is not a disadvantage because most times many objects depend on each other logically (e.g. use this texture with that shader together).

A refactored version of the last posts example which uses the context managers is available on github.

This post will show you how to set up a framebuffer in OpenGL using Python and Pyglet.

See my previous post for some tips how to use OpenGL functions from Python.

Structure of the Program

As this is a demo program I did not structure it with python classes. However the complexity is already big enough to see that this flat structure is not suitable for any larger program.

The following illustrates the drawing steps. The OpenGL objects in the grey boxes are globals
in my python program.
Flowchart for the Draw Event

There are two steps: first a triangle is rendered to a texture using a framebuffer, and second the texture is copied to the screen. Each step uses

  • A Shader Program containing the GLSL code used for rendering
  • A Vertex Buffer containing vertex data for the objects being drawn
  • A Vertex Array Object containing the information how the data
    in the buffer is linked to the vertex attributes in the shader program

The full program is available on github.

Scene description

In this example the framebuffer is used to render the triangle from the previous post in a very low resolution (30×20 px). This Image is then used for texturing two rectangles in the main window.

Note that both in the framebuffer and on the main screen I do not use any vertex transformation. Therefore only the x and y coordinates are used. The lower left corner of the screen has coordinates (-1, -1) and the upper right corner (1, 1). Texture coordinates however run from 0 to 1.

framebuffer

Vertex Array Objects

In the previous program there was only one GLSL program and only one vertex buffer. Therefore it was convenient to store the vertex attribute bindings in the default OpenGL state. In this program however we switch back and forth between the triangle rendering program which uses color attributes for the vertices and the copy to screen program which uses texture coordinates.

A vertex array object stores the information how data in a buffer is linked to vertex attributes of the shader program. So I can define this connection once (using glVertexAttribPointer) and then I only need to select the correct vertex array object when drawing.

def setup_render_vertexbuffer():
    ...
    gl.glBindVertexArray(render_vao)

    gl.glEnableVertexAttribArray(loc_position)
    gl.glEnableVertexAttribArray(loc_color)

    # the following bindings will be remembered by the vao
    gl.glBindBuffer(gl.GL_ARRAY_BUFFER, render_vertexbuffer)

    gl.glVertexAttribPointer(loc_position, 2, gl.GL_FLOAT, False, 
         ctypes.sizeof(COLOR_VERTEX), 
         ctypes.c_void_p(COLOR_VERTEX.position.offset))
    gl.glVertexAttribPointer(loc_color, 4, gl.GL_FLOAT, False, 
         ctypes.sizeof(COLOR_VERTEX), 
         ctypes.c_void_p(COLOR_VERTEX.color.offset))
    gl.glBindVertexArray(0)


def render_to_texture():
    ...
    # draw using the vertex array for vertex information
    gl.glBindVertexArray(render_vao)
    gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
    gl.glBindVertexArray(0)

Gotcha: The Viewport

OpenGL needs to know the size of the rendering target. In this program there are two targets, so you need to tell OpenGL the new size every time you switch the drawing target.

def render_to_texture():
    gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, framebuffer)
    gl.glViewport(0, 0, FB_WIDTH, FB_HEIGHT)
    ...

def copy_texture_to_screen():
    gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
    gl.glViewport(0, 0, window.width, window.height)
    ...

Putting it together

The program has 300 lines, and as I mentioned before, it could benefit well from more structure and also from deduplication of code. Setting up two GLSL shader programs, two vertex array objects, two vertex buffers and two drawing steps has lead to some duplication, which could be avoided if these were all each two instances of a shader class, a vertex array class and a vertex buffer class.

def draw():
    render_to_texture()
    copy_texture_to_screen()

def main():
    global window
    window = pyglet.window.Window()

    setup_framebuffer()
    setup_render_program()
    setup_render_vertexbuffer()
    setup_copy_program()
    setup_copy_vertexbuffer()

    window.on_draw = draw
    pyglet.clock.schedule_interval(lambda dt:None, 0.01)
    pyglet.app.run()

Try out the complete program. You can get it on github.
Please comment below if something does not work or what I could explain better.