QuickDraw 3D is a Code Fragment Manager-based shared library, with a C-based API. Here we'll cover some concepts you need to know to get basic QuickDraw 3D support into your application. This issue's CD contains a prerelease version of the QuickDraw 3D shared library, the 3D Viewer shared library, programming interfaces, preliminary Inside Macintosh: QuickDraw 3D documentation, sample code, utility libraries, and other goodies. Two of the sample programs are discussed in this article.
QuickDraw 3D comes with a set of human interface guidelines to foster the adoption of a consistent look and feel between applications (see "The QuickDraw 3D Human Interface"). 3D applications today are geared toward the trained 3D expert; what you learn in one application is generally not transferable to another application. By following the QuickDraw 3D human interface guidelines, however, developers can help make 3D graphics an integral part of the user experience within their applications.
QuickDraw 3D provides human interface guidelines (in version 1.0) and a toolkit for implementing the guidelines (to come in the second major release). A sample application on this issue's CD illustrates our current ideas for a 3D human interface. By getting a preview of our plans, you can start taking your applications along the common path.
Our main goal is to provide integration into the Macintosh experience. We feel that 3D graphics will be the next popular multimedia data type -- in the way that 2D graphics, sound, and movies have been in the past -- and users will want to incorporate 3D data into their documents in the same way that they can now incorporate other multimedia data types. To do this they'll need an interaction model built on the 2D principles that they're familiar with.
Our guidelines offer suggestions and examples of how things can be done. If your applications are targeted for a very specific audience, and you know that audience well, you may decide to communicate with them in a different way, and that's perfectly OK.
One of our guidelines, about direct manipulation through the use of a widget, is illustrated in Figure 1. Here we've appropriated the 2D grab handles that are popular in many "draw" programs and extended them to 3D. A widget is a set of handles for control of spatial parameters. Some widgets, such as the scale tool shown in Figure 1, indicate selection of a shape, while others make an invisible object, such as a light or a camera, visible.
Figure 1. A scaling widget
Figure 2 shows what a full-featured 3D application might look like. The emphasis here is on what's the same as in 2D applications rather than on what's unique. The illustration shows a shape selected with a rotation widget, a material selection palette, a room metaphor, and a document containing multiple views of a scene.
Figure 2. Conceptual sketch of a 3D application>
Unlike some libraries, QuickDraw 3D will be able to take advantage of a number of 3D hardware acceleration solutions, since acceleration was one of its design criteria. Another important criterion was cross-platform support. For example, a renderer could be written to take advantage of low-level 3D libraries, such as the Silicon Graphics OpenGL graphics library.
Figure 3. Dinosaur mesh mapped with a skin-like texture
Figure 4. QuickDraw 3D architecture
Let's take a quick look at each of these functional areas, which we'll expand on later. Here we'll use the word scene to describe not only the objects being modeled, but also the lighting, camera settings, shaders, and other entities that affect the final appearance on output devices.
Widgets are used to enhance the user experience for 3D applications. For example, to allow the user to interact with an object, the application can draw grab handles, in the form of a translation widget, to allow the object to be manipulated.
Geometries are the encapsulation of data used to describe an object. Some geometries are provided as part of QuickDraw 3D, resulting in a very concise representation; for more information, see "QuickDraw 3D Geometries." (QuickDraw 3D uses geometries to draw widgets.)
In addition, the following geometries are planned for the second major release of QuickDraw 3D: torus, ellipse, ellipsoid, disk, cylinder, cone, and triangle strip. (In version 1.0, you can create any of these geometries by representing them as meshes.)
Where applicable, the geometries are parameterized so that they're ready for texture mapping or other shading effects.
Picking is used to determine which object a user chose. QuickDraw 3D's picking facilities are more extensive than in other 3D libraries, not only providing several different types but also returning quite a bit of information to the application beyond whether a hit took place.
Light objects supply the lighting for a scene. QuickDraw 3D provides four types of light sources: ambient, directional, point, and spot. Based on the light sources for a given scene and the illumination shader, the renderer makes intensity calculations for each object's surface and vertex contained in the scene.
Camera objects define a point of view into a particular scene. QuickDraw 3D provides three different camera types: view angle, orthographic, and view plane.
Attributes are used to specify different characteristics for each object (or parts of an object, such as its vertices or faces), and also to attach custom data to an object.
Shaders are used to modify or add data, on either a per vertex or a per pixel basis, as geometries are being processed by the renderer -- for example, illumination and texturing shaders.
Renderers are the business end of QuickDraw 3D. A renderer is a set of routines used to create a shaded synthetic model of the scene, based on the information stored in the geometry and taking into account the lighting, surface attributes, and camera location. QuickDraw 3D provides two basic renderers: a wireframe and an interactive renderer. You can extend QuickDraw 3D by writing a plug-in renderer, developing an accelerator card, or implementing a combination of both -- a renderer tied to a particular hardware setup.
Figure 5. Viewer implementation in the Scrapbook
We'll now look at how your application can create and use a QuickDraw 3D Viewer object. In the application named Simple 3D Viewer on this issue's CD, we create a window in which the only object is a Viewer.
viewerObj = Q3ViewerNew((CGrafPtr)theWindow, &theRect, 0L);This function takes a WindowPtr, a pointer to a Rect that describes the window area where you want the 3D scene to appear, and a long word containing flags for modifying the behavior of the Viewer. When you're finished with the Viewer, you need to dispose of it with the Q3ViewerDispose function:
Q3ViewerDispose(viewerObj);
Q3ViewerUseFile(viewerObj, fileRefNum);Q3ViewerUseFile takes a reference to the Viewer object and a file reference to a previously opened QuickDraw 3D metafile. You can also display data from the Clipboard or data you created yourself, with the Q3ViewerUseData function:
Q3ViewerUseData(viewerObj, myDataPtr, myDataSize);This function takes a reference to a Viewer object, a pointer to the data, and the size of the data in bytes. The data must be in metafile format.
wasViewerEvent = Q3ViewerEvent(viewerObj, theEvent);Q3ViewerEvent takes a reference to a Viewer object and a pointer to an event record (usually obtained from WaitNextEvent). This function allows the Viewer to respond to events, such as a mouse-down event in one of its controls. It returns a value of type Boolean that indicates whether the event was handled.
If the area occupied by the Viewer needs to be updated, you need to redraw the data in your update event handler by calling Q3ViewerDraw:
theErr = Q3ViewerDraw(viewerObj);
Error checking may seem like a weird place to start, but checking and responding to what QuickDraw 3D is trying to tell you will save a great deal of trouble and strife during development. The QuickDraw 3D error manager provides several levels of error checking along with functions for checking the last error that occurred. The error checking, which is similar to that in QuickDraw GX, has three levels: errors, warnings, and notices.
TQ3Boolean Q3Error_IsFatalError(TQ3Error theError);For a complete list of errors provided by QuickDraw 3D, look in the QuickDraw 3D header files.
Listing 1. Error handler
static void MyErrorHandler(TQ3Error firstError, TQ3Error lastError, long refCon) { char buf[512]; sprintf(buf, "ERROR %d - %s\n", lastError, getErrorString(lastError)); // Get the error as a C string. if (gErrorFile == NULL) gErrorFile = fopen("error.output", "w+"); if (gErrorFile != NULL) fputs(buf, gErrorFile); }Once handlers have been defined, it's a snap to install them. For example, you would install the error handler defined in Listing 1 as follows:
Q3Error_Register(MyErrorHandler, 0L);
Listing 2. Initializing and closing the connection to the library
void Initialize3DStuff(void) { if (Q3Initialize() == kQ3Failure) { // Handle the error. StopAlert(kQD3DInitFailed); ExitToShell(); } MyErrorInit(); } void Exit3DStuff(void) { if (Q3Exit() == kQ3Failure) { // Handle the error. StopAlert(kQD3DExitFailed); ExitToShell(); } }When your application is about to quit, you should shut down your connection to the QuickDraw 3D library by calling Q3Exit, also shown in Listing 2. (Obviously a real application would have more sophisticated error handling here.)
Figure 6. A window from the Box sample program
struct _documentRecord {
TQ3ViewObject fView; // The view for the scene TQ3GroupObject fModel; // Object in scene being modeled TQ3StyleObject fInterpolation; // Style used when rendering TQ3StyleObject fBackFacing; // Whether to draw shapes that face // away from the camera TQ3StyleObject fFillStyle; // Drawn as solid filled objects or // decomposed to components TQ3Matrix4x4 fRotation; // The transform for the model }; typedef struct _documentRecord DocumentRec, *DocumentPtr, **DocumentHdl;We can create a new instance of this type, initialize it with the required values, and store a reference to it in each window's refCon field.
Listing 3. Creating four boxes
TQ3GroupObject MyNewModel() { TQ3GroupObject myGroup; TQ3GeometryObject myBox; TQ3BoxData myBoxData; TQ3GroupPosition myGroupPosition; TQ3ShaderObject myIlluminationShader; TQ3Vector3D translation; TQ3SetObject faces[6]; short face; // Create a group for the complete model. if ((myGroup = Q3DisplayGroup_New()) != NULL) { // Define a shading type for the group and add the shader to // the group. myIlluminationShader = Q3PhongIllumination_New(); Q3Group_AddObject(myGroup, myIlluminationShader); // Set up the colored faces for the box data. myBoxData.faceAttributeSet = faces; myBoxData.boxAttributeSet = nil; MyColorBoxFaces(&myBoxData); // Create the box itself. Q3Point3D_Set(&myBoxData.origin, 0, 0, 0) Q3Vector3D_Set(&myBoxData.orientation, 0, 1, 0); Q3Vector3D_Set(&myBoxData.majorAxis, 0, 0, 1); Q3Vector3D_Set(&myBoxData.minorAxis, 1, 0, 0); myBox = Q3Box_New(&myBoxData); // Put four references to the box into the group, each one with // its own translation. translation.x = 0; translation.y = 0; translation.z = 0; MyAddTransformedObjectToGroup(myGroup, myBox, &translation); translation.x = 2; translation.y = 0; translation.z = 0; MyAddTransformedObjectToGroup(myGroup, myBox, &translation); translation.x = 0; translation.y = 0; translation.z = -2; MyAddTransformedObjectToGroup(myGroup, myBox, &translation); translation.x = -2; translation.y = 0; translation.z = 0; MyAddTransformedObjectToGroup(myGroup, myBox, &translation); } // Dispose of the objects we created here. if (myIlluminationShader != NULL) Q3Object_Dispose(myIlluminationShader); for (face = 0; face < 6; face++) { if (myBoxData.faceAttributeSet[face] != NULL) Q3Object_Dispose(myBoxData.faceAttributeSet[face]); } if (myBox != NULL) Q3Object_Dispose(myBox); return myGroup; }Notice that we dispose of the boxes after adding them to the document group. QuickDraw 3D will create references to the boxes in the document group, so we can safely dispose of them. To be good QuickDraw 3D citizens and to make more effective use of memory, we need to dispose of each QuickDraw 3D object as soon as we're done with it. QuickDraw 3D keeps track of the reference count of each object to help detect memory leaks. If you're using the debugging version of QuickDraw 3D, it will tell you when you call Q3Exit if there are any objects remaining that need to be disposed of.
In immediate mode, the application keeps the only copy of the geometry. This is particularly useful when your application needs to reference data that's in a format different from the one used by QuickDraw 3D or when a large number of vertices that make up the geometry are being edited continuously -- for example, in the animation of a stress analysis for mechanical design.
The code in Listing 3 creates the boxes in retained mode, by creating objects that encapsulate the box data; QuickDraw 3D then manages the box data for us. If you want to add QuickDraw 3D rendering and drawing to an existing application with its own 3D data structures, you can draw in immediate mode instead. To draw a box in immediate mode, you simply initialize the values in the TQ3BoxData structure to the appropriate values and then draw the data directly in a rendering loop (described later) by calling the following function:
myStatus = Q3Box_Submit(&myBoxData);Because you never create a QuickDraw 3D object, there's no need to call Q3Object_Dispose.
This is where the concept of a draw context comes in. It's a means for QuickDraw 3D to interface with the host environment. There's a special draw context for the Mac OS, called a Macintosh draw context; information describing this context is stored in a TQ3MacDrawContext object, which contains the information necessary for QuickDraw 3D to image the data on a computer running the Mac OS.
Listing 4 is a routine from the Box application that creates a Macintosh draw context the size of a window that we pass in. We're telling QuickDraw 3D to create a buffer in which to image the data; this is referred to as the back buffer. If we're using double buffering (that is, we set the doubleBufferState field of the Macintosh draw context to true), the front buffer will be the window associated with the draw context. The data is copied from the back buffer to the front buffer when Q3View_EndRendering is called. This helps provide flicker-free animation if you're animating the object being viewed.
Listing 4. Creating a Macintosh draw context
TQ3DrawContextObject MyNewDrawContext(WindowPtr theWindow) { TQ3DrawContextData myDrawContextData; TQ3MacDrawContextData myMacDrawContextData; TQ3DrawContextObject myDrawContext; TQ3ColorRGB clearColor; Q3ColorRGB_Set(&clearColor, 1, 1, 1); myDrawContextData.clearImageState = kQ3True; myDrawContextData.clearImageMethod = kQ3ClearMethodWithColor; myDrawContextData.clearImageColor = clearColor; myDrawContextData.paneState = kQ3False; myDrawContextData.maskState = kQ3False; myDrawContextData.doubleBufferState = kQ3True; myMacDrawContextData.drawContextData = myDrawContextData; myMacDrawContextData.window = (CGrafPtr) theWindow; // The window // associated with the view myMacDrawContextData.library = kQ3Mac2DLibraryNone; myMacDrawContextData.viewPort = nil; myMacDrawContextData.grafPort = nil; // Create draw context and return it; if nil, caller must handle it. myDrawContext = Q3MacDrawContext_New(&myMacDrawContextData); return myDrawContext; }Sometimes you might want to be able to get at the back buffer yourself; for example, you might want to create a picture preview of some metafile data to place on the Clipboard along with the metafile data, so that applications that don't support metafiles can display the picture. QuickDraw 3D makes this possible by providing a different type of draw context, called a pixmap draw context, which can be based on a GWorld. First you need to create a GWorld the size of the window area; then you can create a pixmap draw context as shown in Listing 5.
Listing 5. Creating a pixmap draw context
TQ3DrawContextObject MyNewPixmapDrawContext(GWorldPtr theGWorld) { TQ3PixmapDrawContextData myPixmapDCData; TQ3ColorRGB clearColor; PixMapHandle hPixMap; Rect srcRect; Q3ColorRGB_Set(&clearColor, 1, 1, 1); // Fill in the draw context data. myPixmapDCData.drawContextData.clearImageState = kQ3True; myPixmapDCData.drawContextData.clearImageMethod = kQ3ClearMethodWithColor; myPixmapDCData.drawContextData.clearImageColor = clearColor; myPixmapDCData.drawContextData.paneState = kQ3False; myPixmapDCData.drawContextData.maskState = kQ3False; myPixmapDCData.drawContextData.doubleBufferState = kQ3False; hPixMap = GetGWorldPixMap(theGWorld); LockPixels(hPixMap); srcRect = theGWorld->portRect; myPixmapDCData.pixmap.width = srcRect.right - srcRect.left; myPixmapDCData.pixmap.height = srcRect.bottom - srcRect.top; myPixmapDCData.pixmap.rowBytes = (**hPixMap).rowBytes & 0x7FFF; myPixmapDCData.pixmap.pixelType = kQ3PixelTypeRGB32; myPixmapDCData.pixmap.pixelSize = 32; myPixmapDCData.pixmap.bitOrder = kQ3EndianBig; myPixmapDCData.pixmap.byteOrder = kQ3EndianBig; myPixmapDCData.pixmap.image = (**hPixMap).baseAddr; return Q3PixmapDrawContext_New(&myPixmapDCData); }When using a pixmap draw context, you must keep the GWorld's PixMap locked all the time (which implies that you need to call LockPixels on it, to help avoid heap fragmentation). Also, the PixMap must be 32 bits deep -- other depths are not supported.
Listing 6. Creating the camera
TQ3CameraObject MyNewCamera(WindowPtr theWindow) { TQ3ViewAngleAspectCameraData perspectiveData; TQ3CameraObject camera; TQ3Point3D from = { 0.0, 0.0, 13.0 }; TQ3Point3D to = { 0.5, 0.5, -1.5 }; TQ3Vector3D up = { 0.0, 1.0, 0.0 }; float fieldOfView = 0.523593333; float hither = 0.001; float yon = 1000; perspectiveData.cameraData.placement.cameraLocation = from; perspectiveData.cameraData.placement.pointOfInterest = to; perspectiveData.cameraData.placement.upVector = up; perspectiveData.cameraData.range.hither = hither; perspectiveData.cameraData.range.yon = yon; perspectiveData.cameraData.viewPort.origin.x = -1.0; perspectiveData.cameraData.viewPort.origin.y = 1.0; perspectiveData.cameraData.viewPort.width = 2.0; perspectiveData.cameraData.viewPort.height = 2.0; perspectiveData.fov = fieldOfView; perspectiveData.aspectRatioXToY = (float) (theWindow->portRect.right - theWindow->portRect.left) / (float) (theWindow->portRect.bottom - theWindow->portRect.top); camera = Q3ViewAngleAspectCamera_New(&perspectiveData); return camera; }
Listing 7. Creating a point light in a light group
lightGroup = Q3LightGroup_New(); pointData.lightData.isOn = kQ3True; pointData.lightData.brightness = 0.80; pointData.lightData.color.r = 1.0; pointData.lightData.color.g = 1.0; pointData.lightData.color.b = 1.0; pointData.location.x = -10.0; pointData.location.y = 0.0; pointData.location.z = 10.0; pointData.castsShadows = kQ3False; pointData.attenuation = kQ3AttenuationTypeNone; light = Q3PointLight_New(&pointData); Q3Group_AddObject(lightGroup, light);Q3Object_Dispose(light);
Listing 8. Creating the View object
TQ3ViewObject MyNewView(WindowPtr theWindow) { TQ3Status myStatus; TQ3ViewObject myView; TQ3DrawContextObject myDrawContext; TQ3RendererObject myRenderer; TQ3CameraObject myCamera; TQ3GroupObject myLights; myView = Q3View_New(); // Create and set the draw context. myDrawContext = MyNewDrawContext(theWindow); myStatus = Q3View_SetDrawContext(myView, myDrawContext); Q3Object_Dispose(myDrawContext); // Create and set the renderer. Use the interactive software renderer. myRenderer = Q3Renderer_NewFromType(kQ3RendererTypeInteractive); myStatus = Q3View_SetRenderer(myView, myRenderer); Q3Object_Dispose(myRenderer); // Create and set the camera. myCamera = MyNewCamera(theWindow); myStatus = Q3View_SetCamera(myView, myCamera); Q3Object_Dispose(myCamera); // Create and set the lights. myLights = MyNewLights(); myStatus = Q3View_SetLightGroup(myView, myLights); Q3Object_Dispose(myLights); return myView; }
Listing 9. The rendering loop
TQ3Status DocumentDraw3DData(DocumentPtr theDocument) { Q3View_StartRendering(theDocument->fView); do { Q3Style_Submit(theDocument->fInterpolation, theDocument->fView); Q3Style_Submit(theDocument->fBackFacing, theDocument->fView); Q3Style_Submit(theDocument->fFillStyle, theDocument->fView); Q3MatrixTransform_Submit(&theDocument->fRotation, theDocument->fView); Q3DisplayGroup_Submit(theDocument->fModel, theDocument->fView); } while (Q3View_EndRendering(theDocument->fView) == kQ3ViewStatusRetraverse); return kQ3Success; }Recall that earlier we set up our Macintosh draw context to use double buffering; this causes all drawing to take place in the back buffer. The calls in the rendering loop draw into the active buffer, which we have set up to be the back buffer. The image data is copied from the back buffer to the front buffer (in this case the window) when Q3View_EndRendering is called.
A rendering loop for a pixmap draw context would be similar to the routine in Listing 9, except you would need to copy the data from your PixMap to the screen yourself, generally with CopyBits.
The QuickDraw 3D metafile comes in two forms: plain-text (ASCII) and binary. Table 1 shows the differences between these two forms. The plain-text form is more useful for debugging purposes; once your application is debugged, it's more efficient to use the binary form, which may be read and written much faster and may require less storage space on disk.
The metafile format supports a wide range of primitive data types, including 1-, 2-, 4-, and 8-byte signed and unsigned integers and 4- and 8-byte IEEE floating-point numbers, together with a range of types for describing 3D data. In addition, metafiles may contain big- or little-endian numbers, making them ideal for storing data that may be used in a cross-platform manner.
Figure 7. Three types of metafile organization (representing Figure 6)
The usual method for using File and Storage objects is to create a new instance of a Storage object and attach it to a newly created File object using Q3File_SetStorage, as shown in Listing 10.
Listing 10. Attaching a Storage object to a fileTQ3FileObject MyGetNewFile(FSSpec *myFSSpec, TQ3Boolean *isText) { TQ3FileObject myFileObj; TQ3StorageObject myStorageObj; OSType myFileType; FInfo fndrInfo; // We assume the FSSpec passed in was valid and get the file // information. We need to know the file type; this routine may get // called by an Apple-event handler, so we can't assume a type -- we // need to get it from the FSSpec. FSpGetFInfo(myFSSpec, &fndrInfo); myFileType = fndrInfo.fdType; if (myFileType == '3DMF') *isText = kQ3False; else if (myFileType == 'TEXT') *isText = kQ3True; else return NULL; // Create a new Storage object and new File object. if (((myStorageObj = Q3FSSpecStorage_New(myFSSpec)) == NULL) || ((myFileObj = Q3File_New()) == NULL)) { if (myStorageObj != NULL) Q3Object_Dispose(myStorageObj); return NULL; } // Set the storage for the File object. Q3File_SetStorage(myFileObj, myStorageObj); Q3Object_Dispose(myStorageObj); return myFileObj; }Reading data from metafiles. There are three routines that you can use to help with reading the data: Q3File_GetNextObjectType, Q3File_ReadObject, and Q3File_SkipObject. Listing11 illustrates the technique used to read drawable data from a metafile. The code loops through the file, getting each object and checking to see if the object is drawable; if so, it adds the object to a group object. Listing 11. Reading from a metafile
TQ3Status MyReadModelFromFile(TQ3FileObject theFile, TQ3GroupObject myGroup) { if (theFile != NULL) { TQ3Object myTempObj; TQ3Boolean isEOF; // Read objects from the file. do { Q3File_ReadObject(theFile, &myTempObj); if (myTempObj != NULL) { // We want the object in our main group only if we can // draw it. if (Q3Object_IsDrawable(myTempObj)) Q3Group_AddObject(myGroup, myTempObj); // We either added the object to the main group, or we don't // care, so we can safely dispose of it. Q3Object_Dispose(myTempObj); } // Check to see if we've reached the end of the file yet. Q3File_IsEndOfFile(theFile, &isEOF); } while (isEOF == kQ3False); } if (myGroup != NULL) return kQ3Success; else return kQ3Failure; }Because we're isolating the implementation details of how the metafile data is stored in the Storage object that we associated with the File object at its creation time, we don't care how the metafile data we're reading is physically stored. What this means is that we could use the routine above to read data from the scrap, from a handle supplied by the Drag Manager, or from a file, as long as the storage object attached to the file is set up properly.
Writing data to metafiles. Data is written to files similarly to the way it's drawn in a rendering loop. Depending on the available memory and the complexity of the model, QuickDraw 3D may need to traverse the model in the group more than once in order to write all the data out (this is the same reason that the rendering needs to be done in a loop). As shown below, you need to preface your file-writing loop with a call to Q3File_BeginWrite, and test the value returned by Q3File_EndWrite to see if there's a need to traverse the data again.
Q3File_OpenWrite(file, kQ3FileModeNormal);
Q3File_BeginWrite(file); do { Q3Object_Write(group, file); } while (Q3File_EndWrite(file) == kQ3FileStatusRetraverse); Q3File_Close(file);
PABLO FERNICOLA (AppleLink PFF, eWorld EscherDude) After spending many years working in 3D graphics under operating systems named **IX, in a faraway land called Alabama, Pablo made the transition to real computers. After moving to Silicon Valley, he learned to beat the traffic jams by getting to work before 8 A.M. and going home after 10 P.M. Now he can be found staring out the window and wondering how he's going to get home on Interstate 280 after the next earthquake.
NICK THOMPSON (AppleLink NICKT) is currently establishing himself as the Mountain Dew-guzzling fat fool of Developer Technical Support. Unable to work the winter blubber off due to killer waves that are preventing him from surfing on the California coast, Nick has been consoling himself with learning the wonder that is QuickDraw 3D. He was last seen wandering down one of the corridors at Apple mumbling to himself.
Thanks to our technical reviewers Kent Davidson, Eiichiro Mikami, Don Moccia, and Dan Venolia, and to all the members of the QuickDraw 3D team. Special thanks to Kent and Dan for supplying information used in this article and to David Vasquez for his Viewer sample. Thanks also to the Shawn and John team (Shawn Hopwood, Apple's 3D evangelist, and our marketing weenie, John Alfano) for their input.