5.5 - Example 1: One Color per Model¶
We will render our simple pyramid model using a single color for all its faces. We need to examine the following issues:
- How does the shader program process the model vertices?
- How is the model data organized in an object buffer?
- How is rendering actually performed?
The Shader Programs¶
Examine the shader programs in the following demo and then study their descriptions below.
A simple, manually defined 3D model. You can spin the model with your mouse.
Animate
Vertex Shader¶
Lines | Description |
---|---|
7-8 | The model will be rendered with the same transformation matrix for all
vertices. And all faces will be rendered with the same color. These are
uniform values that will be set once before a rendering starts.
Notice the convention to start their names with a u_. This will
help you track the types of your variables. (WebGL shaders could care
less what you name your variables. This convention is for you - the
programmer!) |
7 | a mat4 is a 4x4 matrix; 16 floating point values. |
8 | a vec4 is a vector of 4 floating point values; in this case a
color defined as RGBA (red, green, blue, alpha). |
10 | The vertices will change for every triangle that is rendered. Therefore
the variable, a_Vertex , is an attribute variable. It will get
its values from a buffer object. A vec3 is an array of 3
floating point values; (x,y,z). |
12-15 | The main() function is always the entry point for a shader. This
function will be executed once for each vertex in the model. |
14 | This shader executes a single command on each vertex. It is multiplying
a 4x4 matrix times a 4x1 vector using linear algebra matrix mulplication.
WebGL shaders have matrix math built-in! Notice that the vertex has only
three values (x,y,z) but 4 values are required for the matrix
multiplication. The vertex is converted to a 4-component array, with the
last component, the w component, set to 1.0. |
Fragment Shader¶
Lines | Description |
---|---|
7 | The color of the pixels will be the same, so we have one uniform
value, u_Color . Because the variable is global and has the same
name as a global vertex shader variable, the variable is linked to the
vertex shader‘s variable of the same name. |
9-11 | The main() function is always the entry point for a shader. This
function will be executed once for each fragment of the rendered
primitive (either a point, line or triangle.) |
10 | This shader executes a single command on each fragment. It is setting the color of the fragment. |
The Buffer Object(s)¶
The only attribute that is changing to render this model is its vertices.
Therefore, we only need one buffer object. Remember that a buffer object
is a 1-dimensional, homogeneous array of floating point values.
We need to gather up all of the vertices that define
the triangles into a single array, and the order of the vertices matter.
Before you create the array you need to
decide how the triangles will be rendered. There are three choices:
gl.TRIANGLE
, gl.TRIANGLE_STRIP
, or g.TRIANGLE_FAN
.
In addition, the vertices of each triangle must be in counter-clockwise order
when looking at the front of the face. Our pyramid model can be rendered
in a single ‘pass’ through the buffer object using either the gl.TRIANGLE
or the gl.TRIANGLE_STRIP
mode. Let’s keep it simple and use the
gl.TRIANGLE
mode for this example. (If you changed rendering modes,
you would have to change the ordering of the vertices.)
Creating the buffer object is a pre-processing step which needs to happen only once. Let’s break the task into 2 parts:
- Get the triangle vertices into a 1D array in the correct order. This is
done by the
_buildBufferObjectData()
function in the code shown below. - Create and upload the vertices into the GPU’s memory. This is done by the
_createBufferObject()
function in the code shown below.
Note that these two functions are private functions of the JavaScript class definition and they are only called once when the constructor code is executed.
The 1D array that is created to fill the buffer object contains the data below for this example. Note that each consecutive three values represent one vertex.
[0.5, -0.25, 0.25, 0, 0.25, 0, -0.5, -0.25, 0.25, -0.5, -0.25, 0.25, 0, 0.25, 0, 0, -0.25, -0.5, 0, -0.25, -0.5, 0, 0.25, 0, 0.5, -0.25, 0.25, 0, -0.25, -0.5, 0.5, -0.25, 0.25, -0.5, -0.25, 0.25]
A simple, manually defined 3D model. You can spin the model with your mouse.
Animate
Access to Shader Variables¶
To render a model, you will set some values in your shader program and then
issue the gl.drawArrays()
command. But you have a problem. Your
data is in RAM in your JavaScript program, while your shader program is a
compiled program on the GPU. How can you pass data to the shader? This
is a 2 step process:
- Pre-processing: Get the location of a variable in the shader program. This is equivalent to an index that could be used as an array lookup. The location of a variable in a shader program will never change, so the location only needs to be retrieved once.
- Use a variable’s location in a shader program to update it.
To get a variable’s location in a shader program, use one of the appropriate WebGL functions:
uint getUniformLocation(Object shader_program, string variable_name);
ulong getAttribLocation(Object shader_program, string variable_name);
Examples of these functions can be found in lines 133-135 of the demo code above.
To set a variable’s value in a shader program, use one of the appropriate WebGL functions:
void uniform[1234][fi](uint location, value1, value2, value3, ...);
void uniform[1234][fi]v(uint location, Array values);
void uniformMatrix[234]fv(uint location, bool transpose, Array matrix);
The characters in brackets, [234], represent the number of values to be set. The [fi] means you are setting either a floating point value, f, or an integer value, i. You can send values as discrete variables, or as multiple values in a single array. The functions that use arrays end in a v. Examples of setting the values of shader program variables can be found at lines 158, 161, and 176 in the above demo code.
Linking a Buffer Object to an Attribute Variable¶
An attribute variable pulls its values from an array. The array will typically
be stored in a buffer object. The positions in the array that are used
is determined by the 2nd and 3rd parameters in your call to
gl.drawArrays(mode, start, count)
. The start
parameter gives
the starting array index, while the count
parameter specifies how many
vertices to use. You must enable the use of ‘vertex arrays’ before you can
access values from a buffer object. Linking our a_Vertex
attribute
variable in our shader program to the buffer object looks like this:
gl.bindBuffer(gl.ARRAY_BUFFER, triangles_vertex_buffer_id);
gl.enableVertexAttribArray(a_Vertex_location);
gl.vertexAttribPointer(a_Vertex_location, 3, gl.FLOAT, false, 0, 0);
First, you must make a particular buffer object active. This is done with the
bindBuffer
command. Then you enable getting data from buffer objects
with the enableVertexAttribArray
function. Then you describe the
organization of the values in the buffer object using a call to
vertexAttribPointer
.
The 1st parameter, a_Vertex_location, is the location in the compiled
shader program of the variable that will be set from the buffer’s values. Let’s
delay a formal description of the other parameters until later. For now,
the other parameters tell the shader that the vertex data has 3 values per
vertex and the values are all floating point.
Rendering¶
In the example code, the gl.drawArrays(mode, start, count)
command
is called 5 times. (See lines 171 and 180.) Let’s discuss why.
The shader is written to render with a single color. We want to render
a red pyramid with black edges. Therefore we must call drawArrays()
at least
twice: once when the color is set to red, and a second time after the color
has been changed to black. Examine the demo code. Notice that the color
is set to red in line 161 and then the triangles are rendered in line 171.
Then the color is changed to black in line 176 (the edge_color
), and
then the same vertices in the buffer object are used to draw the triangle
edges using the gl.LINE_LOOP
mode. However, this can’t be done with
a single draw command because the vertices are not organized that way
in the buffer object. So we have to step through the buffer and change
the starting index each time.
Summary¶
In the next few lessons we will modify our model, our shader programs, and our rendering code to produce more sophisticated graphics. It is important that you understand the concepts presented above before proceeding to the next lesson. So make a second pass through this lesson before proceeding.