WebGL Basics 2 – White 2D triangle

This part is longer than the first one and I hope that it will be the longest in this mini tutorial. WebGL is such that for the simplest rendering, there is a great amount of things to setup.

Introduction

The basic mechanism of hardware accelerated 3D rendering engines is that the graphics card achieves the computing intensive operations using specialized hardware and the main processor focuses on the remaining tasks such as managing user interactions. It is the same with WebGL, but with the particularity that the main processor controls everything through Javascript, which is not as quick as a compiled program would be.

The rendering of a static model in 3D must roughly achieve the following steps:

  1. Vertices are taken from a buffer and processed to move the model in the camera referential (rotation, translation, scaling) and to transform the 3D coordinates in 2D points on the viewable area (perspective projection).
  2. Faces (delimited by the vertices) are computed, i.e. non-visible areas are eliminated (back faces culling and Z buffering), and extremities of pixel lines belonging to faces are determined (rasterization).
  3. For each pixel line (called a fragment) the color of each pixel is computed.

In the OpenGL ES 2 scheme, on which WebGL is based, the first and third steps are fully programmable! It has the obvious advantages of flexibility and optimization (only the necessary operations are done) but it makes everything complicated for the newcomer. According to the OpenGL terminology, the "vertex shader" is the programmable part responsible for vertex transformations and the "fragment shader" the one that computes pixel colors. Both are compiled and linked to a "program", which is in turn attached to the WebGL context. The vertices are stored by the Javascript code in a "buffer", from where they are piped into the vertex buffer via an "attribute" variable (i.e. input variable of the vertex shader).

Fragment shader compilation

We begin with the HTML page started in the first part of this tutorial and continue in the start() function. The following lines of code show the creation and compilation of the fragment shader:

// Creates fragment shader (returns white color for any position)
var fshader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fshader, 'void main(void) {gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);}');
gl.compileShader(fshader);
if (!gl.getShaderParameter(fshader, gl.COMPILE_STATUS))
{alert("Error during fragment shader compilation:\n" + gl.getShaderInfoLog(fshader)); return;}

The compilation is triggered by the 3 WebGL functions createShader, shaderSource and compileShader. The source code of the shader is passed to shaderSource as a simple character string. The language used for shader programming is called GLSL ES (GL Shader Language for Embedded Systems). It features on a C-like syntax with specific data types and predefined variables. In our example, the shader is programmed with the following source code:

void main(void)
{
  gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
}

The function main() is called as often as necessary (maybe not for each pixel but for each vertex) such that the color of each pixel can be interpolated. Each time main() is called, the color at this position is returned in the predefined variable gl_FragColor. It is given as a vector with 4 components between 0.0 and 1.0: red, green, blue and alpha (transparency). In our example, the shader is simplistic as it produces only a constant color independent of the position. For more elaborated fragment shaders, some geometric data related to the model being rendered is necessary (more about this in a future post).

Vertex shader compilation

The vertex shader is created and compiled with the same functions as for the fragment shader:

// Creates vertex shader (converts 2D point position to coordinates)
var vshader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vshader, 'attribute vec2 ppos; void main(void)
  { gl_Position = vec4(ppos.x, ppos.y, 0.0, 1.0);}');
gl.compileShader(vshader);
if (!gl.getShaderParameter(vshader, gl.COMPILE_STATUS))
{alert('Error during vertex shader compilation:\n' + gl.getShaderInfoLog(vshader)); return;}

The code of the shader is given hereafter:

attribute vec2 ppos;
void main(void)
{
  gl_Position = vec4(ppos.x, ppos.y, 0.0, 1.0);
}

The shader transforms vertices in a very simple way: for each call of main(), the 2D position given as input in ppos is extended with a Z coordinate equal to 0.0 plus an additional component equal to 1.0, and is returned as a 4-components vector in the predefined variable gl_Position. This last component is due to the usage of "homogeneous coordinates" (more about this in a future post). The most important thing to note is that the input variable ppos is marked with the prefix "attribute". Any attribute variable is a link to the Javascript code. We will see below how to pipe vertices into the vertex shader, but before that additional data structures must be set up.

