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.
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) |
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.
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.
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.
1
, 2
, 3
, or 4
, and it indicates how many numbers the data type can store.i
or f
, and it indicates the type of each number stored in the data type: i
is for integer, and f
is for floating point.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 |
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.
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.
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.
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);