CS424 Notes, 25 January 2012
- About GPUs
- Your computer has a GPU (Graphics Processing Unit) that is separate from your CPU. The GPU is optimized for doing graphics.
- Commands and data from the CPU (running JavaScript) have to be sent over the system bus to the GPU, which can be a performance bottleneck.
- The GPU has some number of multiprocessing units that allow it to do graphics computations in parallel. Your vertex and fragment shaders are run on these processors.
- The GPU can store: shader programs, data in "data buffers", and textures. It can store several of each of these (possibly a lot).
- WebGL has commands for allocating programs, data buffers, and textures on the GPU, and for sending information to the GPU. Shader programs have access to the data buffers and the textures. Data buffers can hold attributes, for example.
- (In a given implementation, some of this stuff might not actually be on the GPU, but things are fastest when it is.)
- (In OpenGL, the GPU is considered to be a "server" and the program running on the CPU is a "client". With OpenGL, the server and the client can actually be on different computers, with all the client/server communication going over the network.)
- Creating Shader Programs
- Shader programs are created from two strings giving the vertex shader source code and the fragment shader short code. There are a number of steps, but they are pretty much always the same. See the createProgram function in jan23-minimal-shaders.html.
- var prog = gl.createProgram(); doesn't really create the program. It just gives you a handle, prog, that essentially identifies storage space for the program's resources on the GPU. You still have to construct the actual program (by attaching and linking the two shaders).
- If an error occurs during compilation and linking, it's possible to discover that fact and get the error messages. Note that OpenGL does not generally crash when there is an error. It just fails silently and things stop working. But it's always possible to check whether an error has occurred.
- If prog is a handle to a shader program, you say gl.useProgram(prog) to tell the GPU to use that program. The program is used whenever you draw a primitive. You can have several programs and switch among them. In the sample program, this is done in init().
- Using Uniform Variables
- If your shader program contains any uniform variables, you will have to provide values for them. This involves two things: Get a handle to the uniform location in the program. Send a value to the GPU to be stored in that location.
- gl.getUniformLocation( prog, uniformName ) is used to get a handle to the location of the uniform named uniformName in the shader program prog. The uniformName is a string that gives the name used for the uniform variable in the shader source code of the program.
- For setting the value of a uniform, there is a whole set of functions. Which one you use depends on the type of value that you are providing and whether you are providing values as separate parameters or in an array.
- For example if the type of the uniform is vec3 and the handle to its
location, as returned by gl.getUniformLocation, is uniformLoc,
then you could do either of the following:
gl.uniform3f( uniformLoc, 1, 2.7, 3.1 ); gl.uniform3fv( uniformLoc, [ 1, 2.7, 3.1 ]);
In the second form, the three numbers for the vector are in an array. You'd be more likely to use this form if the array were the value of a JavaScript variable. - The 3 in gl.uniform3f and gl.uniform3fv says that you are providing 3 numbers. The f means that the numbers are to be interpreted as floats. The v in the second form means that you are providing the numbers in an array (the v actually stands for vector). Similarly, there are functions gl.uniform2i, gl.uniform4fv, gl.uniform1f, and so on. An i in the name stands for int. bool values can be set using int. The number of numbers can be 1 through 4. This type of naming is common in OpenGL.
- Example: A shader program declares uniform vec4 color. The WebGL program
would likely declare a global variable such as var colorLoc to represent
the uniform location. It would probably call
colorLoc = getUniformLocation( prog, "color" );
during initialization (since the location won't change). Before drawing a primitive, it might saygl.uniform4f( colorLoc, 0, 1, 0, 1 );
to make the primitive green. Or if primitiveColor is a JavaScript variable that holds the color array, it could saygl.uniform4fv( colorLoc, primitiveColor );
Note that the value of a uniform is part of the OpenGL state: Once you set the value, it stays set until you change it. - Values for uniforms of matrix type are specified using gl.uniformMatrix2fv, gl.uniformMatrix3fv, and gl.uniformMatrix4fv. More on this later.
- Drawing Primitives
- There are only two functions for drawing primitives. For now, we only consider
gl.drawArrays( primitiveType, startingVertexNumber, numberOfVertices )
This function assumes that the data for attributes has already been set up in "arrays" (actually in data buffers on the GPU). This function tells the GPU to draw the primitive, taking the attribute value for each vertex from the arrays. - primitiveType is one of the constants specifying a WebGL primitive, such as gl.LINE_LOOP or gl.TRIANGLE_FAN.
- numberOfVertices specifies how many vertices the primitive has. The attribute values for the specified number of vertices must be lined up, one after the other, in the arrays.
- You can have more data in the arrays than you need for the primitive. For one thing, this makes it possible to store data for several primitives in one set of arrays. startingVetexNumber tells where to start reading data from the arrays. The first startingVertexNumber items in each array are skipped. Often, it is zero.
- There are only two functions for drawing primitives. For now, we only consider
- Setting up the attribute "arrays"
- Unfortunately, this is rather complicated...
- As with uniforms, you need a handle to the attribute location
in the shader program, prog:
attribLoc = gl.getAttributeLocation( prog, attributeName );
- You need a place on the GPU where you can store the data. This
place is called a buffer (or data buffer). You need to
allocate the buffer (just like you allocated prog):
attribBuffer = gl.createBuffer();
- You need to tell the GPU to use a buffer as the source for
attribute values for the attribute with location attribLoc:
gl.enableVertexAttribArray( attribLoc );
- The following two commands (gl.bufferData and gl.vertexAttribPointer)
refer to some data buffer, but they don't say which data buffer they are referring
to. This means that before you can use them, you must "bind" a buffer by calling
gl.bindBuffer( attribBuffer );
This tells WebGL, "the buffer that I am working on is attribBuffer." Commands that act on a buffer, but don't specify the buffer explicitely, will use attribBuffer. The bound buffer is part of the OpenGL state. If you are only using one buffer, you could call gl.bindBuffer once in an init() function. If you have several buffers, call gl.bindBuffer whenever you want to switch the buffer that you are working on. This type of "bind" operation is pretty common in OpenGL, so get used to it. - To load data into the buffer -- the one that is currently bound -- use
gl.bufferData. The data is an array of numbers. If the data
is in a standard JavaScript array named attribData, the call
could be:
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array(attribData), gl.STREAM_DRAW );
The first parameter will always be gl.ARRAY_BUFFER for the type of drawing that we are doing. The second parameter is a so-called "typed array" containing the actual data; more on that later. The third parameter is one of three constants: gl.STREAM_DRAW, gl.STATIC_DRAW, or gl.DYNAMIC_DRAW. The value tells OpenGL how the data will be used and might affect how it is stored. Basically, for now, use gl.STREAM_DATA to mean that you are going to draw one primitive with the data, then discard it. use gl.STATIC_DRAW to mean that the data will be reused a bunch of times. (With STREAM_DRAW, it is possible that the data might never be stored in the GPU at all -- it might just be "streamed" to the GPU as it is used.) - Finally, you have to tell WebGL how to interpret the data in the buffer.
What type of data is it (integer or floating point, for example)? How many
numbers are there per vertex? These questions (and a few more) are answered
by gl.vertexAttribPointer. Here is an example, in pretty much the
only form we will use for now:
gl.vertexAttribPointer( attribLoc, 2, gl.FLOAT, false, 0, 0 );
Here, 2 is the number of numbers per vertex, and gl.FLOAT specifies that the data consists of floating point numbers. This would likely be for a vec2 attribute. For a vec3 attribute, you would provide 3 as the second parameter. Remember that this function is talking about the data buffer that is currently bound using gl.bindBuffer. - Here is all the code used in jan23-minimal-shaders.html
for drawing a triangle, though you won't find all these statements in the same
part of the program:
var vertexAttributeLocation; // The location of the attribute. var vertexAttributeBuffer; // Handle to buffer that holds the attribute data. vertexAttributeLocation = gl.getAttribLocation(prog, "vertexCoords"); vertexAttributeBuffer = gl.createBuffer(); gl.enableVertexAttribArray(vertexAttributeLocation); var vertexData = [ -0.7,-0.6, 0.7,-0.6, 0,0.8 ]; gl.bindBuffer(gl.ARRAY_BUFFER,vertexAttributeBuffer); . gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STREAM_DRAW); gl.vertexAttribPointer(vertexAttributeLocation, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(gl.TRIANGLES, 0, 3);