Linking to program and validation

Once the shaders are correctly compiled, they must be linked in a so-called program using the functions createProgram, attachShader and linkProgram. The code is straightforward:

// Creates program and links shaders to it
var program = gl.createProgram();
gl.attachShader(program, fshader);
gl.attachShader(program, vshader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS))
 {alert("Error during program linking:" + gl.getProgramInfoLog(program));return;}

After compilation, the code is "validated" and finally "used":

// Validates and uses program in the GL context
gl.validateProgram(program);
if (!gl.getProgramParameter(program, gl.VALIDATE_STATUS))
{alert("Error during program validation:\n" + gl.getProgramInfoLog(program));return;}
gl.useProgram(program);

I assume that there is a good reason to separate the different program/shader operations (attach, link, validate, use), maybe for performance reasons. As soon as several programs and contexts are used in the same page, the need for such steps will certainly appear clearer.

Attribute variable reference

Once the program is set up, it is possible to find the reference to the input "attribute" variable ppos, which is the interface between the Javascript code and the vertex shader code:

// Gets address of the input 'attribute' of the vertex shader
var vattrib = gl.getAttribLocation(program, 'ppos');
if(vattrib == -1)
{alert('Error during attribute address retrieval');return;}
gl.enableVertexAttribArray(vattrib);

Note that the returned reference is a simple integer value (a pointer to some memory area accessible from the vertex shader). This kind of low-level access mechanism, unusual and potentially unsecure in a high-level interpreted language like Javascript, is directly inherited from the (compiled) OpenGL API. According to the WebGL specification, the access to non-allowed areas must fail with the error "invalid operation", but in my opinion we can expect some new exploits based on that. In any case, this raw mechanism does not facilitate debugging.

Buffer filled with vertices

The next step is to define the object to be renderd. First, a buffer object is created with createBuffer and "bound" with bindBuffer(gl.ARRAY_BUFFER, buffer). The bind operation sets the buffer as being the current one (the next commands will use it without specifying it) and states that its type is array (and not indices).

// Initializes the vertex buffer and sets it as current one
var vbuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbuffer);

Once the buffer is there, the object is created. In our example, it is a simple 2D triangle. The xy coordinates are put sequentially in one array that is converted into a Float32Array object (with bufferData), and then the buffer is linked together with the vertex shader via the attribute variable (with vertexAttribPointer):

// Puts vertices to buffer and links it to attribute variable 'ppos'
var vertices = new Float32Array([0.0,0.5,-0.5,-0.5,0.5,-0.5]);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(vattrib, 2, gl.FLOAT, false, 0, 0);

The "target" gl.ARRAY_BUFFER is the same as the one uses in bindBuffer. In the same command, the "usage" set to gl.STATIC_DRAW indicates that the data in the buffer won’t be changed during the subsequent operations.

The next command vertexAttribPointer(vattrib, 2, gl.FLOAT, false, 0, 0) takes more parameters than one would expect intuitively. These parameters provide some flexibility for the vertex shader to read the values from the buffer:

  • vattrib is the attribute variable (determined using getAttribLocation)
  • 2 is the size, i.e. the number of values to be read from the buffer before the main() function of the shader is called (here we put a 2 as our vertices define a 2D object)
  • gl.FLOAT is the type of the data in the buffer
  • false is the "normalized" flag equal to false for floats (used only for integers to indicate that integer values represent values between -1 and 1)
  • 0 is the "stride" that represents the increment in bytes between two consecutive elements (i.e. tuples of values), if greater than 0
  • 0 (the last parameter) is the "offset" that defines the number of bytes skipped at the beginning of the buffer

In our examples, the values of stride and offset will be set to 0. As a side note, when stride and offset are used, their values must be multiples of the size of the data type considered (here floats).

Face rendering

Now that everything is set up, the object must be rendered with drawArrays:

