December 96 - New QuickDraw 3D Geometries
New QuickDraw 3D Geometries
Philip J. Schneider
A number of new QuickDraw 3D geometric primitives can save you
time as you create 3D objects -- from footballs to the onion domes
of the Taj Mahal. Most of these new primitives are very versatile,
but this versatility comes at the cost of some complexity. Here
you'll find a discussion of their various features and uses, with
special attention to the differences among the features and
structural characteristics of the polyhedral primitives. Being
aware of these differences will help you make the right choices
when you're using these primitives in a particular application.
When QuickDraw 3D version 1.0 made its debut, it came with 12 geometric
primitives that you could use to model pretty much anything you wanted. With applied
cleverness, you could make arbitrary shapes by combining and manipulating such
primitives as polylines, polygons, parametric curves and surfaces, and polyhedra.
Because some shapes are so commonly used, recent versions of QuickDraw 3D have
added them as high-level primitives, including two new polyhedral primitives. This
frees each developer from having to reinvent them and ensures that the new
primitives are implemented in such a way as to fit nicely with the existing paradigm
in QuickDraw 3D.
We'll start by looking at how the new ellipse primitive was designed. A similar
paradigm was used in creating most of the other new high-level primitives.
Understanding their design will help you use them effectively. Later, we'll move on to
the two new polyhedral primitives -- the polyhedron and the trimesh -- which you
can use to model very complex objects. We'll also take a fresh look at the mesh and
trigrid, which have been around for a while, and compare the usefulness of all four
polyhedral primitives. Along the way, you'll find some relevant background
information about the QuickDraw 3D team's design philosophy.
I'm going to assume that you're already familiar with the capabilities of QuickDraw
3D, including how to use the original 12 geometric primitives. But if you want more
basic information, see the articles "QuickDraw 3D: A New Dimension for Macintosh
Graphics" in develop Issue 22 and "The Basics of QuickDraw 3D Geometries" in Issue
23. The book 3D Graphics Programming With QuickDraw 3D has complete
documentation for the QuickDraw 3D programming interfaces for version 1.0. Version
1.5 of QuickDraw 3D, which supports these new primitives, is now available.
To get you started using the new primitives, the code listings shown here accompany
this article on this issue's CD and develop's Web site.
Aficionados of QuickDraw 3D use a variety of terms to refer to a
geometric primitive. But a geometric primitive by any other name (primitive
geometric shape, basic geometric object, geometric primitive object, or
geometry) is still a geometric primitive.*
CONICS, QUADRICS, AND QUARTICS
One category of geometric primitives is conics, quadrics, and quartics; this class
includes such shapes as ellipses, disks, ellipsoids (the generalization of spheres),
cones, cylinders, and tori (doughnuts). Each of these shapes is a recently introduced
primitive that's defined with a paradigm similar to the one already used in the box
primitive. I'll begin by explaining how the ellipse primitive works because the same
basic approach is used for the more complex geometries.
ELLIPSES
My article "NURB Curves: A Guide for the Uninitiated" in develop Issue 25 describes
how you can make circles and partial circles with NURB curves. Though you can
further use NURB curves to make ellipses and elliptical arcs by manipulating the
locations of the control points, this isn't necessarily the most convenient way to do it.
So QuickDraw 3D now provides an ellipse primitive. The data structure for the ellipse
is as follows:
typedef struct TQ3EllipseData {
TQ3Point3D origin;
TQ3Vector3D majorRadius;
TQ3Vector3D minorRadius;
float uMin, uMax;
TQ3AttributeSet ellipseAttributeSet;
} TQ3EllipseData;
Let's assume we have a variable declared like this:
TQ3EllipseData ellipseData;
As we go over the ellipse primitive, I'll explain the various fields in the data
structure, then fill them in as I tell you how they work. Let's take for our example a
special case of an ellipse -- a circle of radius 2 that lies in the {x, y} plane, with an
origin at {3, 2, 0} -- and show how we'd define it in QuickDraw 3D. For starters, a
circle must have a center. One way QuickDraw 3D could do this is always center the
circle at the point {0, 0, 0} and then have us translate the circle to the desired
location. However, it seems a bit odd to be able to make, say, a line with arbitrary
endpoints, but not be able to make a circle with an arbitrary center. So, as shown in
Figure 1, a QuickDraw 3D circle follows the paradigm for primitives and has an
explicit center, called the origin in the data structure:
Q3Point3D_Set(&ellipseData.origin, 3, 2, 0);
Figure 1. Defining a circle's origin, size, and plane
Of course, circles must have a size. Again, QuickDraw 3D could make all circles a unit
size (that is, have a radius of 1) and then require us to scale them appropriately. But,
for the same reason that the circle has an explicit center, it has an explicit size.
Given an origin and size, we have to specify the plane in which the circle lies in 3D
space. Though it would be possible for QuickDraw 3D to define a circle's plane by
default -- say, the {x, z} plane -- and require us to rotate the circle into the desired
plane, QuickDraw 3D lets us define the radius with a vector whose length is the radius.
Then we similarly define a second radius perpendicular to the first radius. The cross
product of these two vectors (majorRadius and minorRadius) defines the plane the
ellipse lies in:
Q3Vector3D_Set(&ellipseData.majorRadius, 2, 0, 0);
Q3Vector3D_Set(&ellipseData.minorRadius, 0, 2, 0);
In other words, the plane the circle lies in passes through the origin of the circle, and
the cross product of the majorRadius and minorRadius vectors is perpendicular to the
plane (see Figure 1).
For a full circle, we need to set uMin to 0 and uMax to 1 (more on this later):
ellipseData.uMin = 0;
ellipseData.uMax = 1;
As for the final field in the data structure, ellipseAttributeSet, QuickDraw 3D includes
this field so that we can, for instance, make screaming yellow ellipses:
ellipseData.ellipseAttributeSet = Q3AttributeSet_New();
Q3ColorRGB_Set(&color, 1, 1, 0);
Q3AttributeSet_Add(ellipseData.ellipseAttributeSet,
kQ3AttributeTypeDiffuseColor, &color);
Finally, we create an ellipse object that describes the circle:
ellipse = Q3Ellipse_New(&ellipseData);
Or we can use the data structure in a Submit call in immediate mode (for rendering,
bounding, picking, or writing):
Q3Ellipse_Submit(&ellipseData, view);
The ellipse comes with the usual array of calls for getting and setting individual
definitional properties, as well as the entire definition, just like the other primitives.
Why all of this power just for a circle? This power gives us flexibility. Let's use some
of the object-editing calls to make some more interesting shapes. We'll start by
making an ellipse out of the circle we've just constructed. If you recall, we originally
made the circle with majorRadius and minorRadius equal to 2. So to make an ellipse
instead of a mere circle, all we have to do is make majorRadius and minorRadius
different lengths. To get the first ellipse you see in Figure 2, we can use this:
Q3Vector3D_Set(&vector, 0, 1, 0);
Q3Ellipse_SetMinorRadius(ellipse, &vector);
"But wait," you say, "vectors have direction as well as size!" Well, we can get really
carried away and make the two defining vectors nonperpendicular to get something like
the second ellipse shown in Figure 2:
Q3Vector3D_Set(&vector, 1, 2, 0);
Q3Ellipse_SetMinorRadius(ellipse, &vector);
Figure 2. Defining a regular and skewed ellipse
All we have left is how to define partial ellipses. We can do this by taking a parametric
approach. Let's say that an ellipse starts at u = 0 and goes to u = 1. Then we have to
define the starting point. Let's make it be the same point that's at the end of the vector
defining majorRadius in the first circle in Figure 3. To make a partial ellipse (that is,
an elliptical or circular arc), we specify the parametric values of the starting and
ending points for the arc:
Q3Ellipse_SetParameterLimits(ellipse, 0.05, 0.3);
Figure 3. Defining a partial ellipse
This gives us an elliptical (or in this case, circular) arc, as shown in Figure 3. (The
dotted line isn't actually rendered -- it's just there for diagrammatic reasons.)
Though the starting and ending points must be between 0 and 1, inclusive, we can make
the starting point have a greater value than the ending point:
Q3Ellipse_SetParameterLimits(ellipse, 0.875, 0.125);
As you can also see in Figure 3, this allows us to "wrap around" the point u = 0.
In version 1.5 of QuickDraw 3D, the feature for defining partials is not
enabled, so until it is, you must set the minimum and maximum parameter
limits to 0 and 1, respectively.*
About now, you're probably thinking that all this power and flexibility for just a
simple ellipse is overkill and that the preceding explanation is overkill, too. Sorry
about that, but there is a reason -- it turns out that we can take this same approach to
defining disks, ellipsoids, cones, cylinders, and tori.
DISKS
If you go back over the past few pages and substitute disk for ellipse, you pretty much
get everything you need to know. The data structure and functionality are analogous,
except that disks are filled primitives, like polygons, while ellipses are curvilinear
primitives, like polylines. So, partial disks are like pie slices rather than arcs. The
only other difference is that since disks are surfaces rather than curves, they have
parameters in two directions. Figure 4 illustrates the definition of a disk, including
the U and V parameters.
Figure 4. Defining a disk
Note that the UV surface parameterization for the disk is different from the
parametric limit values around the perimeter of the disk. The UV surface
parameterization was chosen so that an image applied as a texture would appear on the
disk or end cap as if it were applied as a decal. The values associated with positions
around the perimeter are used for making partial disks, just as we used them to make
partial ellipses. The distinct parametric limit values (uMin and uMax) are necessary
so that the partial end caps on partial cones and cylinders will properly match. If the
surface parameterization for the disk meant that the U direction went around the
perimeter, you'd have a nearly impossible time applying decal-like textures.
ELLIPSOIDS, CONES, CYLINDERS, AND TORI
Now, I want you to hold two thoughts in your head at the same time: recall that the box
primitive is defined by an origin and three vectors, which define the lengths and
orientations of the edges of the box, and then think about the definition of the ellipse.
Doing that, you should be able to imagine how we define, say, a sphere -- we just add
another vector to the definition of the ellipse!
Figure 5 shows how an ellipsoid (a sphere), cone, cylinder, and torus are defined with
respect to an origin and three vectors (the labels being fields in the corresponding data
structures). Note that the torus requires one more piece of information to allow for
elliptical cross sections: the ratio between the length of the orientation vector (which
gives the radius of the "tube" of the torus in the orientation direction) and the radius
of the tube of the torus in the majorRadius direction. With the resulting torus
primitive, you can make a circular torus with an elliptical cross section, or an
elliptical torus with a circular cross section, or an elliptical torus with an elliptical
cross section. (Hmm...perhaps I was drinking too much coffee when I designed the
torus.)
Figure 5. Creating four primitive objects and applying texture
You use the U and V parameters to map a texture onto a shape. In Figure 5, the U and V
parameters have their origins and orientations relative to the surface in what should
be the most intuitive layout. If you apply a texture to the object, the image appears as
most people would expect.
• For the ellipsoid, the parametric origin is at the south pole, with V going
upward toward the north pole, and U going around the axis in a
counterclockwise direction (when viewed from the north pole).
• For the cone and cylinder, the parametric origin is on the bottom edge, at
the point where the majorRadius vector ends. V goes up while U goes around in
the direction shown by the arrows. The bottom of the cone, and the top and
bottom of the cylinder, are parameterized exactly like the disk.
• For the torus, the parametric origin is located at the point on the inner
edge where the majorRadius goes through it. V goes around as shown by the
arrow, and U goes around the "outside" of the entire torus.
By changing the relative lengths of the majorRadius, minorRadius, and orientation
vectors, you can get ellipsoids, cones, cylinders, and tori with elliptical cross
sections, similar to how we made a circle into an ellipse earlier.
So to make an ellipsoid that's a sphere, you make the majorRadius, minorRadius, and
orientation vectors the same length as well as mutually perpendicular. To make an
elliptical cylinder, you can vary the lengths of the three vectors. Even more fun can be
had by making the vectors nonperpendicular -- this makes skewed or sheared objects.
This is easy to see with a cylinder (Figure 6).
Figure 6. Creating an elliptical or sheared cylinder
You can make partial disks, cones, cylinders, and tori in a fashion analogous to what we
did with the ellipse (see Figure 7). Since these are surfaces, you can set a minimum
and maximum for each direction.
Figure 7. A partial cylinder and cone
One important thing to notice is that the "wraparound" effect I showed with the ellipse,
by making uMin be greater than uMax, is possible with all the other primitives in this
category, but the equivalent feature in the V direction is possible only with the torus.
For example, the cone wraps around naturally in the U direction because the face itself
is one continuous surface in that direction, but the surface doesn't wrap in the V
direction.
Some of you must be wondering what we can do with the ends of cones and cylinders. Do
we want them left open so that the cones look like dunce caps and the cylinders look
like tubes? Or do we want them to be closed so that they appear as if they were true
solid objects? You may have already wondered about a similar issue when we used the
uMax and uMin parameter values to cut away part of the object. Do we make a sphere
look like a hollow ball, or like a solid ball that's been cut into?
To take care of these issues, the ellipsoid, cone, cylinder, and torus have an extra field
in their data structures that you can use to tell the system which of these end caps to
draw:
typedef enum TQ3EndCapMasks {
kQ3EndCapNone = 0,
kQ3EndCapMaskTop = 1 << 0,
kQ3EndCapMaskBottom = 1 << 1,
kQ3EndCapMaskInterior = 1 << 2
} TQ3EndCapMasks;