5.6 - Example 2: One Color per Triangle

Suppose we would like to render a triangular mesh where each triangle has a different color. This can be done using the previous version of our shader program by calling gl.drawArrays() to render each triangle separately. The code would look like this:

// Draw each triangle separately
for (var start = 0, color_index = 0;
  start < number_vertices;
  start += 3, color_index += 1) {

  // Set the color of the triangle
  gl.uniform4fv(u_Color_location, colors[color_index]);

  // Draw a single triangle
  gl.drawArrays(gl.TRIANGLES, start, 3);
}
Show: Code   Canvas   Run Info
../_static/05_color_per_triangle/simple_pyramid.html

Rendering each triangle with a different execution of the graphics pipeline.

Please use a browser that supports "canvas"
Animate
Draw the edges of the triangles using a gl.LINE_LOOP
Render the model as a "wireframe"
Render the global axes (x:red, y:green, z:blue)
Average time to render the model is 0.00000 milliseconds.
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

Performing a graphics pipeline for individual triangles is very inefficient. In the example above the average rendering time for the pyramid model is being calculated and displayed. If the model was composed of 100’s, or perhaps 1000’s, of triangles, we would have a major speed problem. Every call in your JavaScript program to a WebGL command is a huge time sink. If we want the graphics to be fast, we need to draw an entire model using a single call to gl.drawArrays().

If we change our shader program to allow for a different color for each vertex, we must also change our model representation and our buffer objects. So let’s walk through all the changes required.

The Model

To implement a different color for each triangle, let’s store a RGBA color value with each of our “triangle” objects. A new version of our 3D model is shown in the following WEBGL program example. Please note the following changes:

Lines Description
37-41 A Triangle2 object now stores a color. (line 40)
67-71 Various RGBA colors are defined.
74-77 A different color is passed to the creation of each Triangle2 object.
Show: Code   Canvas   Run Info
../_static/05_color_per_triangle2/simple_pyramid2.html

Rendering each triangle with a different execution of the graphics pipeline.

Please use a browser that supports "canvas"
Animate
Draw the edges of the triangles using a gl.LINE_LOOP
Render the model as a "wireframe"
Render the global axes (x:red, y:green, z:blue)
Average time to render the model is 0.00000 milliseconds.
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

The Shader Programs

Our shader programs must change because each vertex of our model now has two attributes: a location, (x,y,z), and a color (r,g,b,a). Therefore, our vertex shader program has two attribute variables: a_Vertex and a_Color. Examine the shader programs in the following demo and then study their descriptions below.

Show: Code   Canvas   Run Info
../_static/05_color_per_triangle2/simple_pyramid2.html

Rendering each triangle with a different execution of the graphics pipeline.

Please use a browser that supports "canvas"
Animate
Draw the edges of the triangles using a gl.LINE_LOOP
Render the model as a "wireframe"
Render the global axes (x:red, y:green, z:blue)
Average time to render the model is 0.00000 milliseconds.
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

