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:
- 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 |
- Modify M via matrix multiplication: M = M ● T
|