In more recent times, beginning in the 1960s,
designers in the automotive and aerospace industries started
using computer software to generate smooth curves when
designing their vehicles.
Their key innovation was to use parametric cubic curves and
parametric bicubic surfaces,
and that is the topic we are covering this week.
PARAMETRIC CUBIC CURVES
The underlying math for creating smooth splines
is generally to create a set of parametric cubic curves
that fit together seamlessly, so that they appear to
form a single smooth curve.
The general recipe is as follows:
- Divide the curve into simple curved segments.
- The position and orientation at the end of each segment is the
same as the position and orientation at the beginning of the next segment.
- The points that begin and end segments are called "key points".
- Each segment is a parametric curve x(t), y(t), (and optionally z(t)),
where t varies between 0.0 and 1.0.
- In particular, the position along each dimension of every segment is a cubic function:
x(t) = ax * t3 + bx * t2 + cx * t + dx
y(t) = ay * t3 + by * t2 + cy * t + dy
|
|
|
After defining the above functions x(t) and y(t), it's easy to draw the resulting spline segment.
For example:
function drawSpline(x, y, dt) {
let a = [x(0), y(0)];
for (let t = dt ; t <= 1 ; t += dt) {
let b = [x(t), y(t)];
drawLine(a, b); // we are assuming some drawLine() function has been defined.
a = b;
}
}
Creating more complex curves
You can also string together multiple cubic spline curves
together to create a more complex curve, as you
can see in the figure to the right.
Suppose you want to define a parametric curve with many twists and turns,
that varies along some parameter 0.0 ≤ u ≤ 1.0.
You can define that curve as a succession of N parametric cubic curves:
[ [ax,bx,cx,dx] , [ay,by,cy,dy] ]0,
[ [ax,bx,cx,dx] , [ay,by,cy,dy] ]1,
[ [ax,bx,cx,dx] , [ay,by,cy,dy] ]2,
...
To evaluate this curve at some value of u,
first compute which cubic curve is to be evaluated:
i = floor(N * u)
Then compute the parametric position within that cubic curve:
t = N * u - i
|
|
|
In the above discussion, the curve can be three dimensional, rather
than two dimensional. In that case, each cubic curve segment would be described by:
[ [ax,bx,cx,dx] , [ay,by,cy,dy] , [az,bz,cz,dz] ]i
TRANSLATING FROM THE HUMAN DESIGNER TO THE COMPUTER
For most human beings, it would be extremely difficult to design
such curves by directly typing the coefficients of cubic polynomials.
For this reason, we create other ways of defining those coefficients,
which are more human-friendly.
All such methods work by transforming some easier to understand set of values
into the underlying cubic coefficients (a,b,c,d).
In class we went over two of the more important such methods,
Hermite splines and Bezier splines.
HERMITE SPLINES
If our human user wants to define things in terms of the position and orientation
at the beginning and end of each cubic spline segment, then we use the Hermite spline.
We do all the math independently for each coordinate (eg: x and y, or x,y and z).
P0 = value at start of the segment (where t = 0).
P1 = value at end of the segment (where t = 1).
R0 = slope at start of the segment (where t = 0).
R1 = slope at end of the segment (where t = 1).
We create four "basis functions", each of which varies only one thing:
only P0: | 2t3 - 3t2 + 1 |
only P1: | -2t3 + 3t2 |
only R0: | t3 - 2t2 + t |
only R1: | t3 - t2 |
|
|
So to get from (P0,P1,R0,R1) to the coefficients (a,b,c,d) that define cubic polynomial a * t^3 + b * t^2 + c * t + d,
we apply the Hermite Basis Matrix, which is just a way of describing these four basis functions.
Each of the four functions is described in a single column of the Hermite Matrix:
a b c d |
⇐ |
2 -3 0 1 |
-2 3 0 0 |
1 -2 1 0 |
1 -1 0 0 |
× |
P0 P1 R0 R1 |
BEZIER SPLINES
If our human user wants to define things by moving points around on a screen,
then the Bezier spline is a good choice.
Again, we do the math independently for each coordinate.
A = value at start of the segment (where t = 0).
B = value at a first "guide point".
C = value at a second "guide point".
D = value at end of the segment (where t = 1).
The math is basically successive linear interpolations:
mix (
mix ( mix(A,B,t) , mix(B,C,t) , t ),
mix ( mix(B,C,t) , mix(C,D,t) , t ),
t
)
where we define mix(a,b,t) as linear interpolation:
(a,b,t) ⇒ (1 - t) * a + t * b
|
|
|
In other words:
(1-t) * ( (1-t) * ((1-t)*A + t*B) + t * ((1-t)*B + t*C) )
+
t * ( (1-t) * ((1-t)*B + t*C) + t * ((1-t)*C + t*D) )
When you multiply everything out, this turns into:
(1-t) * (1-t) * (1-t) * A +
3 * (1-t) * (1-t) * t * B +
3 * (1-t) * t * t * C +
t * t * t * D
This gives us what we need to go from key values (A,B,C,D) to cubic coefficients (a,b,c,d).
Each line of the above equation can be turned into a column of the characteristic Bezier Basis Matrix:
a b c d |
⇐ |
-1 3 -3 1 |
3 -6 3 0 |
-3 3 0 0 |
1 0 0 0 |
× |
A B C D |
BEZIER BICUBIC SURFACE PATCHES
We can extend the idea of parametric cubic curves
to bicubic patches.
Instead of mapping a single parameter t to a curve,
we map two parameters u and v to a patch of curved surface.
As with cubic curves, we generally find it useful
to transform our representation of such surface
patches into something easier to understand,
perhaps using a Bezier or Hermite transformation.
|
|
For example, each point in the above patch was determined
by applying a Bezier transformation to a 4x4 matrix P of
16 key points:
[u3 u2 u 1]
• Bz
• P
• BzT
• [v3 v2 v 1]T
A NOTE ON COMPUTING SURFACE NORMALS TO MAKE SHAPES LOOK ROUNDED
As we discussed in class,
here is a general purpose way to compute surface normals
to create the appearance of a smooth rounded surface.
- For each polygonal face of the surface, compute an unnormalized face normal.
- For each vertex, sum up the unnormalized face normals of faces that adjoin that vertex,
and then normalize the result.
To compute the unnormalized face normal of a polygonal face for step 1 above:
V = [0,0,0]
For every three successive vertices A,B,C of the face, going around counterclockwise:
V += cross(B - A, C - B)
Recall that cross product of two vectors [ax,ay,az] and [bx,by,bz] is:
[ ay*bz - az*by , az*bx - ax*bz , ax*by - ay*bx ]
Note that you will need to do some bookkeeping
to know which faces adjoin which vertices.
This is actually quite easy in the special case where you have a rectangular mesh.
In that special case, you just need to compute the face
normal for every quad (that is, rectangular face) lower bounded on the grid by
(col,row)
Then for every vertex at (col,row) ,
you can sum up the face normals for
the four quads that are lower bounded on the grid by, respectively:
(col-1,row-1) , (col,row-1) , (col-1,row) , (col,row)
HOMEWORK
In the last part of class I showed you my implementation
of cubic splines and bicubic patches as a 3D scene.
hw7.zip contains a variation on that scene,
with key parts of the implementation missing.
Your first job is to fill in those missing parts.
There are lots of implementation notes in the
code comments to help you along.
When the scene is fully implemented, it will look
like the image to the right.
I have already
implemented for you the definition of
both the Hermite and Bezier basis matrices,
a function to convert from any such basis to
cubic curve coefficients,
and
another function to convert from any such basis to
bicubic patch coefficients.
Rather than just use the shapes I've created,
I would like you to
create your own
shapes. As usual, be creative, have fun,
animate things in an interesting way.
In the included code, I've also implemented texture mapping.
Feel free to add cool and interesting textures
to the scene you create.
|
|
|
Some additional notes on the code in the homework:
-
The mesh creation function
createMeshVertices()
is responsible for calling the function that creates vertices at various values of u and v.
In the case of creating a ribbon shape,
uvToCubicCurvesRibbon()
is that function.
It gets called by the mesh creation function
each time a vertex needs to be added.
uvToCubicCurvesRibbon()
will be called by
createMeshVertices()
with different values of u and v passed in. As u
varies, we get points along the length of the ribbon. As v varies, we get points along the width of the ribbon.
-
To figure out the direction along the ribbon at any given value of u, we take
close spaced samples along the center line of the ribbon.
For example, we can take
the difference between the point P0 at (u,0) and the point P1 at (u+.001,0),
which gives the direction (dx,dy) along the ribbon.
More precisely,
(dx,dy) = normalize(P1 - P0).
Once we have point P0 along u along the center line of the ribbon, as well as direction vector (dx,dy) along
the direction of the ribbon, then we use the
perpendicular direction (-dy,dx) across the width of the ribbon, which varies in v. One edge of the ribbon (where v = 0.0) is at P0 -
width*(-dy,dx) and the other edge of the ribbon (where v = 1.0) is at P0 + width*(-dy,dx).
| |