Rendering geometry in WebGL

The following are the steps that we will follow in this section to render an object in WebGL:

  1. First, we will define a geometry using JavaScript arrays.
  2. Second, we will create the respective WebGL buffers.
  3. Third, we will point a vertex shader attribute to the VBO that we created in the previous step to store vertex coordinates.
  4. Finally, we will use the IBO to perform the rendering.

Defining a geometry using JavaScript arrays

Let's see what we need to do to create a trapezoid. We need two JavaScript arrays: one for the vertices and one for the indices.

Defining a geometry using JavaScript arrays

As you can see from the previous screenshot, we have placed the coordinates sequentially in the vertex array and then we have indicated in the index array how these coordinates are used to draw the trapezoid. So, the first triangle is formed with the vertices having indices 0, 1, and 2; the second with the vertices having indices 1, 2, and 3; and finally, the third, with vertices having indices 2, 3, and 4. We will follow the same procedure for all possible geometries.

Creating WebGL buffers

Once we have created the JavaScript arrays that define the vertices and indices for our geometry, the next step consists of creating the respective WebGL buffers. Let's see how this works with a different example. In this case, we have a simple square on the x-y plane (z coordinates are zero for all four vertices):

var vertices = [-50.0, 50.0, 0.0,
-50.0,-50.0, 0.0,
50.0,-50.0, 0.0,
50.0, 50.0, 0.0];/* our JavaScript vertex array */
var myBuffer = gl.createBuffer(); /*gl is our WebGL Context*/

In the previous chapter, you may remember that WebGL operates as a state machine. Now, when myBuffer is made the currently bound WebGL buffer, this means that any subsequent buffer operation will be executed on this buffer until it is unbound or another buffer is made the current one with a bound call. We bind a buffer with the following instruction:

gl.bindBuffer(gl.ARRAY_BUFFER, myBuffer);

The first parameter is the type of buffer that we are creating. We have two options for this parameter:

  • gl.ARRAY_BUFFER: Vertex data
  • gl.ELEMENT_ARRAY_BUFFER: Index data

In the previous example, we are creating the buffer for vertex coordinates; therefore, we use ARRAY_BUFFER. For indices, the type ELEMENT_ARRAY_BUFFER is used.

Note

WebGL will always access the currently bound buffer looking for the data. Therefore, we should be careful and make sure that we have always bound a buffer before calling any other operation for geometry processing. If there is no buffer bound, then you will obtain the error INVALID_OPERATION

Once we have bound a buffer, we need to pass along its contents. We do this with the bufferData function:

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices),
gl.STATIC_DRAW);

In this example, the vertices variable is a JavaScript array that contains the vertex coordinates. WebGL does not accept JavaScript arrays directly as a parameter for the bufferData method. Instead, WebGL uses typed arrays, so that the buffer data can be processed in its native binary form with the objective of speeding up geometry processing performance.

Note

The specification for typed arrays can be found at: http://www.khronos.org/registry/typedarray/specs/latest/

The typed arrays used by WebGL are Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, UInt32Array, Float32Array, and Float64Array.

Note

Please observe that vertex coordinates can be float, but indices are always integer. Therefore, we will use Float32Array for VBOs and UInt16Array for IBOs throughout the examples of this book. These two types represent the largest typed arrays that you can use in WebGL per rendering call. The other types can be or cannot be present in your browser, as this specification is not yet final at the time of writing the book.

Since the indices support in WebGL is restricted to 16 bit integers, an index array can only be 65,535 elements in length. If you have a geometry that requires more indices, you will need to use several rendering calls. More about rendering calls will be seen later on in the Rendering section of this chapter.

Finally, it is a good practice to unbind the buffer. We can achieve that by calling the following instruction:

gl.bindBuffer(gl.ARRAY_BUFFER, null);

We will repeat the same calls described here for every WebGL buffer (VBO or IBO) that we will use.

Let's review what we have just learned with an example. We are going to code the initBuffers function to create the VBO and IBO for a cone. (You will find this function in the file named ch2_Cone.html):