// Draws the objext
gl.drawArrays(gl.TRIANGLES, 0, 3);
gl.flush()

The function drawArrays takes the current buffers (the "bound" ones) and the corresponding "enabled" attribute variables as input for the vertex shader. The first parameter of the function gives the "mode", i.e. the way the vertices returned by the vertex shader will be used during the draw operation. The value gl.TRIANGLES means vertices are used 3-by-3 to draw triangles. OpenGL ES and WebGL support other draw modes such as "triangle strips", "triangle fans", points and lines (more about that in a future post).

Finally, the second parameter of drawArrays gives the first element of the array to be drawn, and the third one the number of elements to be used (elements are here pairs of vertices, due to the number 2 given in vertexAttribPointer). The flush function is called to be sure that the operation are started (not mandatory).

Please have a look at the result online. The canvas part should look like this:

Viewable area between -1 and +1

It is now time to look precisely at the result. The coordinates of the triangle are defined between -1 and +1. But even if there is no more transformation than adding a Z coordinate equal to 0 to each 2D point (in the vertex shader), the triangle is not rendered on two pixels but stretched to the size of the canvas. Namely, the top spike of the triangle with a Y coordinate set to +0.5 is rendered at a distance of the top border equal to 1/4 of the complete area. As one can deduce, the default viewable area in WebGL is the cube centered on the origin (0, 0, 0) and with size 2, i.e. with faces parallel to axis pairs and at coordinates +1 or -1. With such a definition, if the canvas is not a square, the image is distorted (i.e. like with an incorrect aspect ratio).

Summary

The main points to remember (gl is the GL context object created from the canvas), first about the shader functions:

  • gl.createShader(gl.FRAGMENT_SHADER) returns a fragment shader object
  • gl.createShader(gl.VERTEX_SHADER) returns a vertex shader object
  • gl.shaderSource(shader, source-code-string) associates the source code to a shader object
  • gl.compileShader(shader) compiles the shader
  • In the vertex shader, the input data is piped through a global variable tagged as attribute, and the variable gl_Position receives the output vertex as a 4 components vector (x, y, z, 1.0)
  • In the fragment shader, the variable gl_FragColor receives the output color as a 4 components vector (r, g, b, alpha)

The program functions:

  • gl.createProgram() returns the program object
  • gl.attachShader(program, shader) attaches a shader object
  • gl.linkProgram(program) links everything together
  • gl.validateProgram(program) validates
  • gl.useProgram(program) uses finally the program

The buffer and attribute functions:

  • gl.getAttribLocation(program, attribute-name) returns the reference on the attribute variable
  • gl.enableAttribute(attrib) enables it
  • gl.createBuffer() returns a new buffer object
  • gl.bindBuffer(gl.ARRAY_BUFFER, buffer) sets it as current buffer
  • gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW) puts the values contained in the given Float32Array (usually vertex coordinates) in the current buffer
  • gl.vertexAttribPointer(attrib, size, gl.FLOAT, false, 0, 0) links the current buffer (containing floats) and the attribute reference (size is the number of values per element, e.g. 3 for 3D points)

Finally the draw function:

  • gl.drawArrays(gl.TRIANGLES, 0, count); draws the values contained in the current buffer (linked to a valid attribute) as triangles (count is the number of elements to read from the buffer)
  • The default viewable area is defined as a cube centered on (0.0.0) with faces at coordinates -1/+1
Advertisements

,

  1. #1 by Jason Slemons on September 28, 2011 - 02:01

    it does seem like a lot of work just for a white triangle. I’m guessing 3D rendering isn’t a whole lot harder…

    • #2 by blogoben on September 28, 2011 - 07:39

      WebGL is awfully complex to get started with (buffers, shaders for the simplest triangle)! Once you know the basic concepts and functions it becomes simpler but still not very easy as many utility functions are left to the developer. Look at my other posts!

  1. High quality WebGL learning resources : Smashinglabs
  2. WebGL Basics 3 – Rotating triangle | The Blog-o-Ben

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: