Design Patterns and Video Games

OpenGL 2D Facade (4): Shaders

Shaders are one of the powerful features of OpenGL. I introduce them in this post and show how to color vertices using vertex and fragment shaders.

This post is part of the OpenGL 2D Facade series

The OpenGL rendering pipeline

Rendering with OpenGL can be divided into the following steps:

OpenGL Rendering Pipeline

The input data, at the beginning of the pipeline, is data like the vertices, faces, and colors we created in the previous post.

The first processing transforms this data using programs called Vertex Shaders. For example, we can move and rotate objects during this step. In our 2D context, we scroll through the world map using these shaders.

The second processing assembles vertices to create primitives like triangles using programs called Geometry Shaders. With these shaders, you can generate more complex primitives, for instance, stars from points. In out 2D context, default primitives are rectangles (or quads).

The third processing rasterizes primitives. It is an automatic step; we can't program it. It generates fragments, where each fragment is a set of pixel properties. The usual properties are color, depth, and UV coordinates. Note that OpenGL interpolates the value of these properties, depending on the location of the pixel on the primitive.

The last processing does the rendering and computes the color of each pixel using programs called Fragment Shaders. The most simple fragment shader is the one that returns the color computed during the rasterization. There is more exciting processing, like computing texture colors or modulate according to light intensity.

Create a vertex shader

Let's now create a basic vertex shader:

vertexShaderCode = '''#version 330
layout (location=0) in vec2 vertex;
layout (location=1) in vec3 inputColor;
                   out vec3 vertexColor;
void main() {
    gl_Position = vec4(vertex, 0.0, 1.0);
    vertexColor = inputColor;
}'''
vertexShaderId = glCreateShader(GL_VERTEX_SHADER)
glShaderSource(vertexShaderId, vertexShaderCode)
glCompileShader(vertexShaderId)

The vertexShaderCode string contains the code of the vertex shader. We can't write shader using Python, or any other usual language. We must use the shader language, as defined in OpenGL. This language is close to C; for instance, we must end each line with a semi-colon. The good news is that it is the same language, whatever the host language: if you want to translate these examples in Java or C#, you don't have to update the shaders.

Vertex shader code

The first line of the shader defines the required version: in this tutorial, we need version 3.30 or higher.

Lines 2-4 define the inputs and outputs. In this example, we use the two vertex buffer objects (VBOs) in our vertex array object (VAO) with vertices and colors:

layout (location=0) in vec2 vertex;
layout (location=1) in vec3 inputColor;

The location indices are the ones we defined during the creation of the VBOs. Vertices have two values (vec2) because we are working we 2D coordinates. The colors have three values (vec3): red, green, and blue.

There is a single output, the color of the vertex:

                   out vec3 vertexColor;

Lines 5-8 contain the main function of the shader:

void main() {
    gl_Position = vec4(vertex, 0.0, 1.0);
    vertexColor = inputColor;
}

The first line of this function converts the 2D vertex into a 4D vertex because OpenGL expects this output format. We just set the z coordinate (the depth) with a zero value, and the w coordinate with 1.0. The w coordinate is hard to describe in a few sentences, and we won't need it. We store the 4D vertex in gl_Position, the name of the main vertex output, as defined by OpenGL.

The second line of the main function copies the vertex colors. In practice, it does nothing but tells OpenGL we will need this value in the fragment shader.

Compile the shader

The three last lines compile the shader:

vertexShaderId = glCreateShader(GL_VERTEX_SHADER)
glShaderSource(vertexShaderId, vertexShaderCode)
glCompileShader(vertexShaderId)

The first line creates a vertex shader id with the glCreateShader() function.

The second line sends the vertex shader code using the glShaderSource() function.

The last line compiles the shader using the glCompileShader() function. Note that we won't know if there is something wrong after this call. We'll see that during the linking.

Create a fragment shader

This first fragment shader is also very simple:

fragmentShaderCode = '''#version 330
in vec3 vertexColor;
out vec3 color;
void main() {
    color = vertexColor;
}'''
fragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER)
glShaderSource(fragmentShaderId, fragmentShaderCode)
glCompileShader(fragmentShaderId)

The first line of the shader also defines the version.

The two next lines define the inputs and outputs: in our case, the input and output color.

The main function copies the input color into the output color. It can't be more basic!

Link the shaders

We can only use the shaders if they are linked:

shaderProgramId = glCreateProgram()
glAttachShader(shaderProgramId, vertexShaderId)
glAttachShader(shaderProgramId, fragmentShaderId)
glLinkProgram(shaderProgramId)

The operation is straightforward: create a shader program with glCreateProgram(), attach the two shaders with glAttachShader() and link with glLinkProgram().

If something is wrong, you can get more help with the glGetProgramInfoLog():

print(glGetProgramInfoLog(shaderProgramId))

Enable the shaders

Enable the shaders is very easy. Before rendering our VAO, enable the corresponding shader using glUseProgram() and disable it with a zero id after:

glUseProgram(shaderProgramId)

glBindVertexArray(vaoId)
glEnableVertexAttribArray(0)
glEnableVertexAttribArray(1)
glDrawElements(GL_QUADS, 4 * faceCount, GL_UNSIGNED_INT, None)
glDisableVertexAttribArray(0)
glDisableVertexAttribArray(1)
glBindVertexArray(0)

glUseProgram(0)

Delete the shaders

At the end of the program, we delete the shaders and the program. It is not mandatory, except if you want to change the shaders during the execution of your program:

glDeleteShader(vertexShaderId)
glDeleteShader(fragmentShaderId)
glDeleteProgram(shaderProgramId)

