Creating a Context#

Before we can do anything with ModernGL we need a Context. The Context object makes us able to create OpenGL resources. ModernGL can only create headless contexts (no window), but it can also detect and use contexts from a large range of window libraries. The moderngl-window library is a good start or reference for rendering to a window.

Most of the example code here assumes a ctx variable exists with a headless context:

# standalone=True makes a headless context
ctx = moderngl.create_context(standalone=True)

Detecting an active context created by a window library is simply:

ctx = moderngl.create_context()

More details about context creation can be found in the Context Creation section.

ModernGL Types#

Before throwing you into doing shaders we’ll go through some of the most important types/objects in ModernGL.

  • Buffer is an OpenGL buffer we can for example write vertex data into. This data will reside in graphics memory.

  • Program is a shader program. We can feed it GLSL source code as strings to set up our shader program

  • VertexArray is a light object responsible for communication between Buffer and Program so it can understand how to access the provided buffers and do the rendering call. These objects are currently immutable but are cheap to make.

  • Texture, TextureArray, Texture3D and TextureCube represents the different texture types. Texture is a 2d texture and is most commonly used.

  • Framebuffer is an offscreen render target. It supports different attachments types such as a Texture and a depth texture/buffer.

All of the objects above can only be created from a Context object:

The ModernGL types cannot be extended as in; you cannot subclass them. Extending them must be done through substitution and not inheritance. This is related to performance. Most objects have an extra property that can contain any python object.

Shader Introduction#

Shaders are small programs running on the GPU (Graphics Processing Unit). We are using a fairly simple language called GLSL (OpenGL Shading Language). This is a C-style language, so it covers most of the features you would expect with such a language. Control structures (for-loops, if-else statements, etc) exist in GLSL, including the switch statement.

Note

The name “shader” comes from the fact that these small GPU programs was originally created for shading (lighting) 3D scenes. This started as per-vertex lighting when the early shaders could only process vertices and evolved into per-pixel lighting when the fragment shader was introduced. They are used in many other areas today, but the name have stuck around.

Examples of types are:

bool value = true;
int value = 1;
uint value = 1;
float value = 0.0;
double value = 0.0;

Each type above also has a 2, 3 and 4 component version:

// float (default) type
vec2 value = vec2(0.0, 1.0);
vec3 value = vec3(0.0, 1.0, 2.0);
vec4 value = vec4(0.0);

// signed and unsigned integer vectors
ivec3 value = ivec3(0);
uvec3 value = ivec3(0);
// etc ..

More about GLSL data types can be found in the Khronos wiki.

The available functions are for example: radians, degrees sin, `cos, tan, asin, acos, atan, pow exp, log, exp2, log2, sqrt, inversesqrt, abs, sign, floor, ceil, fract, mod, min, max, clamp, mix, step, smoothstep, length, distance, dot, cross, normalize, faceforward, reflect, refract, any, all etc.

All functions can be found in the OpenGL Reference Page (exclude functions starting with gl). Most of the functions exist in several overloaded versions supporting different data types.

The basic setup for a shader is the following:

#version 330

void main() {
}

The #version statement is mandatory and should at least be 330 (GLSL version 3.3 matching OpenGL version 3.3). The version statement should always be the first line in the source code. Higher version number is only needed if more fancy features are needed. By the time you need those you probably know what you are doing.

What we also need to realize when working with shaders is that they are executed in parallel across all the cores on your GPU. This can be everything from tens, hundreds, thousands or more cores. Even integrated GPUs today are very competent.

For those who have not worked with shaders before it can be mind-boggling to see the work they can get done in a matter of microseconds. All shader executions / rendering calls are also asynchronous running in the background while your python code is doing other things (but certain operations can cause a “sync” stalling until the shader program is done)

Vertex Shader (transforms)#

Let’s get our hands dirty right away and jump into it by showing the simplest forms of shaders in OpenGL. These are called transforms or transform feedback. Instead of drawing to the screen we simply capture the output of a shader into a Buffer.

The example below shows shader program with only a vertex shader. It has no input data, but we can still force it to run N times. The gl_VertexID (int) variable is a built-in value in vertex shaders containing an integer representing the vertex number being processed.

Input variables in vertex shaders are called attributes (we have no inputs in this example) while output values are called varyings.

import struct
import moderngl

ctx = moderngl.create_context(standalone=True)

program = ctx.program(
    vertex_shader="""
    #version 330

    // Output values for the shader. They end up in the buffer.
    out float value;
    out float product;

    void main() {
        // Implicit type conversion from int to float will happen here
        value = gl_VertexID;
        product = gl_VertexID * gl_VertexID;
    }
    """,
    # What out varyings to capture in our buffer!
    varyings=["value", "product"],
)

NUM_VERTICES = 10

# We always need a vertex array in order to execute a shader program.
# Our shader doesn't have any buffer inputs, so we give it an empty array.
vao = ctx.vertex_array(program, [])

# Create a buffer allocating room for 20 32 bit floats
buffer = ctx.buffer(reserve=NUM_VERTICES * 8)

# Start a transform with buffer as the destination.
# We force the vertex shader to run 10 times
vao.transform(buffer, vertices=NUM_VERTICES)

# Unpack the 20 float values from the buffer (copy from graphics memory to system memory).
# Reading from the buffer will cause a sync (the python program stalls until the shader is done)
data = struct.unpack("20f", buffer.read())
for i in range(0, 20, 2):
    print("value = {}, product = {}".format(*data[i:i+2]))

Output the program is:

value = 0.0, product = 0.0
value = 1.0, product = 1.0
value = 2.0, product = 4.0
value = 3.0, product = 9.0
value = 4.0, product = 16.0
value = 5.0, product = 25.0
value = 6.0, product = 36.0
value = 7.0, product = 49.0
value = 8.0, product = 64.0
value = 9.0, product = 81.0

The GPU is at the very least slightly offended by the meager amount work we assigned it, but this at least shows the basic concept of transforms. We would in most situations also not read the results back into system memory because it’s slow, but sometimes it is needed.

This shader program could for example be modified to generate some geometry or data for any other purpose you might imagine useful. Using modulus (mod) on gl_VertexID can get you pretty far.