\( \def\sc#1{\dosc#1\csod} \def\dosc#1#2\csod{{\rm #1{\small #2}}} \newcommand{\dee}{\mathrm{d}} \newcommand{\Dee}{\mathrm{D}} \newcommand{\In}{\mathrm{in}} \newcommand{\Out}{\mathrm{out}} \newcommand{\pdf}{\mathrm{pdf}} \newcommand{\Cov}{\mathrm{Cov}} \newcommand{\Var}{\mathrm{Var}} \newcommand{\ve}[1]{\mathbf{#1}} \newcommand{\mrm}[1]{\mathrm{#1}} \newcommand{\ves}[1]{\boldsymbol{#1}} \newcommand{\etal}{{et~al.}} \newcommand{\sphere}{\mathbb{S}^2} \newcommand{\modeint}{\mathcal{M}} \newcommand{\azimint}{\mathcal{N}} \newcommand{\ra}{\rightarrow} \newcommand{\mcal}[1]{\mathcal{#1}} \newcommand{\X}{\mathcal{X}} \newcommand{\Y}{\mathcal{Y}} \newcommand{\Z}{\mathcal{Z}} \newcommand{\x}{\mathbf{x}} \newcommand{\y}{\mathbf{y}} \newcommand{\z}{\mathbf{z}} \newcommand{\tr}{\mathrm{tr}} \newcommand{\sgn}{\mathrm{sgn}} \newcommand{\diag}{\mathrm{diag}} \newcommand{\Real}{\mathbb{R}} \newcommand{\sseq}{\subseteq} \newcommand{\ov}[1]{\overline{#1}} \DeclareMathOperator*{\argmax}{arg\,max} \DeclareMathOperator*{\argmin}{arg\,min} \)

12   Uniform variables


In this last chapter, we saw examples on how vertex shaders can be used to render more complex shapes by transforming simple shapes. One thing that should become clear is that a GLSL program can only transform shapes that are passed to them through vertex and index buffers. The data that are to be stored in these buffers are prepared first by the Javascript part of our application, which we shall call the host program. They are then stored in the computer's main memory (i.e., RAM). Then, the host program transfers the data to the buffers, which reside in GPU memory. As a result, communication between the host program and the GLSL program is a crucial part of 3D application development with OpenGL.

So far, we have been using only one mechanism for communication between a host program and a GLSL program: buffers. Buffers are specialized for communicating vertex and index data, and they have many restrictions. GLSL programs do not have direct access to data inside index buffers. A vertex shader can access data inside vertex buffers, but a fragment shader cannot. However, there are two more such mechanisms: uniform variables and textures. This chapter discuss the former, but the reader would have to wait until Chapter XXX for a discussion of the latter.

A uniform variable is a piece of GPU memory for storing a small piece of information, which can be directly set by the host program. Unlike buffers, both vertex shaders and fragment shaders have access to uniform variables. Both shaders would see the same value when referencing the same uniform variable, and this is why it is called "uniform." Moreover, the shaders cannot change the value themsevles, so this value would remain constant until the host program rewrites the variable. As a result, uniform variables can serve as "global" variables for GLSL programs.

In this chapter, we will how to declare and manipulate uniform variables with both GLSL and Javascript. We start with a simple program where uniform variables are used to communicate user inputs to GLSL programs. We will then see how to manipulate uniform variables so that they change with time, and this would allow use to create simple animations. Lastly, we will study uniform variables that have array types, which would allow us to transfer a larger chunk of data to GLSL programs.

12.1   Program 1: Basic usage of uniform variables

Program 1's source code is available in the chapter-12/program-01 directory of the code repository. Its screenshots are shown in Figure 12.1.

(a) (b) (c)
Figure 12.1 Screenshots of Program 1 (a) when it is started afresh (b) when the color sliders are manipulated, and (c) when the "phase," "peroid," and "amplitude" sliders are manipulated.

Program 1 draws a sine curve on its canvas. The curve's color and shape can be control by the 6 sliders in the web page. The first three sliders are used to pick the RGB color of the sine curve. The next three sliders control the phase, period, and amplitude of the curve, respectively.

Recall that this is not the first time we control what appears on the screen with UI elements. In Program 4 of Chapter 8, we control the color of the canvas through sliders. Moreover, just in the last chapter, we choose what shapes appear in canvases through radio boxes. However, the mechanism used to change the canvas in these programs do not require any information to be passed from the host program to the GLSL shader. In Program 4 of Chapter 8, we use the slider values as arguments to gl.clearColor, a Javascript method. In the last chapters, we use the radio boxes' values to choose which GLSL program to use, and this process is carried out entirely in Javascript. In this chapter, though, we explicitly pass information from UI elements (i.e., the sliders) to the GLSL program, and let the program process the information itself.

12.1.1   Declaring and using uniform variables in GLSL

As said earlier, the mechanism we use to pass information from host to GLSL in this chapter is the uniform variable. Let's see first hand how they are declared and used in the shaders of our program. We will start with the easier one, the fragment shader, whose source code is reproduced in full below.

#version 300 es

precision highp float;

out vec4 fragColor;

uniform vec3 color;

void main() {
    fragColor = vec4(color, 1.0);
}

The code above is very similar to those of the fragment shaders in the last chapter. There are two main differences. The first is that there is a new variable called color declared with the uniform keyword.

uniform vec3 color;

As you may guess, this is the uniform variable in question. It is just a variable declared with the keyword uniform in front of the data type of the variable. One important thing to note is that a uniform variable must be declared outside any functions. This makes sense because a uniform variable is supposed to be a global variable that should be accessible from any functions. So, its scope should be the entire program, not just inside a particular function. However, remember that a uniform variable is read only, so one should not assign any value to it. In our fragment shader, we use the RGB values stored in the color variable to fill the first 3 components of the output fragment color.

Next, we look at the vertex shader, which is a little more complicated because it has three uniform variables instead of one.

#version 300 es

const float PI = 3.14159265359;

in float t;

uniform float amplitude;
uniform float period;
uniform float phase;

void main() {
    float x = t;
    float y = amplitude * sin(2.0 * PI * t /period + phase);
    gl_Position = vec4(x, y, 0.0, 1.0);    
}

We can see creates a sine curve by plotting points $(x,y)$ where \begin{align*} x &= t, \\ y &= \mathrm{amplitude} \times \sin\bigg(\frac{2\pi t}{\mathrm{period}} + \mathrm{phase}\bigg). \end{align*} Here the amplitude, period, and phase variables are uniform variables whose types are float. The names of the variables speak for themselves. The amplitude controls how tall the sine curve is. The period controls with width of one copy of the sine wave. Lastly, the phase controls the horizontal position of where a copy of the sine curve begins.

12.1.2   Manipulating uniform variables in Javascript

It is not enough to modify the GLSL code to declare and use uniform variables. We must change our Javascript code so that it becomes aware of them and assign their values. The code that manipulates uniform variables is in the updateWebGL method in the index.js file. Let us start with how to assign a value to the amplitude uniform variable. The first thing we do is to fetch the value to assign from a slider.

let amplitude = this.amplitudeSlider.slider("value") / 1000.0;

Here, the amplitudeSlider is a JQuery UI slider that was prepared in the createUi method. How the slider came about is not important as the important thing is that we need a way to figure out what to assign to the uniform variable.

Before we can assign a value to a uniform variable, we need to use the GLSL program that the uniform variable is a part of. So, next part of the updateWebGL method looks like the following.

useProgram(this.gl, this.program, () => {
  //
  // Assigning values to uniform variables.
  //

  setupVertexAttribute(
    self.gl, self.program, "t", self.vertexBuffer, 1, 4, 0);
  drawElements(
    self.gl, self.indexBuffer, self.gl.LINES, (self.numVertices-1)*2, 0);
});

We see that the code uses the useProgram function that we learned about in Section 9.4.1. All the manipulation of the uniform variables are done inside the closure that we give the useProgram function as an input. Moreover, notice that the segment that assign uniform variable comes before the last two statements, which draw primitives. This makes sense because uniform variables can affect the shape and appearance of the primitives being drawn, so we must assign them before drawing.

The segment that assigns values to uniform variables is quite long because we have four variables (rgb, amplitude, period, and phase) to take care of. However, assigning a uniform variable only involves two lines of code. We reproduce the piece of code that assigns the amplitude variable below.

// ******************
// * Using uniforms *
// ******************
// Step 1: Get its location.
let amplitudeLocation = self.gl.getUniformLocation(self.program, "amplitude");
// Step 2: Set its value using the right function.
self.gl.uniform1f(amplitudeLocation, amplitude);

Assigning value to a uniform variable involves two steps. The first is to find the "location" of the uniform variable. Here, a location is just a unique identifier that can be used to refer to the variable at a later time. It can be fetched by the getUniformLocation method of the WebGL context. The method requires two inputs. The first is the object representing the GLSL program that contains the uniform variable, and the second is the name of the variable as declared in either the vertex and/or fragment shader.

The second step is to assign the value with a method of the WebGL context whose name is of the form uniformXXX where XXX depends on the type of the uniform variable in question. For non-array types, the rule for the name is quite simple. XXX has two characters.

As a result, the right methods to use for a number of types of uniform variables are as follows.

Uniform variable type Element type Number of elements Method name
int int 1 uniform1i
ivec2 int 2 uniform2i
ivec3 int 3 uniform3i
int int 4 uniform4i
float float 1 uniform1f
vec2 float 2 uniform2f
vec3 float 3 uniform3f
vec4 float 4 uniform4f
Table 12.1 Names of the uniformXXX method to be used for certain types of uniform variables.

In our case, the amplitude uniform variable is of type float, which means the type can only hold one floating point number. Thus, the appropriate method to use is uniform1f. This method takes two arguments. The first is the location of the uniform variable, which we just fetched in the last step, and the second is the floating point value we want to assign.

Because the period and phase uniform variables are also of type float, we deal with these variables the same way we deal with amplitude.

let periodLocation = self.gl.getUniformLocation(self.program, "period");
self.gl.uniform1f(periodLocation, period);

let phaseLocation = self.gl.getUniformLocation(self.program, "phase");
self.gl.uniform1f(phaseLocation, phase);

We deal with the color uniform variable slightly differently because it is of type vec3 instead of float. As vec3 stores 3 floating point numbers, the WebGL context method used to assign its value is uniform3f instead of uniform1f.

let colorLocation = self.gl.getUniformLocation(self.program, "color");
self.gl.uniform3f(colorLocation, color[0], color[1], color[2]);

We can see that uniform3f takes 4 parameters instead of 2. The first parameter is still the location of the uniform variable, but 3 floating point values have to be supplied instead of 1. Here, we feed the three values in the color array that was prepared earlier in the updateWebGL function.

let color = this.rgbSliders.map(s => s.slider("value") / 255);

This line of code computes the color array by applying the closure that extracts the slider value and divides it by $255$ to the three sliders that correspond to the R, G, and B values that the user can specify.

There are 6 other methods in Table 12.1 that we have not seen actual usage. However, their usage follows the same rule that applies to uniform1f and uniform3f. The first argument is the location of the uniform variable. The method then takes $N$ extra arguments where $N$ is the number in the method name, and these are arguments are the values to be assigned to the components of the uniform variable.

12.2   Program 2: Using a uniform variable to create a moving square

In this section, we are going to study another important way to use uniform variables: animation. This principle is simple. We can use Javascript to keep track of how much time have passed since our program started. Then, we can use this information to change how primitives are rendered. This is done by passing the time or other information derived from it to GLSL programs. This section's program renders a square moving in a circular path at a constant angular speed. This source code of the program is available in the chapter-12/program-02 directory of the book's Github repository. Screenshots of the program are available below.

Figure 12.2 Screenshots of Program 2. The square moves in a circular path.

12.2.1   How to keep track of time

An animation is an image that moves. This means that it changes with time. Creating animation thus requires us to draw different images at different time. As a r esult, a program that creates an animation must be aware of time. In our case, we need to keep track of how much time has passed since the program begins because the exact time (say, 10:10AM) does not matter.

We keep track of time through the use of the method performance.now in the host Javascript code. Here, performance is a global variable that comes with the Javascript language. The now method takes no arguments and returns the number of milliseconds since a time which is always before our Javascript program starts. The program has an infinite loop that keeps calling the updateWebGL method in order to repeatedly draw something on the WebGL canvas. As a result, if we call performance.now inside the updateWebGL, we can easily know the time (since the start time) that each interation of the loop is run.

As stated earlier, we will not use the time directly, but we will keep track of how much time has passed since the program started. To do so, we keep track of the last time the updateWebGL was called. This is stored in the lastTime field of the WebGLApp object that represents our application. We initialize the field in the constructor as follows.

this.lastTime = performance.now();

When an instance of WebGLApp is constructed, its updateWebGL method has not been called yet. As a result, we set lastTime field to a time returned from performance.now to get an arbitrary starting point. This point in time is not exactly where the program starts, but it should be pretty close.

We use another field called elaspedTime to store the time since the lastTime field was initialized. We initialize the field to $0$.

Inside the updateWebGL method, we need to update both fields so that elapsedTime has the correct value. The code to do so is as follows.

let currentTime = performance.now();
let delta = currentTime - this.lastTime;   
this.elaspedTime += delta;
this.lastTime = currentTime;

The logic of the above code is quite simple. We call performance.now to get the current time. We then subtract from it the lastTime field to get the number of milliseconds since last time the updateWebGL method was called. We add this value to the elapsedTime field to update its value. Then, we set the lastTime field to the current time so that we can use this value in the next iteration. The elapsedTime field now stores the number of milliseconds since the program starts, and it gets updated every time the updateWebGL method is executed.

12.2.3   Time-dependent displacement

One of the simplest form of animation is when an object continuously moves withouth changing its shape. The animation Program 2 shows is exactly this: a white square that moves in a circular path. To move is to change position. A displacement is a vector that represents a change in position. It is defined as: \begin{align*} \mathrm{displacement} = (\mathrm{new\ position}) - (\mathrm{old\ position}). \end{align*} Now, an object in our context is a mesh. A mesh is a collection of vertices that are connected together. Each vertex has a position, and it is stored in the mesh's vertex buffer. We often think of these positions as the vertices' "rest position" as they are the positions that define the shape of the object when it is not move or deformed. To move an object without changing its shape, we just have to move all vertices by the same displacement. In this way, all the relative distances between the vertices will be preserved. Mathematically, this is done by adding the displacement to the position of each vertex, and this is exactly what is done in the vertex shader of Program 2.

#version 300 es

in vec2 position;

uniform vec2 displacement;

void main() {
    gl_Position = vec4(position + displacement, 0, 1);
}

Much like many GLSL programs have seen before, we use the position attribute variable to store vertex positions. What is new is the displacement uniform variable, which represents the displacement. We can see that what the vertex shader does is to add the displacement to the vertex position and output the result.

Now that we know how to move an object from its rest position to another position, we now need to figure out how to make the object's position change over time. This is easy: we only need to make the displacement changes over time. So, every time the screen is update, we need to set the displacement uniform variable to a new value. Let's look at the part of the updateWebGL method that does this.

let angularPosition = 0.001 * this.elaspedTime;
        
let displacementX = 0.5 * Math.cos(angularPosition);
let displacementY = 0.5 * Math.sin(angularPosition);

useProgram(this.gl, this.program, () => {
  let displacementLocation = self.gl.getUniformLocation(
    self.program, "displacement");
  self.gl.uniform2f(
    displacementLocation, displacementX, displacementY);
  
    :
    :
    :
});

Because we want our square to move in a circular path, the displacement should be vectors from the center of a circle to points on the circumference of the circle. In case of unit circles, the vectors are given by \begin{align*} \begin{bmatrix} x \\ y \end{bmatrix} = \begin{bmatrix} \cos \theta \\ \sin \theta \end{bmatrix} \end{align*} where $\theta$ is an angle. As a result, we can our square to move in a circular path by changing the angle over time. This is basically what the Javascript code do in this code line:

let angularPosition = 0.001 * this.elaspedTime;

Here, angularPosition is the variable we use to represent the angle. As discussed in the last section, the field elapsedTime changes every time the updateWebGL method is called. The factor 0.001 is applied because elapsedTime's unit is milliseconds, which means that using the value directly would make the square move 1 radian (about one sixth of a circle) per millisecond, which is too fast for our naked eyes to see. From angularPosition, the code computes the $x$- and $y$-componets of the displacement with: \begin{align*} \begin{bmatrix} \mathrm{displacementX} \\ \mathrm{displacementY} \end{bmatrix} &= \begin{bmatrix} 0.5 \cos(\mathrm{angularPosition}) \\ 0.5 \sin(\mathrm{angularPosition}) \end{bmatrix}. \end{align*} The number $0.5$ here is the radius of the circle we want our square to move on. Because the canvas corresponds to the rectangle $[-1,1] \times [-1,1]$, choosing the radius of $0.5$ will make the square's path fit inside the screen.

Lastly, after the $x$- and $y$-components have been computed, we set the uniform variable displacement to these values with glUniform2f.

let displacementLocation = self.gl.getUniformLocation(
  self.program, "displacement");
self.gl.uniform2f(
  displacementLocation, displacementX, displacementY);

12.3   Program 3: Using uniform variables to animate a boucing ball

12.4   Program 4: Using an array of uniform variables to animate multiple boucing balls


<< Contents