Final program

If you run this program, the rectangles are now colored:

import os

import numpy as np
import pygame
from OpenGL.GL import glClear, GL_COLOR_BUFFER_BIT, GL_QUADS, glClearColor, glGenVertexArrays, glBindVertexArray, \
    glGenBuffers, glBindBuffer, GL_ARRAY_BUFFER, glBufferData, \
    GL_STATIC_DRAW, glVertexAttribPointer, GL_FLOAT, GL_FALSE, GL_ELEMENT_ARRAY_BUFFER, \
    glEnableVertexAttribArray, glDrawElements, GL_UNSIGNED_INT, glDisableVertexAttribArray, glCreateShader, \
    GL_VERTEX_SHADER, glShaderSource, glCompileShader, glCreateProgram, glAttachShader, glLinkProgram, glUseProgram, \
    glGetProgramInfoLog, GL_FRAGMENT_SHADER, glDeleteBuffers, glDeleteVertexArrays, glDeleteShader, glDeleteProgram

os.environ['SDL_VIDEO_CENTERED'] = '1'
windowSize = (1280, 800)
pygame.display.set_mode(windowSize, pygame.DOUBLEBUF | pygame.OPENGL)
pygame.display.set_caption("OpenGL 2D Facade - https://www.patternsgameprog.com/")

glClearColor(0.0, 0.0, 0.0, 1.0)

# All components of our scene
vertices = np.array([
    [-0.5, -0.5],
    [-0.5, 0.5],
    [0.5, 0.5],
    [0.5, -0.5],
    [-0.3, -0.7],
    [-0.3, 0.3],
    [0.7, 0.3],
    [0.7, -0.7]
], dtype=np.float32)

colors = np.array([
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 1.0],
    [0.0, 0.0, 1.0],
    [0.0, 1.0, 0.0],
    [0.0, 1.0, 0.0],
    [1.0, 0.0, 0.0],
    [1.0, 0.0, 0.0]
], dtype=np.float32)

faces = np.array([
    [0, 1, 2, 3],
    [4, 5, 6, 7]
], dtype=np.uint)
faceCount = faces.shape[0]

# Create one Vertex Array Object (VAO)
vaoId = glGenVertexArrays(1)
# We will be working on this VAO
glBindVertexArray(vaoId)

# Create three Vertex Buffer Objects (VBO)
vboIds = glGenBuffers(3)
# Commodity variables to memorize the id of each VBO
vertexVboId = vboIds[0]
colorVboId = vboIds[1]
indexVboId = vboIds[2]

# Copy vertices data to GPU
glBindBuffer(GL_ARRAY_BUFFER, vertexVboId)
vertices = np.ascontiguousarray(vertices.flatten())
glBufferData(GL_ARRAY_BUFFER, 4 * len(vertices), vertices, GL_STATIC_DRAW)
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, None)

# Copy colors data to GPU
glBindBuffer(GL_ARRAY_BUFFER, colorVboId)
colors = np.ascontiguousarray(colors.flatten())
glBufferData(GL_ARRAY_BUFFER, 4 * len(colors), colors, GL_STATIC_DRAW)
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, None)

# Copy index data to GPU
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexVboId)
faces = np.ascontiguousarray(faces.flatten())
glBufferData(GL_ELEMENT_ARRAY_BUFFER, 4 * len(faces), faces, GL_STATIC_DRAW)

# Release focus on VBO and VAO
glBindBuffer(GL_ARRAY_BUFFER, 0)
glBindVertexArray(0)

# Create vertex shader
vertexShaderCode = '''#version 330
layout (location=0) in vec2 vertex;
layout (location=1) in vec3 inputColor;
                   out vec3 vertexColor;
void main() {
    gl_Position = vec4(vertex, 0.0, 1.0);
    vertexColor = inputColor;
}'''
vertexShaderId = glCreateShader(GL_VERTEX_SHADER)
glShaderSource(vertexShaderId, vertexShaderCode)
glCompileShader(vertexShaderId)

# Create fragment shader
fragmentShaderCode = '''#version 330
in vec3 vertexColor;
out vec3 color;
void main() {
    color = vertexColor;
}'''
fragmentShaderId = glCreateShader(GL_FRAGMENT_SHADER)
glShaderSource(fragmentShaderId, fragmentShaderCode)
glCompileShader(fragmentShaderId)

# Create main shader program
shaderProgramId = glCreateProgram()
glAttachShader(shaderProgramId, vertexShaderId)
glAttachShader(shaderProgramId, fragmentShaderId)
glLinkProgram(shaderProgramId)

print(glGetProgramInfoLog(shaderProgramId))

# Main loop
clock = pygame.time.Clock()
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
            break
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                running = False
                break

    glClear(GL_COLOR_BUFFER_BIT);

    glUseProgram(shaderProgramId)

    glBindVertexArray(vaoId)
    glEnableVertexAttribArray(0)
    glEnableVertexAttribArray(1)
    glDrawElements(GL_QUADS, 4 * faceCount, GL_UNSIGNED_INT, None)
    glDisableVertexAttribArray(0)
    glDisableVertexAttribArray(1)
    glBindVertexArray(0)

    glUseProgram(0)

    pygame.display.flip()
    clock.tick(60)

glDeleteBuffers(3, vboIds)
glDeleteVertexArrays(1, vaoId)

glDeleteShader(vertexShaderId)
glDeleteShader(fragmentShaderId)
glDeleteProgram(shaderProgramId)

pygame.quit()
quit()

In the next post, I'll show how to use these shaders to render rectangles with a texture.