Shaders - Demo 20

Purpose

Learn what “shaders” are, with a brief introduction to how to use them in OpenGL 2.1.

How to Execute

On Linux or on MacOS, in a shell, type “python src/demo20/demo.py”. On Windows, in a command prompt, type “python src\demo20\demo.py”.

Move the Paddles using the Keyboard

Keyboard Input

Action

w

Move Left Paddle Up

s

Move Left Paddle Down

k

Move Right Paddle Down

i

Move Right Paddle Up

d

Increase Left Paddle’s Rotation

a

Decrease Left Paddle’s Rotation

l

Increase Right Paddle’s Rotation

j

Decrease Right Paddle’s Rotation

UP

Move the camera up, moving the objects down

DOWN

Move the camera down, moving the objects up

LEFT

Move the camera left, moving the objects right

RIGHT

Move the camera right, moving the objects left

q

Rotate the square around its center

e

Rotate the square around paddle 1’s center

Description

In the previous demo, in the footnote, we found that “glVertex” did a lot behind the scenes, sneakily grabbing data from the matrix stacks and the current color (and lots of other things we haven’t covered in this book, like texturing and lighting).

What did the graphics driver do with the data? This older style of graphics programming is called “fixed-function” graphics programming, in that we can tweak some values, but the graphics card and its driver are going to do whatever they were programmed to, and we can’t control it. Kind of like having a graphing calculator, but not having the ability to program it - OpenGL is in control at this point, and we can just parameterize it.

Programmers wanted more control, first over how lighting works (which we don’t cover in this book). Rather than having 3 or so lighting models to choose from, the programmers wanted to be able to specify the math of how their custom lighting worked. So OpenGL 2.1 allowed the programmer to create “shaders”, which I believe were called such because they allow the programmer to change the “shade” of a fragment [1].

In this demo, we keep most of the code from the previous demo the same, we just add in programmable shaders. So the calls to “glVertex” are no longer a black box, in which something happens of the graphics card; instead, before sending modelspace data to the graphics card, we program a sequence of transformations that happen on the graphics card per vertex, which a driver could implement in parallel.

We have some new imports, “glUseProgram”, “GL_VERTEX_SHADER”, “GL_FRAGMENT_SHADER”

src/demo20/demo.py
34from OpenGL.GL import (
35    GL_COLOR_BUFFER_BIT,
36    GL_DEPTH_BUFFER_BIT,
37    GL_DEPTH_TEST,
38    GL_FRAGMENT_SHADER,
39    GL_LEQUAL,
40    GL_MODELVIEW,
41    GL_PROJECTION,
42    GL_QUADS,
43    GL_SCISSOR_TEST,
44    GL_VERTEX_SHADER,
45    glBegin,
46    glClear,
47    glClearColor,
48    glClearDepth,
49    glColor3f,
50    glDepthFunc,
51    glDisable,
52    glEnable,
53    glEnd,
54    glLoadIdentity,
55    glMatrixMode,
56    glPopMatrix,
57    glPushMatrix,
58    glRotatef,
59    glScissor,
60    glTranslate,
61    glUseProgram,
62    glVertex3f,
63    glViewport,
64)
65from OpenGL.GLU import gluPerspective
66

We now ask the OpenGL driver to accept OpenGL 2.1 function calls.

src/demo20/demo.py
74glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 2)
75glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 1)

Before the event loop starts, we send two mini programs to the GPU, the vertex shader and the fragment shader. The purpose of the vertex shader is to transform the modelspace data to clip-space (x,y,z,w), where to get NDC you would divide by the w; i.e. (x/w,y/w,z/w,1)

I like to think of a matrix multiplication to a vector as a “function-application”, in the same way we did in the lambda stack.

src/demo20/demo.py
233# compile shaders
234
235# initialize shaders
236pwd = os.path.dirname(os.path.abspath(__file__))
237
238with open(os.path.join(pwd, "triangle.vert"), "r") as f:
239    vs = shaders.compileShader(f.read(), GL_VERTEX_SHADER)
240
241with open(os.path.join(pwd, "triangle.frag"), "r") as f:
242    fs = shaders.compileShader(f.read(), GL_FRAGMENT_SHADER)
243
244shader = shaders.compileProgram(vs, fs)
245glUseProgram(shader)

In the vertex shader, there are a lot of predefined variable names with values provided to us. “gl_Modelview_matrix” is the matrix at the top of the modelview matrix stack at the time “glVertex” is called, “glProjectionMatrix” is the top of the projection matrix stack (although there is typically only one). “glColor” is the color that was defined for this vertex by calling “glColor3f”.

In OpenGL 2.1, the output of the vertex shader that we use here is “glFrontColor”, which is a variable name that we must use to get the data to the fragment shader, which we haven’t covered. (Actually there are a bunch of other predefined variable outputs, including “glBackColor”, for the case that we are looking at the back face of the geometry.

//Copyright (c) 2018-2024 William Emerison Six
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.

#version 120

void main(void)
{
    // gl_Vertex is modelspace data

    vec4 camera_space = gl_ModelViewMatrix * gl_Vertex;
    // in camera space, the frustum is on the negative z axis
    vec4 clip_space = gl_ProjectionMatrix * camera_space;
    vec3 ndc_space = clip_space.xyz / clip_space.w;
    // in ndc space, x y and z need to be divided by w

    // normal MVP transform
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;

    // Copy the primary color
    gl_FrontColor = gl_Color;
}

This is the fragment shader. Unlike the vertex shader, which took an vertex in (x,y,z) and outputted clip-space (NDC), the fragment shader for a given pixel is determining the color [2]. Is “glColor” in the fragment shader the same “glColor” from the vertex shader, or is it the value of the vertex shader’s output “glFrontColor”? Honestly, the author doesn’t know, but it is set to the output of the fragment shader, and such questions are a moot point once we get to OpenGL 3.3 Core profile in the next section.

//Copyright (c) 2018-2024 William Emerison Six
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in all
//copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//SOFTWARE.

#version 120

void main(void)
{
    gl_FragColor = gl_Color;
}

The explicit mapping of variable names to values in OpenGL 3.3 Core Profile will make the flow of data from the CPU program, to the GPU vertex shader, to the fragment shader much more clear, albeit at the expense of verbosity.