Notes for October 7 class:

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 nz
As 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 uColor, which I am using in my fragment shader as a cheap fake version of phong shading. uniform

   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() and m.restore() pair. This allows us to do a sequence of local transformations, after which the state is restored to its previous value. That pattern allows us to create tree structured hierarchies of movement.

   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 aNor, vNor and current pixel location vXY (which is needed to properly display the cursor).

   // 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.

  1. Implement functions identity(), rotateX(), rotateY(), rotateZ(), scale(), translate().

    You should have already done this for your previous assignment, so you can just reuse those functions.

  2. Build an interesting animated scene with multiple instances of cubes with different transformations.

    Have fun with it. Make something cool, like a person or room or building or electric motor or something out of your experience or imagination.

  3. Add phong shading to the fragment shader. You've already implemented this, so it should be easy.

  4. Extra credit: polyhedra other than cubes. This is really just a matter of creating a vertices array with a different sequence of (x,y,z,nx,ny,nz).

    Hint: If you want to draw more than one kind of shape, you can create different versions of the vertices array, such as cubeVertices, octahedronVertices, etc. Then in your onDraw() function you can, for example:

    1. Send cubeVertices down to the GPU by calling gl.bufferData(),
      then do some drawArrays() calls,

    2. send octahedronVertices down to the GPU by calling gl.bufferData().
      then do some drawArrays() calls,

    3. and so on, for every different kind of shape in your scene.

  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.