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.
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.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);