OpenGL and OS/2
All About NURBS
Written by Perry Newhook
Well here we are again with another month of OpenGL programming. As promised, this month we describe NURBS, what they are for, and most importantly, how to use them. I hope everyone benefitted from last month's column. In it we described the different toolkits available to OpenGL programmers such as yourselves, so that you don't have to rely on any one compiler (or platform for that matter,) to create your applications.
Ok, here we go. Before getting into the OpenGL implementation of NURBS, I will give a bit of background as to what they actually are. Since this is almost impossible without at least a bit of math, those who are not mathematically inclined can simply skip to the next section. Even if you don't like the math, this section will be useful because to create the effect that you want, you have to understand at least a bit of what is going on underneath. However, since this is not a computer science course, I will try to keep the math to only what is necessary.
NURBS stands for Non-Uniform Rational B-Spline. B-Spline is derived from the term basis spline. (Don't you love it when acronyms contain acronyms? If you do, come join the space program.) Now to understand this fully, let's go all the way back and describe the simple curve.
If you recall our OpenGL primitives, you may notice that a curve is not one of them. All we can really do in OpenGL is draw points, and line segments; all other OpenGL objects are made up from them (i.e. a triangle is simply three lines connected together and filled in. A polygon is an n-sided "triangle".) The curves that we have encountered so far are simply segmented with lines; the shorter the line segments the more accurate the curve. You would have noticed this with our previous examples when we used a wire cylinder and wire sphere.
If we wanted a really accurate curve so that we could not visually see the individual segments, we would need to specify a lot of individual line segments. If we wanted to show a smooth surface, we would have a large number of very small polygons. This would quickly get tedious and impractical for large scenes. Many curves and surfaces, however, can instead be described by a small number of parameters in a mathematical formula. A third order function would describe a unique curve with only three parameters. Such a surface could be described with nine parameters, and take up much less storage space than the equivalent surface described with polygons. The polygon surface would also only approximate the surface (because at some scale it still consists of flat polygons) while the mathematical version is perfectly accurate. In OpenGL we use evaluators to specify the points on the curve using only the control points.
OpenGL evaluators always make splines and surfaces based on a Bezier basis. If you create evaluators that use some other basis, they must be converted to a Bezier basis before you can use them with OpenGL. What is a Bezier function? Well, I'm glad you asked.
A Bezier curve is a vector based function of one variable in the form:
and a Bezier surface is a vector valued function of two variables:
For each u (and v for surfaces) C() and S() calculates a point on the curve or surface. To use an evaluator, define and enable a function C() or S() that you wish to use, and where you normally call glVertex(), call glEvalCoord1() or glEvalCoord2() instead.
The definition of the evaluator is done with the functions glMap1() for a one dimensional evaluator (a line) or glMap2() for a two dimensional evaluator (a surface). glMap1() uses the following equations that define a curve:
represents a Bernstein polynomial of degree n. Now if Pi represents a set of control points then:
represents a Bezier curve as u varies from 0 to 1. To allow the same curve to vary from u1 to u2 instead of 0 to 1, evaluate:
which is what is used by glMap1().
glMap1() is defined as follows:
Example code of how to use the above could be:
in the above, ctrlPoints would be a pointer to an array of four XYZ points.
Two dimensional evaluators are very similar, except where before we had
just u, we now have u and v. This defines a surface instead of just a
line. Instead of glMap1() and glEvalCoord1(), we call glMap2() and
glEvalCoord2(). glMap2() has the following parameters:
the parameters mean exactly what they meant with glMap1() but now we have independent settings for both u and v.
GLU NURBS Interface
Instead of using evaluators directly, most applications use the NURBS interface provided by the OpenGL utility library. This interface removes much of the math from creating NURBS and hides it behind a few very simple to use functions.
Before calling any of the NURBS functions, we have to tell OpenGL that
we are going to create a NURB. To do this simply call the function
Every call to this function returns a pointer to a GLUnurbsObj, that must be used with each of the other NURBS interface functions. In this manner we can create any number of individual NURBS patches, and set properties for each independently.
To set the properties, we call (duh), gluNurbsProperty().
property can be one of the following
For our example we will set the tolerance down to 25, and specify that
the nurbs should be filled in.
Now that we have the NURB set up, we just have to call the code that actually displays it. All NURB drawing functions have to be surrounded by a gluBeginSurface(), gluEndSurface() pair. The only parameter that these functions take is the pointer to the NURB object returned above.
The surface is actually drawn with the function gluNurbsSurface().
If you wish to draw a curved line instead of a surface, you can call
gluNurbsCurve(). The parameters have the same meaning as gluNurbsSurface()
except there is only a u direction because it is one-dimensional.
For an example that has 26 knots and thirteen control points in each of
the u and v directions, the code could look like:
Complete source code fo a simple example can be downloaded here. For this sample we used the control points shown in the following image:
As we stated before, we can vary the quality of the rendered image by varying the NURB sampling tolerance. The following images show the resulting NURB in mesh and in solid form for varying tolerance values.
GLU_SAMPLING_TOLERANCE of 5:
GLU_SAMPLING_TOLERANCE of 25:
GLU_SAMPLING_TOLERANCE of 100:
While a NURBS patch may follow the desired shape, the patch itself may extend outside of the desired render area. This is because the NURBS patch can be thought of as a rectangular rubber sheet stretched over the control points. While the sheet does stretch to the correct shape, the edges are still rectangular. The NURBS interface allows you to trim away any section, or multiple sections that are unwanted, leaving only the desired surface.
Trimming is performed by specifying a gluNurbsCurve() or a gluPwlCurve(). Since gluNurbsCurve() was described above, I will describe gluPwlCurve() here.
gluPwlCurve() creates a piecewise linear trimming curve for the NURBS
A trimming curve has to be surrounded by a gluBeginTrim(), gluEndTrim() pair, and because the trimming commands operate on the nurb, the commands must occur between the gluBeginSurface() and gluEndSurface() pair as well.
For the surface above, if we picked eight values for a trim curve, the
resulting code could look like:
To determine what is included and what is thrown away, simply remember that everything to the left is kept and everything to the right is trimmed. Curves have to be closed and if multiple curves are specified, then they cannot intersect. You can cut multiple holes out, and you can also create islands within holes by creating a curve in the opposite direction within a trimmed out section.
The complete code that shows trimming can be downloaded here .
Well that's it for this month. Be sure to check back again next time when we create a full application from start to finish, one that I'm sure you'll all enjoy.