10 Bezier
10 Bezier
1 Additional Reading
Most of what I know about Curves and Surfaces I learned from Angel’s book, so check that chapter
first. It’s pretty mathematical in places, though. There’s also a chapter of the Red Book (the Red
OpenGL Programming Guide).
2 Organization
This reading is organized as follows. First, we look at why we try to represent curves and surfaces
in graphics models, but I think most of us are already pretty motivated by that. Then, we look
at major classes of mathematical functions, discussing the pros and cons, and finally choosing
cubic parametric equations. Next, we describe different ways to specify a cubic equation, and
we ultimately settle on Bézier curves. Finally, we look at how the mathematical tools that we’ve
discussed are reflected in OpenGL code.
The preceding develops curves (that is, 1D objects — wiggly lines). In graphics, we’re mostly
interested in surfaces (that is, 2D objects — wiggly planes). The last sections define surfaces as a
generalization of what we’ve already done with curves.
3 Introduction
Bézier curves were invented by a mathematician working at Renault, the French car company. He
wanted a way to make formal and explicit the kinds of curves that had previously been designed
by taking flexible strips of wood (called splines, which is why these mathematical curves are often
called splines) and bending them around pegs in a pegboard.
To give credit where it’s due, another mathematician named de Castelau independently invented
the same family of curves, although the mathematical formalization is a little different.
4 Representing Curves
Most of us know about different kinds of curves, such as parabolas, circles, the square root func-
tion, cosines and so forth. Most of those curves can be represented mathematically in several
ways:
• explicit equations
• implicit equations
• parametric equations
1
4.1 Explicit Equations
The explicit equations are the ones we’re most familiar with. For example, consider the following
functions:
y = mx + b
y = ax2 + bx + c
√
y = r 2 − x2
An explicit equation has one variable that is dependent on the others; here it is always y that is the
dependent variable: the one that is calculated as a function of the others.
An advantage of the explicit form is that it’s pretty easy to compute a bunch of values on the
curve: just iterate x from some minimum to some maximum.
One trouble with the explicit form is that there are often special cases (for example, vertical
lines). Another is that the limits on x will change from funtion to function (the domain is infinite
for the first two examples, but limited to ±r for the third). The deadly blow is that it’s hard to
handle non-functions, such as a complete circle, or a parabola of the form x = ay 2 + by + c.
You could certainly get a computer program to handle this form, but you’d need to encode lots of
extra stuff, like which variable is the dependent one and so forth. Bézier curves can be completely
specified by just an array of coefficients.
These equations have a nice advantage that, given a point, it’s easy to tell whether it’s on the
curve or not: just evaluate the function and see if the function is zero. Moreover, each of these
functions divides space in two: the points where the function is negative and the points where it’s
positive. Interestingly, the surfaces do as well, so the sign of the function value tells you which
side of the surface you’re on. (It can even tell you how close you are.)
The fact that no variable is distinguished helps to handle special cases. In fact, it would be
pretty easy to define a large general polynomial in x, y and z as our representation.
The deadly blow for this representation, though, is that it’s hard to generate points on the
surface. Imagine that I give you a value for a, b, c and d and you have to find a value for x, y, and
z that work for the two examples above. Not easy in general. Also, it’s hard to do curves (wiggly
lines); in general, those are the intersection of two surfaces.
2
Join point
p1
p(v ) q(v ) p6
p3 p4
p0 p0 p1 p3
p(u) p5
p2 p2
With parametric equations, we invent some new variables, the parameters, typically s and t.
These variables are then used to define a function for each coordinate:
x(s, t)
y(s, t)
z(s, t)
Parametric functions have the advantage that they’re easy to generalize to 3D, as we already
saw with lines.
The parameters tell us where we are on the surface (or curve) rather than where we are in
space. Therefore, we have a conventional domain, namely the unit interval. That means that, like
our line segments, our curves will all go from t = 0 to t = 1. (They don’t have to, but they almost
always do.) Similarly, surfaces are all points where 0 ≤ s, t ≤ 1. Thus, another advantage of
parametric equations is that it’s easy to define finite segments and sheets, by limiting the domains
of the parameters.
The problem that remains is what family of functions we will use for the parametric functions.
One standard approach is to use polynomials, thereby avoiding trigonometric and exponential func-
tions, which are expensive to compute. In fact, we usually choose a cubic:
x(t) = C0 + C1 t + C2 t2 + C3 t3 (1)
3
Ci ti
X
= (2)
i=0
Another problem comes with finding these coefficients. We’ll develop that in later sections, but
the solution is essentially to appeal to some nice techniques from linear algebra that let us solve
for the desired coefficients given some desired constraints on the curve, such as where it starts and
where it stops.
3
Figure 2: Three ways of specifying a curve: (a) interpolation, (b) Hermite, and (c) Bézier
• It takes less information to specify the curve. Since there are four unknown coefficients,
we need four points (or similar constraints) to solve for the coefficients. If we were using a
quartic, we’d need 5 points, and so forth.
• If we want more wiggles, we can join up several splines. Because of the low degree, we have
good control of the derivative at the end points, so we can make sure that the curve is smooth
through the joint.
• Finally, it’s just less computation and therefore easier for the graphics card to render.
OpenGL will permit you to use higher (and lower) degree functions, but for this presentation we’ll
stick to cubics. If you’d like to play with higher degree functions, I’m happy to help you with that.
Figure 1 shows points defining a curve, and how curves might be put together to be smooth at the
joints.
4
Interpolation That is, you specify 4 points on the curve and the curve goes through (interpolates)
the points. This is pretty intuitive, and a lot of drawing programs allow you to do this, but
it’s not often used in CG. This is primarily because such curves have an annoying way of
suddenly lurching as they struggle to get through the next specified point.
One exception is that this technique is often used when you have a lot of data, as with a
digitally scanned face or figure. Then you have thousands of points, and the curve pretty
much has no choice but to be what you want (although the graphics artist may still want to
do some smoothing, say for measurement error or something).
Hermite In the Hermite case, the four pieces of information you specify are 2 points and 2 vectors:
the points are where the curve starts and ends, and the vectors indicate the direction of
the curve at that point. They are, in fact, derivatives. (If you’ve done single-dimensional
calculus, you know that the derivative gives the slope at any point and the slope is just the
direction of the line; the same idea holds in more dimensions.) This is a very important
technique, because we often have this information. For example, if I want a nice rounded
corner on a square box, I know the slope at the beginning (vertical, say) and at the end
(horizontal).
Bézier With a Bézier curve, we specify 4 points, as follows: the curve starts at one, heading for
second, ends at fourth, coming from third. (See the picture in figure 2.) This is a very
important technique, because you can easily specify a point using a GUI, while a vector is
a little harder. It turns out there are other reasons that Bézier is preferred, and in practice
the first two techniques are implemented by finding the Bézier points that draw the desired
curve.
Figure 2 compares these three approaches. An X11 drawing program that will let you experiment
with the examples drawn in the figure is xfig. (Xfig uses quadratic Bézier curves.) Try it! For a
demo of drawing Bézier curves in 2D using OpenGL, try
˜cs307/public_html/demos/curves-and-surfaces/CurveDraw.cc
OpenGL curves are drawn by calculating points on the line and drawing straight line seg-
ments between those points. The more segments, the smoother the resulting curve looks. The
CurveDraw.cc demo requires you to specify on the command line the number of segments in
the drawing of the curve. Try different numbers of segments until the curves look pretty smooth.
5
dimensions work the same way. If we substitute t = {0, 31 , 32 , 1} into the cubic equations 1, we get
the following:
P0 = x(0) = C0
1 1 2 1 3
P1 = x(1/3) = C0 + C1 + C2 + C3
3 3 3
2 3
2 2 2
P2 = x(2/3) = C0 + C1 + C2 + C3
3 3 3
P3 = x(1) = C0 + C1 + C2 + C3
What does this mean? It means that the x coordinate of the first point, P0 , is x(0) This makes
sense: since the function x starts at P0 , it should evaluate to P0 at t = 0. (This is also exactly
what happens with a parametric equation for a straight line: at t = 0, the function evaluates to the
first point.) Similarly, the x coordinate of the second point, P1 is x(1/3) and that evaluates to the
expression that you see there. Most of those coefficients are still unknown, but we’ll get to how to
find them soon enough.
Putting these four equations into a matrix notation, we get the following:
1 0 0 0
1 1 2 1 3
1 3 (3) (3)
P= C
1 23 ( 23 )2 ( 32 )3
1 1 1 1
The P matrix is a matrix of points. It could just be the x coordinates of our points P , or more
generally it could be a matrix each of whose four entries is an (x, y, z) point. We’ll view it as a
matrix of points. The matrix C is a matrix of coefficients, where each element — each coefficient
— is a triple (Cx , Cy , Cz ), meaning the coeffients of the x(t) function, the y(t) function and the
z(t) function.
If we let A stand for the array of numbers, we get the following deceptively simple equation:
P = AC
By inverting the matrix A, we can solve for the coefficients! (The inverse of a matrix is the
analog to a reciprocal. It’s also equivalent to solving the simultaneous equations in a very general
way.) The inverse of A is called the interpolating geometry matrix and is denoted MI :
MI = A−1
Notice that the matrix MI does not depend on the particular points we choose, so that matrix
can simply be stored in the graphics card. When we send an array of control points, P, down the
pipeline, the graphics card can easily compute the coefficients it needs for calculating points on the
curve:
C = MI P
6
b1(u) b2(u)
1 b0(u)
b3(u)
u
1 2
3 3
1
The same approach works with Hermite and Bézier curves, yielding the Hermite geometry
matrix and the Bézier geometry matrix. In the Hermite case, we take the derivative of the cubic
and evaluate it at the endpoints and set it equal to our desired vector (instead of points P1 and
P2 ). In the Bézier case, we use a point to define the vector and reduce it to the previously solved
Hermite case.
7
1
t**3-t**2
2*t**3-3*t**2+1
-2*t**3+3*t**2
t**3-2*t**2+t
0.8
0.6
0.4
0.2
-0.2
0 0.2 0.4 0.6 0.8 1
−9 1 2
B3 (t) = t(t − )(t − )
2 3 3
These functions are plotted in figure 3. First, notice that the curves always sum to 1. When a curve
is near zero, the control point has little influence. When a curve is high, the weight is high and the
associated control point has a lot of influence: a lot of “pull.” In fact, when a control point has a
lot of pull, the curve passes near it. When a weight is 1, the curve goes through the control point.
Notice that the blending functions are negative sometimes, which means that this isn’t a normal
weighted average. (A normal weighted average has all non-negative weights.) What would a
negative weight mean? If a positive weight means that a control point has a certain “pull,” a
negative value gives it “push” — the curve is repelled from that control point. This repulsion effect
is part of the reason that interpolation curves are hard to control.
7 Hermite Representation
The coefficients for the Hermite curves and therefore the blending functions can be computed from
the control points in a similar way (we have to deal with derivatives, but that’s not the point right
now).
Note that the derivative (tangent) of a curve in 3D is a 3D vector, indicating the direction of
the curve at this moment. (This follows directly from the fact that if you subtract two points, you
get the vector between them. The derivative of a curve is simply the limit as the points you’re
subtracting become infinitely close to each other.)
8
p1
p2
p(u)
p0 p3
Figure 5: The Bézier control points determine the vectors used in the Hermite method.
8 Bézier Curves
The Bézier curve is based on the Hermite, but instead of using vectors, we use two control points.
Those control points are not interpolated though: they exist only in order to define the vectors for
the Hermite, as follows:
p1 − p0
p′ (0) = = 3(p1 − p0 )
1/3
p3 − p2
p′ (1) = = 3(p3 − p2 )
1/3
That is, the derivative vector at the beginning is just three times the vector from the first control
point to the second, and similarly for the other vector. Figure 5 shows the derivative vectors and
the Bézier control points.
9
1
0.8
0.6
0.4
0.2
0
0 0.2 0.4 0.6 0.8 1
Like the Hermite, Bézier curves are easily joined up. We can easily get continuity through a
joint by making sure that the last two control points of the first curve line up with the first two
control points of the next. Even better, the interior control points should be equally distant from
the joint. This ensures that the derivatives are equal and not just proportional.
The blending functions are especially nice, as seen in equation 3. (In that equation we are using
u as the parameter instead of t.)
(1 − u)3
3u(1 − u)2
b(u) = MTB u = (3)
3u2 (1 − u)
u3
The functions in eq 3, which are plotted in figure 6, are from the Bernstein polynomials:
!
d k
bkd (u) = u (1 − u)d−k
k
• Be bounded by 1
• Sum to 1
Perfect for mixing! Thus, our Bezier curve is a weighted sum, so geometrically, all points must lie
within the convex hull of the control points, as shown in the following figure:
p1
p2
p(u)
p0 p3
10
To be concrete, let’s take an example of the Bézier blending functions. For example, what is
the midpoint of a Bézier curve? The midpoint is at a parameter value of u = 0.5. Evaluating the
four functions in equation 3, we get:
(1 − 21 )3
1/8 1
1 1 3 12 (1 − 21 )2 3/8 1 3
b( ) = MTB =
= = (4)
2
2 3 12 (1 − 21 ) 3/8 8 3
2
13 1/8 1
2
Thus, to find the coordinates of the midpoint of a Bezier curve, we only need to compute this
weighted combination of the control points. Essentially:
1 3 3 1
P (0.5) = P0 + P1 + P2 + P3
8 8 8 8
= (P0 + 3P1 + 3P2 + P3 )/8
It turns out that there’s a very nice recursive algorithm for computing every point on the curve
by successively dividing it in half like this.
• normals
• colors
• texture coordinates
They all work the same way. For example, if we specify four control points, we can have OpenGL
compute any point on the curve, and if we specify four colors (one for each of those points), we
can have OpenGL compute the color of the associated point, using the same blending functions.
Each of these is called an evaluator. You can have multiple evaluators active at once, say for
vertices and colors.
The basic OpenGL functions for curves are (in C):
glMap1f(target, u_min, u_max, stride, order, point_array);
glEnable(target);
glEvalCoord1f(u);
And in Python, because Python will figure out the stride for us:
glMap1f(target, u_min, u_max, point_array);
glEnable(target);
glEvalCoord1f(u);
11
The first two are setup functions. The first, glMap1f(), is how we specify all the control
information (points, colors, or whatever). The target is the kind of control information you are
specifying and the kind of information you want generated, such as:
• GL_MAP1_VERTEX_3, a vertex in 3D
The u_min and u_max arguments are just the min and max paramater, so they are typically 0 and
1, though if you just want a piece out of the curve, you can use different values. For example, to
get the middle fifth of the curve, you could give u_min=0.4 and u_max=0.6.
The stride is a complicated thing that we’ll talk about below. The order is one more than the
degree of the polynomial (4 for a cubic) and is therefore equal to the number of control points we
are supplying. Since Python knows how long its arrays are, it can count that for you. Finally, an
array of the control information: vertices, RGBA values or whatever. They should be of the same
type as the target.
The second function, glEnable(), simply enables the evaluator; you can enable and disable
them like lights or textures.
Finally, the last function, glEvalCoord1f(), replaces functions like glVertexf(), glColorf()
and glNormalf(), depending on the target. In other words, that’s the one that actually calculates
a point on the curve or its color or whatever, and sends it down the pipeline. Here’s an example of
how you might do 100 evenly spaced steps on a curve:
glBegin(GL_LINE_STRIP)
for i in range(101):
glEvalCoord1f(i/100.0) # instead of glVertex3f
glEnd()
If you know that you just want evenly spaced steps (which is often the case), you can use the
following two functions instead of calls to glEvalCoord.
glMapGrid1f(steps,u_min,u_max)
glEvalMesh1(GL_LINE,start,stop)
This is a grid of ‘steps’ (say 100), from the minimum u to the maximum u (typically 0.0 to 1.0, but
not necessarily). The second actually evals the mesh from ‘start’ (typically 0) to ‘stop’ (typically
the same as ‘steps’).
The demo ˜cs307/public_html/demos/curves-and-surfaces/FunkyCurve.
cc shows a particular curve that starts and ends along the edges of a box. The code is relatively
terse, and is well worth looking at. See figure 7 on page 13.
""" This program displays a funky curve through the unit cube. This
program aims to be as simple as possible.
Scott D. Anderson
Fall 2003
Fall 2009, ported to Python
"""
import sys
from TW import *
### ================================================================
ShowControlPoints = False
def draw_funky_curve():
curveCP = ((-1,-1,-1,),
(+0.7,-1,-1),
(1,-0.7,1),
(1,1,1))
glPushAttrib(GL_ALL_ATTRIB_BITS);
if ShowControlPoints:
glPointSize(5)
twColorName(TW_CYAN)
glBegin(GL_POINTS)
for cp in curveCP:
glVertex3fv(cp)
glEnd()
glLineWidth(3)
twColorName(TW_YELLOW)
twDrawBezierCurve(curveCP,8)
glPopAttrib()
def display():
twDisplayInit()
twCamera()
draw_funky_curve()
glFlush()
glutSwapBuffers()
# ================================================================
def main():
glutInit(sys.argv)
glutInitDisplayMode( GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH)
twBoundingBox(-1,+1,-1,+1,-1,+1)
twInitWindowSize(500,500)
glutCreateWindow(sys.argv[0])
glutDisplayFunc(display)
twMainInit()
twKeyCallback(’c’,toggles,"toggle showing control points")
glutMainLoop()
if __name__ == ’__main__’:
main()
13
9.1 Strides
What the heck is a stride? Since the control point data is given to OpenGL in one flat array, the
stride is the number of elements to skip to get from one row/column to the next row/column.
For curves, the stride is almost always 3, because the elements are 3-place coordinates consec-
utive in memory. If you’re specifying colors, the stride will be 4, because the elements are 4-place
RGBA values consecutive in memory. The stride becomes more complicated when we deal with
2D surfaces instead of 1D curves.
And similarly for y and z. Since there are 16 coefficients, need 16 points on the patch.
y y y
p(umax) u=1
p(u, v)
v=0
p(umin) v=1
p(u)
x x u=0 x
z z z
Note that the four edges of a Bézier surface are Bézier curves, which helps a great deal in
understanding how to control them. That helps us to understand 12 of the 16 control points.
The four interior points are hard to interpret. Concentrating on one corner: if it lies in the plane
determined by the three boundary points, the patch is locally flat. Otherwise, it tends to twist. This
is hard to picture. It is, of course, related to the double partial derivative of the curve:
∂2p
(0, 0) = 9(p00 − p01 + p10 − p11 ) (5)
∂u∂v
14
11 Bezier Surfaces in OpenGL
To handle surfaces, we just convert the OpenGL functions from section 9 above to 2D. The basic
functions are:
And in Python, because Python will figure out the strides for us:
Here, it’s even more common to let OpenGL do the work of generating all the points:
glMapGrid2f(u_steps,u_min,u_max,v_steps,v_min,v_max)
glEvalMesh2(GL_FILL,u_start,u_stop,v_start,v_stop)
12 Texture Mapping
If you’re doing texture mapping, you can have OpenGL calculate texture coordinates for you at
each of the surface points, just by setting up and enabling that target:
Because texture-mapping is 2D, the texture coordinates are often generated by a linear Bézier
function, such as:
glMap2f(GL_MAP2_TEXTURE_COORD_2, 0, 1, 0, 1,
(((0,0), (0,1)),
((1,0), (1,1))))
glEnable(GL_MAP2_TEXTURE_COORD_2)
Here the texture coordinates (0,0) will correspond to the first spatial corner, and the (s, t)
texture coordinates will equal the (u, v) Bézier parameters.
13 Normal Vectors
If you want to use lighting on a surface, you have to generate normals as well. You can do it
yourself using
15
D C
U
A B
This works because the u dimension is what we would call “left to right” and so we give the
points as A then B and D then C (with a u-stride of 3). The v dimension is what we would call
“bottom to top” and so we give the points as A then D and B then C (with a v-stride of 6). The u
dimension nests “inside” the v dimension, and so the control points are given in the order ABDC.
The right-hand-rule tells us that “left-to-right” crossed with “bottom-to-top” yields a vector that
points towards us.
Of course, in that example, we decided to give the lower left corner (vertex A) first. If we
decided to have the upper left corner first (vertex D), and we still wanted to have the surface normal
face towards us, we would give the control points in the order DACB, because the u dimension is
16
determined by DA and CB, which is “top to bottom” and the v dimension is “left to right” so AB
and DC (as before).
You can have any control point first and still determine the surface normal. An example of this
coding can be seen in
˜cs307/public_html/pytw/demos/curves-and-surfaces/Normals.py
14 Demos
We’ll look at several demos of how to do Bézier curves. All of the following are in public_
html/pytw/demos/curves-and-surfaces.
• FunkyCurve.py We put the code for this above. Note how the curve control points are
defined and drawn. The curve isn’t very smooth. How can we make it smoother? Notice the
stride.
• CokeSilhouette.py This draws does two three-part curves, reflected across the y axis.
– See how we ensure that the curves line up properly at the joints.
– Note the reflection code and the alternative using glScalef.
– We could still use affine transformations to place the silhouette anywhere we want.
– Look at how the arrays are defined.
• Dome.cc This program shows a nicely symmetrical dome, with an interface that lets you
modify one of the points. The code needs to be updated to use TW, but the code might still
be interesting, particularly the way we take one curve and turn it into a symmetrical dome.
– Before we look at this, we’ll discuss circular arcs, described in section 15, below.
– Look at how the arrays are defined and used, and contrast this with CokeSilhouette.
py.
– Look at how lighting and normals are done.
17
First, note that we can’t do a full circle with one curve, because the first and last control points
would be the same and the bounding region would have no area.
So, let’s try a half-circle. Observe that the requirement that the curve is tangent to the line be-
tween the first two control points and between the last two control points, together with symmetry,
gives us:
(−1,a) (1,a)
(−1,0) (1,0)
Using the formula for the midpoint of the curve, in equation 4, we get:
1 = (0 + 3a + 3a + 0)/8
= 6a/8
a = 4/3
Trying this value, however, yields an approximation that isn’t really good enough:
(1,a)
(1,0)
Using the formula for the midpoint of the curve again, we get:
" √ # " # " # " # " #
√ 2/2 0 a 1 1
= ( +3 +3 + )/8
2/2 1 1 a 0
" √ # " #
4 √2 4 + 3a
=
4 2 4 + 3a
√
4( 2 − 1) = 3a
√
4( 2 − 1)
a = ≈ 0.55
3
Trying this value yields a pretty good approximation:
18
We can then get the rest of the circle with rotations.
19