Design Patterns and Video Games

OpenGL 2D Facade (21): Shaders and Layers

In this post, I show how to update shaders based on the current layer.

This post is part of the OpenGL 2D Facade series

Objective

I choose the translation as the property to update for each layer or group of layers. The ground layer has a translation we can control with the arrows. Simultaneously, the translation of the front layers (frame box and text) remains unchanged. The rendering is fast (compared to a pure Python implementation) since the GPU performs most computations:

Design

First of all, we need allow properties per layer in the facade. I propose a solution with groups of layers where each group has specific properties like the translation:

OpenGL 2D Facade: Layer groups

It reduces the number of methods in the GUIFacade interface, a better design since too much functionality in a single class is not optimal.

The GUIFacade interface has new methods to create and move layer groups between levels. These levels are the order in which we render the groups: level 0 is the first one (background), level 1 the next one, etc.

The LayerGroup interface contains methods similar to those previously in GUIFacade, except for the level argument that controls the rendering order. There is a setTranslation() method that sets the translation for the layer group. You can imagine any other property like the brightness or the scale and then create the corresponding methods.

The Layer interface is as before. We could also add layer-specific properties: you can add methods for each case.

The OpenGLLayerGroup class stores a reference to the facade, the list of layers, and the translation vector (translationX, translationY).

Shader updates: two approaches

There are several solutions to update shaders during the rendering.

The first one is to create several shader programs and select them during the rendering using glUseProgram(). This approach is interesting if the behavior of each case is a complex combination of properties. In other words, if you need to write a lot of code in your shader to select or build the proper layer rendering, you should create several shader programs. However, this approach is not efficient because shader switching is slow.

A second approach consists of using a single shader and update uniform variables during the rendering. These updates are fast compared to shader switching. We can use the values directly, in which case there is no overhead. We can also use them in branches (if statement) to select a behavior. In this case, there is a small overhead that depends on the number of if to evaluate.

Implementation

Most methods are straightforward to implement since the design already contains the main part of the logic.

Set the translation in OpenGL coordinates

The setTranslation() method in the OpenGLLayerGroup class computes and stores the translation in OpenGL Coordinates:

def setTranslation(self, x: float, y: float):
    self.__translationX = -x * self.__gui.screenPixelWidth
    self.__translationY = y * self.__gui.screenPixelHeight

We use a usual orientation, where the horizontal axis is left-right, and the vertical one is top-bottom. The OpenGL vertical axis is bottom-top, which is why translationX is opposed to translationY.

The screenPixelWidth and screenPixelHeight contain the pixel size in OpenGL screen coordinates. Remind that these coordinates are float values from -1 to 1 (see this series's first posts for details).

Render with different properties

The render() method of the OpenGLGUIFacade class renders all the layer groups:

def render(self):
    glClear(GL_COLOR_BUFFER_BIT)

    glUseProgram(self.__shaderProgramId)

    translationShaderVar = glGetUniformLocation(self.__shaderProgramId, "translation")
    for layerGroup in self.__layerGroups:
        if layerGroup is None:
            continue
        glUniform4f(translationShaderVar,
                    layerGroup.translationX,
                    layerGroup.translationY,
                    0.0, 0.0)
        layerGroup.draw()

    glUseProgram(0)

    pygame.display.flip()
    self.__clock.tick(60)

Line 2 clears all the screen (with a black color).

Line 4 selects the shaders. We use the following vertex shader:

#version 330
layout (location=0) in vec4 vertex;
layout (location=1) in vec2 inputUV;
                   out vec2 outputUV;
uniform vec4 translation;
void main() {
    gl_Position = vertex + translation;
    outputUV = inputUV;
}

Line 6 gets an id to manipulate the uniform variable value in the shader.

Lines 7-14 iterate through the layer groups.

Lines 10-13 sets the translation in the shader.

Line 14 calls the draw() method of the LayerGroup class, which calls the draw() method of all layers in the group.

Final program

Download code & assets

In the next post, I'll show how to load and save game state.