Notes for March 12-14

 

What we discussed on Tuesday Mar 12:

This lecture focused on principles, rather than on details of implementation.

We first discussed triangles versus triangle strips at a high level.

Passing vertex/triangle data to the GPU shader as triangle strips can save a lot of time and space, since it provides a way to tell the GPU when triangles share vertices.

On Thursday we will dive into vertex strips in more detail.

You can also opt to send vertex/triangle data down to the GPU as separate triangles, but then you don't get the advantages of triangle strips.

Then we discussed vertex shaders at a high level.

The general goal of vertex shaders is to give you a chance to use the power of the GPU to transform the position, as well as any other data, of each vertex in your geometry. Doing this in a vertex shader is much faster than doing it on your CPU.

On Thursday we will dive into this in more detail.

Then we discussed (u,v) parametric meshes.

We can think of a (u,v) parametric mesh as a kind of a rubber sheet, which we can then stretch and deform to create lots of different surface shapes. Describing a surface via a (u,v) parametric mesh makes it easier to create such shapes.

The basic idea is that we have two parameters u and v, each of which varies between 0.0 and 1.0. This gives us a unit square, consisting of all of the values of (u,v) between 0.0 and 1.0.

We can iterate through fractional values of u and v to divide this square into little rectangles. Each of those little rectangles can then be sent down to the GPU as two triangles in a triangle strip.

The power of this approach becomes apparent when we start using those values of u,v to define positions for the vertices of those triangles. By mapping values of (u,v) to geometric points (x,y,z) in various ways, we are essentially creating different shapes out of our rubber sheet.

We showed how to create a sphere as a parametric mesh:

sphere(u,v) = [ cos θ cos φ, sin θ cos φ, sin φ ]

where   θ = 2 π u   and   φ = π (v - ½)

We showed how to create a torus as a parametric mesh:

torus(u,v,r) = [ cos θ (1 + r cos φ), sin θ (1 + r cos φ), r sin φ ]

where   θ = 2 π u   and   φ = 2 π v

We had a high level discussion about the human body and bipedal movement.

On an abstract level, a biped is a floating head and a pair of floating hands. The rest of the body serves to allow the head and hands to achieve whatever position and orientation are optimal both for survival and for social expressiveness.

As a biped moves through the world, the most important decision it makes is where to place the next footstep. This is generally done in a way that optimizes balance, so that the weight of the body (and generally the head itself) stays balanced between the two feet. Otherwise the biped would fall over.

Once the placement of feet has been established, the pelvis needs to be positioned in a way that helps to maximize balance. Then the spine is positioned.

If the biped is reaching for or grasping objects, or otherwise using the hands in a purposeful way, then the spine is positioned in a way that optimizes the positions of the shoulders, so that the arms can best place the hands in their intended position and orientation.

We discussed why we need a matrix stack, with save() and restore() operations to push and pop matrix values, respectively.
Let's assume that our matrix object contains a data array, where each element of the array is one matrix, as well as a stackpointer sp, initialized to 0.

We can then implement the Matrix.save() and Matrix.restore() methods as follows:

   Matrix.save = () => {
      let src = this.data[this.sp],      // CURRENT TOP OF STACK
          dst = this.data[++this.sp];    // NEXT TOP OF STACK
      if (! dst)
         dst = this.data[this.sp] = [];  // FIRST TIME? CREATE VALUES ARRAY
      for (let n = 0 ; n < 16 ; n++) 
         dst[n] = src[n];                // COPY ALL 16 VALUES
   }

   Matrix.restore = () => --this.sp;
We began to create a simple animated human, but left the rest of that discussion until Thursday.

What we discussed on Thursday Mar 14:

First we built a simple animated human. The code we wrote for that is here:

   let limb = (x,y,z) => { // DRAW A LIMB AS
      m.save();            // AN ELLIPSOID
         m.scale(x,y,z);
	 mSphere();
      m.restore();
   }
   let joint = () => {     // DRAW A JOINT AS A
      m.save();            // SMALL RED SPHERE
         m.scale(.05);
	 mSphere().color('red');
      m.restore();
   }
   this.update = () => {
      let c = .5 * cos(2 * time);
      let s = .5 * sin(2 * time);
      m.save();

         joint();			// ROOT

	 // LEGS

         for (let sgn = -1 ; sgn <= 1 ; sgn += 2) {
	    m.save();
	       m.translate(sgn * .15,0,0);

	       joint();			// HIP
	       m.rotateX(-.5 - sgn * s + sgn * c);
	       m.translate(0,-.3,0);
	       limb(.07,.3,.07);
	       m.translate(0,-.3,0);

	       joint();			// KNEE
	       m.rotateX(1 + 2 * sgn * s);
	       m.translate(0,-.3,0);
	       limb(.07,.3,.07);
	       m.translate(0,-.3,0);

	       joint();			// ANKLE
	       m.translate(0,0,.1);
	       limb(.07,.05,.1);

	    m.restore();
         }

	 // TORSO

         m.translate(0,.2,0);
         limb(.2,.2,.1);
         m.translate(0,.1,0);

	 m.rotateX(c);			// WAIST
	 m.rotateZ(s);

         m.translate(0,.1,0);
         limb(.2,.25,.1);

	 m.translate(0,.12,0);

	 // ARMS

         for (let sgn = -1 ; sgn <= 1 ; sgn += 2) {
	    m.save();
	       m.translate(sgn * .2,0,0);

	       joint();			// SHOULDER
	       m.rotateZ(sgn * s);
	       m.translate(sgn * .2,0,0);
	       limb(.2,.05,.05);
	       m.translate(sgn * .2,0,0);

	       joint();			// ELBOW
	       m.rotateZ(sgn * s);
	       m.translate(sgn * .2,0,0);
	       limb(.2,.05,.05);

	    m.restore();
         }

	 m.translate(0,.1,0);

	 // NECK

	 joint();
	 m.rotateZ(s);
	 m.translate(0,.1,0);
         limb(.04,.15,.04);

	 // HEAD

	 joint();
	 m.rotateZ(s);
	 m.translate(0,.1,0);
         limb(.1,.13,.1);

	 // NOSE

	 m.save();
	    m.translate(0,0,.1);
	    limb(.03);
	 m.restore();
      m.restore();
Then we implemented a mesh based on triangle strips, and looked at how to send that data down to the GPU so that we can render more complex shapes.

We didn't get entirely through that discussion, so I will not expect you to implement that material yet.

But here, for reference, is a function that builds a triangle mesh given a shape-defining function (like sphere or torus):

   let createTriMesh = (uvToVertexFunction, nCols, nRows) => {
      let triMesh = [];
      let appendToTriMesh = p => {
         for (let n = 0 ; n < p.length ; n++)
	    triMesh.push(p[n]);
      }
      for (let row = 0 ; row < nRows ; row++) {
         let v0 =  row    / nRows,
             v1 = (row+1) / nRows;
         for (let col = 0 ; col <= nCols ; col++) {
            let u = col / nCols;
            if (row % 2)
               u = 1 - u;
            appendToTriMesh(uvToVertexFunction(u, v0));
            appendToTriMesh(uvToVertexFunction(u, v1));
         }
      }
      return triMesh;
   }
Once you have a function to build a triangle mesh, you can then pass it different shape making functions to "fold the rubber sheet". Your shape-making function should take (u,v) as its argument, and return a surface point and a surface normal.

Here, for example, is the shape-making function for a unit sphere:

   let sph = (u,v) => {
      let theta = 2 * Math.PI * u,
          phi = Math.PI * (v - .5),
          x = Math.cos(theta) * Math.cos(phi),   // For a unit sphere,
          y = Math.sin(theta) * Math.cos(phi),   // surface point and
          z = Math.sin(phi);                     // surface normal are
      return [ x,y,z, x,y,z ];                   // the same.
   }

   triMesh = createTriMesh(sph, 16, 8);
   stride = 6;
Note that I set stride to 6. That's because each of the vertices in our triMesh contains 6 numbers (three for position, and another three for surface normal). When we pass the triMesh data to the GPU we will need to use that stride length.

Homework due before class on Thursday March 28:

Start implementing a 4x4 Matrix object type in Javascript. Try to get as much done as you can by Tuesday March 26. We will go over any final questions you may have during class on Tuesday March 26.

You shouldn't have any trouble implementing Javascript methods for identity(), translate(x,y,z), rotateX(θ), rotateY(θ), rotateZ(θ) and scale(x,y,z).

You will also need to implement a matrix multiply method. Remember, as we said in class, that if I have two 4x4 matrices A and B, where Ai,j represents the value at column i and row j, then the result of the matrix product C = A ● B is also a 4x4 matrix, where each element Ci,j of this product is given by:

Ci,j = A0,j * Bi,0 + A1,j * Bi,1 + A2,j * Bi,2 + A3,j * Bi,3
Once you have implemented matrix multiply, then to implement matrix translation, for example, you could:
  1. Construct translation matrix T(x,y,z) using data:
    [ 1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1 ]
    which represents the column-major 4x4 matrix:
    1   0   0   x
    0   1   0   y
    0   0   1   z
    0   0   0   1
  2. Modify M via matrix multiplication: M = M ● T