CS424 Notes, 17 February 2012
- Polygonal Meshes
- In computer graphics, objects are often modeled as polyhedra -- solids whose sides are polygons. These are also called polygonal meshes, as in Blender's "mesh" objects. Today, we'll look at representing and rendering mesh objects in WebGL.
- Mesh Objects and gl.TRIANGLE_STRIP
- Consider a curved surface that is modeled by a polygonal mesh. (Of course, the mesh is only an approximation.) One common way to do this is to use triangle strips. Here are two triangle strips that might be part of such a mesh: For the strip shown with solid lines, the vertices would be specified in the order shown, using the gl.TRINAGLE_STRIP primitive.
- Triangle strips of this sort could used for Blender's UVSphere and for the sides of cones and cylinders.
- They would also be used for "terrain" grids like those you saw in Lab 5 and in mathematics for the graphs of functions of two variables (which are really the same thing as the terrain grids).
- gl.drawElements() and gl.ELEMENT_ARRAY_BUFFER
- In the picture above, note that a single vertex can be shared by several triangles. For example, vertex number 3 is a vertex of six triangles. If you draw the triangle strips in the usual way, using gl.drawArrays(), you will have to specify the same vertex coordinates six times in your vertex array buffer. If you are modeling a curved surface, so that you always use the same normal vector for that vertex, the same normal vector will occur six times in your arrays of normals. And the same applies to texture coordinates for a textured surface.
- To avoid this redundancy, OpenGL makes it possible to draw using indexed access to array buffers. That is, put the attributes for all the vertices into array buffers -- but only store the data for each vertex once. Each vertex can be identified its index, or position, in the list of vertices. The data for vertex 0 is the first item in each array buffer, the data for vetex 1 in the second item in each array buffer, and so on. Now, you can specify a primitive by listing the indices of its vertices. For example, you might say that the vertices of a triangle are vertices number 182, 347, and 26.
- Now, to draw a primitive, you just have to put the
indices for its vertices into a buffer. In this case,
the type of buffer that you want is of type gl.ELEMENT_ARRAY_BUFFER.
Recall that ordinary array buffers are of type gl.ARRAY_BUFFER.
For example, an array buffer to hold normal vectors might be used
as follows:
normalAttributeLocation = gl.getAttribLocation(prog, "normalCoords"); normalAttributeBuffer = gl.createBuffer(); gl.enableVertexAttribArray(normalAttributeLocation); gl.bindBuffer(gl.ARRAY_BUFFER,normalAttributeBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexNormals), gl.STATIC_DRAW); gl.vertexAttribPointer(normalAttributeLocation, 3, gl.FLOAT, false, 0, 0);
You would still have to do this for the vertex coordinates, normal vectors, and texture coordinates. The setup for the vertex index array is similar, but it uses gl.ELEMENT_ARRAY_BUFFER and there is no corresponding attribute in the shader program:indexBuffer = gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,indexBuffer); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
Here, indices would be an array containing the vertex indices. Note that indices are integers, so the indices are put in an array of type Uint16Array instead of Float32Array. - With this setup, you can use gl.drawElements() to draw primitives.
To use this, an element array buffer must first be bound, as above.
The function gl.drawElements() will read the indices of the vertices
of a primitive from the element array. It will then pull the attribute values
for the vertices from the array buffers that hold them, using the index
values from the element array to tell it which vertices to use. For
example,
gl.drawElements(gl.TRIANGLE_STRIP, indices.length, gl.UNSIGNED_SHORT, 0);
This assumes that indices was the array of indices and that they were loaded into the element array from a UInt16Array. The last parameter tells the starting point for the primitive in the element array buffer. In this case, we start from the beginning of the buffer and use the entire array. In general, you can have index data for several primitives in the buffer, and you can use different starting points and lengths for each primitive.
- Indexed Face Sets
- Triangle strips are a nice way to model some surfaces, but in general, a model is a collection of individual triangles, or possibly more general polygons. In the case of triangles, such a model can be drawn in one step, using a gl.TRIANGLES primitive. Blender's IcoShpere is an example of this type of model. A cube is an example of a model consisting of squares instead of triangles (although in WebGL, you would have to break each square into two triangles to model it).
- Such models are examples of indexed face sets. An indexed face
set consists of a numbered list of vertices and a list of faces, where each
face is specified as a list of indices into the vertex list. For general
indexed face sets, there is no requirement that all sides be triangles or
even that all faces have the same number of vertices. For example,
here is a pyramid with five vertices and four faces:
We might store the data for this indexed face set in JavaScript arrays:
vertexList = [ [1,0,1], [1,0,-1], [-1,0,-1], [-1,0,1], [0,1,0] ]; faceList = [ [4,3,0], [4,0,1], [4,1,2], [4,2,3], [0,3,2,1] ];
although this data structure is not well-suited to OpenGL as it stands. - For a triangular indexed face set, like the icosphere, the data
can be stored in format that is immediately usable in WebGL. For
example, here what the data for the model in my Teapot
example looks like:
var teapotData = { "vertexPositions" : [5.929688,4.125,0,5.387188,4.125,2.7475,5.2971,4.494141,... "vertexNormals" : [-0.966742,-0.255752,0,-0.893014,-0.256345,-0.369882,... "vertexTextureCoords" : [2,2,1.75,2,1.75,1.975,2,1.975,1.75,1.95,2,1.95,1.75,... "indices" : [0,1,2,2,3,0,3,2,4,4,5,3,5,4,6,6,7,5,7,6,8,8,9,7,1,10,11,11,2,1,... };
The model data is in JSON format, so it can be used easily in JavaScript. The vertexPositions array contains vertex coordinates, with three numbers per vertex. The vertexNormals array contains normal vectors, given as three numbers per vertex. The vertexTextureCoords array contains two numbers per vertex. And the indices array contains three integers for each triangle in the model, giving the index of each vertex of the triangle. Once this data has been put into three array buffers and one element array buffer, the teapot can be drawn with one call to gl.drawElements. - Note: For reasons that will become clearer later, the vertices of a polygon are generally listed in counterclockwise order, when when looking at the front of the polygon (and so in clockwise order, when looking at the back). This is how OpenGL tells which is the front face and which is the back face of the polygon.
- Discussion
- How to draw a cube?
- How to represent a triangular prism as an indexed face set?
- How to draw a cylinder?
- How to draw a dodecahedron?
- How to draw a general indexed face set?