var coneVBO = null; //Vertex Buffer Object
var coneIBO = null; //Index Buffer Object
function initBuffers() {
var vertices = []; //JavaScript Array that populates coneVBO
var indices = []; //JavaScript Array that populates coneIBO;
//Vertices that describe the geometry of a cone
vertices =[1.5, 0, 0,
-1.5, 1, 0,
-1.5, 0.809017, 0.587785,
-1.5, 0.309017, 0.951057,
-1.5, -0.309017, 0.951057,
-1.5, -0.809017, 0.587785,
-1.5, -1, 0.0,
-1.5, -0.809017, -0.587785,
-1.5, -0.309017, -0.951057,
-1.5, 0.309017, -0.951057,
-1.5, 0.809017, -0.587785];
//Indices that describe the geometry of a cone
indices = [0, 1, 2,
0, 2, 3,
0, 3, 4,
0, 4, 5,
0, 5, 6,
0, 6, 7,
0, 7, 8,
0, 8, 9,
0, 9, 10,
0, 10, 1];
coneVBO = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, coneVBO);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices),
gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
coneIBO = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, coneIBO);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices),
gl.STATIC_DRAW);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
}

If you want to see this scene in action, launch the file ch2_Cone.html in your HTML5 browser.

To summarize, for every buffer, we want to:

  • Create a new buffer
  • Bind it to make it the current buffer
  • Pass the buffer data using one of the typed arrays
  • Unbind the buffer

Operations to manipulate WebGL buffers

The operations to manipulate WebGL buffers are summarized in the following table:

Method

Description

var aBuffer = createBuffer(void)

Creates the aBuffer buffer

deleteBuffer(Object aBuffer)

Deletes the aBuffer buffer

bindBuffer(ulong target, Object buffer)

Binds a buffer object. The accepted values for target are:

  • ARRAY_BUFFER (for vertices)
  • ELEMENT_ARRAY_BUFFER (for indices)

bufferData(ulong target, Object data, ulong type)

The accepted values for target are:

  • ARRAY_BUFFER (for vertices)
  • ELEMENT_ARRAY_BUFFER(for indices)

The parameter type is a performance hint for WebGL. The accepted values for type are:

  • STATIC_DRAW: Data in the buffer will not be changed (specified once and used many times)

    DYNAMIC_DRAW: Data will be changed frequently (specified many times and used many times)

  • STREAM_DRAW: Data will change on every rendering cycle (specified once and used once)

Associating attributes to VBOs

Once the VBOs have been created, we associate these buffers to vertex shader attributes. Each vertex shader attribute will refer to one and only one buffer, depending on the correspondence that is established, as shown in the following diagram:

Associating attributes to VBOs

We can achieve this by following these steps:

  1. First, we bind a VBO.
  2. Next, we point an attribute to the currently bound VBO.
  3. Finally, we enable the attribute.

Let's take a look at the first step.

Binding a VBO

We already know how to do this:

gl.bindBuffer(gl.ARRAY_BUFFER, myBuffer);

where myBuffer is the buffer we want to map.

Pointing an attribute to the currently bound VBO

In the next chapter, we will learn to define vertex shader attributes. For now, let's assume that we have the aVertexPosition attribute and that it will represent vertex coordinates inside the vertex shader.

The WebGL function that allows pointing attributes to the currently bound VBOs is vertexAttribPointer. The following is its signature:

gl.vertexAttribPointer(Index,Size,Type,Norm,Stride,Offset);

Let us describe each parameter individually:

  • Index: An attribute's index that we are going to map the currently bound buffer to.
  • Size: Indicates the number of values per vertex that are stored in the currently bound buffer.
  • Type: Specifies the data type of the values stored in the current buffer. It is one of the following constants: FIXED, BYTE, UNSIGNED_BYTE, FLOAT, SHORT, or UNSIGNED_SHORT.
  • Norm: This parameter can be set to true or false. It handles numeric conversions that lie out of the scope of this introductory guide. For all practical effects, we will set this parameter to false.
  • Stride: If stride is zero, then we are indicating that elements are stored sequentially in the buffer.
  • Offset: The position in the buffer from which we will start reading values for the corresponding attribute. It is usually set to zero to indicate that we will start reading values from the first element of the buffer.

Note

vertexAttribPointer defines a pointer for reading information from the currently bound buffer. Remember that an error will be generated if there is no VBO currently bound.

Enabling the attribute

Finally, we just need to activate the vertex shader attribute. Following our example, we just need to add:

gl.enableVertexAttribArray (aVertexPosition);

