Shapes from triangles and Matrix class
TRIANGLES All of the geometry that you send down from your CPU to the vertex and fragment shaders running on your GPU consists of triangles. The square that we have been ray tracing to up to now, which is made from two triangles, was described as a triangle strip. That is a very compact way to describe geometry, but sometimes you want to sacrifice compactness for generality. In such cases, you can send each triangle separately, by using the "gl.TRIANGLES" option in function gl.drawArrays(); When you do that, you need to organize vertices in threes: a0,b0,c0,a1,b1,c1,...where each ai,bi or ci consists of all of the numbers needed to describe one vertex. For example, to describe a square as two separate triangles, you could specify: [ -1,-1,0, 1,1,0, -1,1,0, 1,1,0, -1,-1,0, 1,-1,0 ]Note that the above is a description of two triangles, each consisting of three vertices. Also, in the above example we specified only the x,y,z coordinates for each vertex, so there are a total of 2 x 3 x 3 = 18 values. Sometimes we want to specify more values for a vertex, such as a surface normal or texture coordinates. For this reason, it is useful to have some constant describing how many values are contained in our description of each vertex, such as: const VERTEX_SIZE = 3;Suppose your triangle data is in an array called vertices .
then to actually draw your triangles at each animation
frame you will call:
gl.drawArrays(gl.TRIANGLES, 0, vertices.length / VERTEX_SIZE);
CREATING CUBE VERTICES In class we showed how a cube can be created procedurally as twelve triangles (3 coords, 2 square faces per coord, 2 triangles per face). Below is the code I used to create a cube. The commented out lines in blue show what the code looked like before adding normals. /* const VERTEX_SIZE = 3; */ const VERTEX_SIZE = 6; let createCube = () => { let v = []; let addVertex = a => { for (let i = 0 ; i < a.length ; i++) v.push(a[i]); } let addSquare = (a,b,c,d) => { addVertex(a); addVertex(b); addVertex(c); addVertex(b); addVertex(c); addVertex(d); } /* let P = [[-1,-1,-1],[ 1,-1,-1],[-1, 1,-1],[ 1, 1,-1], [-1,-1, 1],[ 1,-1, 1],[-1, 1, 1],[ 1, 1, 1]]; */ let P = [[-1,-1,-1, 0,0,-1],[ 1,-1,-1, 0,0,-1],[-1, 1,-1, 0,0,-1],[ 1, 1,-1, 0,0,-1], [-1,-1, 1, 0,0, 1],[ 1,-1, 1, 0,0, 1],[-1, 1, 1, 0,0, 1],[ 1, 1, 1, 0,0, 1]]; for (let n = 0 ; n < 3 ; n++) { addSquare(P[0],P[1],P[2],P[3]); addSquare(P[4],P[5],P[6],P[7]); for (let i = 0 ; i < P.length ; i++) { /* P[i] = [P[i][1],P[i][2],P[i][0]]; */ P[i] = [P[i][1],P[i][2],P[i][0], P[i][4],P[i][5],P[i][3]]; } } return v; } let vertices = createCube(); BUILDING A MATRIX CLASS I can be awkward to use functions like translate(), rotateX(), multiply(), etc. without building a higher level of structure. As we discussed in class, it is useful to create a class of object that manages matrix operations, including the ability to save and restore matrices in a stack to perform local matrix operations. The lines below in blue show the parts of the code that support a matrix stack. let Matrix = function() { let topIndex = 0, value = identity(), getVal = () => value, setVal = m => value = m; stack = [ identity() ], getVal = () => stack[topIndex], setVal = m => stack[topIndex] = m; this.identity = () => setVal(identity()); this.restore = () => --topIndex; this.rotateX = t => setVal(multiply(getVal(), rotateX(t))); this.rotateY = t => setVal(multiply(getVal(), rotateY(t))); this.rotateZ = t => setVal(multiply(getVal(), rotateZ(t))); this.save = () => stack[++topIndex] = stack[topIndex-1].slice(); this.scale = (x,y,z) => setVal(multiply(getVal(), scale(x,y,z))); this.translate = (x,y,z) => setVal(multiply(getVal(), translate(x,y,z))); this.value = () => getVal(); } ADDING NORMALS To add surface normals to each vertex, you need to expand VERTEX_SIZE to 6. Also, you need to map the second half of the larger vertex description to the normal attribute. Each vertex in your vertices array will now look like this: INDEX: 0 1 2 3 4 5 VALUE: x y z nx ny nzAs we discussed in class, you can express this mapping in your Javascript code as follows: let bpe = Float32Array.BYTES_PER_ELEMENT; let aPos = gl.getAttribLocation(state.program, 'aPos'); gl.enableVertexAttribArray(aPos); gl.vertexAttribPointer(aPos, 3, gl.FLOAT, false, bpe * VERTEX_SIZE, bpe * 0); let aNor = gl.getAttribLocation(state.program, 'aNor'); gl.enableVertexAttribArray(aNor); gl.vertexAttribPointer(aNor, 3, gl.FLOAT, false, bpe * VERTEX_SIZE, bpe * 3); MULTIPLE INSTANCES By setting different values for uniform variables and then doing multiple draw calls within your onDraw() function, you can create multiple instances of the same shape. In class we started with the following example, and then gradually modified it to a pair of waving arms (which is in the zip file that you can upload to do this week's homework assignment).
Note that the lines in blue below refer to function onDraw(t, projMat, viewMat, state, eyeIdx) { let m = state.m; gl.uniformMatrix4fv(state.uViewLoc, false, new Float32Array(viewMat)); gl.uniformMatrix4fv(state.uProjLoc, false, new Float32Array(projMat)); m.identity(); m.translate(.8,0,-4); m.rotateX(state.time); m.rotateY(state.time); m.scale(.3,.3,.3); gl.uniform3fv(state.uColorLoc, [1,.1,.1] ); gl.uniformMatrix4fv(state.uModelLoc, false, m.value() ); gl.drawArrays(gl.TRIANGLES, 0, vertices.length / VERTEX_SIZE); m.save(); m.translate(-3,0,0); m.rotateY(-state.time * 2.5); m.rotateZ(-state.time * 2.5); gl.uniform3fv(state.uColorLoc, [.1,.1,1] ); gl.uniformMatrix4fv(state.uModelLoc, false, m.value() ); gl.drawArrays(gl.TRIANGLES, 0, vertices.length / VERTEX_SIZE); m.restore(); } HIERARCHICAL MATRICES
Note especially from the above example how we can
nest code between a matching m.save() ... // KEEP DOING THAT TO ARBITRARY NESTING LEVELS ... m.restore(); VERTEX SHADER Here is the vertex shader that I used in the example I showed in class.
The lines below in blue
show where we add info for surface normal // input vertex in vec3 aPos; in vec3 aNor; // interpolated position out vec3 vPos; out vec3 vNor; out vec2 vXY; // matrices uniform mat4 uModel; uniform mat4 uView; uniform mat4 uProj; // time in seconds uniform float uTime; void main(void) { vec4 pos = uProj * uView * uModel * vec4(aPos, 1.); gl_Position = pos; vXY = pos.xy / pos.z; vPos = aPos; vNor = (vec4(aNor, 0.) * inverse(uModel)).xyz; } FRAGMENT SHADER Here is the fragment shader that I used in the example I showed in class. Note that I am not really doing Phong shading. I'm just faking it. You are going to add proper Phong shading as part of your homework. uniform vec3 uColor; uniform vec3 uCursor; // CURSOR: xy=pos, z=mouse up/down uniform float uTime; // TIME, IN SECONDS in vec2 vXY; // POSITION ON IMAGE in vec3 vPos; // POSITION in vec3 vNor; // NORMAL out vec4 fragColor; // RESULT WILL GO HERE void main() { vec3 lDir = vec3(.57,.57,.57); vec3 shade = vec3(.1,.1,.1) + vec3(1.,1.,1.) * max(0., dot(lDir, normalize(vNor))); vec3 color = shade; // HIGHLIGHT CURSOR POSITION WHILE MOUSE IS PRESSED if (uCursor.z > 0. && min(abs(uCursor.x - vXY.x), abs(uCursor.y - vXY.y)) < .01) color = vec3(1.,1.,1.); fragColor = vec4(sqrt(color * uColor), 1.0); } COOL VIDEO WE WATCHED At the end of class we watched The Centrifuge Brain Project. HOMEWORK (DUE BEFORE CLASS OCT 15) For homework, I would like you to do the following. Note: The example of the guy waving his arms won't show up until you implement the 6 matrix functions in the first item below.
An updated library package that you can use, which also contains the example I created in class, as well as hot reloading whenever you save week5.js, is in hw5.zip. |