CS424 Notes, 1 February 2012
- Transformations in WebGL
- We start with left-over sections from Monday's notes on Modeling Transformations in WebGL and World-to-clip Transformations.
- Hierarchical Modeling
- A modeling transform places an object in a "scene," but the scene can actually be another model, of a more complex object. Another modeling transform can then be applied to the complex object to place it into a scene -- or into an even more complex object.
- In this way, one can build up a complex scene in a hierarchical fashion: At the bottom level, there are basic objects that are described directly in terms of their vertices. These basic objects can be combined to make more complex objects, which can then be combined to make even more complex objects, and so on. This is called hierarchical modeling.
- It is important to note that you can use multiple copies of an object simply by applying different modeling transforms to the same object. The same set of vertices is generated in each case; the vertices show up at different positions in the scene because of the different modeling transforms.
- A Stack of Transforms
- Suppose that an object obj is placed into a complex object using a modeling transform T, and the complex object is then placed into a scene using its own modeling transform S. The overall transform that is applied to obj is then the composed transform ST, that is, T followed by S. The vertices of obj are first transformed by T to place them into the complex object, then further transformed by S to place then into the scene.
- Suppose that win is a "window" and that we want to include a copy of win into a house (along with some other simple objects). The house in turn is placed into a scene. Suppose that T transforms the window into the house, and S transforms the house into the scene. The overall transform that is applied to the window is ST while the transform applied to the rest of the house is T.
- Let's consider how the house would be drawn:
1. Apply the transform T 2. Draw some of the house (e.g. body and roof) 3. Apply the transform S (i.e. add it on to T) 4. Draw the window (with overall transform ST) 5. Go back to using transform T instead of ST 6. Draw the rest of the house
Note that to do step 5, we need some way to remember T while we are drawing the window. Furthermore, in hierarchical graphics, there can be more levels of object nesting. For example, the window might have a component (say, a "pane"), with a transform R that places the pane into the window. While the pane is being drawn, the overal transform is STR, so the process becomes1. Apply the transform T 2. Draw some of the house (e.g. body and roof) 3. Apply the transform S (i.e. add it on to T) 4. Draw the window (with overall transform ST): 4a. Draw some of the window 4b. Apply the transform R (i.e. add it on to ST) 4c. Draw the pane (with overall transform STR) 4d. Go back to using the transform ST instead of STR 4e. Draw the rest of the window 5. Go back to using transform T instead of ST 6. Draw the rest of the house
This can go on to any level of nesting, so we might have to remember any number of transforms. How can we do this? - The solution is to use a stack of transforms. Before drawing a sub-object, push the current transform onto the stack. After finishing with the sub-object, pop the transform from the stack. If the sub-object contains sub-sub-objects, then additional transforms will be pushed and popped while the sub-object is being drawn, but in the end, the stack will be in the same state as it was before drawing the sub-object. (Each push must be matched by a pop.)
- In terms of the stack,
the process of drawing the house becomes:
1. Apply the transform T 2. Draw some of the house (e.g. body and roof) 3. PUSH current transform (T) Apply the transform S (i.e. add it on to T) 4. Draw the window (with overall transform ST): 4a. Draw some of the window 4b. PUSH the current transform (ST) Apply the transform R (i.e. add it on to ST) 4c. Draw the pane (with overall transform STR) 4d. POP the transform (restoring it to ST) 4e. Draw the rest of the window 5. POP the transform (restoring it to T) 6. Draw the rest of the house
(You will probably need some experience and thought before you completely understand this.)
- A Hierarchical Example
- The sample program hierarchical2d-example.html shows a simple scene constructed using hierarchical graphics. (Actually, it's an animation. The animation is done by changing the modeling transforms from frame to frame. The same vertex coordinates are used in every frame.)
- This picture illustrates the hierarchy of objects in that example:
There are just 6 basic objects, shown at the bottom of the picture. These are the only objects for which the vertices are specified directly. Each arrow represents an inclusion of one object into a more complex object. (There should be lots of arrows from the line to the sun and to the wheel.) - For example, a windmill is made up of a filled square and three vanes The square is scaled to make it very narrow and is used as the pole of the windmill. Each of the three copies of vane has its own modeling transform, so they end up at different places in the windmill. There are, in turn, three copies of the windmill in the overall scene, each with its own modeling transform.
- Push and Pop in AffineTransform2D
- My AffineTransform2D class already includes implementation of a stack of transforms. If transform is an object of type AffineTransform2D, then transform has its own internal stack. Calling transform.push() will push a copy of the current transform onto the stack. Calling transform.pop() will replace the current transform with the transform on the top of stack, and it will remove that transform from the stack.
- For example, the sample program uses a function drawWindmill() to draw
a windmill. This function is called three times when drawing the scene, with
different modeling transformations. The program uses a variable transform
to hold the current transform. Here is the code for drawing the windmills:
transform.push(); transform.translate(0.75,1); // modeling transform for first windmill transform.scale(0.6,0.6); drawWindmill(); transform.pop(); transform.push(); transform.translate(2.2,1.6); // modeling transform for first windmill transform.scale(0.4,0.4); drawWindmill(); transform.pop(); transform.push(); transform.translate(3.7,0.8); // modeling transform for first windmill transform.scale(0.7,0.7); drawWindmill(); transform.pop();
Note how push and pop are used to save/restore the transform. Additional pushes and pops are used internally in drawWindmill().
- A Note on Basic Objects in the Hierarchical Graphics Example
- The hierarchical graphics program hierarchical2d-example.html uses a small number of basic objects. These are mostly simple objects such as squares and circles. A basic object is always drawn using the same vertex coordinates, and it seems silly to send those coordinates to the GPU every time the object is drawn. In fact, the coordinates can be sent just once to the GPU and stored there in a buffer. When the object is drawn, the vertex coordinates are already on the GPU, so they don't have to be retransmitted. This can be a very important performance consideration, especially for large models with a lot of vertex data.
- The program defines small JavaScript class named SimpleObject to represent a basic object.
The object is constructed from its vertices and primitive type. Each SimpleObject
has its own array buffer, which is created and filled with data by the constructor.
The object has a draw() method that simply pulls the vertex data from that
buffer:
/** * Defines the class SimpleObject, representing a geometric object that can be * drawn usind WebGL. The vertexData parameter must be an array of numbers. * The length of the array must be even. Each pair of numbers in the array * specifies the xy-coordinates for one vertex of the object. The primitiveType * parameter must be a constant specifying one of WebGL's geometric primitives: * gl.POINTS, gl.LINES, gl.LINE_LOOP, gl.LINE_STRIP, gl.TRIANGLES, gl.TRIANGLE_STRIP, * or gl.TRIANGLE_FAN. A SimpleObject includes a draw() method that draws the object. * (Requires the webgl context, gl, as a global variable.) */ function SimpleObject(vertexData, primitiveType) { var buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW); gl.bindBuffer(gl.ARRAY_BUFFER, null); this.buffer = buf; this.size = vertexData.length / 2; this.primitive = primitiveType; this.draw = function() { gl.bindBuffer( gl.ARRAY_BUFFER, this.buffer ); gl.vertexAttribPointer(vertexAttributeLocation, 2, gl.FLOAT, false, 0, 0); gl.drawArrays(this.primitive, 0, this.size); } }
- This method uses gl.STATIC_DRAW as the third parameter to gl.bufferData. This value tells the GL that the data that is being put into the buffer will be used several or many times without being changed. This means that it makes sense to store the data in memory on the graphics card, where it can be accessed by the GPU quickly during drawing. The other possible values are gl.STREAM_DATA and gl.DYNAMIC_DATA. "Stream data" might only be used once, and so might never even be stored on the GPU. "Dynamic data" will be used several times, but might be changed at any time; it would make most sense to store it in memory that is easily accessible by both the CPU and the GPU. All these values are just hints to the GL.