The following diagram summarizes the mapping procedure:

Enabling the attribute

Rendering

Once we have defined our VBOs and we have mapped them to the corresponding vertex shader attributes, we are ready to render!

To do this, we use can use one of the two API functions: drawArrays or drawElements.

The drawArrays and drawElements functions

The functions drawArrays and drawElements are used for writing on the framebuffer.

drawArrays uses vertex data in the order in which it is defined in the buffer to create the geometry. In contrast, drawElements uses indices to access the vertex data buffers and create the geometry.

Both drawArrays and drawElements will only use enabled arrays. These are the vertex buffer objects that are mapped to active vertex shader attributes.

In our example, we only have one enabled array: the buffer that contains the vertex coordinates. However, in a more general scenario, we can have several enabled arrays. For instance, we can have arrays with information about vertex colors, vertex normals texture coordinates, and any other per-vertex data required by the application. In this case, each one of them would be mapped to an active vertex shader attribute.

Tip

Using several VBOs

In the next chapter, we will see how we use a vertex normal buffer in addition to vertex coordinates to create a lighting model for our geometry. In that scenario, we will have two active arrays: vertex coordinates and vertex normals.

Using drawArrays

We will call drawArrays when information about indices is not available. In most cases, drawArrays is used when the geometry is so simple that defining indices is an overkill; for instance, when we want to render a triangle or a rectangle. In that case, WebGL will create the geometry in the order in which the vertex coordinates are defined in the VBO. So if you have contiguous triangles (like in our trapezoid example), you will have to repeat these coordinates in the VBO.

If you need to repeat a lot of vertices to create geometry, probably drawArrays is not the best way to go. The more vertex data you duplicate, the more calls you will have on the vertex shader. This could reduce the overall application performance since the same vertices have to go through the pipeline several times. One for each time that they appear repeated in the respective VBO.

Using drawArrays

The signature for drawArrays is:

gl.drawArrays(Mode, First, Count)

Where:

  • Mode: Represents the type of primitive that we are going to render. Possible values for mode are: gl.POINTS, gl.LINE_STRIP, gl.LINE_LOOP, gl.LINES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN, and gl.TRIANGLES (more about this in the next section).
  • First: Specifies the starting element in the enabled arrays.
  • Count: The number of elements to be rendered.

Note

From the WebGL specification: "When drawArrays is called, it uses count sequential elements from each enabled array to construct a sequence of geometric primitives, beginning with the element first. Mode specifies what kinds of primitives are constructed and how the array elements construct those primitives."

Using drawElements

Unlike the previous case where no IBO was defined, drawElements allows us to use the IBO, to tell WebGL how to render the geometry. Remember that drawArrays uses VBOs. This means that the vertex shader will process repeated vertices as many times as they appear in the VBO. Contrastingly, drawElements uses indices. Therefore, vertices are processed just once, and can be used as many times as they are defined in the IBO. This feature reduces both the memory and processing required on the GPU.

Let's revisit the following diagram of this chapter:

Using drawElements

When we use drawElements, we need at least two buffers: a VBO and an IBO. The vertex shader will get executed on each vertex in the VBO and then the rendering pipeline will assemble the geometry into triangles using the IBO.

Note

When using drawElements, you need to make sure that the corresponding IBO is currently bound.

The signature for drawElements is:

gl.drawElements(Mode, Count, Type, Offset)

Where:

  • Mode: Represents the type of primitive that we are going to render. Possible values for mode are POINTS, LINE_STRIP, LINE_LOOP, LINES, TRIANGLE_STRIP, TRIANGLE_FAN, and TRIANGLES (more about this later on).
  • Count: Specifies the number of elements to be rendered.
  • Type: Specifies the type of the values in indices. Must be UNSIGNED_BYTE or UNSIGNED_SHORT, as we are handling indices (integer numbers).
  • Offset: Indicates which element in the buffer will be the starting point for rendering. It is usually the first element (zero value).

Note

WebGL inherits without any change this function from the OpenGL ES 2.0 specification. The following applies:

"When drawElements is called, it uses count sequential elements from an enabled array, starting at offset to construct a sequence of geometric primitives. Mode specifies what kinds of primitives are constructed and how the array elements construct these primitives. If more than one array is enabled, each is used."

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.23.30