Notes for Tuesday April 7 class -- Making shapes with splines, part 1

Review of previous notes

First we reviewed the notes from April 2.

Organizing vector functions into a static object
Then, starting with the code from March 31, spent the rest of the class we made some modifications. The first modification was to create a static object V3 to hold operations on length 3 vectors, such as cross() and normalize():
let V3 = {
  cross     : (a, b) => [ a[1] * b[2] - a[2] * b[1],
                          a[2] * b[0] - a[0] * b[2],
                          a[0] * b[1] - a[1] * b[0] ],

  normalize : v => { let s = Math.sqrt( v[0] * v[0] + v[1] * v[1] + v[2] * v[2] );
                     return [ v[0] / s, v[1] / s, v[2] / s ]; },
}
Implementing a spline basis matrix
Then we added a transform() method to the Matrix class, to transform vectors of length 3 or 4. We will need this operation in order to create and use an Hermite or Bezier basis matrix:
this.transform = function(v) {
   let m = value, x = v[0], y = v[1], z = v[2], w = v[3] === undefined ? 1 : v[3];

   let X = m[0] * x + m[4] * y + m[ 8] * z + m[12] * w,
       Y = m[1] * x + m[5] * y + m[ 9] * z + m[13] * w,
       Z = m[2] * x + m[6] * y + m[10] * z + m[14] * w,
       W = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
   return v[3] === undefined ? [X, Y, Z]  : [X, Y, Z, W];
}
Note that we made sure to handle the special case where the argument is a vector of length 3, treating it like a position vector [x,y,z,1].

Then we defined hermiteBasis and bezierBasis as instances of Matrix:

let hermiteBasis = new Matrix();
hermiteBasis.setValue([2,-3,0,1, -2,3,0,0, 1,-2,1,0, 1,-1,0,0]);

let bezierBasis = new Matrix();
bezierBasis.setValue([-1,3,-3,1, 3,-6,3,0, -3,3,0,0, 1,0,0,0]);
Evaluating a spline
After that, we added a function to the library that lets us evaluate a multi-segment Bezier spline at a particular parametric value 0.0 ≤ t ≤ 1.0, given an array of keys:
let evalBezier = (keys, t) => {
   t = Math.max(0, Math.min(t, .9999));
   let n = Math.floor(keys.length / 3); // FIND NUMBER OF KEYS
   let i = Math.floor(n * t) * 3;       // FIND INDEX OF FIRST KEY IN SEGMENT
   let f = n * t % 1;                   // FIND FRACTION WITHIN SEGMENT
   let C = bezierBasis.transform([ keys[i],keys[i+1],keys[i+2],keys[i+3] ]);

   // return f*f*f*C[0] + f*f*C[1] + f*C[2] + C[3];  // LESS EFFICIENT

   return f * (f * (f * C[0] + C[1]) + C[2]) + C[3]; // MORE EFFICIENT
}
Surface of revolution
This gave us what we needed for a first implementation of the Lathe object, for creating surfaces of revolution. In the library, we defined:
let uvToLathe = (u,v,keys) => {
  let theta =2 * Math.PI * u;
  let r = evalBezier(keys, v);
  return [ r * Math.cos(theta), r * Math.sin(theta), 2 * v - 1 ];
}
The problem with that implementation is that it doesn't let us create surfaces of revolution with rounded tips at the end. In order to do that, we need to pass in not one but two keys arrays, one to control radius r, and the other to control coordinate z. After making that adjustment, here was our full implementation:
let uvToLathe = (u,v,keys) => {
  let theta =2 * Math.PI * u, r, z;
  if (! Array.isArray(keys[0])) {
     r = evalBezier(keys, v);
     z = 2 * v - 1;
  }
  else {
     r = evalBezier(keys[0], v);
     z = evalBezier(keys[1], v);
  }
  return [ r * Math.cos(theta), r * Math.sin(theta), z ];
}
Then in index.html we created an example of a surface of revolution:
let myLathe = createTriangleMesh(uvToLathe, 20, 20,
   [
      [ 0,2/3, 1/6,1/4,1/3, 1/3,0], // r
      [-1,-1, 1,1]                  // z
   ]
);
Sampling a spline path
Finally, we showed how you can sample a spline path, then we visualized that path by stringing small spheres along the path.

First, in the library we defined:

let sampleBezierPath = (X,Y,Z,n) => {
   let P = [];
   for (let i = 0 ; i <= n ; i++)
     P.push([evalBezier(X, i/n),
             evalBezier(Y, i/n),
             evalBezier(Z, i/n)]);
   return P;
}
Then in index.html we created an example of a sampled spline path:
let path = sampleBezierPath( [-1,-1/3, 1/3, 1],  // X keys
                             [ 1,-1,   1  ,-1],  // Y keys
                             [ 1,-1,  -1  , 1],  // Z keys
                             30);                // number of samples

for (let n = 0 ; n < path.length ; n++) {
   test.add(sphere, blue_plastic)
       .translate(path[n][0], path[n][1], path[n][2])
       .scale(.1);
}