Lines Description
5 There is only one variable that is constant for an execution of gl.drawArrays() for this shader, the uniform model transformation matrix, u_Transform.
7-8 Each vertex has two attributes: a location and a color.
10 Values are passed from a vertex shader to a fragment shader using the varying storage quantifier. This will make more sense later. For now, we need a ‘varying’ variable to pass a vertex’s color to the fragment shader. (Note that the vertex’s location is being passed to the frament shader through the gl_Position variable.
17 Pass the RGBA color value for this vertex to the fragment shader.
Lines Description
5 Declare a varying variable using the same name as the vertex shader. When the shaders are compiled and linked, this variable will contain the value set in the vertex shader.
8 Use the color of the vertex to set the color of every pixel inside the triangle that is being rendered.

The Buffer Object(s)

Since we have two attributes for each vertex, a location and a color, we will create two buffer objects. Since all data is “per-vertex”, each vertex must be assigned a color, even though this requires that the same color value be stored three times. Study the code in the model_to_gpu2.js file in the WebGL example below. Make sure you find where the data for the two buffer objects are collected and then the separate buffer objects are created in the GPU (lines 104 and 105).

Show: Code   Canvas   Run Info
../_static/05_color_per_triangle2/simple_pyramid2.html

Rendering each triangle with a different execution of the graphics pipeline.

Please use a browser that supports "canvas"
Animate
Draw the edges of the triangles using a gl.LINE_LOOP
Render the model as a "wireframe"
Render the global axes (x:red, y:green, z:blue)
Average time to render the model is 0.00000 milliseconds.
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

Below is the data in the buffer objects for our pyramid model. Notice that the color values are repeated 3 times to make each vertex of a specific triangle be the same color.

// The vertex location array
[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]

// The color-per-vertex array
[ 1,0,0,1,  1,0,0,1,  1,0,0,1,
  0,1,0,1,  0,1,0,1,  0,1,0,1,
  0,0,1,1,  0,0,1,1,  0,0,1,1,
  1,0,1,1,  1,0,1,1,  1,0,1,1 ]

Access to Shader Variables

Since your variables have changed in the shader programs, you need to modify your rendering code to get the location of the shader variables. Lines 135-139 of the demo code below get the shader variable locations:

// Get the location of the shader variables for the color_program
color_program.u_Transform_location = gl.getUniformLocation(color_program, 'u_Transform');

color_program.a_Vertex_location    = gl.getAttribLocation(color_program, 'a_Vertex');
color_program.a_Color_location     = gl.getAttribLocation(color_program, 'a_Color');
Show: Code   Canvas   Run Info
../_static/05_color_per_triangle2/simple_pyramid2.html

Rendering each triangle with a different execution of the graphics pipeline.

Please use a browser that supports "canvas"
Animate
Draw the edges of the triangles using a gl.LINE_LOOP
Render the model as a "wireframe"
Render the global axes (x:red, y:green, z:blue)
Average time to render the model is 0.00000 milliseconds.
Show: Process information    Warnings    Errors
Open this webgl program in a new tab or window

A common convention in WebGL programs is to store the location of shader variables as properties of the “program” object. This is especially convenient if you have more than one shader program being used for rendering.

Linking a Buffer Object to an Attribute Variable

We now have two buffer objects to link variables to when we render the model. Lines 83-91 in the above demo performs the linkage:

// Activate and attach the model's vertex Buffer Object
gl.bindBuffer(gl.ARRAY_BUFFER, triangles_vertex_buffer_id);
gl.vertexAttribPointer(color_program.a_Vertex_location, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(color_program.a_Vertex_location);

// Activate and attach the model's color Buffer Object
gl.bindBuffer(gl.ARRAY_BUFFER, triangles_color_buffer_id);
gl.vertexAttribPointer(color_program.a_Color_location, 4, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(color_program.a_Color_location);

Notice that you have to make a buffer object active using the gl.bindBuffer() function before linking it to a variable with the gl.vertexAttribPointer() function.

Rendering

We can now render the entire model with a single call to gl.drawArrays() in line 95. Study the rendering function in the example above in lines 68-126.

It should be noted that we can no longer render the triangle edges as we did in the previous lesson. Why? The shader program now requires a color from a buffer object for each vertex. Using the shader program we have defined above, we could render the triangle edges by creating a 3rd buffer object and repeating the color black for each vertex. We could then connect the a_Color variable to this new buffer and render the edges as we did in the previous lesson. This is very wasteful of memory. Another option would be to have two separate shader programs: draw the faces with one shader program, activate a different shader program, and then render the edges. There are trade-offs for both options.

You can change the active shader program like this:

gl.useProgram(program);

Changing your active shader program changes the rendering context, which takes time, which slows down rendering. Therefore, you should switch between shader programs as few times as possible.

Summary

To use a different color for each triangle of a model, we had to modify the model’s definition, the shader programs, the buffer objects, and the rendering code. All of this code is interdependent. This makes code development challenging because you have to make many changes in many different places before you can test and verify your code. Here is a partial list of things you might verify as you debug a rendering program:

  • The shader program compiled and linked correctly.
  • The location of all shader program variables were retrieved correctly.
  • The data for an object buffer was created correctly.
  • All object buffers were created correctly.
  • The function that renders a model is called with the correct parameters.
  • The rendering function assigns all uniform variables an appropriate value.
  • The rendering function links all attribute variables to appropriate object buffers.
  • The gl.drawArrays() command is being called with the appropriate parameters.

Self-Assessments

    Q-342: Rendering a model by calling gl.drawArrays() for each individual triangle is a bad idea because…
  • the rendering is very slow.
  • Correct. You lose most the GPU's builtin efficiencies.
  • it doesn't work.
  • Incorrect. See the first WebGL program in this lesson for an example that works.
  • the rendering is too fast for real-time graphics.
  • Incorrect. It is not possible to render models too fast!
  • shader programs must always be executed on multiple triangles at one time.
  • Incorrect. A shader program can execute on a single vertex if your set the gl.drawArrays' count parameter to 1.

    Q-343: The second WebGL example program in this lesson stores a RGBA color value for each vertex of the model. The data for rendering the pyramid looks like the 1D array of values below. Why are the four color values for the four triangles each stored 3 times?

    // The color-per-vertex array
    [ 1,0,0,1,  1,0,0,1,  1,0,0,1,
      0,1,0,1,  0,1,0,1,  0,1,0,1,
      0,0,1,1,  0,0,1,1,  0,0,1,1,
      1,0,1,1,  1,0,1,1,  1,0,1,1 ]
    
  • Because the graphics pipeline processes vertices, and all data must be store in a per-vertex order.
  • Correct. The color values in the color object-buffer must match up one-to-one with the vertex data.
  • Because three is the magic number for all vertex data.
  • Incorrect. Three has no special meaning in WebGL programming.
  • Because both object-buffers for the pyramid model are required to have the same number of floats.
  • Incorrect. They are required to represent the same number of vertices, but the number of floats can be different. Notice that the vertex object-buffer holds 36 floats, while the color object-buffer holds 48 floats.
  • Because GPU memory is cheap and we can waste it without worry.
  • Incorrect. GPU memory tends to be large, but still a limited resource.
    Q-344: Can you render a scene using more than one shader program?
  • Yes, though it does slow down overall rendering speed.
  • Correct. Switching shader programs requires a "context switch" which does take a little time.
  • Yes, but it is not recommended.
  • Incorrect. Actually is very common because special rendering effects require special shader programs.
  • No.
  • Incorrect.
Next Section - 5.7 - Example 3: One Color per Vertex