Drawing and Transforming Triangles in WebGL
- Drawing Multiple Points
- Hello Triangle
- Moving, Rotating, and Scaling
- Summary
Chapter 2, “Your First Step with WebGL,” explained the basic approach to drawing WebGL graphics. You saw how to retrieve the WebGL context and clear a <canvas> in preparation for drawing your 2D/3DCG. You then explored the roles and features of the vertex and fragment shaders and how to actually draw graphics with them. With this basic structure in mind, you then constructed several sample programs that drew simple shapes composed of points on the screen.
This chapter builds on those basics by exploring how to draw more complex shapes and how to manipulate those shapes in 3D space. In particular, this chapter looks at
- The critical role of triangles in 3DCG and WebGL’s support for drawing triangles
- Using multiple triangles to draw other basic shapes
- Basic transformations that move, rotate, and scale triangles using simple equations
- How matrix operations make transformations simple
By the end of this chapter, you will have a comprehensive understanding of WebGL’s support for drawing basic shapes and how to use matrix operations to manipulate those shapes. Chapter 4, “More Transformations and Basic Animation,” then builds on this knowledge to explore simple animations.
Drawing Multiple Points
As you are probably aware, 3D models are actually made from a simple building block: the humble triangle. For example, looking at the frog in Figure 3.1, the figure on the right side shows the triangles used to make up the shape, and in particular the three vertices that make up one triangle of the head. So, although this game character has a complex shape, its basic components are the same as a simple one, except of course for many more triangles and their associated vertices. By using smaller and smaller triangles, and therefore more and more vertices, you can create more complex or smoother objects. Typically, a complex shape or game character will consist of tens of thousands of triangles and their associated vertices. Thus, multiple vertices used to make up triangles are pivotal for drawing 3D objects.
Figure 3.1 Complex characters are also constructed from multiple triangles
In this section, you explore the process of drawing shapes using multiple vertices. However, to keep things simple, you’ll continue to use 2D shapes, because the technique to deal with multiple vertices for a 2D shape is the same as dealing with them for a 3D object. Essentially, if you can master these techniques for 2D shapes, you can easily understand the examples in the rest of this book that use the same techniques for 3D objects.
As an example of handling multiple vertices, let’s create a program, MultiPoint, that draws three red points on the screen; remember, three points or vertices make up the triangle. Figure 3.2 shows a screenshot from Multipoint.
Figure 3.2 MultiPoint
In the previous chapter, you created a sample program, ClickedPoints, that drew multiple points based on mouse clicks. ClickedPoints stored the position of the points in a JavaScript array (g_points[]) and used the gl.drawArrays() method to draw each point (Listing 3.1). To draw multiple points, you used a loop that iterated through the array, drawing each point in turn by passing one vertex at a time to the shader.
Listing 3.1 Drawing Multiple Points as Shown in ClickedPoints.js (Chapter 2)
65 for(var i = 0; i<len; i+=2) { 66 // Pass the position of a point to a_Position variable 67 gl.vertexAttrib3f(a_Position, g_points[i], g_points[i+1], 0.0); 68 69 // Draw a point 70 gl.drawArrays(gl.POINTS, 0, 1); 71 }
Obviously, this method is useful only for single points. For shapes that use multiple vertices, you need a way to simultaneously pass multiple vertices to the vertex shader so that you can draw shapes constructed from multiple vertices, such as triangles, rectangles, and cubes.
WebGL provides a convenient way to pass multiple vertices and uses something called a buffer object to do so. A buffer object is a memory area that can store multiple vertices in the WebGL system. It is used both as a staging area for the vertex data and a way to simultaneously pass the vertices to a vertex shader.
Let’s examine a sample program before explaining the buffer object so you can get a feel for the processing flow.
Sample Program (MultiPoint.js)
The processing flowchart for MultiPoint.js (see Figure 3.3) is basically the same as for ClickedPoints.js (Listing 2.7) and ColoredPoints.js (Listing 2.8), which you saw in Chapter 2. The only difference is a new step, setting up the positions of vertices, which is added to the previous flow.
Figure 3.3 Processing flowchart for MultiPoints.js
This step is implemented at line 34, the function initVertexBuffers(), in Listing 3.2.
Listing 3.2 MultiPoint.js
1 // MultiPoint.js 2 // Vertex shader program 3 var VSHADER_SOURCE = 4 'attribute vec4 a_Position;\n' + 5 'void main() {\n' + 6 ' gl_Position = a_Position;\n' + 7 ' gl_PointSize = 10.0;\n' + 8 '}\n'; 9 10 // Fragment shader program ... 15 16 function main() { ... 20 // Get the rendering context for WebGL 21 var gl = getWebGLContext(canvas); ... 27 // Initialize shaders 28 if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) { ... 31 } 32 33 // Set the positions of vertices 34 var n = initVertexBuffers(gl); 35 if (n < 0) { 36 console.log('Failed to set the positions of the vertices'); 37 return; 38 } 39 40 // Set the color for clearing <canvas> ... 43 // Clear <canvas> ... 46 // Draw three points 47 gl.drawArrays(gl.POINTS, 0, n); // n is 3 48 } 49 50 function initVertexBuffers(gl) { 51 var vertices = new Float32Array([ 52 0.0, 0.5, -0.5, -0.5, 0.5, -0.5 53 ]); 54 var n = 3; // The number of vertices 55 56 // Create a buffer object 57 var vertexBuffer = gl.createBuffer(); 58 if (!vertexBuffer) { 59 console.log('Failed to create the buffer object '); 60 return -1; 61 } 62 63 // Bind the buffer object to target 64 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 65 // Write date into the buffer object 66 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); 67 68 var a_Position = gl.getAttribLocation(gl.program, 'a_Position'); ... 73 // Assign the buffer object to a_Position variable 74 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); 75 76 // Enable the assignment to a_Position variable 77 gl.enableVertexAttribArray(a_Position); 78 79 return n; 80 }
The new function initVertexBuffers() is defined at line 50 and used at line 34 to set up the vertex buffer object. The function stores multiple vertices in the buffer object and then completes the preparations for passing it to a vertex shader:
33 // Set the positions of vertices 34 var n = initVertexBuffers(gl);
The return value of this function is the number of vertices being drawn, stored in the variable n. Note that in case of error, n is negative.
As in the previous examples, the drawing operation is carried out using a single call to gl.drawArrays() at Line 48. This is similar to ClickedPoints.js except that n is passed as the third argument of gl.drawArrays() rather than the value 1:
46 // Draw three points 47 gl.drawArrays(gl.POINTS, 0, n); // n is 3
Because you are using a buffer object to pass multiple vertices to a vertex shader in initVertexBuffers(), you need to specify the number of vertices in the object as the third parameter of gl.drawArrays() so that WebGL then knows to draw a shape using all the vertices in the buffer object.
Using Buffer Objects
As indicated earlier, a buffer object is a mechanism provided by the WebGL system that provides a memory area allocated in the system (see Figure 3.4) that holds the vertices you want to draw. By creating a buffer object and then writing the vertices to the object, you can pass multiple vertices to a vertex shader through one of its attribute variables.
Figure 3.4 Passing multiple vertices to a vertex shader by using a buffer object
In the sample program, the data (vertex coordinates) written into a buffer object is defined as a special JavaScript array (Float32Array) as follows. We will explain this special array in detail later, but for now you can think of it as a normal array:
51 var vertices = new Float32Array([ 52 0.0, 0.5, -0.5, -0.5, 0.5, -0.5 53 ]);
There are five steps needed to pass multiple data values to a vertex shader through a buffer object. Because WebGL uses a similar approach when dealing with other objects such as texture objects (Chapter 4) and framebuffer objects (Chapter 8, “Lighting Objects”), let’s explore these in detail so you will be able to apply the knowledge later:
- Create a buffer object (gl.createBuffer()).
- Bind the buffer object to a target (gl.bindBuffer()).
- Write data into the buffer object (gl.bufferData()).
- Assign the buffer object to an attribute variable (gl.vertexAttribPointer()).
- Enable assignment (gl.enableVertexAttribArray()).
Figure 3.5 illustrates the five steps.
Figure 3.5 The five steps to pass multiple data values to a vertex shader using a buffer object
The code performing the steps in the sample program in Listing 3.2 is as follows:
56 // Create a buffer object <- (1) 57 var vertexBuffer = gl.createBuffer(); 58 if (!vertexBuffer) { 59 console.log('Failed to create a buffer object'); 60 return -1; 61 } 62 63 // Bind the buffer object to a target <- (2) 64 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); 65 // Write date into the buffer object <- (3) 66 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); 67 68 var a_Position = gl.getAttribLocation(gl.program, 'a_Position'); ... 73 // Assign the buffer object to a_Position variable <- (4) 74 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0); 75 76 // Enable the assignment to a_Position variable <- (5) 77 gl.enableVertexAttribArray(a_Position);
Let’s start with the first three steps (1–3), from creating a buffer object to writing data (vertex coordinates in this example) to the buffer, explaining the methods used within each step.
Create a Buffer Object (gl.createBuffer())
Before you can use a buffer object, you obviously need to create the buffer object. This is the first step, and it’s carried out at line 57:
57 var vertexBuffer = gl.createBuffer();
You use the gl.createBuffer() method to create a buffer object within the WebGL system. Figure 3.6 shows the internal state of the WebGL system. The upper part of the figure shows the state before executing the method, and the lower part is after execution. As you can see, when the method is executed, it results in a single buffer object being created in the WebGL system. The keywords gl.ARRAY_BUFFER and gl.ELEMENT_ARRAY_BUFFER in the figure will be explained in the next section, so you can ignore them for now.
Figure 3.6 Create a buffer object
The following shows the specification of gl.createBuffer().
gl.createBuffer () |
||
Create a buffer object. |
||
Return value |
non-null |
The newly created buffer object. |
|
null |
Failed to create a buffer object. |
Errors |
None |
|
The corresponding method gl.deleteBuffer() deletes the buffer object created by gl.createBuffer().
gl.deleteBuffer (buffer) |
||
Delete the buffer object specified by buffer. |
||
Parameters |
buffer |
Specifies the buffer object to be deleted. |
Return Value |
None |
|
Errors |
None |
|
Bind a Buffer Object to a Target (gl.bindBuffer())
After creating a buffer object, the second step is to bind it to a “target.” The target tells WebGL what type of data the buffer object contains, allowing it to deal with the contents correctly. This binding process is carried out at line 64 as follows:
64 gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
The specification of gl.bindBuffer() is as follows.
gl.bindBuffer(target, buffer) |
||
Enable the buffer object specified by buffer and bind it to the target. |
||
Parameters |
Target can be one of the following: |
|
|
gl.ARRAY_BUFFER |
Specifies that the buffer object contains vertex data. |
|
gl.ELEMENT_ARRAY_BUFFER |
Specifies that the buffer object contains index values pointing to vertex data. (See Chapter 6, “The OpenGL ES Shading Language [GLSL ES].) |
|
buffer |
Specifies the buffer object created by a previous call to gl.createBuffer(). |
|
|
When null is specified, binding to the target is disabled. |
Return Value |
None |
|
Errors |
INVALID_ENUM |
target is none of the above values. In this case, the current binding is maintained. |
In the sample program in this section, gl.ARRAY_BUFFER is specified as the target to store vertex data (positions) in the buffer object. After executing line 64, the internal state in the WebGL system changes, as shown in Figure 3.7.
Figure 3.7 Bind a buffer object to a target
The next step is to write data into the buffer object. Note that because you won’t be using the gl.ELEMENT_ARRAY_BUFFER until Chapter 6, it’ll be removed from the following figures for clarity.
Write Data into a Buffer Object (gl.bufferData())
Step 3 allocates storage and writes data to the buffer. You use gl.bufferData() to do this, as shown at line 66:
66 gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
This method writes the data specified by the second parameter (vertices) into the buffer object bound to the first parameter (gl.ARRAY_BUFFER). After executing line 66, the internal state of the WebGL system changes, as shown in Figure 3.8.
Figure 3.8 Allocate storage and write data into a buffer object
You can see in this figure that the vertex data defined in your JavaScript program is written to the buffer object bound to gl.ARRAY_BUFFER. The following table shows the specification of gl.bufferData().
gl.bufferData(target, data, usage) |
|||
Allocate storage and write the data specified by data to the buffer object bound to target. | |||
Parameters |
target |
Specifies gl.ARRAY_BUFFER or gl.ELEMENT_ARRAY_BUFFER. |
|
|
data |
Specifies the data to be written to the buffer object (typed array; see the next section). |
|
|
usage |
Specifies a hint about how the program is going to use the data stored in the buffer object. This hint helps WebGL optimize performance but will not stop your program from working if you get it wrong. |
|
|
|
gl.STATIC_DRAW |
The buffer object data will be specified once and used many times to draw shapes. |
|
|
gl.STREAM_DRAW |
The buffer object data will be specified once and used a few times to draw shapes. |
|
|
gl.DYNAMIC_DRAW |
The buffer object data will be specified repeatedly and used many times to draw shapes. |
Return value |
None |
|
|
Errors |
INVALID_ENUM |
target is none of the preceding constants |
Now, let us examine what data is passed to the buffer object using gl.bufferData(). This method uses the special array vertices mentioned earlier to pass data to the vertex shader. The array is created at line 51 using the new operator with the data arranged as <x coordinate and y coordinate of the first vertex>, <x coordinate and y coordinate of the second vertex>, and so on:
51 var vertices = new Float32Array([ 52 0.0, 0.5, -0.5, -0.5, 0.5, -0.5 53 ]); 54 var n = 3; // The number of vertices
As you can see in the preceding code snippet, you are using the Float32Array object instead of the more usual JavaScript Array object to store the data. This is because the standard array in JavaScript is a general-purpose data structure able to hold both numeric data and strings but isn’t optimized for large quantities of data of the same type, such as vertices. To address this issue, the typed array, of which one example is Float32Array, has been introduced.
Typed Arrays
WebGL often deals with large quantities of data of the same type, such as vertex coordinates and colors, for drawing 3D objects. For optimization purposes, a special type of array (typed array) has been introduced for each data type. Because the type of data in the array is known in advance, it can be handled efficiently.
Float32Array at line 51 is an example of a typed array and is generally used to store vertex coordinates or colors. It’s important to remember that a typed array is expected by WebGL and is needed for many operations, such as the second parameter data of gl.bufferData().
Table 3.1 shows the different typed arrays available. The third column shows the corresponding data type in C as a reference for those of you familiar with the C language.
Table 3.1 Typed Arrays Used in WebGL
Typed Array |
Number of Bytes per Element |
Description (C Types) |
Int8Array |
1 |
8-bit signed integer (signed char) |
Uint8Array |
1 |
8-bit unsigned integer (unsigned char) |
Int16Array |
2 |
16-bit signed integer (signed short) |
Uint16Array |
2 |
16-bit unsigned integer (unsigned short) |
Int32Array |
4 |
32-bit signed integer (signed int) |
Uint32Array |
4 |
32-bit unsigned integer (unsigned int) |
Float32Array |
4 |
32-bit floating point number (float) |
Float64Array |
8 |
64-bit floating point number (double) |
Like JavaScript, these typed arrays have a set of methods, a property, and a constant available that are shown in Table 3.2. Note that, unlike the standard Array object in JavaScript, the methods push() and pop() are not supported.
Table 3.2 Methods, Property, Constant of Typed Arrays
Methods, Properties, and Constants |
Description |
get(index) |
Get the index-th element |
set(index, value) |
Set value to the index-th element |
set(array, offset) |
Set the elements of array from offset-th element |
length |
The length of the array |
BYTES_PER_ELEMENT |
The number of bytes per element in the array |
Just like standard arrays, the new operator creates a typed array and is passed the array data. For example, to create Float32Array vertices, you could pass the array [0.0, 0.5, -0.5, -0.5, 0.5, -0.5], which represents a set of vertices. Note that the only way to create a typed array is by using the new operator. Unlike the Array object, the [] operator is not supported:
51 var vertices = new Float32Array([ 52 0.0, 0.5, -0.5, -0.5, 0.5, -0.5 53 ]);
In addition, just like a normal JavaScript array, an empty typed array can be created by specifying the number of elements of the array as an argument. For example:
var vertices = new Float32Array(4);
With that, you’ve completed the first three steps of the process to set up and use a buffer (that is, creating a buffer object in the WebGL system, binding the buffer object to a target, and then writing data into the buffer object). Let’s now look at how to actually use the buffer, which takes place in steps 4 and 5 of the process.
Assign the Buffer Object to an Attribute Variable (gl.vertexAttribPointer())
As explained in Chapter 2, you can use gl.vertexAttrib[1234]f() to assign data to an attribute variable. However, these methods can only be used to assign a single data value to an attribute variable. What you need here is a way to assign an array of values—the vertices in this case—to an attribute variable.
gl.vertexAttribPointer() solves this problem and can be used to assign a buffer object (actually a reference or handle to the buffer object) to an attribute variable. This can be seen at line 74 when you assign a buffer object to the attribute variable a_Position:
74 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
The specification of gl.vertexAttribPointer() is as follows.
gl.vertexAttribPointer(location, size, type, normalized, stride, offset) |
||||
Assign the buffer object bound to gl.ARRAY_BUFFER to the attribute variable specified by location. | ||||
Parameters |
location |
Specifies the storage location of an attribute variable. |
||
|
size |
Specifies the number of components per vertex in the buffer object (valid values are 1 to 4). If size is less than the number of components required by the attribute variable, the missing components are automatically supplied just like gl.vertexAttrib[1234]f(). |
||
|
|
For example, if size is 1, the second and third components will be set to 0, and the fourth component will be set to 1. |
||
|
type |
Specifies the data format using one of the following: |
||
|
|
gl.UNSIGNED_BYTE |
unsigned byte |
for Uint8Array |
|
|
gl.SHORT |
signed short integer |
for Int16Array |
|
|
gl.UNSIGNED_SHORT |
unsigned short integer |
for Uint16Array |
|
|
gl.INT |
signed integer |
for Int32Array |
|
|
gl.UNSIGNED_INT |
unsigned integer |
for Uint32Array |
|
|
gl.FLOAT |
floating point number |
for Float32Array |
|
normalized |
Either true or false to indicate whether nonfloating data should be normalized to [0, 1] or [–1, 1]. |
||
|
stride |
Specifies the number of bytes between different vertex data elements, or zero for default stride (see Chapter 4). |
||
|
offset |
Specifies the offset (in bytes) in a buffer object to indicate what number-th byte the vertex data is stored from. If the data is stored from the beginning, offset is 0. |
||
Return value |
None |
|||
Errors |
INVALID_OPERATION |
There is no current program object. |
||
|
INVALID_VALUE |
location is greater than or equal to the maximum number of attribute variables (8, by default). stride or offset is a negative value. |
So, after executing this fourth step, the preparations are nearly completed in the WebGL system for using the buffer object at the attribute variable specified by location. As you can see in Figure 3.9, although the buffer object has been assigned to the attribute variable, WebGL requires a final step to “enable” the assignment and make the final connection.
Figure 3.9 Assign a buffer object to an attribute variable
The fifth and final step is to enable the assignment of the buffer object to the attribute variable.
Enable the Assignment to an Attribute Variable (gl.enableVertexAttribArray())
To make it possible to access a buffer object in a vertex shader, we need to enable the assignment of the buffer object to an attribute variable by using gl.enableVertexAttribArray() as shown in line 77:
77 gl.enableVertexAttribArray(a_Position);
The following shows the specification of gl.enableVertexAttribArray(). Note that we are using the method to handle a buffer even though the method name suggests it’s only for use with “vertex arrays.” This is not a problem and is simply a legacy from OpenGL.
gl.enableVertexAttribArray(location) |
||
Enable the assignment of a buffer object to the attribute variable specified by location. | ||
Parameters |
location |
Specifies the storage location of an attribute variable. |
Return value |
None |
|
Errors |
INVALID_VALUE |
location is greater than or equal to the maximum number of attribute variables (8 by default). |
When you execute gl.enableVertexAttribArray() specifying an attribute variable that has been assigned a buffer object, the assignment is enabled, and the unconnected line is connected as shown in Figure 3.10.
Figure 3.10 Enable the assignment of a buffer object to an attribute variable
You can also break this assignment (disable it) using the method gl.disableVertexAttribArray().
gl.disableVertexAttribArray(location) | ||
Disable the assignment of a buffer object to the attribute variable specified by location. | ||
Parameters |
location |
Specifies the storage location of an attribute variable. |
Return Value |
None |
|
Errors |
INVALID_VALUE |
location is greater than or equal to the maximum number of attribute variables (8 by default). |
Now, everything is set! All you need to do is run the vertex shader, which draws the points using the vertex coordinates specified in the buffer object. As in Chapter 2, you will use the method gl.drawArrays, but because you are drawing multiple points, you will actually use the second and third parameters of gl.drawArrays().
Note that after enabling the assignment, you can no longer use gl.vertexAttrib[1234]f() to assign data to the attribute variable. You have to explicitly disable the assignment of a buffer object. You can’t use both methods simultaneously.
The Second and Third Parameters of gl.drawArrays()
Before entering into a detailed explanation of these parameters, let’s take a look at the specification of gl.drawArrays() that was introduced in Chapter 2. Following is a recap of the method with only the relevant parts of the specification shown.
gl.drawArrays(mode, first, count) |
||
Execute a vertex shader to draw shapes specified by the mode parameter. | ||
Parameters |
mode |
Specifies the type of shape to be drawn. The following symbolic constants are accepted: gl.POINTS, gl.LINES, gl.LINE_STRIP, gl.LINE_LOOP, gl.TRIANGLES, gl.TRIANGLE_STRIP, and gl.TRIANGLE_FAN. |
|
first |
Specifies what number-th vertex is used to draw from (integer). |
|
count |
Specifies the number of vertices to be used (integer). |
In the sample program this method is used as follows:
47 gl.drawArrays(gl.POINTS, 0, n); // n is 3
As in the previous examples, because you are simply drawing three points, the first parameter is still gl.POINTS. The second parameter first is set to 0 because you want to draw from the first coordinate in the buffer. The third parameter count is set to 3 because you want to draw three points (in line 47, n is 3).
When your program runs line 47, it actually causes the vertex shader to be executed count (three) times, sequentially passing the vertex coordinates stored in the buffer object via the attribute variable into the shader (Figure 3.11).
Note that for each execution of the vertex shader, 0.0 and 1.0 are automatically supplied to the z and w components of a_Position because a_Position requires four components (vec4) and you are supplying only two.
Remember that at line 74, the second parameter size of gl.vertexAttribPointer() is set to 2. As just discussed, the second parameter indicates how many coordinates per vertex are specified in the buffer object and, because you are only specifying the x and y coordinates in the buffer, you set the size value to 2:
74 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
After drawing all points, the content of the color buffer is automatically displayed in the browser (bottom of Figure 3.11), resulting in our three red points, as shown in Figure 3.2.
Figure 3.11 How the data in a buffer object is passed to a vertex shader during execution
Experimenting with the Sample Program
Let’s experiment with the sample program to better understand how gl.drawArrays() works by modifying the second and third parameters. First, let’s specify 1 as the third argument for count at line 47 instead of our variable n (set to 3) as follows:
47 gl.drawArrays(gl.POINTS, 0, 1);
In this case, the vertex shader is executed only once, and a single point is drawn using the first vertex in the buffer object.
If you now specify 1 as the second argument, only the second vertex is used to draw a point. This is because you are telling WebGL that you want to start drawing from the second vertex and you only want to draw one vertex. So again, you will see only a single point, although this time it is the second vertex coordinates that are shown in the browser:
47 gl.drawArrays(gl.POINTS, 1, 1);
This gives you a quick feel for the role of the parameters first and count. However, what will be happen if you change the first parameter mode? The next section explores the first parameter in more detail.