OpenGL Performer™ Programmer’s Guide Version 3.2 007-1680-100 CONTRIBUTORS Written by George Eckel and Ken Jones Illustrated by Chrystie Danzer and Chris Wengelski Production by Karen Jacobson Engineering contributions by Angus Dorbie, Paolo Farinelli, Tom Flynn, Yair Kurzion, Radomir Mech, Alexandre Naaman, Marcin Romaszewicz, Stace Peterson, Allan Schaffer, and Jenny Zhao COPYRIGHT © 1994, 2000–2004 Silicon Graphics, Inc. All rights reserved; provided portions may be copyright in third parties, as indicated elsewhere herein. No permission is granted to copy, distribute, or create derivative works from the contents of this electronic documentation in any manner, in whole or in part, without the prior written permission of Silicon Graphics, Inc. LIMITED RIGHTS LEGEND The software described in this document is "commercial computer software" provided with restricted rights (except as to included open/free source) as specified in the FAR 52.227-19 and/or the DFAR 227.7202, or successive sections. Use beyond license provisions is a violation of worldwide intellectual property laws, treaties and conventions. This document is provided with limited rights as defined in 52.227-14. TRADEMARKS AND ATTRIBUTIONS Silicon Graphics, SGI, the SGI logo, IRIS, IRIX, ImageVision Library, Indigo, Indy, InfiniteReality, O2, Octane, Onyx, Onyx2, and OpenGL are registered trademarks and CASEVision, Crimson, Elan Graphics, IRIS Geometry Pipeline, IRIS GL, IRIS Graphics Library, IRIS InSight, IRIS Inventor, Indigo Elan, Indigo2, InfinitePerformance, InfiniteReality2, InfiniteReality4, Onyx4, OpenGL Multipipe, OpenGL Performer, Performance Co-Pilot, REACT, RealityEngine, RealityEngine2, Showcase, Silicon Graphics Prism, UltimateVision, and VPro are trademarks of Silicon Graphics, Inc., in the United States and/or other countries worldwide. AutoCAD is a registered trademark of Autodesk, Inc. CATIA is a registered trademark of DASSAULT SYSTEMES S.A. Designer’s Workbench is a trademark of Centric Software, Inc. Lightscape is a trademark of Autodesk, Inc. Linux is a registered trademark of Linus Torvalds. Maya is a registered trademark and Wavefront is a trademark of Alias Systems, a division of Silicon Graphics Limited in the United States and/or other countries worldwide. Motif is a registered trademark and X Window System and OSF/Motif are trademarks of The Open Group. Purify is a registered trademark of Rational Software Corporation. Red Hat is a registered trademark of Red Hat, Inc. RPC is a trademark of ArchVision. VTune is a trademark of Intel Corporation. WindView is a trademark of Wind River Systems. Microsoft, Windows, and Windows NT are registered trademarks of Microsoft Corporation in the United States and other countries. Maya is a trademark of Alias Systems. All other trademarks mentioned herein are the property of their respective owners. PATENT DISCLOSURE Many of the techniques and methods disclosed in this Programmer’s Guide are covered by patents held by Silicon Graphics including U.S. Patent Nos. 5,051,737; 5,369,739; 5,438,654; 5,394,170; 5,528,737; 5,528,738; 5,581,680; 5,471,572 and patent applications pending. We encourage you to use these features in your OpenGL Performer application on SGI systems. This functionality and OpenGL Performer are not available for re-implementation and distribution on other platforms without the explicit permission of Silicon Graphics. New Features in This Guide This revision of the guide documents OpenGL Performer 3.2, which has support for the following features: 007-1680-100 • Silicon Graphics Prism visualization systems • OpenGL 2.0 Shading Language • Scene graph optimizer • New compositor API • Maya 6.0 exporter iii Record of Revision Version Description 020 1994 Original publication. 007-1680-100 060 November 2000 Updated for the 2.4 version of OpenGL Performer. 070 November 2001 Updated for the 2.5 version of OpenGL Performer. 080 December 2002 Updated for the 3.0 version of OpenGL Performer. 090 December 2003 Updated for the 3.1 version of OpenGL Performer. 100 November 2004 Updated for the 3.2 version of OpenGL Performer. v Contents Figures . . . . . . . . . . . . . . . . . . . . . . . . . Tables . . . . . . . . . . . . . . . . . . . . . . . . . xxxvii Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xlv . . xlv . xlvi . xlvi . xlvi . xlviii . xlix . xlix . . l . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . About This Guide. . . . . . . . . . . . . . Why Use OpenGL Performer? . . . . . . . . . . What You Should Know Before Reading This Guide . . . . How to Use This Guide . . . . . . . . . . . . What This Guide Contains . . . . . . . . . . Sample Applications . . . . . . . . . . . . Conventions . . . . . . . . . . . . . . Internet and Hardcopy Reading for the OpenGL Performer Series Reader Comments . . . . . . . . . . . . . . 1. 007-1680-100 OpenGL Performer Programming Interface General Naming Conventions . . . . Prefixes. . . . . . . . . . Header Files . . . . . . . . Naming in C and C++ . . . . . Abbreviations . . . . . . . . Macros, Tokens, and Enums. . . . Class API . . . . . . . . . . Object Creation . . . . . . . Set Routines . . . . . . . . Get Routines . . . . . . . . Action Routines . . . . . . . Enable and Disable of Modes . . . Mode, Attribute, or Value . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxxi . xli . . . . . . . . . . . . . . 1 1 1 2 2 3 3 3 4 4 4 5 5 6 vii Contents Base Classes . . . . . . . . Inheritance Graph . . . . . libpr and libpf Objects . . User Data . . . . . . . pfDelete() and Reference Counting Copying Objects with pfCopy() . Printing Objects with pfPrint() . Determining Object Type . . . viii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2. Setting Up the Display Environment . . . . . . . Using Pipes . . . . . . . . . . . . . . . The Functional Stages of a Pipeline . . . . . . . Creating and Configuring a pfPipe . . . . . . . Example of pfPipe Use . . . . . . . . . . Using Channels . . . . . . . . . . . . . . Creating and Configuring a pfChannel . . . . . . Setting Up a Scene . . . . . . . . . . . . Setting Up a Viewport . . . . . . . . . . Setting Up a Viewing Frustum . . . . . . . . Setting Up a Viewpoint . . . . . . . . . . Example of Channel Use . . . . . . . . . . Controlling the Video Output. . . . . . . . . . Using Multiple Channels . . . . . . . . . . . One Window per Pipe, Multiple Channels per Window . Using Channel Groups. . . . . . . . . . . . Multiple Channels and Multiple Windows . . . . . Importing OpenGL Multipipe SDK (MPK) Configuration Files 3. Nodes and Node Types Nodes . . . . . Attribute Inheritance pfNode . . . . pfGroup. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 . 7 . 10 . 10 . 11 . 15 . 15 . 17 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19 . 21 . 21 . 23 . 25 . 26 . 26 . 27 . 27 . 28 . 30 . 32 . 34 . 35 . 36 . 40 . 43 . 44 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 . 49 . 49 . 51 . 53 007-1680-100 Contents Working with Nodes . . Instancing . . . . Bounding Volumes . Node Types. . . . . pfScene Nodes. . . pfSCS Nodes . . . pfDCS Nodes . . . pfFCS Nodes . . . pfDoubleSCS Nodes . pfDoubleDCS Nodes . pfDoubleFCS Nodes . pfSwitch Nodes . . pfSequence Nodes . pfLOD Nodes . . . pfASD Nodes . . . pfLayer Nodes . . pfGeode Nodes . . pfText Nodes . . . pfBillboard Nodes . pfPartition Nodes . . Sample Program . . . 4. 007-1680-100 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 56 59 61 61 62 62 63 64 64 67 67 67 70 70 70 71 73 75 78 80 Database Traversal . . . . . . . . Scene Graph Hierarchy . . . . . . . Database Traversals . . . . . . . State Inheritance . . . . . . . . Database Organization . . . . . . Application Traversal . . . . . . . . Cull Traversal . . . . . . . . . . Traversal Order . . . . . . . . Visibility Culling . . . . . . . . Organizing a Database for Efficient Culling Sorting the Scene . . . . . . . . Paths through the Scene Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 . 87 . 87 . 87 . 88 . 88 . 90 . 90 . 91 . 94 . 97 .100 ix Contents Draw Traversal . . . . . . . . . Optimizing the Drawing of Sub-bins . Bin Draw Callbacks . . . . . . Controlling and Customizing Traversals . pfChannel Traversal Modes . . . . Cull Programs . . . . . . . . pfNode Draw Mask . . . . . . pfNode Cull and Draw Callbacks . . Process Callbacks . . . . . . . . Process Callbacks and Passthrough Data Intersection Traversal . . . . . . . Testing Line Segment Intersections . . Intersection Requests: pfSegSets . . . Intersection Return Data: pfHit Objects . Intersection Masks. . . . . . . Discriminator Callbacks . . . . . Line Segment Clipping . . . . . Traversing Special Nodes. . . . . Picking . . . . . . . . . . Performance . . . . . . . . Intersection Methods for Segments . . 5. x . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100 101 101 102 102 103 112 113 116 118 120 120 121 121 122 124 125 125 126 126 127 Frame and Load Control . . . . . . . . Frame Rate Management . . . . . . . . Selecting the Frame Rate . . . . . . . Achieving the Frame Rate . . . . . . Fixing the Frame Rate . . . . . . . . Level-of-Detail Management . . . . . . . Level-of-Detail Models . . . . . . . Level-of-Detail States . . . . . . . . Level-of-Detail Range Processing . . . . Level-of-Detail Transition Blending . . . . Run-Time User Control Over LOD Evaluation. Terrain Level-of-Detail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129 129 130 130 131 136 136 140 141 145 146 147 007-1680-100 Contents 6. 007-1680-100 Maintaining Frame Rate Using Dynamic Video Resolution . . . The Channel in DVR . . . . . . . . . . . . . DVR Scaling . . . . . . . . . . . . . . . Customizing DVR . . . . . . . . . . . . . Understanding the Stress Filter . . . . . . . . . . Dynamic Load Management . . . . . . . . . . . . Successful Multiprocessing with OpenGL Performer . . . . . Review of Rendering Stages . . . . . . . . . . . Choosing a Multiprocessing Model. . . . . . . . . Asynchronous Database Processing . . . . . . . . Placing Multiple OpenGL Performer Processes on a Single CPU Rules for Invoking Functions While Multiprocessing . . . Multiprocessing and Memory . . . . . . . . . . Shared Memory and pfInit() . . . . . . . . . . pfDataPools . . . . . . . . . . . . . . . Passthrough Data . . . . . . . . . . . . . . CULL Process Optimizations. . . . . . . . . . . . Cull Sidekick Processes . . . . . . . . . . . . Configuring CULL_SIDEKICK Processes . . . . . . . CULL Sidekick Optimization Mask. . . . . . . . . CULL Sidekick Synchronization Policy . . . . . . . CULL Sidekick User Functions . . . . . . . . . . Modifying Attributes of Cloned pfGeoSets . . . . . . Marking pfGeoSets for Optimization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .148 .149 .150 .150 .151 .152 .155 .155 .156 .163 .165 .166 .168 .169 .170 .170 .171 .173 .174 .174 .175 .176 .178 .179 Creating Visual Effects . Using pfEarthSky . . . Atmospheric Effects . . . . . . . . . . . . . . . . . . . . . . . .181 .181 .182 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xi Contents Patchy Fog and Layered Fog . . . . . . . . . Creating Layered Fog . . . . . . . . . . Creating Patchy Fog . . . . . . . . . . Initializing a pfVolFog . . . . . . . . . Updating the View . . . . . . . . . . Drawing a Scene with Fog . . . . . . . . Deleting a pfVolFog . . . . . . . . . . Specifying Fog Parameters . . . . . . . . Advanced Features of Layered Fog and Patchy Fog . Performance Considerations and Limitations . . . Real-Time Shadows . . . . . . . . . . . Creating a pfShadow . . . . . . . . . . Drawing a Scene with Shadows . . . . . . . Specifying Shadow Parameters . . . . . . . Assigning Data with Directions . . . . . . . Limitations of Real-Time Shadows . . . . . . Image-Based Rendering . . . . . . . . . . Creating a pfIBRnode . . . . . . . . . . Creating a pfIBRnode Using a Proxy . . . . . Creating a pfIBRtexture . . . . . . . . . Parameters Controlling Drawing of a pfIBRnode . . The Simplify Application . . . . . . . . . Creating Images of an Object with makeProxyImages Creating Images of an Object with makeIBRimages . Limitations . . . . . . . . . . . . . 7. xii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Importing Databases . . . . . . . . . . . . . . . . . . Overview of OpenGL Performer Database Creation and Conversion . . . . libpfdu - Utilities for Creating Efficient OpenGL Performer Run-Time Structures pfdLoadFile - Loading Arbitrary Databases into OpenGL Performer . . . Database Loading Details. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186 187 187 188 191 191 191 192 193 196 198 199 201 202 202 203 204 204 205 206 208 209 215 219 220 . . . . . . . . . . . . . . . 221 221 222 223 225 007-1680-100 Contents Developing Custom Importers . . . . . . . . . . . . . . . . Structure and Interpretation of the Database File Format . . . . . . . Scene Graph Creation Using Nodes as Defined in libpf . . . . . . . Defining Geometry and Graphics State for libpr . . . . . . . . . Creating an OpenGL Performer Database Converter using libpfdu . . . . Maximizing Database Loading and Paging Performance with PFB and PFI Formats . pfconv. . . . . . . . . . . . . . . . . . . . . . pficonv . . . . . . . . . . . . . . . . . . . . . Supported Database Formats. . . . . . . . . . . . . . . . . 007-1680-100 . . . . . . . . . . . . . . . . . . .228 .229 .229 .230 .230 .240 .240 .241 .241 xiii Contents Description of Supported Formats . . . . . AutoDesk 3DS Format . . . . . . . SGI BIN Format . . . . . . . . . Side Effects POLY Format . . . . . . Brigham Young University BYU Format . . Optimizer CSB Format . . . . . . . Virtual Cliptexture CT Loader . . . . . Designer’s Workbench DWB Format . . . AutoCAD DXF Format . . . . . . . MultiGen OpenFlight Format . . . . . McDonnell-Douglas GDS Format . . . . SGI GFO Format . . . . . . . . . SGI IM Format . . . . . . . . . . AAI/Graphicon IRTP Format . . . . . SGI Open Inventor Format . . . . . . Lightscape Technologies LSA and LSB Formats Medit Productions MEDIT Format . . . . NFF Neutral File Format . . . . . . . Wavefront Technology OBJ Format . . . . SGI PFB Format . . . . . . . . . SGI PFI Format. . . . . . . . . . SGI PHD Format . . . . . . . . . SGI PTU Format . . . . . . . . . ArchVision RPC Format . . . . . . . USNA Standard Graphics Format . . . . SGI SGO Format . . . . . . . . . USNA Simple Polygon File Format . . . . Sierpinski Sponge Loader. . . . . . . Star Chart Format . . . . . . . . . 3D Lithography STL Format . . . . . . SuperViewer SV Format . . . . . . . Geometry Center Triangle Format . . . . UNC Walkthrough Format . . . . . . xiv . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244 244 244 246 248 249 249 249 250 252 254 254 256 257 258 259 263 263 265 267 268 268 270 273 275 276 280 280 281 282 283 287 287 007-1680-100 Contents 8. 007-1680-100 WRL Format . . . . . . . . . . . . . . . Database Operators with Pseudo Loaders . . . . . . . . The Maya Database Exporter. . . . . . . . . . . . Installation Requirements . . . . . . . . . . . Exporting a Scene Using the Graphical Interface . . . . . Exporting a Scene Using the Maya Embedded Language (MEL) Translation Details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .287 .288 .290 .290 .291 .296 .297 Geometry . . . . . . . . . . pfGeoSet (Geometry Set) . . . . . . Primitive Types . . . . . . . pfGeoSet Draw Mode . . . . . Primitive Connectivity . . . . . Attributes . . . . . . . . . Attribute Bindings . . . . . . Indexed Arrays . . . . . . . pfGeoSet Operations . . . . . . pfGeoArray (Geometry Array) . . . . Creating pfGeoArrays . . . . . pfGeoArray Attributes . . . . . pfGeoArray Attribute Types. . . . pfGeoArray Primitive Types . . . Example Code . . . . . . . . Converting pfGeoSets to pfGeoArrays Optimizing Geometry for Rendering . . Function pfdMergeGraph() . . . . Function pfdStripGraph() . . . . Function pfdSpatializeGraph() . . . The Optimization Pipeline . . . . Using the libpfgopt Pseudo Loader. . Rendering 3D Text. . . . . . . . pfFont . . . . . . . . . . pfString . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .305 .305 .307 .308 .310 .313 .315 .316 .318 .319 .320 .320 .323 .323 .324 .328 .329 .329 .330 .331 .332 .333 .335 .335 .337 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xv Contents 9. xvi Higher-Order Geometric Primitives . . . . . . . . . . Features and Uses of Higher-Order Geometric Primitives . . . . Reps and the Rendering Process . . . . . . . . . . . Trimmed NURBS . . . . . . . . . . . . . . . Objects Required by Reps . . . . . . . . . . . . . . New Types Required for Reps . . . . . . . . . . . Classes for Scalar Functions . . . . . . . . . . . . Matrix Class: pfRMatrix . . . . . . . . . . . . . Geometric Primitives: The Base Class pfRep and the Application repTest Class Declaration for pfRep . . . . . . . . . . . . Main Features of the Methods in pfRep. . . . . . . . . Planar Curves . . . . . . . . . . . . . . . . . Mathematical Description of a Planar Curve . . . . . . . Lines in the Plane . . . . . . . . . . . . . . . Circles in the Plane . . . . . . . . . . . . . . Superquadric Curves: pfSuperQuadCurve2d . . . . . . . Hermite-Spline Curves in the Plane . . . . . . . . . . NURBS Overview . . . . . . . . . . . . . . . NURBS Curves in the Plane . . . . . . . . . . . . Piecewise Polynomial Curves: pfPieceWisePolyCurve2d. . . . Discrete Curves in the Plane . . . . . . . . . . . . Spatial Curves . . . . . . . . . . . . . . . . . Lines in Space . . . . . . . . . . . . . . . . Circles in Space . . . . . . . . . . . . . . . Superquadrics in Space . . . . . . . . . . . . . Hermite Spline Curves in Space . . . . . . . . . . . NURBS Curves in Space . . . . . . . . . . . . . Curves on Surfaces: pfCompositeCurve3d . . . . . . . . Discrete Curves in Space . . . . . . . . . . . . . Example of Using pfDisCurve3d and pfHsplineCurve3d . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 341 342 342 342 342 343 343 345 346 348 348 348 349 353 354 356 359 360 365 367 368 371 371 372 372 373 373 374 375 375 007-1680-100 Contents Parametric Surfaces . . . . . . . . . . . . . . Mathematical Description of a Parametric Surface . . . . Defining Edges of a Parametric Surface: Trim Loops and Curves Adjacency Information: pfEdge . . . . . . . . . . Base Class for Parametric Surfaces: pfParaSurface . . . . pfPlaneSurface . . . . . . . . . . . . . . pfSphereSurface . . . . . . . . . . . . . . pfCylinderSurface . . . . . . . . . . . . . pfTorusSurface . . . . . . . . . . . . . . pfConeSurface. . . . . . . . . . . . . . . Swept Surfaces . . . . . . . . . . . . . . pfFrenetSweptSurface . . . . . . . . . . . . Ruled Surfaces . . . . . . . . . . . . . . Coons Patches . . . . . . . . . . . . . . . NURBS Surfaces . . . . . . . . . . . . . . Hermite-Spline Surfaces . . . . . . . . . . . . Meshes . . . . . . . . . . . . . . . . . . Mesh Faces. . . . . . . . . . . . . . . . Mesh Vertices . . . . . . . . . . . . . . . Subdivision Surfaces . . . . . . . . . . . . . . Creating a Subdivision Surface . . . . . . . . . . Loop and Catmull-Clark Subdivisions . . . . . . . . Dynamic Modification of Vertices . . . . . . . . . The libpfsubdiv Pseudo Loader . . . . . . . . . . Special Notes . . . . . . . . . . . . . . . 10. 007-1680-100 . . . . . . . . . . . . . . . . . . . . . . . . . Creating and Maintaining Surface Topology . . . . . . . . Overview of Topology Tasks . . . . . . . . . . . . . Summary of Scene Graph Topology: pfTopo . . . . . . . . Building Topology: Computing and Using Connectivity Information Reading and Writing Topology Information: Using Pseudo Loaders Class Declaration for pfTopo . . . . . . . . . . . Main Features of the Methods in pfTopo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .375 .376 .377 .379 .380 .384 .386 .389 .391 .393 .395 .399 .400 .402 .404 .411 .413 .416 .417 .420 .421 .424 .424 .424 .425 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .427 .427 .428 .431 .434 .435 .436 xvii Contents Collecting Connected Surfaces: pfSolid . . Class Declaration for pfSolid . . . . Main Features of the Methods in pfSolid xviii . . . . . . . . . . . . . . . . . . . . . . . . . . . 437 437 438 11. Rendering Higher-Order Primitives: Tessellators . . . . Features of Tessellators . . . . . . . . . . . . Tessellators for Varying Levels of Detail . . . . . . Tessellators Act on a Whole Graph or Single Node . . . Tessellators and Topology: Managing Cracks . . . . . Base Class pfTessellateAction . . . . . . . . . . . Retessellating a Scene Graph . . . . . . . . . . Class Declaration for pfTessellateAction . . . . . . Main Features of the Methods in pfTessellateAction . . . Tessellating Parametric Surfaces . . . . . . . . . . pfTessParaSurfaceAction . . . . . . . . . . . Sample From repTest: Tessellating and Rendering a Sphere . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 439 440 441 442 442 443 443 443 444 445 445 448 12. Graphics State . . . . . . . Immediate Mode . . . . . . Rendering Modes . . . . . Rendering Values . . . . . Enable / Disable . . . . . Rendering Attributes . . . . Graphics Library Matrix Routines Sprite Transformations . . . Display Lists . . . . . . State Management . . . . . State Override . . . . . . pfGeoState . . . . . . . 13. Shaders . . . . . . . . . . . The pfShaderProgram Class . . . . . Allocating Memory for a Shader Program Creating a Shader Program . . . . Applying Shader Programs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 451 451 453 458 458 459 477 478 479 480 482 482 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491 491 492 493 496 007-1680-100 Contents The pfShaderObject Class. . . Creating New Shader Objects Specifying Shader Objects . Specifying the Object Type . Compiling Shader Objects . Example Code . . . . . . 14. 007-1680-100 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Using Scalable Graphics Hardware . . . . . . . . . Using OpenGL Performer with a DPLEX . . . . . . . . Hyperpipe Concepts . . . . . . . . . . . . . Configuring Hyperpipes . . . . . . . . . . . . Configuring pfPipeWindows and pfChannels . . . . . Programming with Hyperpipes . . . . . . . . . . Using OpenGL Performer with an SGI Scalable Graphics Compositor How the Compositor Functions . . . . . . . . . . The pfCompositor Class . . . . . . . . . . . . Querying the System for Hardware Compositors. . . . . Creating a pfCompositor. . . . . . . . . . . . Querying pfCompositors . . . . . . . . . . . Load Balancing . . . . . . . . . . . . . . Setting Compositor Modes . . . . . . . . . . . Querying Compositor Modes . . . . . . . . . . Managing Screen Space, Channel Clipping, and Antialiasing . Using OpenGL Performer with GPUs . . . . . . . . . The pfGProgram Class . . . . . . . . . . . . The pfGProgramParms Class . . . . . . . . . . The pfVertexProgram and pfFragmentProgram Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .497 .498 .498 .499 .499 .500 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .503 .503 .504 .504 .511 .515 .517 .517 .519 .519 .520 .522 .524 .525 .528 .529 .532 .532 .534 .535 xix Contents 15. xx ClipTextures . . . . . . . . . . . . . Overview . . . . . . . . . . . . . . Cliptexture Levels . . . . . . . . . . . Cliptexture Assumptions . . . . . . . . . Image Cache . . . . . . . . . . . . Toroidal Loading . . . . . . . . . . . Updating the Clipcenter . . . . . . . . . Virtual Cliptextures on InfiniteReality Systems . . Cliptexture Support Requirements . . . . . . Special Features . . . . . . . . . . . How Cliptextures Interact with the Rest of the System Cliptexture Support in OpenGL Performer. . . . Cliptexture Manipulation. . . . . . . . . Cliptexture API . . . . . . . . . . . . . Preprocessing ClipTextures . . . . . . . . . Building a MIPmap . . . . . . . . . . Formatting Image Data . . . . . . . . . Tiling an Image . . . . . . . . . . . Cliptexture Configuration . . . . . . . . . . Configuration Considerations . . . . . . . Load-Time Configuration. . . . . . . . . Post-Load-Time Configuration . . . . . . . Configuration API . . . . . . . . . . . . libpr Functionality . . . . . . . . . . Configuration Utilities . . . . . . . . . Configuration Files . . . . . . . . . . Post-Scene Graph Load Configuration . . . . . . MPClipTextures . . . . . . . . . . . pfMPClipTexture Utilities . . . . . . . . Using Cliptextures with Multiple Pipes. . . . . Texture Memory and Hardware Support Checking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 537 538 539 540 541 544 546 546 548 548 549 550 552 553 553 554 555 555 556 556 557 557 557 558 561 563 579 579 581 584 586 007-1680-100 Contents Manipulating Cliptextures . . . . . . . Cliptexture Load Control . . . . . . Invalidating Cliptextures . . . . . . Virtual ClipTextures . . . . . . . . Custom Read Functions . . . . . . . Using Cliptextures. . . . . . . . . . Cliptexture Insets . . . . . . . . . Estimating Cliptexture Memory Usage. . . Using Cliptextures in Multipipe Applications. Virtualizing Cliptextures. . . . . . . Customizing Load Control . . . . . . Custom Read Functions . . . . . . . Cliptexture Sample Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .587 .587 .592 .593 .599 .601 .601 .605 .609 .610 .611 .612 .612 16. Windows . . . . . . . . . . . . . . pfWindows . . . . . . . . . . . . . . Creating a pfWindow . . . . . . . . . . . Configuring the Framebuffer of a pfWindow . . . . pfWindows and GL Windows . . . . . . . . Manipulating a pfWindow . . . . . . . . . Alternate Framebuffer Configuration Windows . . Window Share Groups . . . . . . . . . Synchronization of Buffer Swap for Multiple Windows Communicating with the Window System . . . . . More pfWindow Examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .617 .617 .618 .621 .623 .625 .626 .627 .628 .628 .629 17. pfPipeWindows and pfPipeVideoChannels . . . . Using pfPipeWindows . . . . . . . . . . Creating, Configuring and Opening pfPipeWindow . pfPipeWindows in Action . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .633 .633 .633 .644 007-1680-100 xxi Contents 18. xxii Controlling Video Displays . . . . . . . . Creating a pfPipeVideoChannel . . . . . . Multiple pfPipeVideoChannels in a pfPipeWindow Configuring a pfPipeVideoChannel . . . . . Use pfPipeVideoChannels to Control Frame Rate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 646 647 648 648 649 Managing Nongraphic System Tasks . . . Handling Queues . . . . . . . . . Multiprocessing . . . . . . . . Queue Contents . . . . . . . . Adding or Retrieving Elements . . . . pfQueue Modes . . . . . . . . Running the Sort Process on a Different CPU High-Resolution Clocks . . . . . . . Video Refresh Counter (VClock). . . . Memory Allocation. . . . . . . . . Allocating Memory With pfMalloc() . . Shared Arenas . . . . . . . . . Allocating Locks and Semaphores . . . Datapools . . . . . . . . . . CycleBuffers . . . . . . . . . Asynchronous I/O (IRIX only) . . . . . Error Handling and Notification . . . . . File Search Paths . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 651 651 652 652 653 654 657 658 659 659 660 661 662 662 663 666 666 668 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 007-1680-100 Contents 19. Dynamic Data . . . . . . . . . . . pfFlux . . . . . . . . . . . . . Creating and Deleting a pfFlux . . . . . Initializing the Buffers . . . . . . . pfFlux Buffers . . . . . . . . . . Coordinating pfFlux and Connected pfEngines Synchronized Flux Evaluation . . . . . Fluxed Geosets . . . . . . . . . Fluxed Coordinate Systems . . . . . . Replacing pfCycleBuffer with pfFlux . . . pfEngine . . . . . . . . . . . . Creating and Deleting Engines . . . . . Setting Engine Types and Modes . . . . Setting Engine Sources and Destinations . . Setting Engine Masks. . . . . . . . Setting Engine Iterations . . . . . . . Setting Engine Ranges . . . . . . . Evaluating pfEngines. . . . . . . . Animating a Geometry . . . . . . . . 20. Active Surface Definition Overview . . . . . Using ASD . . . . . LOD Reduction . . Hierarchical Structure ASD Solution Flow Chart . A Very Simple ASD . . Morphing Vector . . A Very Complex ASD ASD Elements . . . . Vertices. . . . . Evaluation Function . 007-1680-100 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .671 .671 .672 .673 .674 .677 .679 .682 .683 .685 .686 .686 .687 .694 .694 .694 .695 .695 .696 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .699 .699 .701 .701 .702 .704 .705 .706 .707 .707 .708 .710 xxiii Contents 21. xxiv Data Structures . . . . . . . . . . . . . Triangle Data Structure . . . . . . . . . Attribute Data Array . . . . . . . . . . Vertex Data Structure . . . . . . . . . . Default Evaluation Function . . . . . . . . pfASD Queries . . . . . . . . . . . . . Aligning an Object to the Surface . . . . . . Adding a Query Array . . . . . . . . . Using ASD for Multiple Channels . . . . . . . Connecting Channels . . . . . . . . . . Combining pfClipTexture and pfASD . . . . . . ASD Evaluation Function Timing . . . . . . . Query Results . . . . . . . . . . . . Aligning a Geometry With a pfASD Surface Example Aligning Light Points Above a pfASD Surface Example Paging . . . . . . . . . . . . . . . Interest Area . . . . . . . . . . . . Preprocessing for Paging . . . . . . . . . Multi-resolution Paging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 711 713 718 719 720 722 722 723 724 724 725 725 726 726 728 730 730 731 732 Light Points . . . . . . . . . Uses of Light Points . . . . . . Creating a Light Point . . . . . . Setting the Behavior of Light Points . . Intensity . . . . . . . . Directionality . . . . . . . Emanation Shape . . . . . . Distance. . . . . . . . . Attenuation through Fog . . . . Size . . . . . . . . . . Fading . . . . . . . . . Callbacks . . . . . . . . . Multisample, Size, and Alpha . . Reducing CPU Processing Using Textures . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 735 735 736 737 737 738 738 740 741 742 743 743 746 748 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 007-1680-100 Contents 22. 007-1680-100 Preprocessing Light Points . . . . . . . . . . Stage Configuration Callbacks . . . . . . . . How the Light Point Process Works . . . . . . Calligraphic Light Points . . . . . . . . . . . Calligraphic Versus Raster Displays . . . . . . LPB Hardware Configuration . . . . . . . . Visibility Information . . . . . . . . . . Required Steps For Using Calligraphic Lights. . . . Accounting for Projector Differences . . . . . . Callbacks . . . . . . . . . . . . . . Frame to Frame Control . . . . . . . . . . Significance . . . . . . . . . . . . . Debunching . . . . . . . . . . . . . Defocussing Calligraphic Objects . . . . . . . Using pfCalligraphic Without pfChannel . . . . . . Timing Issues . . . . . . . . . . . . . Light Point Process and Calligraphic . . . . . . Debugging Calligraphic Lights on Non-Calligraphic Systems Calligraphic Light Example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .749 .749 .750 .751 .752 .754 .756 .757 .759 .761 .763 .764 .764 .765 .765 .766 .766 .766 .767 Math Routines. . . . . . . . Vector Operations . . . . . . . Matrix Operations . . . . . . . Quaternion Operations . . . . . Matrix Stack Operations . . . . . Creating and Transforming Volumes . Defining a Volume . . . . . Creating Bounding Volumes . . Transforming Bounding Volumes . Intersecting Volumes . . . . . . Point-Volume Intersection Tests . Volume-Volume Intersection Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .775 .775 .777 .781 .783 .784 .784 .786 .787 .788 .788 .788 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxv Contents Creating and Working with Line Segments . Intersecting with Volumes . . . . Intersecting with Planes and Triangles . Intersecting with pfGeoSets . . . . General Math Routine Example Program . xxvi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 790 791 792 792 794 23. Statistics. . . . . . . . . . . . . . . . . Interpreting Statistics Displays . . . . . . . . . . Status Line . . . . . . . . . . . . . . . Stage Timing Graph . . . . . . . . . . . . Load and Stress . . . . . . . . . . . . . CPU Statistics . . . . . . . . . . . . . . Rendering Statistics . . . . . . . . . . . . Fill Statistics . . . . . . . . . . . . . . Collecting and Accessing Statistics in Your Application . . . Displaying Statistics Simply . . . . . . . . . . Enabling and Disabling Statistics for a Channel . . . . Statistics in libpr and libpf—pfStats Versus pfFrameStats Statistics Rules of Use . . . . . . . . . . . . Reducing the Cost of Statistics . . . . . . . . . Statistics Output . . . . . . . . . . . . . Customizing Displays. . . . . . . . . . . . Setting Update Rate . . . . . . . . . . . . The pfStats Data Structure . . . . . . . . . . Setting Statistics Class Enables and Modes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 799 800 800 801 804 805 807 808 808 809 810 810 811 814 815 817 817 817 818 24. Performance Tuning and Debugging . . . . . Performance Tuning Overview . . . . . . . How OpenGL Performer Helps Performance . . . Draw Stage and Graphics Pipeline Optimizations . Cull and Intersection Optimizations. . . . . Application Optimizations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 819 819 820 820 822 823 . . . . . . . . . . . . . . . . . . . . . . . 007-1680-100 Contents Specific Guidelines for Optimizing Performance . . . . . . Graphics Pipeline Tuning Tips . . . . . . . . . . Process Pipeline Tuning Tips . . . . . . . . . . Database Concerns . . . . . . . . . . . . . Special Coding Tips . . . . . . . . . . . . . Performance Measurement Tools . . . . . . . . . . Using pixie, prof, and gprof to Measure Performance . . Using ogldebug to Observe Graphics Calls . . . . . . Guidelines for Debugging . . . . . . . . . . . . Shared Memory . . . . . . . . . . . . . . Use the Simplest Process Model. . . . . . . . . . Avoid Floating-Point Exceptions . . . . . . . . . When the Debugger Will Not Give You a Stack Trace . . . Tracing Members of OpenGL Performer Objects . . . . . Memory Corruption and Leaks . . . . . . . . . . . Purify . . . . . . . . . . . . . . . . . libdmalloc (IRIX only) . . . . . . . . . . . Notes on Tuning for RealityEngine Graphics . . . . . . . Multisampling. . . . . . . . . . . . . . . Transparency . . . . . . . . . . . . . . . Texturing . . . . . . . . . . . . . . . . Other Tips . . . . . . . . . . . . . . . . EventView—A Performance Analyzer . . . . . . . . . Viewing Events—evanalyzer . . . . . . . . . . Controlling the Collection of OpenGL Performer Internal Events Sample Use of EventView . . . . . . . . . . . Using EventView Tools . . . . . . . . . . . . Understanding OpenGL Performer Internal Events . . . . 25. 007-1680-100 Building a Visual Simulation Application Using libpfv Overview . . . . . . . . . . . . . . The Simplest pfvViewer Program . . . . . . . Adding Interaction to a pfvViewer Program . . . . Reading XML Configuration Files . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .824 .824 .828 .832 .836 .837 .837 .838 .839 .839 .840 .841 .841 .842 .842 .843 .844 .844 .845 .845 .846 .846 .847 .847 .849 .849 .853 .860 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .863 .863 .864 .865 .866 xxvii Contents 26. xxviii Module Scoping, Multiple Worlds and Multiple Views . . Extending a pfvViewer—Writing Custom Modules . . . Extending a pfvViewer—Module Entry Points . . . . . Picking, Selection, and Interaction . . . . . . . . More Sample Programs, Configuration Files, and Source Code . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 870 873 875 876 879 Programming with C++ . . . . . . . . . Overview . . . . . . . . . . . . . Class Taxonomy . . . . . . . . . . . Public Structs . . . . . . . . . . . libpr Classes. . . . . . . . . . . libpf Classes . . . . . . . . . . . pfType Class . . . . . . . . . . . Programming Basics . . . . . . . . . . Header Files . . . . . . . . . . . Creating and Deleting OpenGL Performer Objects Invoking Methods on OpenGL Performer Objects. Passing Vectors and Matrices to Other Libraries . Porting from C API to C++ API . . . . . . . Typedefed Arrays Versus Structs . . . . . Interface Between C and C++ API Code . . . Subclassing pfObjects . . . . . . . . . . Initialization and Type Definition . . . . . Defining Virtual Functions . . . . . . . Accessing Parent Class Data Members . . . . Multiprocessing and Shared Memory . . . . . Initializing Shared Memory . . . . . . . Data Members and Shared Memory. . . . . Multiprocessing and libpf Objects . . . . . Performance Hints . . . . . . . . . . . Constructor Overhead . . . . . . . . Math Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 881 881 882 882 882 883 883 883 883 887 889 889 889 890 891 891 892 893 894 894 894 896 897 898 898 898 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 007-1680-100 Contents Glossary . . . . . . . . . . . . . . . . . . . . . . . . .899 . . . . . . . . . . . . . . . . . . . . . . . . . .921 Index 007-1680-100 xxix Figures Figure 1-1 Figure 2-1 Figure 2-2 Figure 2-3 Figure 2-4 Figure 2-5 Figure 2-6 Figure 2-7 Figure 3-1 Figure 3-2 Figure 3-3 Figure 3-4 Figure 3-5 Figure 4-1 Figure 4-2 Figure 4-3 Figure 4-4 Figure 5-1 Figure 5-2 Figure 5-3 Figure 5-4 Figure 5-5 Figure 5-6 Figure 5-7 Figure 5-8 Figure 6-1 Figure 6-2 007-1680-100 Partial Inheritance Graph of OpenGL Performer Data Types . From Scene Graph to Visual Display. . . . . . . . . . . 9 . 20 Single Graphics Pipeline . . . . . . . . . Dual Graphics Pipeline . . . . . . . . . Symmetric Viewing Frustum . . . . . . . . Heading, Pitch, and Roll Angles . . . . . . . Single-Channel and Multiple-Channel Display . . . The libpfmpk Import Operation . . . . . . Nodes in the OpenGL Performer Hierarchy . . . Shared Instances . . . . . . . . . . . Cloned Instancing . . . . . . . . . . . A Scenario for Using Double-Precision Nodes . . . pfDoubleDCS Nodes in a Scene Graph . . . . . Culling to the Frustum. . . . . . . . . . Sample Database Objects and Bounding Volumes . . How to Partition a Database for Maximum Efficiency . Intersection Methods . . . . . . . . . . Frame Rate and Phase Control . . . . . . . Level-of-Detail Node Structure . . . . . . . Level-of-Detail Processing. . . . . . . . . Real Size of Viewport Rendered Under Increasing Stress Stress Processing . . . . . . . . . . . Multiprocessing Models . . . . . . . . . Loose Culling of pfGeosets . . . . . . . . CULL_SIDEKICK Processing . . . . . . . . Layered Atmosphere Model . . . . . . . . Patchy Fog Versus Layered Fog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 . 23 . 29 . 31 . 37 . 46 . 50 . 57 . 58 . 65 . 66 . 92 . 94 . 96 .128 .132 .137 .139 .149 .153 .162 .172 .174 .183 .186 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . xxxi Figures Figure 6-3 Figure 6-4 Figure 7-1 Figure 7-2 Figure 7-3 Figure 7-4 Figure 7-5 Figure 7-6 Figure 7-7 Figure 7-8 Figure 7-9 Figure 7-10 Figure 7-11 Figure 7-12 Figure 7-13 Figure 7-14 Figure 7-15 Figure 7-16 Figure 8-1 Figure 8-2 Figure 8-3 Figure 8-4 Figure 9-1 Figure 9-2 Figure 9-3 Figure 9-4 Figure 9-5 Figure 9-6 Figure 9-7 Figure 9-8 Figure 9-9 Figure 9-10 xxxii The Default Simplify Pane . . . . . . . . . . . The Simplify Pane for Simplifying an Object. . . . . . BIN-Format Data Objects . . . . . . . . . . . Soma Cube Puzzle in DWB Form . . . . . . . . . The Famous Teapot in DXF Form . . . . . . . . . Spacecraft Model in OpenFlight Format . . . . . . . GFO Database of Mies van der Rohe’s German Pavilion . . Aircar Database in IRIS Inventor Format . . . . . . . LSA-Format City Hall Database . . . . . . . . . LSB-Format Operating Room Database . . . . . . . SGI Office Building as OBJ Database . . . . . . . . Plethora of Polyhedra in PHD Format . . . . . . . Terrain Database Generated by PTU Tools . . . . . . Model in SGO Format . . . . . . . . . . . . Sample STLA Database . . . . . . . . . . . . Early Automobile in SuperViewer SV Format . . . . . Maya Export Screen. . . . . . . . . . . . . Maya Export Options . . . . . . . . . . . . Primitives and Connectivity . . . . . . . . . . pfGeoSet Structure . . . . . . . . . . . . . Indexing Arrays . . . . . . . . . . . . . . Deciding Whether to Index Attributes . . . . . . . Class Hierarchy for Higher-Order Primitives . . . . . Parametric Curve: Parameter Interval (0,1). . . . . . . Line in the Plane Parameterization . . . . . . . . Circle in the Plane Parameterization . . . . . . . . Superquadric Curve’s Dependence on the Parameter α. . . Hermite Spline Curve Parameterization . . . . . . . Discrete Curve Definition . . . . . . . . . . . Parametric Surface: Unit-Square Coordinate System . . . Trim Loops and Trimmed Surface: Both Trim Loops Made of Four Trim Curves . . . . . . . . . . . . . Plane Parameterization . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210 213 245 250 251 253 255 258 261 262 266 269 271 277 282 284 292 293 312 314 317 318 347 350 353 354 357 359 368 377 . . 378 384 007-1680-100 Figures Figure 9-11 Figure 9-12 Figure 9-13 Figure 9-14 Figure 9-15 Figure 9-16 Figure 9-17 Figure 9-18 Figure 9-19 Figure 10-1 Figure 10-2 Figure 11-1 Figure 11-2 Figure 12-1 Figure 12-2 Figure 14-1 Figure 14-2 Figure 14-3 Figure 14-4 Figure 14-5 Figure 14-6 Figure 14-7 Figure 14-8 Figure 14-9 Figure 14-10 Figure 14-11 Figure 15-1 Figure 15-2 Figure 15-3 Figure 15-4 007-1680-100 Sphere Parameterization . . . . . . . . . . Cylinder Parameterization . . . . . . . . . Torus Parameterization . . . . . . . . . . Cone Parameterization. . . . . . . . . . . Swept Surface: Moving Reference Frame and Effect of Profile Function. . . . . . . . . . . . . Ruled Surface Parameterization . . . . . . . . Coons Patch Construction . . . . . . . . . . NURBS Surface Control Hull Parameterization . . . Hermite Spline Surface With Derivatives Specified at Knot Points . . . . . . . . . . . . . . Topological Relations Maintained by Topology Classes . Consistently Tessellated Adjacent Surfaces and Related Objects . . . . . . . . . . . . . . . Class Hierarchy for Tessellators . . . . . . . . Tessellations Varying With Changes in Control Parameter pfGeoState Structure . . . . . . . . . . . Generating the Color of a Multitextured Pixel . . . . pfPipes Creating pfHyperpipes . . . . . . . . Multiple Hyperpipes . . . . . . . . . . . Default Hyperpipe Mapping to Graphic Pipes . . . . Attaching Objects to the Master pfPipe . . . . . . Hardware Composition Schemes. . . . . . . . Horizontal Stripes (pfCompositor Mode) . . . . . Vertical Stripes (pfCompositor Mode) . . . . . . Left Tiles (pfCompositor Mode) . . . . . . . . Right Tiles (pfCompositor Mode) . . . . . . . Bottom Tiles (pfCompositor Mode) . . . . . . . Top Tiles (pfCompositor Mode) . . . . . . . . Cliptexture Components . . . . . . . . . . Image Cache Components. . . . . . . . . . Mem Region Update . . . . . . . . . . . Tex Region Update . . . . . . . . . . . . . . . . . . . . .386 .389 .391 .393 . . . . . . . . .396 .401 .403 .407 . . . . .411 .429 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .430 .440 .441 .487 .488 .505 .506 .507 .512 .518 .526 .527 .527 .527 .528 .528 .538 .539 .542 .543 xxxiii Figures Figure 15-5 Figure 15-6 Figure 15-7 Figure 15-8 Figure 15-9 Figure 15-10 Figure 15-11 Figure 15-12 Figure 15-13 Figure 15-14 Figure 17-1 Figure 18-1 Figure 18-2 Figure 19-1 Figure 19-2 Figure 19-3 Figure 19-4 Figure 20-1 Figure 20-2 Figure 20-3 Figure 20-4 Figure 20-5 Figure 20-6 Figure 20-7 Figure 20-8 Figure 20-9 Figure 20-10 Figure 20-11 Figure 20-12 Figure 20-13 Figure 20-14 Figure 20-15 xxxiv Cliptexture Cache Hierarchy . . . . . . . . Invalid Border . . . . . . . . . . . . Clipcenter Moving . . . . . . . . . . . Virtual Cliptexture Concepts . . . . . . . . pfMPClipTexture Connections . . . . . . . pfuClipCenterNode Connections . . . . . . . Master and Slave Cliptexture Resource Sharing . . . Cliptexture Insets . . . . . . . . . . . Supersampled Inset Boundary. . . . . . . . Offset Slave Tex Regions . . . . . . . . . Directing Video Output . . . . . . . . . pfQueue Object . . . . . . . . . . . . pfCycleBuffer and pfCycleMemory Overview . . . How pfFlux and Processes Use Frame Numbers . . pfFlux Buffer Structure . . . . . . . . . . Timing Diagram Showing the Use of Sync Groups . . pfEngine Driving a pfFlux That Animates a pfFCS Node Morphing Range Between LODs . . . . . . . Large Geometry . . . . . . . . . . . . ASD Information Flow . . . . . . . . . . A Very Simple pfASD . . . . . . . . . . Reference Positions . . . . . . . . . . . Triangulated Image . . . . . . . . . . . LOD1 Replaced by LOD2 . . . . . . . . . Data Structures . . . . . . . . . . . . ASD Data Structures . . . . . . . . . . Discontinuous, Neighboring LODs . . . . . . Triangle Mesh . . . . . . . . . . . . Using the tsid Field . . . . . . . . . . . Vertex and Reference Point Arrays, Counter-Clockwise Ordering . . . . . . . . . . . . . . Vertex Neighborhoods . . . . . . . . . . pfASD Evaluation Process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 544 545 546 547 580 583 584 602 604 610 646 651 665 672 675 681 683 703 704 705 706 709 709 710 711 712 715 715 716 . . . . . . . . . 717 720 726 007-1680-100 Figures Figure 20-16 Figure 20-17 Figure 20-18 Figure 21-1 Figure 21-2 Figure 21-3 Figure 21-4 Figure 21-5 Figure 23-1 Figure 23-2 Figure 23-3 Figure 24-1 Figure 24-2 Figure 24-3 Figure 24-4 Figure 24-5 007-1680-100 Example Setup for Geometry Alignment . . . Aligning Light Points Above a pfASD Surface . . Tiles at Different LODs . . . . . . . . VASI Landing Light . . . . . . . . . Attenuation Shape . . . . . . . . . . Attenuation of Light . . . . . . . . . Lit Multisamples . . . . . . . . . . Calligraphic Hardware Configuration . . . . Stage Timing Statistics Display . . . . . . Conceptual Diagram of a Draw-Stage Timing Line Other Statistics Classes. . . . . . . . . The evanalyzer Main Display . . . . . . User Event myDrawCB . . . . . . . . Up-Close View of a Single Event . . . . . . evhist Sample Screen . . . . . . . . evgraph Sample Screen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .727 .728 .731 .736 .739 .740 .747 .755 .800 .802 .806 .848 .853 .857 .858 .859 xxxv Tables Table 1-1 Table 2-1 Table 3-1 Table 3-2 Table 3-3 Table 3-4 Table 3-5 Table 3-6 Table 3-7 Table 3-8 Table 3-9 Table 3-10 Table 3-11 Table 4-1 Table 4-2 Table 4-3 Table 4-4 Table 4-5 Table 4-6 Table 4-7 Table 4-8 Table 4-9 Table 4-10 Table 5-1 Table 5-2 Table 5-3 Table 5-4 007-1680-100 Routines that Modify libpr Object Reference Counts Attributes in the Share Mask of a Channel Group . . OpenGL Performer Node Types . . . . . . . . . . . . . . . . . 12 . 41 . 51 pfGroup Functions . . . . . . . . . . . . . . pfDCS Transformations . . . . . . . . . . . . pfFCS Functions . . . . . . . . . . . . . . pfSequence Functions . . . . . . . . . . . . . pfLOD Functions . . . . . . . . . . . . . . pfLayer Functions . . . . . . . . . . . . . . pfGeode Functions . . . . . . . . . . . . . . pfText Functions . . . . . . . . . . . . . . pfBillboard Functions . . . . . . . . . . . . . pfPartition Functions . . . . . . . . . . . . . Traversal Attributes for the Major Traversals . . . . . . Test Instructions . . . . . . . . . . . . . . Assign Instructions . . . . . . . . . . . . . . Jump Instructions . . . . . . . . . . . . . . Functions Available for User-Defined Cull Program Instructions Cull Callback Return Values . . . . . . . . . . . Intersection-Query Token Names . . . . . . . . . Database Classes and Corresponding Node Masks . . . . Representing Traversal Mask Values . . . . . . . . Possible Traversal Results . . . . . . . . . . . . Frame Control Functions . . . . . . . . . . . . LOD Transition Zones . . . . . . . . . . . . . Multiprocessing Models . . . . . . . . . . . . Trigger Routines and Associated Processes . . . . . . . . 54 . 63 . 63 . 67 . 70 . 71 . 72 . 73 . 75 . 79 . 86 .106 .106 .107 .109 .113 .121 .123 .124 .125 .131 .145 .156 .168 xxxvii Tables Table 6-1 Table 6-2 Table 6-3 Table 6-4 Table 6-5 Table 6-6 Table 6-7 Table 6-8 Table 7-1 Table 7-2 Table 7-3 Table 7-4 Table 7-5 Table 7-6 Table 7-7 Table 7-8 Table 7-9 Table 7-10 Table 7-11 Table 7-12 Table 7-13 Table 7-14 Table 7-15 Table 7-16 Table 7-17 Table 7-18 Table 8-1 Table 8-2 Table 8-3 Table 8-4 Table 8-5 Table 8-6 Table 10-1 xxxviii pfEarthSky Functions . . . . . . . . . . pfEarthSky Attributes . . . . . . . . . . pfVolFog Functions . . . . . . . . . . . pfVolFog Attributes. . . . . . . . . . . pfVolFog Flags . . . . . . . . . . . . pfShadow Functions . . . . . . . . . . pfShadow Attributes . . . . . . . . . . Key Command-Line Options of makeProxyImages . Database-Importer Source Directories . . . . . libpfdu Database Converter Functions . . . . . Loader Name Composition . . . . . . . . libpfdu Database Converter Management Functions. pfdBuilder Modes and Attributes . . . . . . . Supported Database Formats . . . . . . . . Geometric Definitions in LSA Files . . . . . . RPC Converter Values . . . . . . . . . . RPC Converter Attributes . . . . . . . . . Object Tokens in the SGO Format . . . . . . . Mesh Control Tokens in the SGO Format . . . . OpenGL Performer Pseudo Loaders . . . . . . Default Path for the Maya Export Plug-in . . . . Maya Export Options . . . . . . . . . . Maya Features Supported by the Exporter . . . . Maya Exporter Support for UV Mapping Methods . . Maya Exporter Support for Material Properties . . . Maya Exporter Support for Texture Properties . . . pfGeoSet Routines . . . . . . . . . . . Geometry Primitives . . . . . . . . . . pfGeoSet PACKED_ATTR Formats . . . . . . Attribute Bindings . . . . . . . . . . . pfFont Routines . . . . . . . . . . . . pfString Routines . . . . . . . . . . . Topology Building Methods . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184 184 188 189 190 200 201 215 221 223 225 226 239 242 259 273 275 278 279 289 291 294 297 301 302 303 306 307 310 315 336 339 433 007-1680-100 Tables Table 10-2 Table 10-3 Table 12-1 Table 12-2 Table 12-3 Table 12-4 Table 12-5 Table 12-6 Table 12-7 Table 12-8 Table 12-9 Table 12-10 Table 12-11 Table 12-12 Table 12-13 Table 13-1 Table 14-1 Table 14-2 Table 14-3 Table 14-4 Table 14-5 Table 14-6 Table 14-7 Table 15-1 Table 15-2 Table 15-3 Table 15-4 Table 15-5 Table 15-6 Table 16-1 Table 16-2 Table 16-3 007-1680-100 Adding Topology and Tessellations to .iv and .csb Files . . Reading and Writing .pfb Files: with and without Tessellations pfGeoState Mode Tokens . . . . . . . . . . . . pfTransparency Tokens . . . . . . . . . . . . pfGeoState Value Tokens . . . . . . . . . . . . Enable and Disable Tokens . . . . . . . . . . . Rendering Attribute Tokens . . . . . . . . . . . Texture Image Sources . . . . . . . . . . . . . Texture Load Modes . . . . . . . . . . . . . Texture Generation Modes . . . . . . . . . . . pfFog Tokens . . . . . . . . . . . . . . . pfHlightMode() Tokens . . . . . . . . . . . . Matrix Manipulation Routines . . . . . . . . . . pfSprite Rotation Modes . . . . . . . . . . . . pfGeoState Routines . . . . . . . . . . . . . Uniform Variable Types . . . . . . . . . . . . pfPipeWindow Functions That Do Not Propagate . . . . . Methods for Querying the System for Hardware Compositors . Methods Used in Creating pfCompositors . . . . . . . Methods for Querying pfCompositors . . . . . . . . Static Methods for Querying pfCompositors . . . . . . Methods to Control the Load Balancing Transitions . . . . Methods for Managing Screen Space, Channel Clipping, and Antialiasing . . . . . . . . . . . . . . . . Tiling Algorithms . . . . . . . . . . . . . . Image Cache Configuration File Fields . . . . . . . . Image Tile Filename Tokens . . . . . . . . . . . Cliptexture Configuration File Fields . . . . . . . . Parameter Tokens . . . . . . . . . . . . . . Image Tile Filename Tokens . . . . . . . . . . . pfWinType() Tokens . . . . . . . . . . . . . pfWinFBConfigAttrs() Tokens . . . . . . . . . . Window System Types . . . . . . . . . . . . .434 .435 .453 .455 .458 .459 .460 .462 .466 .471 .475 .476 .477 .479 .485 .494 .513 .519 .521 .522 .524 .525 .529 .554 .566 .570 .573 .576 .578 .619 .622 .624 xxxix Tables Table 16-4 Table 17-1 Table 17-2 Table 18-1 Table 18-2 Table 18-3 Table 18-4 Table 18-5 Table 18-6 Table 18-7 Table 19-1 Table 20-1 Table 21-1 Table 22-1 Table 22-2 Table 22-3 Table 22-4 Table 22-5 Table 22-6 Table 22-7 Table 22-8 Table 22-9 Table 22-10 Table 22-11 Table 22-12 Table 26-1 Table 26-2 Table 26-3 Table 26-4 Table 26-5 Table 26-6 xl pfWinMode() Tokens . . . . . . . . . . . . pfPWinType Tokens . . . . . . . . . . . . Processes From Which to Call Main pfPipeWindow Functions Thread Information . . . . . . . . . . . . . Default Input and Output Ranges. . . . . . . . . pfVClock Routines . . . . . . . . . . . . . Memory Allocation Routines . . . . . . . . . . pfNotify Routines . . . . . . . . . . . . . Error Notification Levels . . . . . . . . . . . pfFilePath Routines . . . . . . . . . . . . . pfEngine Types . . . . . . . . . . . . . . Fields in the Triangle Data Structure . . . . . . . . Raster Versus Calligraphic Displays . . . . . . . . Routines for 3-Vectors . . . . . . . . . . . . Routines for 4x4 Matrices . . . . . . . . . . . Routines for Quaternions . . . . . . . . . . . Matrix Stack Routines . . . . . . . . . . . . Routines to Create Bounding Volumes . . . . . . . Routines to Extend Bounding Volumes . . . . . . . Routines to Transform Bounding Volumes . . . . . . Testing Points for Inclusion in a Bounding Volume. . . . Testing Volume Intersections . . . . . . . . . . Intersection Results . . . . . . . . . . . . . Available Intersection Tests . . . . . . . . . . Discriminator Return Values . . . . . . . . . . Corresponding Routines in the C and C++ API . . . . . Header Files for libpf Scene Graph Node Classes. . . . Header Files for Other libpf Classes . . . . . . . Header Files for libpr Graphics Classes . . . . . . Header Files for Other libpr Classes . . . . . . . Data and Functions Provided by User Subclasses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 626 636 641 654 657 659 660 667 667 668 686 713 752 776 777 782 783 786 787 787 788 789 789 793 794 882 883 884 885 886 892 007-1680-100 Examples Example 1-1 Example 1-2 Example 1-3 Example 1-4 Example 1-5 Example 1-6 Example 2-1 Example 2-2 Example 2-3 Example 2-4 Example 3-1 Example 3-2 Example 3-3 Example 3-4 Example 3-5 Example 3-6 Example 3-7 Example 3-8 Example 3-9 Example 3-10 Example 3-11 Example 4-1 Example 4-2 Example 4-3 Example 4-4 Example 5-1 Example 5-2 007-1680-100 How to Use User Data . . . Objects and Reference Counts . . . . . . . . . . . . . . . . . . . . Using pfDelete() with libpr Objects . . . . . . . . Using pfDelete() with libpf Objects . . . . . . . . Using pfCopy() . . . . . . . . . . . . . . . General-Purpose Scene Graph Traverser . . . . . . . pfPipes in Action . . . . . . . . . . . . . . Using pfChannels . . . . . . . . . . . . . . Multiple Channels, One Channel per Pipe . . . . . . . Channel Sharing . . . . . . . . . . . . . . Making a Scene . . . . . . . . . . . . . . . Hierarchy Construction Using Group Nodes . . . . . . Creating Cloned Instances. . . . . . . . . . . . Automatically Updating a Bounding Volume . . . . . . Using pfSwitch and pfSequence Nodes . . . . . . . . Marking a Runway with a pfLayer Node . . . . . . . Adding pfGeoSets to a pfGeode . . . . . . . . . . Adding pfStrings to a pfText . . . . . . . . . . . Setting Up a pfBillboard . . . . . . . . . . . . Setting Up a pfPartition . . . . . . . . . . . . Inheritance Demonstration Program . . . . . . . . . Application Callback to Make a Pendulum . . . . . . . pfNode Draw Callbacks . . . . . . . . . . . . Cull-Process Callbacks . . . . . . . . . . . . . Using Passthrough Data to Communicate with Callback Routines Frame Control Excerpt . . . . . . . . . . . . . Setting LOD Ranges . . . . . . . . . . . . . . 11 . 12 . 13 . 13 . 15 . 17 . 25 . 32 . 39 . 42 . 53 . 55 . 59 . 59 . 68 . 71 . 72 . 73 . 76 . 79 . 80 . 89 .114 .116 .119 .135 .143 xli Examples Example 5-3 Example 6-1 Example 6-2 Example 6-3 Example 8-1 Example 8-2 Example 12-1 Example 12-2 Example 12-3 Example 12-4 Example 14-1 Example 14-2 Example 14-3 Example 14-4 Example 14-5 Example 15-1 Example 16-1 Example 16-2 Example 16-3 Example 16-4 Example 17-1 Example 17-2 Example 17-3 Example 17-4 Example 17-5 Example 19-1 Example 19-2 Example 20-1 Example 21-1 Example 21-2 Example 21-3 Example 21-4 xlii Default Stress Function . . . . . . . . . . . . . 154 How to Configure a pfEarthSky . . . . . . . . . . 182 Fog initialization Using pfVolFogAddPoint() . . . . . . 187 Specifying Patchy Fog Boundaries Using pfVolFogAddNode() . 187 Loading Characters into a pfFont . . . . . . . . . . 336 Setting Up and Drawing a pfString . . . . . . . . . 337 Using pfDecal() to a Draw Road with Stripes . . . . . . 457 Pushing and Popping Graphics State . . . . . . . . . 481 Using pfOverride() . . . . . . . . . . . . . . 482 Inheriting State . . . . . . . . . . . . . . . 484 Configuring a System with Three Hyperpipe Groups . . . . 507 Mapping Hyperpipes to Graphic Pipes . . . . . . . . 508 More Complete Example: Mapping Hyperpipes to Graphic Pipes 508 Set FBConfigAttrs for Each pfPipeWindow . . . . . . . 514 Search the pfPipeWindow List of the pfPipe. . . . . . . 515 Estimating System Memory Requirements . . . . . . . 606 Opening a pfWindow . . . . . . . . . . . . . 618 Using the Default Overlay Window . . . . . . . . . 629 Creating a Custom Overlay Window . . . . . . . . . 630 pfWindows and X Input . . . . . . . . . . . . 631 Creating a pfPipeWindow . . . . . . . . . . . . 634 pfPipeWindow With Alternate Configuration Windows for Statistics 638 Custom Initialization of pfPipeWindow State . . . . . . 640 Configuration of a pfPipeWindow Framebuffer. . . . . . 643 Opening and Closing a pfPipeWindow . . . . . . . . 644 Fluxed pfGeoSet . . . . . . . . . . . . . . . 682 Connecting Engines and Fluxes . . . . . . . . . . 696 Aligning Light Points Above a pfASD Surface . . . . . . 728 Raster Callback Skeleton . . . . . . . . . . . . 745 Preprocessing a Display List - Light Point Process code . . . 750 Setting pfCalligraphic Parameters. . . . . . . . . . 763 Calligraphic Lights . . . . . . . . . . . . . . 767 007-1680-100 Examples Example 22-1 Example 22-2 Example 22-3 Example 22-4 Example 22-5 Example 26-1 Example 26-2 Example 26-3 Example 26-4 Example 26-5 007-1680-100 Matrix and Vector Math Examples . . . . . Quaternion Example . . . . . . . . . Quick Sphere Culling Against a Set of Half-Spaces Intersecting a Segment With a Convex Polyhedron Intersection Routines in Action . . . . . . Valid Creation of Objects in C++ . . . . . . Invalid Creation of Objects in C++ . . . . . Class Definition for a Subclass of pfDCS . . . Overloading the libpf Application Traversal . . Changeable Static Data Member . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .780 .782 .790 .791 .794 .888 .888 .892 .893 .896 xliii About This Guide Welcome to the OpenGL Performer application development environment. OpenGL Performer provides a programming interface (with ANSI C and C++ bindings) for creating real-time graphics applications and offers high-performance, multiprocessed rendering in an easy-to-use 3D graphics toolkit. OpenGL Performer interfaces with the OpenGL graphics library; this library combined with the IRIX, Linux, or Microsoft Windows (Windows 2000, Windows NT, and Windows XP) operating system forms the foundation of a powerful suite of tools and features for creating real-time 3D graphics applications. Why Use OpenGL Performer? Use OpenGL Performer for building visual simulation applications and virtual reality environments; for rapid rendering in on-air broadcast and virtual-set applications; for assembly viewing in large, simulation-based design tasks; or to maximize the graphics performance of any application. Applications that require real-time visuals, free-running or fixed-frame-rate display, or high-performance rendering will benefit from using OpenGL Performer. OpenGL Performer drastically reduces the work required to tune your application’s performance. General optimizations include the use of highly tuned routines for all performance-critical operations and the reorganization of graphics data and operations for faster rendering. OpenGL Performer also handles SGI architecture-specific tuning issues for you by selecting the best rendering and multiprocessing modes at run time, based on the system configuration. OpenGL Performer is an integral part of SGI visual simulation systems. It provides the interface to advanced features available exclusively with the SGI product line, such as the Silicon Graphics Prism, Silicon Graphics Onyx4 UltimateVision, InfiniteReality, Silicon Graphics Octane, Silicon Graphics O2, and VPro graphics subsystems. OpenGL Performer teamed with SGI graphics hardware provide a sophisticated image generation system in a powerful, flexible, and extensible software environment. OpenGL Performer is also tuned to operate efficiently on a variety of graphics platforms; you do 007-1680-100 xlv About This Guide not need the hardware sophistication of InfiniteReality graphics to benefit from OpenGL Performer. What You Should Know Before Reading This Guide To use OpenGL Performer, you should be comfortable programming in ANSI C or C++. You should also have a fairly good grasp of graphics programming concepts. Terms such as “texture map” and “homogeneous coordinate” are not explained in this guide. It helps if you are familiar with the OpenGL library. On the other hand, though you need to know a little about graphics, you do not have to be a seasoned C (or C++) programmer, a graphics hardware guru, or a graphics-library virtuoso to use OpenGL Performer. OpenGL Performer puts the engineering expertise behind SGI hardware and software at your fingertips, so you can minimize your application development time while maximizing the application’s performance and visual impact. For a concise description of OpenGL Performer basics, see the OpenGL Performer Getting Started Guide. How to Use This Guide The best way to get started is to read the OpenGL Performer Getting Started Guide. If you like learning from sample code, turn to Chapter 1, “Getting Acquainted with OpenGL Performer,” which takes you on a tour of some demo programs. These programs let you see for yourself what OpenGL Performer does. Even if you are not developing a visual simulation application, you might want to look at the demos to see high-performance rendering in action. At the end of Chapter 2 in that guide, you will find suggestions pointing to possible next steps; alternatively, you can browse through the summary below to find a topic of interest. What This Guide Contains This guide is divided into the following chapters and appendixes: • xlvi Chapter 1, “OpenGL Performer Programming Interface,” describes the fundamental ideas behind the OpenGL Performer programming interface. 007-1680-100 About This Guide 007-1680-100 • Chapter 2, “Setting Up the Display Environment,” describes how to set up rendering pipelines, windows, and channels (cameras). • Chapter 3, “Nodes and Node Types,” describes the data structures used in OpenGL Performer’s memory-based, scene-definition databases. • Chapter 4, “Database Traversal,” explains how to manipulate and examine a scene graph. • Chapter 5, “Frame and Load Control,” explains how to control frame rate, synchronization, and dynamic load management. This chapter also discusses the load management techniques of multiprocessing and level-of-detail. • Chapter 6, “Creating Visual Effects,” describes how to use environmental, atmospheric, lighting, and other visual effects to enhance the realism of your application. • Chapter 7, “Importing Databases,” describes database formats and sample conversion utilities. • Chapter 8, “Geometry,” discusses the classes used to create geometry in OpenGL Performer scenes. • Chapter 9, “Higher-Order Geometric Primitives” describes higher-order primitives, including classes to define discrete curves and surfaces. • Chapter 10, “Creating and Maintaining Surface Topology” describes the connectivity of parametric surfaces—that is, their topology. • Chapter 11, “Rendering Higher-Order Primitives: Tessellators” describes how to control the tessellation of shapes. • Chapter 12, “Graphics State,” describes the graphics state, which contains all of the fields that together define the appearance of geometry. • Chapter 13, “Shaders,” describes the shader, a mechanism that allows complex rendering equations to be applied to OpenGL Performer objects. • Chapter 14, “Using Scalable Graphics Hardware,” describes how to use OpenGL Performer in conjunction with an SGI Video Digital Multiplexer (DPLEX), an SGI Scalable Graphics Compositor, and graphics processing units (GPUs). • Chapter 15, “ClipTextures,” describes how to work with large, high-resolution textures. • Chapter 16, “Windows,” describes how to create, configure, manipulate, and communicate with a window in OpenGL Performer. xlvii About This Guide • Chapter 17, “pfPipeWindows and pfPipeVideoChannels,” describes the unified window and video channel control and management provided by pfPipeWindows and pfPipeVideoChannels. • Chapter 18, “Managing Nongraphic System Tasks,” describes clocks, memory allocation, synchronous I/O, error handling and notification, and search paths. • Chapter 19, “Dynamic Data,” describes how to connect pfFlux, pfFCS, and pfEngine nodes, which together can be used for animating geometries. • Chapter 20, “Active Surface Definition,” describes the Active Surface Definition (ASD): a library that handles real-time surface meshing and morphing. • Chapter 21, “Light Points,” describes the calligraphic lights, which are intensely bright lights. • Chapter 22, “Math Routines,” details the comprehensive math support provided as part of OpenGL Performer. • Chapter 23, “Statistics,” discusses the various kinds of statistics you can collect and display about the performance of your application. • Chapter 24, “Performance Tuning and Debugging,” explains how to use performance measurement and debugging tools and provides hints for getting maximum performance. • Chapter 25, “Building a Visual Simulation Application Using libpfv” describes a modular approach to building an application using a graphical viewer. • Chapter 26, “Programming with C++,” discusses the differences between using the C and C++ programming interfaces. Sample Applications You can find the sample code for all of the sample OpenGL Performer applications installed under /usr/share/Performer/src/pguide on IRIX and Linux and under %PFROOT%\Src\pguide on Microsoft Windows. xlviii 007-1680-100 About This Guide Conventions This guide uses the following typographical conventions: Bold Used for function names, with parentheses appended to the name and also for the names of window menus and buttons. Also, bold lowercase letters represent vectors, and bold uppercase letters denote matrices. Italics Indicates variables, book titles, and glossary items. Fixed-width Used for filenames, operating system command names, command-line option flags, code examples, and system output. Bold Fixed-width Indicates user input, such as items that you type in from the keyboard. Note that in some cases it is convenient to refer to a group of similarly named OpenGL Performer functions by a single name; in such cases an asterisk is used to indicate all the functions whose names start the same way. For instance, pfNew*() refers to all functions whose names begin with “pfNew”: pfNewChan(), pfNewDCS(), pfNewESky(), pfNewGeode(), and so on. Internet and Hardcopy Reading for the OpenGL Performer Series The OpenGL Performer series include the followingmanuals in printed and online formats: • OpenGL Performer Programmer’s Guide (this book) • OpenGL Performer Getting Started Guide To read these online books, point your browser at the following: • http://docs.sgi.com For general information about OpenGL Performer, use the following URL: • http://www.sgi.com/software/performer The info-performer mailing list provides a forum for discussion of OpenGL Performer including technical and nontechnical issues. Subscription requests should be sent to [email protected] Much like the comp.sys.sgi.* newsgroups on the Internet, it is not an official support channel but is monitored by 007-1680-100 xlix About This Guide several interested SGI employees familiar with the toolkit. The OpenGL Performer mailing list archives are at the following URL: • http://oss.sgi.com/projects/performer/mail/info-performer/ Reader Comments If you have comments about the technical accuracy, content, or organization of this document, please tell us. Be sure to include the title and document number of the manual with your comments. (Online, the document number is located in the front matter of the manual. In printed manuals, the document number can be found on the back cover.) You can contact us in any of the following ways: • Send e-mail to the following address: [email protected] • Use the Feedback option on the Technical Publications Library World Wide Web page: http://docs.sgi.com • Contact your customer service representative and ask that an incident be filed in the SGI incident tracking system. • Send mail to the following address: Technical Publications SGI 1600 Amphitheatre Pkwy., M/S 535 Mountain View, California 94043-1351 We value your comments and will respond to them promptly. l 007-1680-100 Chapter 1 1. OpenGL Performer Programming Interface This chapter describes the fundamental ideas behind the OpenGL Performer programming interface in the following sections: • “General Naming Conventions” on page 1 • “Class API” on page 3 • “Base Classes” on page 6. General Naming Conventions The OpenGL Performer application programming interface (API) uses naming conventions to help you understand what a given command will do and even predict the appropriate names of routines for desired functionality. Following similar naming practices in the software that you develop will make it easier for you and others on your team to understand and debug your code. The API is largely object-oriented; it contains classes of objects comprised of methods that do the following: • Configure their parent objects. • Apply associated operations, based on the current configuration of the object. Both C and C++ bindings are provided for OpenGL Performer. In addition, naming conventions provide a consistent and predictable API and indicate the kind of operations performed by a given command. Prefixes The prefix of the command tells you in which library a C command or C++ class is found. All exposed OpenGL Performer base library C commands and C++ classes begin with ’pf’. The utility libraries use an additional prefix letter, such as ’pfu’ for the libpfutil 007-1680-100 1 1: OpenGL Performer Programming Interface general utility library, ’pfi’ for the libpfui input handling library, and ’pfd’ for the libpfdu database utility library. libpr-level commands still have the ’pf’ prefix as they are still in the main libpf library Header Files Each OpenGL Performer library contains a main header file in /usr/include/Performer on IRIX and Linux and in %PFROOT%\Include on Microsoft Windows that contains type and class definitions, the C API for that library, and global routines that are part of the C and C++ API. libpf is broken into two distinct pieces: the low-level rendering layer, libpr, and the application layer, libpf, and each has its own main header file: pr.h and pf.h. Since libpf is considered to include libpr, pf.h includes pr.h. C++ class header files are found under the following directories: /usr/include/Performer/{pf, pr, ...} (IRIX and Linux) %PFROOT%\Include\{pf, pr, ...} (Microsoft Windows) Each class has its own C++ header file and that header must be included to use that class. #include <Performer/pf.h> #include <Performer/pf/pfGroup.h> ..... pfGroup *group; Naming in C and C++ All C++ class method names have an expanded C counterpart. Typically, the C routine (function)will include the class name in the routine, whereas the C++ method will not. C: pfGetPipeScreen(); C++: pipe->getScreen(); For some very general routines on the most abstract classes, the class name is omitted. This is the case with the child API on pfNodes: C: pfAddChild(node,child); C++: node->addChild(child); Command and type names are mixed case where the first letter of a new word in a name is capitalized. C++ method names always start with a lower case letter. 2 007-1680-100 Class API pfTexture *texture; texture->loadFile(); Abbreviations Type names do not use abbreviations. The C API acting on that type will often use abbreviations for the type names, as will the associated tokens and enums. In procedure names, a name will always be abbreviated or never, and the same abbreviation will always be used and will be in the pfNew* C command. For example: the pfTexture object uses ‘Tex’ in its API, such as pfNewTex(). If a type name has multiple words, the abbreviation will use the first letter of the first words and then the first syllable of the last word. pfPipeWindow *pwin = pfNewPWin(); pfPipeVideoChannel *pvchan = pfNewPVChan(); pfTexLOD *tlod = pfNewTLOD(); Macros, Tokens, and Enums Macros, tokens, and enums all use full upper-case. Token names associated with a class and methods of a class start with the abbreviated name for that class, such as texture to “tex” in PFTEX_SHARPEN. Class API The API of a given class, such as pfTexture, is comprised of the following: 007-1680-100 • API to create an instance of the object • API to set parameters on the object • API to get those parameter settings • API to perform actions on the configured object 3 1: OpenGL Performer Programming Interface Object Creation Objects are always created with the following: C: pfThing *thing = pfNewThing(); C++: pfThing *thing = new pfThing; libpf objects are automatically created out of the shared memory arena. libpr objects take as an argument an arena pointer which, if NULL, will cause allocation off the heap. Set Routines A set routine has the following form: C: pfThingParam(thing, ... ) C++: thing->setParam() Note that there is no ‘Set’ in the name in the C version. Set routines are usually very fast and are not order dependent. Work required to process the settings happens once when the object is first used after settings have changed. If particularly expensive options must be done, there will be a pfConfigThing routine or method to explicitly force this work that must be called before the object is to be used. Get Routines For every ‘set’ routine there is a matching ‘get’ routine to get back the value that was set. C: pfGetThingParam(thing, ... ) C++: thing->getParam() If the set/get is for a single value, that value is usually the return value of the routine. If there are multiple values together, the ‘get’ routine will then take as arguments pointers to result variables. Getting Current In-Use Values Get routines return values that have been previously set by the user, or default values if no settings have been made. Sometimes a value other than the user-specified value is currently in use and that is the value that you would like to get. For these cases, there is a separate ‘GetCur’ routine to get the current in-use value. 4 007-1680-100 Class API C: pfGetCurThingParam() C++: thing->getcurParam() These ‘cur’ routines may only be able to give reasonable values in the process which associated operations are happening. For example, to get the current texture (pfGetCurTex()), you need to be in the draw process since that is the only process that has a current texture. Action Routines An action routine has the following form: C: pfVerbThing(), such as pfApplyTex() C++: thing->verb(), such as tex->apply() Action routines can have parameter scope and apply only to that parameter. These routines have the following form C: pfVerbThingParam(), such as pfApplyTexMinLOD() C++: thing->verbParam(), such as tex->applyMinLOD() Apply and Draw Routines The Apply and Draw action routines do graphics operations and must happen either in the draw process or in display list mode. C: pfApplypfGState() pfDrawGSet() C++: gstate->apply() gset->draw() Enable and Disable of Modes Features that can be enabled and disabled are done so with pfEnable() and pfDisable(), respectively. pfGetEnable() takes PFEN_* tokens naming the graphics state operation to enable or disable. A GetEnable() is used to query enable status and will return 1 or 0 if the given mode is enabled or disabled, respectively. ex: pfEnable(PFEN_TEXTURE), pfDisable(PFEN_TEXTURE), pfGetEnable(PFEN_TEXTURE); 007-1680-100 5 1: OpenGL Performer Programming Interface Mode, Attribute, or Value Classes instances are configured by having their internal fields set. These fields may be simple modes or complex attribute structures. Mode values are ints or tokens, attributes are typically pointers to objects, and values are floats. pfGStateMode(gstate, PFSTATE_DECAL, PFDECAL_LAYER) pfGStateAttr(gstate, PFSTATE_TEXTURE, texPtr) pfGStateVal(gstate, PFSTATE_ALPHAREF, 0.5) Base Classes OpenGL Performer provides an object-oriented programming interface to most of its data structures. Only OpenGL Performer functions can change the values of elements of these data structures; for instance, you must call pfMtlColor() to set the color of a pfMaterial structure rather than modifying the structure directly. For a more transparent type of memory, OpenGL Performer provides pfMemory. All object classes are derived from pfMemory. pfMemory instances must be explicitly allocated with the new operator and cannot be allocated statically, on the stack, or included directly in other object definitions. pfMemory is managed memory; it includes special fields, such as size, arena, and ref count, that are initialized by the pfMemory new() function. Some very simple and unmanaged data types are not encapsulated for speed and easy access. Examples include pfMatrix, pfSphere and pfVec3. These data types are referred to as public structures and are inherited from pfStruct. Unlike pfMemory, pfStructs can be handled as follows: • Allocated statically • Allocated on the stack • Included directly in other structure and object definitions pfStructs allocated off the stack or allocated statically are not in the shared memory arena and thus are not safe for multiprocessed use. Also, pfStructs allocated off the stack in a procedure do not exist after the procedure exits so they should not be given to persistent objects, such as a pfVec3 array of vertices for a pfGeoSet. 6 007-1680-100 Base Classes In order to allow some functions to apply to multiple data types, OpenGL Performer uses the concept of class inheritance. Class inheritance takes advantage of the fact that different data types (classes) often share attributes. For example, a pfGroup is a node that can have children. A pfDCS (Dynamic Coordinate System) has the same basic structure as a pfGroup, but also defines a transformation to apply to its children—in other words, the pfDCS data type inherits the attributes of the pfGroup and adds new attributes of its own. This means that all functions that accept a pfGroup* argument will alternatively accept a pfDCS* argument. For example, pfAddChild() takes a pfGroup* argument, but appends child to the list of children belonging to dcs: pfDCS *dcs = pfNewDCS(); pfAddChild(dcs, child); Because the C language does not directly express the notion of classes and inheritance, arguments to functions must be cast before being passed, as shown in this example: pfAddChild((pfGroup*)dcs, (pfNode*)child); In the example above, no such casting is required because OpenGL Performer provides macros that perform the casting when compiling with ANSI C, as shown in this example: #define pfAddChild(g, c) pfAddChild((pfGroup*)g, (pfNode*)c) Note: Using automatic casting eliminates type checking—the macros will cast anything to the desired type. If you make a mistake and pass an unintended data type to a casting macro, the results may be unexpected. No such trickery is required when using the C++ API. Full type checking is always available at compile time. Inheritance Graph The relations between classes can be arranged in a directed acyclic inheritance graph in which each child inherits all of its parent’s attributes, as illustrated in Figure 1-1. OpenGL Performer does not use multiple inheritance, so each class has only one parent in the graph. 007-1680-100 7 1: OpenGL Performer Programming Interface Note: It is important to remember that an inheritance graph is different from a scene graph. The inheritance graph shows the inheritance of data elements and member functions among user-defined data types; the scene graph shows the relationship among instances of nodes in a hierarchical scene definition. 8 007-1680-100 Base Classes pfObject pfLight pfPipe pfMaterial pfNode pfGeoSet pfChannel pfFrustum Some classes found in libpf Some classes found in libpr Figure 1-1 007-1680-100 Partial Inheritance Graph of OpenGL Performer Data Types 9 1: OpenGL Performer Programming Interface OpenGL Performer objects are divided into two groups: those found in the libpf library and those found in the libpr library. These two groups of objects have some common attributes, but also differ in some respects. While OpenGL Performer only uses single inheritance, some objects encapsulate others, hiding the encapsulated object but also providing a functional interface that mimics its original one. For example a pfChannel has a pfFrustum, a pfFrameStats has a pfStats, a pfPipeWindow has a pfWindow, and a pfPipeVideoChannel has a pfVideoChannel. In these cases, the first object in each pair provides functions corresponding to those of the second. For example, pfFrustum has a routine: pfMakeSimpleFrust(frust, 45.0f); pfChannel has a corresponding routine: pfMakeSimpleChan(channel, 45.0f); libpr and libpf Objects All of the major classes in OpenGL Performer are derived from the pfObject class. This common, base class unifies the data types by providing common attributes and functions. libpf objects are further derived from pfUpdatable. The pfUpdatable abstract class provides support for automatic multibuffering for multiprocessing. pfObjects have no special support for multiprocessing and so all processes share the same copy of the pfObject in the shared arena. libpr objects allocated from the heap are only visible in the process in which they are created or in child processes created after the object. Changes made to such an object in one process are not visible in any other process. Explicit multibuffering of pfObjects is available through the pfFlux class. In general, libpr provides lightweight and low-level modular pieces of functionality that are then enhanced by more powerful libpf objects. User Data The primary attribute defined by the pfObject class is the custom data a user gets to define on any pfObject called “user data.” pfUserDataSlot attaches the user-supplied data pointer to user data. pfUserData attaches the user-supplied data pointer to user data slot. Example 1-1 shows how to use user data. 10 007-1680-100 Base Classes Example 1-1 How to Use User Data typedef struct { float coeffFriction; float density; float *dataPoints; } myMaterial; myMaterial *granite; granite = (myMaterial *)pfMalloc(sizeof(myMaterial), NULL); granite->coeffFriction = 0.5f; granite->density = 3.0f; granite->dataPoints = (float *)pfMalloc(sizeof(float)*8, NULL); graniteMtl = pfNewMtl(NULL); pfUserData(graniteMtl, granite); pfDelete() and Reference Counting Most kinds of data objects in OpenGL Performer can be placed in a hierarchical scene graph, using instancing when an object is referenced multiple times. Scene graphs can become quite complex, which can cause problems if you are not careful. Deleting objects can be a particularly dangerous operation, for example, if you delete an object that another object still references. Reference counting provides a bookkeeping mechanism that makes object deletion safe: an object is never deleted if its reference count is greater than zero. All libpr objects (such as pfGeoState and pfMaterial) have a reference count that specifies how many other objects refer to it. A reference is made whenever an object is attached to another using the OpenGL Performer routines shown in Table 1-1. 007-1680-100 11 1: OpenGL Performer Programming Interface Table 1-1 Routines that Modify libpr Object Reference Counts Routine Action pfGSetGState() Attaches a pfGeoState to a pfGeoSet. pfGStateAttr() Attaches a state structure (such as a pfMaterial) to a pfGeoState. pfGSetHlight() Attaches a pfHighlight to a pfGeoSet. pfTexDetail() Attaches a detail pfTexture to a base pfTexture. pfGSetAttr() Attaches attribute and index arrays to a pfGeoSet. pfTexImage() Attaches an image array to a pfTexture. pfAddGSet(), Modify pfGeoSet/pfGeode association. pfReplaceGSet(), pfInsertGSet() When object A is attached to object B, the reference count of A is incremented. Additionally, if A replaces a previously referenced object C, then the reference count of C is decremented. Example 1-2 demonstrates how reference counts are incremented and decremented. Example 1-2 Objects and Reference Counts pfGeoState *gstateA, *gstateC; pfGeoSet *gsetB; /* Attach gstateC to gsetB. Reference count of gstateC * is incremented. */ pfGSetGState(gsetB, gstateC); /* Attach gstateA to gsetB, replacing gstateC. Reference * count of gstateC is decremented and that of gstateA * is incremented. */ pfGSetGState(gsetB, gstateA); This automatic reference counting done by OpenGL Performer routines is usually all you will ever need. However, the routines pfRef(), pfUnref(), and pfGetRef() allow you to increment, decrement, and retrieve the reference count of a libpr object should you wish to do so. These routines also work with objects allocated by pfMalloc(). 12 007-1680-100 Base Classes An object whose reference count is equal to 0 can be deleted with pfDelete(). pfDelete() works for all libpr objects and all pfNodes but not for other libpf objects like pfPipe and pfChannel. pfDelete() first checks the reference count of an object. If the reference count is nonpositive, pfDelete() decrements the reference count of all objects that the current object references, then it deletes the current object. pfDelete() does not stop here but continues down all reference chains, deleting objects until it finds one whose count is greater than zero. Once all reference chains have been explored, pfDelete returns a boolean indicating whether it successfully deleted the first object or not. Example 1-3 illustrates the use of pfDelete() with libpr. Example 1-3 Using pfDelete() with libpr Objects pfGeoState *gstate0, *gstate1; pfMaterial *mtl; pfGeoSet *gset; gstate0 = pfNewGState(arena); /* initial ref count is 0 */ gset = pfNewGSet(arena); /* initial ref count is 0 */ mtl = pfNewMtl(arena); /* initial ref count is 0 */ /* Attach mtl to gstate0. Reference count of mtl is * incremented. */ pfGStateAttr(gstate0, PFSTATE_FRONTMTL, mtl); /* Attach mtl to gstate1. Reference count of mtl is * incremented. */ pfGStateAttr(gstate1, PFSTATE_FRONTMTL, mtl); /* Attach gstate0 to gset. Reference count of gstate0 is * incremented. */ pfGSetGState(gset, gstate0); /* This deletes gset, gstate0, but not mtl since gstate1 is * still referencing it. */ pfDelete(gset); Example 1-4 illustrates the use of pfDelete() with libpf. Example 1-4 Using pfDelete() with libpf Objects pfGroup *group; pfGeode *geode; pfGeoSet *gset; group = pfNewGroup(); /* initial parent count is 0 */ 007-1680-100 13 1: OpenGL Performer Programming Interface geode = pfNewGeode(); /* initial parent count is 0 */ gset = pfNewGSet(arena); /* initial ref count is 0 */ /* Attach geode to group. Parent count of geode is * incremented. */ pfAddChild(group, geode); /* Attach gset to geode. Reference count of gset is * incremented. */ pfAddGSet(geode, gset); /* This has no effect since the parent count of geode is 1.*/ pfDelete(geode); /* This deletes group, geode, and gset */ pfDelete(group); Some notes about reference counting and pfDelete(): • All reference count modifications are locked so that they guarantee mutual exclusion when multiprocessing. • Objects added to a pfDispList do not have their counts incremented due to performance considerations. • In the multiprocessing environment of libpf, the successful deletion of a pfNode does not have immediate effect but is delayed one or more frames until all processes in all processing pipelines are through with the node. This accounts for the fact that pfDispLists do not reference-count their objects. • pfUnrefDelete(obj) is shorthand for the following: if(pfUnref(obj) ==0) pfDelete(obj); This is true when pfUnrefGetRef is atomic. • 14 Objects whose count reaches zero are not automatically deleted by OpenGL Performer. You must specifically request that an object be deleted with pfDelete() or pfUnrefDelete(). 007-1680-100 Base Classes Copying Objects with pfCopy() pfCopy() is currently implemented for libpr (and pfMalloc()) objects only. Object references are copied and reference counts are modified appropriately, as illustrated in Example 1-5. Example 1-5 Using pfCopy() pfGeoState *gstate0, *gstate1; pfMaterial *mtlA, *mtlB; gstate0 = pfNewGState(arena); gstate1 = pfNewGState(arena); mtlA = pfNewMtl(arena); /* initial ref count is 0 */ mtlB = pfNewMtl(arena); /* initial ref count is 0 */ /* Attach mtlA to gstate0. Reference count of mtlA is * incremented. */ pfGStateAttr(gstate0, PFSTATE_FRONTMTL, mtlA); /* Attach mtlB to gstate1. Reference count of mtlB is * incremented. */ pfGStateAttr(gstate1, PFSTATE_FRONTMTL, mtlB); /* gstate1 = gstate0. The reference counts of mtlA and mtlB * are 2 and 0 respectively. Note that mtlB is NOT deleted * even though its reference count is 0. */ pfCopy(gstate1, gstate0); pfMalloc and the related routines provide a consistent method to allocate memory, either from the user’s heap (using the C-library malloc() function) or from a shared memory arena. Printing Objects with pfPrint() pfPrint() can print many different kinds of objects to a file; for example, you can print nodes and geosets. To do so, you specify in the argument of the function the object to print, the level of verbosity, and the destination file. An additional argument, which, specifies different data according to the type of object being printed. The different levels of verbosity include the following: • 007-1680-100 PFPRINT_VB_OFF—no printing 15 1: OpenGL Performer Programming Interface • PFPRINT_VB_ON—minimal printing (default) • PFPRINT_VB_NOTICE—minimal printing (default) • PFPRINT_VB_INFO—considerable printing • PFPRINT_VB_DEBUG—exhaustive printing If the object to print is a type of pfNode, which specifies whether the print traversal should only traverse the current node (PFTRAV_SELF) or the entire scene graph where the node specified in the argument is the root node (PFTRAV_SELF | PFTRAV_DESCEND). For example, to print an entire scene graph, in which scene is the root node, to the file, fp, with default verbosity, use the following line of code: file = fopen (“scene.out”,”w”); pfPrint(scene, PFTRAV_SELF | PFTRAV_DESCEND, PFPRINT_VB_ON, fp); fclose(file); If the object to print is a pfFrameStats, which should specify a bitmask of the frame statistics classes that you want printed. The values for the bitmask include the following: • PFSTATS_ON enables the specified classes. • PFSTATS_OFF disables the specified classes. • PFSTATS_DEFAULT sets the specified classes to their default values. • PFSTATS_SET sets the class enable mask to enmask. For example, to print select classes of a pfFrameStats structure, stats, to stderr, use the following line of code: pfPrint(stats, PFSTATS_ENGFX | PFFSTATS_ENDB | PFFSTATS_ENCULL,PFSTATS_ON, NULL); If the object to print is a pfGeoSet, which is ignored and information about that pfGeoSet is printed according to the verbosity indicator. The output contains the types, names, and bounding volumes of the nodes and pfGeoSets in the hierarchy. For example, to print the contents of a pfGeoSet, gset, to stderr, use the following line of code: pfPrint(gset, NULL, PFPRINT_VB_DEBUG, NULL); Note: When the last argument, file, is set to NULL, the object is printed to stderr. 16 007-1680-100 Base Classes Determining Object Type Sometimes you have a pointer to a pfObject but you do not know what it really is—is it a pfGeoSet, a pfChannel, or something else? pfGetType() returns a pfType which specifies the type of a pfObject. This pfType can be used to determine the class ancestry of the object. Another set of routines, one for each class, returns the pfType corresponding to that class, for example, pfGetGroupClassType() returns the pfType corresponding to pfGroup. pfIsOfType() tells whether an object is derived from a specified type, as opposed to being the exact type. With these functions you can test for class type as shown in Example 1-6. Example 1-6 General-Purpose Scene Graph Traverser void travGraph(pfNode *node) { if (pfIsOfType(node, pfGetDCSClassType())) doSomethingTransforming(node); /* If ’node’ is derived from pfGroup then recursively * traverse its children */ if (pfIsOfType(node, pfGetGroupClassType())) for (i = 0; i < pfGetNumChildren(node); i++) travGraph(pfGetChild(node, i)); } Because OpenGL Performer allows subclassing of built-in types, when decisions are made based on the type of an object, it is usually better to use pfIsOfType() to test the type of an object rather than to test for the strict equality of the pfTypes. Otherwise, the code will not have reasonable default behavior with file loaders or applications that use subclassing. The pfType returned from pfGetType() is useful for programs but it is not in a readable form for you. Calling pfGetTypeName() on a pfType returns a null-terminated ASCII string that identifies an object’s type. For a pfDCS, for example, pfGetTypeName() returns the string “pfDCS.” The type returned by pfGetType() can then be compared to a class type using pfIsOfType(). Class types can be returned by methods such as pfGetGroupClassType(). 007-1680-100 17 Chapter 2 2. Setting Up the Display Environment You can use the library libpf or libpfv as your base to build your application. For the most part, this chapter (and guide) shows how to do so with libpf. For a more modular approach using a graphical viewer, see Chapter 25, “Building a Visual Simulation Application Using libpfv”. The library libpf is a visual-database processing and rendering system. The visual database has at its root a pfScene (as described in Chapter 3 and Chapter 4). The chain of events necessary to proceed from the scene graph to the display includes the following: 1. A pfScene is viewed by a pfChannel. 2. The pfChannel view of the pfScene is rendered by a pfPipe into a framebuffer. 3. A pfPipeWindow manages the framebuffer. 4. The images in the framebuffer are transmitted to a display system that is managed by a pfPipeVideoChannel. Figure 2-1 shows this chain of events. 007-1680-100 19 2: Setting Up the Display Environment pfPipe pfChannel 0 pfChannel 1 w Windo pfPipe 1 pfScene w Windo pfPipe 0 Scene graph Display system pfChannel 0 Figure 2-1 pfChannel 1 From Scene Graph to Visual Display The following sections describe how to implement this chain of events using pfPipes, pfPipeWindows, and pfChannels directly or through the use of a configuration file: 20 • “Using Pipes” on page 21 • “Using Channels” on page 26 • “Controlling the Video Output” on page 34 • “Using Multiple Channels” on page 35 007-1680-100 Using Pipes • “Using Channel Groups” on page 40 • “Importing OpenGL Multipipe SDK (MPK) Configuration Files” on page 44 Using Pipes This section describes rendering pipelines (pfPipes) and their implementation in OpenGL Performer. Each rendering pipeline draws into one or more windows (pfPipeWindows) associated with a single geometry pipeline. A minimum of one rendering pipeline is required, although it is possible to have more than one. The Functional Stages of a Pipeline This rendering pipeline comprises three primary functional stages: APP Simulation processing, which includes reading input from control devices, simulating the vehicle dynamics of moving models, updating the visual database, and interacting with other networked simulation stations. CULL Traverses the visual database and determines which portions of it are potentially visible (a procedure known as culling), selects a level of detail (LOD) for each model, sorts objects and optimizes state management, and generates a display list of objects to be rendered. DRAW Traverses the display list and issues graphics library commands to a Geometry Pipeline in order to create an image for subsequent display. Figure 2-2 shows the process flow for a single-pipe system. The application constructs and modifies the scene definition (a pfScene) associated with a channel. The traversal process associated with that channel’s pfPipe then traverses the scene graph, building an OpenGL Performer libpr display list. As shown in the figure, this display list is used as input to the draw process that performs the actual graphics library actions required to draw the image. 007-1680-100 21 2: Setting Up the Display Environment Application Scene Traversal/Cull Draw Frame Buffer Pipeline 0 Figure 2-2 Single Graphics Pipeline OpenGL Performer also provides additional processes for application processing tasks, such as database loading and intersection traversals, but these processes are optinal and are asynchronous to the software rendering pipeline(s). An OpenGL Performer application renders images using one or more pfPipes. Each pfPipe represents an independent software-rendering pipeline. Most IRIS systems contain only one Geometry Pipeline; so, a single pfPipe is usually appropriate. This single pipeline is often associated with a window that occupies the entire display surface. Alternative configurations include Onyx3 systems with InfiniteReality3 graphics (allowing up to 16 Geometry Pipelines). Applications can render into multiple windows, each of which is connected to a single Geometry Pipeline through a pfPipe rendering pipeline. Figure 2-3 shows the process flow for a dual-pipe system. Notice both the differences and similarities between these two figures. Each pipeline (pfPipe) is independent in multiple-pipe configurations; the traversal and draw tasks are separate, as are the libpr display lists that link them. In contrast, these pfPipes are controlled by the same application process, and in many situations access the same shared scene definition. 22 007-1680-100 Using Pipes Application Pipeline 0 Figure 2-3 Scene Pipeline 1 Traversal/Cull Draw Traversal/Cull Draw Frame Buffer Frame Buffer Dual Graphics Pipeline Each of these stages can be combined into a single process or split into multiple processes (pfMultiprocess) for enhanced performance on multiple CPU systems. Multiprocessing and multiple pipes are advanced topics that are discussed in “Successful Multiprocessing with OpenGL Performer” in Chapter 5. Creating and Configuring a pfPipe pfPipes and their associated processes are created when you call pfConfig(). They exist for the duration of the application. After pfConfig(), the application can get handles to the created pfPipes using pfGetPipe(). The argument to pfGetPipe() indicates which pfPipe to return and is an integer between 0 and numPipes-1, inclusive. The pfPipe handle is then used for further configuration of the pfPipe. pfMultipipe() specifies the number of pfPipes desired; the default is one. pfMultiprocess() specifies the multiprocessing mode used by all pfPipes. These two routines are discussed further in “Successful Multiprocessing with OpenGL Performer” in Chapter 5. 007-1680-100 23 2: Setting Up the Display Environment A key part of pfPipe initialization is the determination of the graphics hardware pipeline (or screen) and the creation of a window on that screen. The screen of a pfPipe can be set explicitly using pfPipeScreen(). Under single pipe operation, pfPipes can also inherit the screen of their first opened window. Under multipipe operation, the screen of all pfPipes must be determined before the pipes are configured by pfConfigStage() or the first call to pfFrame(). There may be other operations that require preset knowledge of the screen even under single pipes, such as custom configuration of video channels, discussed in “Creating and Configuring a pfChannel” on page 26. Once the screen of a pfPipe has been set, it cannot be changed. All windows of a given pfPipe must be opened on the same screen. A graphics window is associated with a pfPipe through the pfPipeWindow mechanism. If you do not create a pfPipeWindow, OpenGL Performer will automatically create and open a full screen window with a default configuration for your pfPipe. Once you create and initialize a pfPipe, you can query information about its configuration parameters. pfGetPipeScreen() returns the index number of the hardware pipeline for the pfPipe, starting from zero. On single-pipe systems the return value will be zero. If no screen has been set, the return value will be (-1). pfGetPipeSize() returns the full screen size, in pixels, of the rendering area associated with a pfPipe. You may have application states associated with pfPipe stages and processes that need special initialization. For this purpose, you may provide a stage configuration callback for each pfPipe stage using pfStageConfigFunc(pipe, stageMask, configFunc) and specify the pfPipe, the stage bitmask (including one or more of PFPROC_APP, PFPROC_CULL, and PFPROC_DRAW), and your stage configuration callback routine. At any time, you may call the function pfConfigStage() from the application process to trigger the execution of your stage configuration callback in the process associated with that pfPipe’s stage. The stage configuration callback will be invoked at the start of that stage within the current frame (the current frame in the application process, and subsequent frames through the cull and draw phases of the software rendering pipeline). Use a pfStageConfigFunc() callback function to configure OpenGL Performer processes not associated with pfPipes, such as the database process, PFPROC_DBASE, and the intersection process, PFPROC_ISECT. A common process initialization task for real-time applications is the selection and/or specification of a CPU on which to run. 24 007-1680-100 Using Pipes Example of pfPipe Use The sample source code shipped with OpenGL Performer includes several simple examples of pfPipe use in both C and C++. Specifically, look at the following examples under the C and C++ directories in /usr/share/Performer/src/pguide/libpf for IRIX and Linux and in %PFROOT%\Src\pguide\libpfc for Microsoft Windows, such as hello.c, simple.c, and multipipe.c. Example 2-1 illustrates the basics of using pipes. The code in this example is adapted from OpenGL Performer sample programs. Example 2-1 pfPipes in Action main() { int i; /* Initialize OpenGL Performer */ pfInit(); /* Set number of pfPipes desired -- THIS MUST BE DONE * BEFORE CALLING pfConfig(). */ pfMultipipe(NumPipes); /* set multiprocessing mode */ pfMultiprocess(PFMP_DEFAULT); ... /* Configure OpenGL Performer and fork extra processes if * configured for multiprocessing. */ pfConfig(); ... /* Optional custom mapping of pipes to screens. * This is actually the reverse as the default. *// for (i=0; i < NumPipes; i++) pfPipeScreen(pfGetPipe(i), NumPipes-(i+1)); { /* set up optional DRAW pipe stage config callback */ pfStageConfigFunc(-1 /* selects all pipes */, PFPROC_DRAW /* stage bitmask */, ConfigPipeDraw /* config callback */); /* Config func should be done next pfFrame */ pfConfigStage(i, PFPROC_DRAW); 007-1680-100 25 2: Setting Up the Display Environment } InitChannels(); ... /* trigger the configuration and opening of pfPipes * and pfWindows */ pfFrame(); /* Application’s simulation loop */ while(!SimDone()) { ... } } /* CALLBACK FUNCTIONS FOR PIPE STAGE INITIALIZATION */ void ConfigPipeDraw(int pipe, uint stage) { /* Application state for the draw process can be initialized * here. This is also a good place to do real-time * configuration for the drawing process, if there is one. * There is no graphics state or pfState at this point so no * rendering calls or pfApply*() calls can be made. */ pfPipe *p = pfGetPipe(pipe); pfNotify(PFNFY_INFO, PFNFY_PRINT, “Initializing stage 0x%x of pipe %d”, stage, pipe); } Using Channels This section describes how to use pfChannels. A pfChannel is a view of a scene. A pfChannel is a required element for an OpenGL Performer application because it establishes the visual frame of reference for what is rendered in the drawing process. Creating and Configuring a pfChannel When you create a new pfChannel, it is attached to a pfPipe for the duration of the application. The pfPipe renders the pfScene viewed by the pfChannel into a 26 007-1680-100 Using Channels pfPipeWindow that is managed by that pipe. Use pfNewChan() to create a new pfChannel and assign it to a pfPipe. pfChannels are automatically assigned to the first pfPipeWindow of the pfPipe. In the sample program, the following statement creates a new channel and assigns it to pipe p. chan = pfNewChan(p); The pfChannel is automatically placed in the first pfPipeWindow of the pfPipe. A pfPipeWindow is created automatically if one is not explicitly created with pfNewPWin(). The simplest configuration uses one pipe, one channel, and one window. You can use multiple channels in a single pfPipeWindow on a pfPipe, thereby allowing channels to share hardware resources. Using multiple channels is an advanced topic that is discussed in the section of this chapter on “Using Multiple Channels.” For now, focus your attention on understanding the concepts of setting up and using a single channel. The primary function of a pfChannel is to define the view of a scene. A view is fully characterized by a viewport, a viewing frustum, and a viewpoint. The following sections describe how to set up the scene and view for a pfChannel. Setting Up a Scene A pfChannel draws the pfScene set by pfChanScene(). A channel can draw only one scene per frame but can change scenes from frame to frame. Other pfChannel attributes such as LOD modifications, described in “pfLOD Nodes” in Chapter 3, affect the scene. A pfChannel also renders an environmental model known as pfEarthSky. A pfEarthSky defines the method for clearing the channel viewport before rendering the pfScene and also provides environmental effects, including ground and sky geometry and fog and haze. A pfEarthSky is attached to a pfChannel by pfChanESky(). Setting Up a Viewport A pfChannel is rendered by a pfPipe into its pfPipeWindow. The screen area that displays a pfChannel’s view is determined by the origin and size of the window and the channel viewport specified by pfChanViewport. The channel viewport is relative to the lower left corner of the window and ranges from 0 to 1. By default, a pfChannel viewport covers the entire window. 007-1680-100 27 2: Setting Up the Display Environment Suppose that you want to establish a viewport that is one-quarter of the size of the window, located in the lower left corner of the window. Use pfChanViewport(chan, 0.0, 0.25, 0.0, 0.25) to set up the one-quarter window viewport for the channel chan. You can then set up other channels to render to the other three-quarters of the window. For example, you can use four channels to create a four-way view for an architectural or CAD application. See “Using Multiple Channels” on page 35 to learn more about multiple channels. Setting Up a Viewing Frustum A viewing frustum is a truncated pyramid that defines a viewing volume. Everything outside this volume is clipped, while everything inside is projected onto the viewing plane for display. A frustum is defined by the following: • field-of-view (FOV) in the horizontal and vertical dimensions • near and far clipping planes A viewing frustum is created by the intersections of the near and far clipping planes with the top, bottom, left, and right sides of the infinite viewing volume formed by the FOV and aspect ratio settings. The aspect ratio is the ratio of the vertical and horizontal dimensions of the FOV. Figure 2-4 shows the parameters that define a symmetric viewing frustum. To establish asymmetric frusta refer to the pfChannel(3pf) or pfFrustum(3pf) man pages for further details. 28 007-1680-100 Using Channels Horizontal FOV x Top Far Left t igh Near e Lin of s y Vertical FOV Right Bottom Eyepoint Aspect Ratio = Figure 2-4 y x = tan(vertical FOV/2) tan(horizontal FOV/2) Symmetric Viewing Frustum The viewing frustum is called symmetric when the vertical half-angles are equal and the horizontal half-angles are equal. Field-of-View The FOV is the angular width of view. Use pfChanFOV(chan, horiz, vert) to set up viewing angles in OpenGL Performer. The quantities horiz and vert are the total horizontal and vertical fields of view in degrees; usually you specify one and let OpenGL Performer compute the other. If you are specifying one angle, pass any amount less than or equal to zero, or greater than or equal to 180, as the other angle. OpenGL Performer automatically computes the unspecified FOV angle to fit the pfChannel viewport using the aspect-ratio preserving relationship tan(vert/2) / tan(horiz/2) = aspect ratio That is, the ratio of the tangents of the vertical and horizontal half-angles is equal to the aspect ratio. For example, if horiz is 45 degrees and the channel viewport is twice as wide as it is high (so the aspect ratio is 0.5), then the vertical field-of-view angle, vert, is 007-1680-100 29 2: Setting Up the Display Environment computed to be 23.4018 degrees. If both angles are unspecified, pfChanFOV() assumes a default value of 45 degrees for horiz and computes the value of vert as described. Clipping Planes Clipping planes define the near and far boundaries of the viewing volume. These distances describe the extent of the visual range in the view, because geometry outside these boundaries is clipped, meaning that it is not drawn. Use pfChanNearFar(chan, near, far) to specify the distance along the line of sight from the viewpoint to the near and far planes that bound the viewing volume. These clipping planes are perpendicular to the line of sight. For the best visual acuity, choose these distances so that near is as far away as possible from the viewpoint and far is as close as possible to the viewpoint. Minimizing the range between near and far provides more resolution for distance comparisons and fog computations. Setting Up a Viewpoint A viewpoint describes the position and orientation of the viewer. It is the origin of the viewing location, the direction of the line of sight from the viewer to the scene being viewed, and an up direction. The default viewpoint is at the origin (0, 0, 0) looking along the +Y axis, with +Z up and +X to the right. Use pfChanView(chan, point, dir) to define the viewpoint for the pfChannel identified by chan. Specify the view origin for point in x, y, z world coordinates. Specify the view direction for dir in degrees by giving the degree measures of the three Euler angles: heading, pitch, and roll. Heading is a rotation about the Z axis, pitch is a rotation about the X axis, and roll is a rotation about the Y axis. The value of dir is the product of the rotations ROTy(roll) * ROTx(pitch) * ROTz(heading), where ROTa(angle) is a rotation matrix about axis A of angle degrees. Angles have not only a degree value, but also a sense, + or –, indicating whether the direction of rotation is clockwise or counterclockwise. Because different systems follow different conventions, it is very important to understand the sense of the Euler angles as they are defined by OpenGL Performer. OpenGL Performer follows the right-hand rule. According to the right-hand rule, counterclockwise rotations are positive. This means that a rotation about the X axis by +90 degrees shifts the +Y axis to the +Z axis, a rotation 30 007-1680-100 Using Channels about the Y axis by +90 degrees shifts the +Z axis to the +X axis, and a rotation about the Z axis by +90 degrees shifts the +X axis to the +Y axis. Figure 2-5 shows a toy plane (somewhat reminiscent of the Ryan S-T) at the origin of a coordinate system with the angles of rotation labeled for heading, pitch, and roll. The arrows show the direction of positive rotation for each angle. Z + Heading Y X Figure 2-5 + Roll + Pitch Heading, Pitch, and Roll Angles A roll motion tips the wings from side to side. A pitch motion tips the nose up or down. Changing the heading, a yaw motion steers the plane. Accurate readings of these angles are critical information for a pilot during a flight, and a thorough understanding of how the angles function together is required for creation of an accurate flight simulation visual with OpenGL Performer. The same is also true of marine and other vehicle simulations. Alternatively, you can use pfChanViewMat(chan, mat) to specify a 4x4 homogeneous matrix mat that defines the view coordinate system for channel chan. The upper left 3x3 submatrix defines the coordinate system axes, and the bottom row vector defines the origin of the coordinate system. The matrix must be orthonormal, or the results will be undefined. You can construct matrices using tools in the libpr library. 007-1680-100 31 2: Setting Up the Display Environment The origin and heading, pitch, and roll angles, or the view matrix, create a complete view specification. The view specification can locate the eyepoint frame-of-reference origin at any point in world coordinates. The gaze vector, the eye’s +Y axis, can point in any direction. The up vector, the eye’s +Z axis, can point in any direction perpendicular to the gaze vector. You can query the system for the view and eyepoint-direction values with pfGetChanView(), or obtain the view matrix directly with pfGetChanViewMat(). The view direction can be modified by one or more offsets, relative to the eyepoint frame-of-reference. View offsets are useful in situations where several channels render the same scene into adjacent displays for a wider field-of-view or higher resolution. Offsets are also used for multiple viewer perspectives, such as pilot and copilot views. Use pfChanViewOffsets(chan, xyz, hpr) to specify additional translation and rotation offsets for the viewpoint and direction; xyz specifies a translation vector and hpr specifies a heading/pitch/roll rotation vector. Viewing offsets are automatically added each frame to the view direction specified by pfChanView() or pfChanViewMat(). For example, to create three different perspectives of the same scene as displayed by three windows in an airplane cockpit, use azimuth offsets of 45, 0, and -45 for left, middle, and right views. To create vertical view groups such as might be seen through the windscreen of a helicopter, use both azimuth and elevation offsets. Once the view offsets have been set up, you need only set the view once per frame. View offsets are applied after the eyepoint position and gaze direction have been established. As with the other angles, be aware that the conventions for measuring azimuth and elevation angles vary between graphics systems; so, you should verify that the sense of the angles is correct. Example of Channel Use Example 2-2 shows how to use various pfChannel-related functions. The code is derived from OpenGL Performer sample programs. Example 2-2 Using pfChannels main() { pfInit(); ... pfConfig(); 32 007-1680-100 Using Channels ... InitScene(); InitPipe(); InitChannel(); /* Application main loop */ while(!SimDone()) { ... } } void InitChannel(void) { pfChannel *chan; chan = pfNewChan(pfGetPipe(0)); /* Set the callback routines for the pfChannel */ pfChanTravFunc(chan, PFTRAV_CULL, CullFunc); pfChanTravFunc(chan, PFTRAV_DRAW, DrawFunc); /* Attach the visual database to the channel */ pfChanScene(chan, ViewState->scene); /* Attach the EarthSky model to the channel */ pfChanESky(chan, ViewState->eSky); /* Initialize the near and far clipping planes */ pfChanNearFar(chan, ViewState->near, ViewState->far); /* Vertical FOV is matched to window aspect ratio. */ pfChanFOV(chan, 45.0f/NumChans, -1.0f); /* Initialize the viewing position and direction */ pfChanView(chan, ViewState->initView.xyz, ViewState->initView.hpr); } /* CULL PROCESS CALLBACK FOR CHANNEL*/ /* The cull function callback. Any work that needs to be * done in the cull process should happen in this function. */ void CullFunc(pfChannel * chan, void *data) { 007-1680-100 33 2: Setting Up the Display Environment static long first = 1; if (first) { if ((pfGetMultiprocess() & PFMP_FORK_CULL) && (ViewState->procLock & PFMP_FORK_CULL)) pfuLockDownCull(pfGetChanPipe(chan)); first = 0; } PreCull(chan, data); pfCull(); /* Cull to the viewing frustum */ PostCull(chan, data); } /* DRAW PROCESS CALLBACK FOR CHANNEL*/ /* The draw function callback. Any graphics functionality * outside OpenGL Performer must be done here. */ void DrawFunc(pfChannel *chan, void *data) { PreDraw(chan, data); /* Clear the viewport, etc. */ pfDraw(); /* Render the frame */ /* draw HUD, or whatever else needs * to be done post-draw. */ PostDraw(chan, data); } Controlling the Video Output Note: This is an advanced topic. You use pfPipeVideoChannel to query and control the configuration of a hardware video channel. The methods allow you to, for example, query or specify the origin and size of the video output and scale the display. 34 007-1680-100 Using Multiple Channels By default, all pfVideoChannels on a pfPipe use the first entire video channel on the screen selected by the pfPipe. Each pfPipeWindow initially has a default pfPipeVideoChannel already assigned to it. When pfChannels are added to pfPipeWindows, they will be using, by default, this first pfPipeVideoChannel. You can get a pfPipeVideoChannel of a pfPipeWindow with pfGetPWinPVChan() and specifying the index of the pfPipeVideoChannel on the pfPipeWindow; the initial default one will be at index 0. You can then reconfigure this pfPipeVideoChannel to select a different video channel or change the attributes of the selected video channel. You can create a pfPipeVideoChannel with pfNewPVChan(). To use this for a given pfChannel, you must add it to a pfPipeWindow that will cover the screen area of the desired video channel. When a pfPipeVideoChannel is added to a pfPipeWindow with pfAddPWinPVChan(), the index into the pfPipeWindow list of video channels is returned and by default the pfPipeVideoChannel will get the next active hardware video channel after the previous pfPipeVideoChannel on that pfPipeWindow. You can explicitly select the hardware video channel with pfPVChanId(). The pfChannel will then reference this pfPipeVideoChannel through the index that you got back from pfAddPWinPVChan() and assign to the pfChannel with pfChanPWinPVChanIndex(). pvc = pfNewPVChan(p); pvcIndex = pfAddPWinPVChan(pw, pvc); pfChanPWinPVChanIndex(chan, pvcIndex); Note that the screen of the pfPipe must be known to fully specify the desired video channel. Queries on the pfPipeVideoChannel will return values indicating unknown configuration until the screen is known. The screen can be determined by OpenGL Performer when the window is opened in the DRAW process but you can also explicitly set the screen of the pfPipe with pfPipeScreen(). You can also get to the hardware video channel structure, pfVideoChannelInfo(), for more configuration options, such as reading gamma data or even a specific video format. For more information on pfPipeWindows and pfPipeVideoChannels, see Chapter 17, “pfPipeWindows and pfPipeVideoChannels.” Using Multiple Channels Each rendering pipeline can render multiple channels with multiple pfPipeVideoChannels to a single pfPipeWindows. Multiple pfPipeWindows can also be used but at the cost of some additional processing overhead. The pfChannel is assigned to the proper pfPipeWindow and selects its pfPipeVideoChannel from that 007-1680-100 35 2: Setting Up the Display Environment pfPipeWindow. The pfChannel must also have a viewport, set with pfChanViewport(), that covers the proper window area to match that of the desired pfPipeVideoChannel. Each channel represents an independent viewpoint into either a shared or an independent visual database. Different types of applications can have vastly different pipeline-window-channel configurations. This section describes two extremes: visual simulation applications, where there is typically one window per pipeline, and highly interactive uses that require dynamic window and channel configuration. One Window per Pipe, Multiple Channels per Window Often there is a single channel associated with each pipeline, as shown in the top half of Figure 2-6. This section describes two important uses for multiple-channel support— multiple pipelines per system and multiple windows per pipeline—the second of which is illustrated in the bottom half of Figure 2-6. 36 007-1680-100 Using Multiple Channels Single Channel Frame Buffer Pipeline Display Device Channel 0 Multiple Channel Frame Buffer Channel 0 Channel 1 Pipeline Channel n-1 Display Device Figure 2-6 007-1680-100 Single-Channel and Multiple-Channel Display 37 2: Setting Up the Display Environment One situation that requires multiple channels occurs when inset views must appear within an image. A simple example of this application is a driving simulator in which the screen image represents the view out the windshield. If a rear-view mirror is to be drawn, it must overlay the main forward view to provide a separate view of the same database within the borders of the simulated mirror’s frame. Channels are rendered in the order that they are assigned to a pfPipeWindow on their parent pfPipe. Channels, upon creation, are assigned to the end of the channel list of the first window of their pfPipe. In the driving simulator example, creating pipes and channels with the following structure creates two channels on a single shared pipeline: pipeline = pfGetPipe(0); frontView = pfNewChan(pipeline); rearView = pfNewChan(pipeline); In this case, OpenGL Performer’s actual drawing order becomes the following: 1. Clear frontView. 2. Draw frontView. 3. Clear rearView. 4. Draw rearView. This default ordering results in the rear-view mirror image always overlaying the front-view image, as desired. You can control and reorder the drawing of channels within a pfPipeWindow with the pfInsertChan(pwin, where, chan) and pfMoveChan(pwin, where, chan) routines. More details about multiple channels and multiple window are discussed in the next section. When the host has multiple Geometry Pipelines, as supported on Onyx RealityEngine2 and InfiniteReality systems, you can create a pfPipe and pfChannel pair for each hardware pipeline. The following code fragment illustrates a two-channel, two-pipeline configuration: leftPipe = pfGetPipe(0); leftView = pfNewChan(leftPipe); rightPipe = pfGetPipe(1); rightView = pfNewChan(rightPipe); This configuration forms the basis for a high-performance stereo display system, since there is a hardware pipeline dedicated to each eye and rendering occurs in parallel. 38 007-1680-100 Using Multiple Channels The two-channel stereo-view application described in this example and the inset-view application described in the previous example can be combined to provide stereo views for a driving simulator with an inset rear-view mirror. The correct management of each eye’s viewpoint and the mirror reflection helps provide a convincing sense of physical presence within the vehicle. The third and most common multiple-channel situation involves support for multiple video outputs per pipeline. To do this, first associate each pipeline with a set of nonoverlapping channels, one for each desired view. Next, use one of the following video-splitting methods: • Use the multi-channel hardware options, available from SGI, for systems such as the 8-channel Display Generator (DG) for InfiniteReality graphics, where you can create up to eight independent video outputs from a single Graphics Pipeline, with each video output corresponding to one of the tiled channels. The Octane video option supports four video outputs and the RealityEngine2 MultiChannel Option supports six video channels per Graphics Pipeline. • Connect multiple video monitors in series to a single pipeline’s video output. Because each monitor receives the same display image, a masking bezel is used to obscure all but the relevant portion of each display surface. The three multiple-channel concepts described here can be used in combination. For example, use of three InfiniteReality pipelines, each equipped with the 8-channel DG , allows creation of up to 24 independent video displays. The channel-tiling method can also be used for some or all of these displays. Example 2-3 shows how to use multiple channels on separate pipes. Example 2-3 Multiple Channels, One Channel per Pipe pfChannel *Chan[MAX_CHANS]; void InitChannel(int NumChans) { /* Initialize each channel on a separate pipe */ for (i=0; i< NumChans; i++) Chan[i] = pfNewChan(pfGetPipe(i)); ... /* Make channel n/2 the master channel (can be any * channel). */ 007-1680-100 39 2: Setting Up the Display Environment ViewState->masterChan = Chan[NumChans/2]; { long share; /* Get the default channel-sharing mask */ share = pfGetChanShare(ViewState->masterChan); /* Add in the viewport share bit */ share |= PFCHAN_VIEWPORT; if (GangDraw) { /* add GangDraw to channel share mask */ share |= PFCHAN_SWAPBUFFERS_HW; } pfChanShare(ViewState->masterChan, share); } /* Attach channels */ for (i=0; i< NumChans; i++) if (Chan[i] != ViewState->masterChan) pfAttachChan(ViewState->masterChan, Chan[i]); ... /* Continue with channel initialization */ } Using Channel Groups In many multiple-channel situations, including the examples described in the previous section, it is useful for channels to share certain attributes. For the three-channel cockpit scenario, each pfChannel shares the same eyepoint while the left and right views are offset using pfChanViewOffsets(). OpenGL Performer supports the notion of channel groups, which facilitate attribute sharing between channels. pfChannels can be gathered into channel groups that share like attributes. A channel group is created by attaching one pfChannel to another, or to an existing channel group. Use pfAttachChan() to create a channel group from two channels or to add a channel to an existing channel group. Use pfDetachChan() to remove a pfChannel from a channel group. 40 007-1680-100 Using Channel Groups A channel share mask defines shared attributes for a channel group. The attribute tokens listed in Table 2-1 are bitwise OR-ed to create the share mask. Table 2-1 Attributes in the Share Mask of a Channel Group Token Shared Attributes PFCHAN_FOV Horizontal and vertical fields of view PFCHAN_VIEW View position and orientation PFCHAN_VIEW_OFFSETS (x, y, z) and (heading, pitch, roll) offsets of the view direction PFCHAN_NEARFAR Near and far clipping planes PFCHAN_SCENE All channels display the same scene. PFCHAN_EARTHSKY All channels display the same earth/sky model. PFCHAN_STRESS All channels use the same stress filter. PFCHAN_LOD All channels use the same LOD modifiers. PFCHAN_SWAPBUFFERS All channels swap buffers at the same time. PFCHAN_SWAPBUFFERS_HW Synchronize swap buffers for channels on different graphics pipelines. Use pfChanShare() to set the share mask for a channel group. By default, channels share all attributes except PFCHAN_VIEW_OFFSETS. When you add a pfChannel to a channel group, it inherits the share mask of that group. A change to any shared attribute is applied to all channels in a group. For example, if you change the viewpoint of a pfChannel that shares PFCHAN_VIEW with its group, all other pfChannels in the group will acquire the same viewpoint. Two attributes are particularly important to share in adjacent-display multiple-channel simulations: PFCHAN_SWAPBUFFERS and PFCHAN_LOD. PFCHAN_LOD ensures that geometry that straddles displays is drawn the same way in each channel. In this case, all channels will use the same LOD modifier when rendering their scenes so that LOD behavior is consistent across channels. PFCHAN_SWAPBUFFERS ensures that channels refresh the display with a new frame at the same time. pfChannels in different pfPipes that share PFCHAN_SWAPBUFFERS_HW will frame-lock the graphics pipelines together. 007-1680-100 41 2: Setting Up the Display Environment Example 2-4 illustrates the use of multiple channels and channel sharing. Example 2-4 Channel Sharing pfChannel *Chan[MAX_CHANS]; main() { pfInit(); ... /* Set number of pfPipes desired. * BEFORE CALLING pfConfig(). */ pfMultipipe(NumPipes); ... pfConfig(); ... InitScene(); THIS MUST BE DONE InitChannels(); pfFrame(); /* Application main loop */ while(!SimDone()) { ... } } void InitChannel(int NumChans) { /* Initialize all channels on pipe 0 */ for (i=0; i< NumChans; i++) Chan[i] = pfNewChan(pfGetPipe(0)); ... /* Make channel n/2 the master channel (can be any * channel). */ ViewState->masterChan = Chan[NumChans/2]; ... 42 007-1680-100 Using Channel Groups /* Attach all Channels as slaves to the master channel */ for (i=0; i< NumChans; i++) if (Chan[i] != ViewState->masterChan) pfAttachChan(ViewState->masterChan, Chan[i]); pfSetVec3(xyz, 0.0f, 0.0f, 0.0f); /* Set each channel’s viewing offset. In this case use * many channels to create one multichannel contiguous * frustum with a 45˚ field of view. */ for (i=0; i < NumChans; i++) { float fov = 45.0f/NumChans; pfSetVec3(hpr, (((NumChans - 1) * 0.5f) - i) * fov, 0.0f, 0.0f); pfChanViewOffsets(Chan[i], xyz, hpr); } ... /* Now, just configure the master channel and all of the * other channels will share those attributes. */ chan = ViewState->masterChan; pfChanTravFunc(chan, PFTRAV_CULL, CullFunc); pfChanTravFunc(chan, PFTRAV_DRAW, DrawFunc); pfChanScene(chan, ViewState->scene); pfChanESky(chan, ViewState->eSky); pfChanNearFar(chan, ViewState->near, ViewState->far); pfChanFOV(chan, 45.0f/NumChans, -1.0f); pfChanView(chan, ViewState->initView.xyz, ViewState->initView.hpr); ... } Multiple Channels and Multiple Windows For some interactive applications, you may want to be able to dynamically control the configuration of channels and windows. OpenGL Performer allows you to dynamically create, open, and close windows. You can also move channels among the windows of the shared parent pfPipe, and reorder channels within a pfPipeWindow. Channels can be 007-1680-100 43 2: Setting Up the Display Environment appended to the end of a pfPipeWindow channel list with pfAddChan() and removed with pfRemoveChan(). A channel can only be attached to one pfPipeWindow — no instancing of pfChannels is allowed. When a pfChannel is put on a pfPipeWindow, it is automatically deleted from its previous pfPipeWindow. A channel that is not assigned to a pfPipeWindow is not drawn (though it may still be culled). You can control and reorder the drawing of channels within a pfPipeWindow with the pfInsertChan(pwin, where, chan) and pfMoveChan(pwin, where, chan) routines. Both of these routines do a type of insertion: pfInsertChan() will add chan to the pwin channel list in front of the channel in the list at location where. pfMoveChan() will delete chan from its old location and move it to where in the pwin channel list. On IRIX systems, if you have pfChannels in different pfPipeWindows or pfPipes that are supposed to combine to form a continuous scene, you will want to ensure that both the vertical retrace and double buffering of these windows is synchronized. This is required for both reasonable performance and visual quality. Use the genlock(7) system video feature to ensure that the vertical retraces of different graphics pipelines are synchronized. To synchronize double buffering, you want to either specify PFCHAN_SWAPBUFFERS_HW in the share mask of the pfChannels and put the pfChannels in a share group, or else create a pfPipeWindow swap group, discussed in Chapter 17, “pfPipeWindows and pfPipeVideoChannels.” Importing OpenGL Multipipe SDK (MPK) Configuration Files OpenGL Multipipe SDK (MPK) is a software package for managing a multipipe rendering environment. MPK uses a configuration file to describe the layout and hierarchy of pipes, windows, and channels used by an application. The manual SGI OpenGL Multipipe SDK User’s Guide describes the format of the configuration file. An OpenGL Performer application can import MPK configuration files and skip the explicit generation of pipes, windows, and channels. The library libpfmpk contains functions for importing and configuring pipes, windows, and channels from an MPK configuration file. The functions in libpfmpk store the display configuration information in a pfvDisplayMngr class for easy access by the application. The pfvDisplayMngr class is part of the pfvViewer implementation, which is described in Chapter 25, “Building a Visual Simulation Application Using libpfv”. The pfMPKImportFile() function takes an MPK configuration filename and generates OpenGL Performer objects (pfPipes, pfPipeWindows, and pfChannels) accordingly. The 44 007-1680-100 Importing OpenGL Multipipe SDK (MPK) Configuration Files function pfMPKImportConfig() is very similar. Instead of accepting a filename, it accepts an MPK configuration class MPKConfig. The result of both these functions is two-fold: • OpenGL Performer is configured with pipes, windows, and channels as specified in the MPK configuration file. • The pfvDisplayMngr class contains a description of the configured display topology (what pipe has what windows and what channels). It also contains pointers to all the newly generated OpenGL Performer classes (pfPipe, pfPipeWindow, and pfChannel). The following is a code sample section for using the pfMPKImportFile() function: // Initialize Performer pfInit(); // Initialize the MultipipeSDK import library. // No need to initialize MPK directly. pfmpkInit(); // Import a MultipipeSDK file. This function calls pfConfig // so we don’t have to. pfMPKImportFile(config_filename); // Load a model file for display. pfNode *root = pfdLoadFile(model_filename); // Attach loaded file to a new pfScene pfScene *scene = new pfScene; scene->addChild(root); // Create a pfLightSource and attach it to scene scene->addChild(new pfLightSource); // Get access to the results of the MultipipeSDK import. // pfvDisplayMngr contains pointers to all the // pipes/windows/channels that the MultipipeSDK file specified. pfvDisplayMngr *dm = pfvDisplayMngr::getMngr(); // All configured channels share the scene graph so we only // have to assign one channel. pfChannel *chan = dm -> getChan(0) -> getHandle(); chan->setScene(scene); 007-1680-100 45 2: Setting Up the Display Environment Note: Since the pfvDisplayMngr class has no C API, you can only use libpfmpk from C++ programs. Figure 2-1 contains a diagram of the various objects participating in any libpfmpk import operation. pfPipe pfChannel pfPipeWindow pfvDisplayMngr *dm pfmpkImportFile() MPK config file Figure 2-7 dm dm dm getPipe(i) getPWin(i) getChan(i) getHandle() getHandle() getHandle() Pointers to pfPipe, pfPipeWindow, pfChannel The libpfmpk Import Operation Both functions pfMPKImportFile() and pfMPKImportConfig() encapsulate the entire OpenGL Performer configuration stage including the call to function pfConfig(). This may be too inflexible for some applications. An additional set of functions in libpfmpk provides lower-level access. The following code sample shows the internal structure of function pfMPKImportConfig(). All calls that pfMPKImportConfig() makes are publicly accessible and an application can call them directly: void pfMPKImportConfig(MPKConfig *cfg) { pfvDisplayMngr *dm = pfvDisplayMngr::getMngr(); pfMPKImportInfo info; // Prepare temporary storage for pipe information. info . numPipes = mpkConfigNPipes(cfg); 46 007-1680-100 Importing OpenGL Multipipe SDK (MPK) Configuration Files info . pipeInfo = (pfMPKImportPipeInfo *) malloc (info.numPipes * sizeof (pfMPKImportPipeInfo)); // Translate contents of MPKConfig into pfvDisplayMngr terms. pfMPKPreConfig(cfg, &info); // Let pfvDisplayMngr run all its pre-pfConfig processing. dm -> preConfig(); // Performer configuration: After this point, we can start // creating Performer windows and channels. pfConfig(); // Inquire pipe sizes, and configure all pfvDisplayMngr // objects that depend on them. pfMPKPostConfig(cfg, &info); // Ask pfDisplayMngr to create all the windows/channels. dm -> postConfig(); // Invoke any pfPipe/pfPipeWindow/pfChannel calls that // pfDisplayMngr doesn’t encapsulate. pfMPKPostDMConfig(cfg, &info); } For completeness, the following is the source code for pfMPKImportFile(): void pfMPKImportFile(char *filename) { // Ask MPK to load the configuration file and pass to // pfMPKImportConfig pfMPKImportConfig(mpkConfigLoad(filename)); } The function pfMPKPreConfig() traverses the MPKConfig class and creates its pfvDisplayMngr equivalent. The function pfMPKPostConfig() patches the previous pfvDisplayMngr configuration using pipe size information. This information becomes available only after the call to pfConfig(); hence, patching cannot happen in pfMPKPreConfig(). The function pfMPKPostDMConfig() traverses the pfvDisplayMngr hierarchy one last time. This time pfvDisplayMngr already contains valid pointers to the OpenGL Performer classes it creates. The function pfMPKPostDMConfig() makes 007-1680-100 47 2: Setting Up the Display Environment OpenGL Performer calls on the pfPipe, pfPipeWindow, and pfChannel pointers. Since pfvDisplayMngr does not encapsulate all configuration details, pfMPKPostDMConfig() makes these configuration calls directly on the new OpenGL Performer classes. 48 007-1680-100 Chapter 3 3. Nodes and Node Types A scene graph holds the data that defines a virtual world. The scene graph includes low-level descriptions of object geometry and their appearance, as well as higher-level, spatial information, such as specifying the positions, animations, and transformations of objects, as well as additional application-specific data. Scene graph data is encapsulated in many different types of nodes. One node might contain the geometric data of an object; another node might contain the transformation for that object to orient and position it in the virtual world. The nodes are associated in a hierarchy that is an adirected, acyclic graph. OpenGL Performer and your application can act on the scene graph to perform various complex operations efficiently, such as database intersection and rendering scenes. This chapter focuses on the data types themselves rather than instances of those types. Chapter 4, “Database Traversal,” discusses traversing sample scene graphs in terms of actual objects rather than abstract data types. Nodes A scene is represented by a graph of nodes. A node is a subclass of pfNode. Only nodes can be in scene graphs and have child nodes. In general, nodes either contain descriptive information about scene graph geometry, or they create groups and hierarchies of nodes. Many classes, such as pfEngine and pfFlux, that are not nodes can interact with nodes. Attribute Inheritance The basic element of a scene hierarchy is the node. While OpenGL Performer supplies many specific types of nodes, it also uses a concept called class inheritance, which allows different node types to share attributes. An attribute is a descriptive element of geometry or its appearance. 007-1680-100 49 3: Nodes and Node Types pfNode OpenGL Performer’s node hierarchy begins with the pfNode class, as shown in Figure 3-1. pfNode pfGeode pfText pfGroup pfASD pfLightSource pfBillboard pfScene pfPartition pfLayer pfLOD pfFCS Figure 3-1 pfSCS pfSwitch pfSequence pfDCS Nodes in the OpenGL Performer Hierarchy All node types are derived from pfNode; they inherit pfNode’s attributes and the libpf routines for setting and getting attributes. In general, a node type inherits the attributes and routines of all its parent nodes in the type hierarchy. 50 007-1680-100 Nodes Table 3-1 lists the basic node class and gives a simple description for each node type. Table 3-1 OpenGL Performer Node Types Node Type Node Class Description pfNode Abstract Basic node type. pfGroup Branch Groups zero or more children.. pfScene Root Parent of the visual database. pfSCS Branch Static coordinate system. pfDCS Branch Dynamic coordinate system. pfFCS Branch Flux coordinate system. pfDoubleSCS Branch Double-precision static coordinate system. pfDoubleDCS Branch Double-precision dynamic coordinate system. pfDoubleFCS Branch Double-precision flux coordinate system. pfSwitch Branch Selects among multiple children. pfSequence Branch Sequences through its children. pfLOD Branch Level-of-detail node. pfLayer Branch Renders coplanar geometry. pfLightSource Leaf Contains specifications for a light source. pfGeode Leaf Contains geometric specifications. pfBillboard Leaf Rotates geometry to face the eyepoint. pfPartition Branch Partitions geometry for efficient intersections. pfText Leaf Renders 2D and 3D text. pfASD Leaf Controls transition between LOD levels. pfNode As shown in Figure 3-1, all libpf nodes are arranged in a type hierarchy, which defines the inheritance of functionality. A pfNode is an abstract class, meaning that a pfNode can 007-1680-100 51 3: Nodes and Node Types never be explicitly created by an application, and all other nodes inherit the functionality of pfNode. Its purpose is to provide a root to the type hierarchy and to define the attributes that are common to all node types. pfNode Attributes The following pfNode attributes are inherited by all other libpf node types: • Node name • Parent list • Bounding geometry • Intersection and traversal masks • Callback functions and data • User data Bounding geometry, intersection masks, user data, and callbacks are advanced topics that are discussed in Chapter 4, “Database Traversal.” The routines that set, get, and otherwise manipulate these attributes can be used by all libpf node types, as indicated by the keyword ‘Node’ in the routine names. Nodes used as arguments to pfNode routines must be cast to pfNode* to match parameter prototypes, as shown in this example: pfNodeName((pfNode*) dcs, "rotor_rotation"); However, you usually do not need to do this casting explicitly. When you use the C API and compile with the –ansi flag (which is the usual way to compile OpenGL Performer applications), libpf provides macro wrappers around pfNode routines that automatically perform argument casting for you. When you use the C++ API, such type casting is not necessary. pfNode Operations In addition to sharing attributes, certain basic operations are provided for all node types. They include the following: 52 New Create and return a handle to a new node. Get Get node attributes. Set Set node attributes. 007-1680-100 Nodes Find Find a node based on its name. Print Print node data. Copy Copy node data. Delete Delete a node. The Set operation is implied in the node attribute name. The names of the attribute-getting functions contain the string “Get.” An Example of Scene Creation Example 3-1 illustrates the creation of a scene that includes two different kinds of pfNodes. (For information about pfScene nodes, see “pfScene Nodes” on page 61; for information about pfDCS nodes, see “pfDCS Nodes” on page 62.) Example 3-1 Making a Scene pfScene *scene; pfDCS *dcs1, *dcs2; scene = pfNewScene(); dcs1 = pfNewDCS(); dcs2 = pfNewDCS(); pfCopy(dcs2, dcs1); /* Create a new scene node */ /* Create a new DCS node */ /* Create a new DCS node */ /* Copy all node attributes */ /* from dcs1 to dcs2 */ pfNodeName(scene, "Scene_Graph_Root"); /* Name scene node */ pfNodeName(dcs1,"DCS_1"); /* Name dcs1 */ pfNodeName(dcs2,"DCS_2"); /* Name dcs2 */ ... /* Use a pfGet*() routine to determine node name */ printf("Name of first DCS node is %s.", pfGetNodeName(dcs1)); ... /* Recursively free this node if it’s no longer referenced */ pfDelete(scene); ... pfGroup In addition to inheriting the pfNode attributes described in the “pfNode” section of this chapter, a pfGroup also maintains a list of zero or more child nodes that are accessed and 007-1680-100 53 3: Nodes and Node Types manipulated using group operators. Children of a pfGroup can be either branch or leaf nodes. Traversals process the children of a pfGroup in left-to-right order. Table 3-2 lists the pfGroup functions, with a description and a visual interpretation of each. Table 3-2 pfGroup Functions Function Name Description pfAddChild(group, child) Appends child to the list for group. pfInsertChild(group, index, child) Inserts child before the child whose place in the list is index. pfRemoveChild(group, child) Detaches child from the list and shifts the list to fill the vacant spot. Returns 1 if child was removed. Returns 0 if child was not found in the list. Note that the “removed” node is only detached, not deleted. pfGetNumChildren(group) Returns the number of children in group. Diagram index = 2 4 The pfGroup nodes can organize a database hierarchy either logically or spatially. For example, if your database contains a model of a town, a logical organization might be to group all house models under a single pfGroup. However, this kind of organization is less efficient than a spatial organization, which arranges geometry by location. A spatial organization improves culling and intersection performance; in the example of the town, spatial organization would consist of grouping houses with their local terrain geometry instead of with each other. Chapter 4 describes how to spatially organize your database for best performance. 54 007-1680-100 Nodes The code fragment in Example 3-2 illustrates building a hierarchy using pfGroup nodes. Example 3-2 Hierarchy Construction Using Group Nodes scene = pfNewScene(); /* The following loop constructs a sample hierarchy by * adding children to several different types of group * nodes. Notice that in this case the terrain was broken * up spatially into a 4x4 grid, and a switch node is used * to cause only one vehicle per terrain node to be * traversed. */ for(j = 0; j < 4; j++) for(i = 0; i < 4; i++) { pfGroup *spatial_terrain_block = pfNewGroup(); pfSCS *house_offset = pfNewSCS(); pfSCS *terrain_block_offset = pfNewSCS(); pfDCS *car_position = pfNewDCS(); pfDCS *tank_position = pfNewDCS(); pfDCS *heli_position = pfNewDCS(); pfSwitch *current_vehicle_type; pfGeode *heli, *car, *tank; pfAddChild(scene, spatial_terrain_block); pfAddChild(spatial_terrain_block, terrain_block_offset); pfAddChild(spatial_terrain_block, house_offset); pfAddChild(spatial_terrain_block, current_vehicle_type); pfAddChild(current_vehicle_type, car_position); pfAddChild(current_vehicle_type, tank_position); pfAddChild(current_vehicle_type, heli_position); pfAddChild(car_position, car); pfAddChild(tank_position, tank); pfAddChild(heli_position, heli); } ... /* The following shows how one might use OpenGL Performer to * manipulate the scene graph at run time by adding and * removing children from branch nodes in the scene graph. 007-1680-100 55 3: Nodes and Node Types */ for(j = 0; j < 4; j++) for(i = 0; i < 4; i++) { pfGroup *this_terrain; this_terrain = pfGetChild(scene, j*4 + i); if (pfGetNumChildren(this_terrain) > 2) this_tank = pfGetChild(this_terrain, 2); if (is_tank_disable(this_tank)) { pfRemoveChild(this_terrain, this_tank); pfAddChild(disabled_tanks, this_tank); } } ... Working with Nodes This section describes the basic concepts involved in working with nodes. It explains how shared instancing can be used to create multiple copies of an object, and how changes made to a parent node propagate down to its children. A sample program that illustrates these concepts is presented at the end of the chapter. Instancing A scene graph is typically constructed at application initialization time by creating and adding new nodes to the graph. If a node is added to two or more parents it is termed instanced and is shared by all its parents. Instancing is a powerful mechanism that saves memory and makes modeling easier. libpf supports two kinds of instancing, shared instancing and cloned instancing, which are described in the following sections. Shared Instancing Shared instancing is the result of simply adding a node to multiple parents. If an instanced node has children, then the entire subgraph rooted by the node is considered to be instanced. Each parent shares the node; thus, modifications to the instanced node or its subgraph are experienced by all parents. Shared instances can be nested—that is, an instance can itself instance other nodes. 56 007-1680-100 Working with Nodes In the following sample code, group0 and group1 share a node: pfAddChild(group0, node); pfAddChild(group1, node); Figure 3-2 shows the structure created by this code. Before the instancing operation, the two groups and the node to be shared all exist independently, as shown in the left portion of the figure. After the two function calls shown above, the two groups both reference the same shared hierarchy. (If the original groups referenced other nodes, those nodes would remain unchanged.) Note that each of the group nodes considers the shared hierarchy to be its own child. Group 1 Group 1 Group 0 Group 0 n Figure 3-2 n Shared Instances Cloned Instancing In many situations shared instancing is not desirable. Consider a subgraph that represents a model of an airplane with articulations for ailerons, elevator, rudder, and landing gear. Shared instances of the model result in multiple planes that share the same articulations. Consequently, it is impossible for one plane to be flying with its landing gear retracted while another is on a runway with its landing gear down. Cloned instancing provides the solution to this problem by cloning—creating new copies of variable nodes in the subgraph. Leaf nodes containing geometry are not cloned and 007-1680-100 57 3: Nodes and Node Types are shared to save memory. Cloning the airplane model generates new articulation nodes, which can be modified independently of any other cloned instance. The cloning operation, pfClone(), is actually a traversal and is described in detail in Chapter 4. Figure 3-3 shows the result of cloned instancing. As in the previous figure, the left half of the drawing represents the situation before the operation, and the right half shows the result of the operation. G1 P1 G2 G1 G2 Root P D1 P2 Dynamic coordinate system D B B C Figure 3-3 D2 A A Leaf C Cloned Instancing The cloned instancing operation constructs new copies of each internal node of the shared hierarchy, but uses the same shared instance of all the leaf nodes. In use, this is an important distinction, because the number of internal nodes may be relatively few, while the number and content of geometry-containing leaf nodes is often quite extensive. Nodes G1 and G2 in Figure 3-3 are the groups that form the root nodes after the cloned instancing operation is complete. Node P is the parent or root node of the instanced object, and D is a dynamic coordinate system contained within it. Nodes A, B, and C are the leaf geometry nodes; they are shared rather than copied. The code in Example 3-3 shows how to create cloned instances. 58 007-1680-100 Working with Nodes Example 3-3 Creating Cloned Instances pfGroup *g1, *g2, *p; pfDCS *d; pfGeode *a, *b, *c; ... /* Create initial instance of scene hierarchy of p under * group g1: add a DCS to p, then add three pfGeode nodes * under the DCS. */ pfAddChild(g1,p); pfAddChild(p,d); pfAddChild(d,a); pfAddChild(d,b); pfAddChild(d,c); ... /* Create cloned instance version of p under g2 */ pfAddChild(g2, pfClone(p,0)); /* Notice that pfGeodes are cloned by instancing rather than * copying. Also notice that the second argument to * pfClone() is 0; that argument is currently required by * OpenGL Performer to be zero. */ ... Bounding Volumes The libpf library uses bounding volumes for culling and to improve intersection performance. libpf computes bounding volumes for all nodes in a database hierarchy unless the bound is explicitly set by the application. The bounding volume of a branch node encompasses the spatial extent of all its children. libpf automatically recomputes bounds when children are modified. By default, bounding volumes are dynamic; that is, libpf automatically recomputes them when children are modified. For instance, in Example 3-4 when the DCS is rotated, nothing more needs to be done to update the bounding volume for g1. Example 3-4 Automatically Updating a Bounding Volume pfAddChild(g1,dcs); pfAddChild(dcs, helicopter); 007-1680-100 59 3: Nodes and Node Types ... pfDCSRot(dcs, heading+10.0f, pitch,roll); ... pfDCSRot(dcs, heading, pitch - 5.0f, roll + 2.0f); In some cases, you may not want bounding volumes to be recomputed automatically. For example, in a merry-go-round with horses moving up and down, you know that the horses stay within a certain volume. Using pfNodeBSphere(), you can specify a bounding sphere within which the horse always remains and tell OpenGL Performer that the bounding volume is “static”—not to be updated no matter what happens to the node’s children. You can always force an update by setting the bounding volume to NULL with pfNodeBSphere(), as follows: pfNodeBSphere(node, NULL, NULL, PFBOUND_STATIC); At the lowest level, within pfGeoSets, bounding volumes are maintained as axially-aligned boxes. When you add a pfGeoSet to a pfGeode or directly invoke pfGetGSetBBox() on the pfGeoSet, a bounding box is created for the pfGeoSet. Neither the bounding box of the pfGeoSet nor the bounding volume of the pfGeode is updated if the geometry changes inside the pfGeoSet. You can force an update by setting the pfGeoSet bounding box and then the pfGeode bounding volume to a NULL bounding box, as follows: • Recompute the pfGeoSet bounding box from the internal geometry: pfGSetBBox(gset, NULL); • Recompute the pfGeode bounding volume from the bounding boxes of its pfGeoSets: pfNodeBSphere(geode, NULL, PFBOUND_DYNAMIC); 60 007-1680-100 Node Types Node Types This section describes the node types and the functions for working with each node type. pfScene Nodes A pfScene is a root node that is the parent of a visual database. Use pfNewScene() to create a new scene node. Before the scene can be drawn, you must call pfChanScene(channel, scene) to attach it to a pfChannel. Any nodes that are within the graph that is parented by a pfScene are culled and drawn once the pfScene is attached to a pfChannel. Because pfScene is a group, it uses pfGroup routines; however, a pfScene cannot be the child of any other node. The following statement adds a pfGroup to a scene: pfAddChild(scene,root); In the simplest case, the pfScene is the only node you need to add. Once you have a pfPipe, pfChannel, and pfScene, you have all the necessary elements for generating graphics using OpenGL Performer. pfScene Default Rendering State The pfScene nodes may specify a global pfGeoState that all other pfGeoStates in nodes below the pfScene will inherit from. Specification of this scene pfGeoState is done via the function pfSceneGState(). This functionality allows for the subtle optimization of pushing the most frequently used pfGeoState attributes for a particular scene graph into a global state and having the individual states inherit these attributes rather than specify them. This can save OpenGL Performer work during culling (by having to ‘unwrap’ fewer pfGeoStates) and thus possibly increase frame rate. There are several database utility functions in libpfdu designed to help with this optimization. pfdMakeSceneGState() returns an ‘optimal’ pfGeoState based on a list of pfGeoStates. pfdOptimizeGStateList() takes an existing global pfGeoState, a new global pfGeoState, and a list of pfGeoStates that should be optimized and cause all attributes of pfGeoStates in the list of pfGeoStates to be inherited if they are the same as the attribute in the new global pfGeoState. Lastly, pfdMakeSharedScene() causes this optimization to happen for all of the pfGeoStates under the pfScene that was passed into the function. For more information on pfGeoStates see Chapter 8, “Geometry,” which discusses libpr in more detail. For more information on the creation and optimization of 007-1680-100 61 3: Nodes and Node Types databases, see Chapter 7, “Importing Databases,” which discusses building database converters and libpfdu. pfSCS Nodes A pfSCS is a branch node that represents a static coordinate system. A pfSCS node contains a fixed modeling transformation matrix that cannot be changed once it is created. pfSCS nodes are useful for positioning models within a database. For example, a house that is modeled at the origin should be placed in the world with a pfSCS because houses rarely move during program execution. Use pfNewSCS(matrix) to create a new pfSCS using the transformation defined by matrix. To find out what matrix was used to create a given pfSCS, call pfGetSCSMat(). For best graphics performance, matrices passed to pfSCS nodes (and the pfDCS node type described in the next section) should be orthonormal (translations, rotations, and uniform scales). Nonuniform scaling requires renormalization of normals in the graphics pipe. Projections and other non-affine transformations are not supported. While pfSCS nodes are useful in modeling, using too many of them can reduce culling, rendering, and intersection performance. For this reason, libpf provides the pfFlatten() traversal. pfFlatten() will traverse a scene graph and apply static transformations directly to geometry to eliminate the overhead associated with managing the transformations. pfFlatten() is described in detail in Chapter 4, “Database Traversal.” pfDCS Nodes A pfDCS is a branch node that represents a dynamic coordinate system. Use a pfDCS when you want to apply an initial transformation to a node and also change the transformation during the application. Use a pfDCS to articulate moving parts and to show object motion. Use pfNewDCS() to create a new pfDCS. The initial transformation of a pfDCS is the identity matrix. Subsequent transformations are set by specifying a new transformation matrix, or by replacing the rotation, scale, or translation in the current transformation matrix. The pfDCS transforms each child C(i) to C(i)∗Scale∗Rotation∗Translation. 62 007-1680-100 Node Types Table 3-3 lists functions for manipulating a pfDCS, including rotating, scaling, and translating the children of the pfDCS. Table 3-3 pfDCS Transformations Function Name Description pfNewDCS() Create a new pfDCS node. pfDCSTrans() Set the translation coordinates to x, y, z. pfDCSRot() Set the rotation transformation to h, p, r. pfDCSCoord() Rotate and translate by coord. pfDCSScale() Scale by a uniform scale factor. pfDCSMat() Use a matrix for transformations. pfGetDCSMat() Retrieve the current matrix for a given pfDCS. pfFCS Nodes A pfFCS is a branch node that represents a flux coordinate system. The transformation matrix of a pfFCS is contained in the pfFlux which is linked to it. This linkage allows a pfEngine to animate the matrix of a pfFCS. The linkage also allows multiple pfFCSs to share the same transformation. Use pfNewFCS(flux) to create a new pfFCS linked to flux. Table 3-4 lists functions for manipulating a pfFCS. pfFCS, pfFlux, and pfEngine are fully described in Chapter 19, “Dynamic Data.” Table 3-4 007-1680-100 pfFCS Functions Function Description pfNewFCS() Create a new pfFCS node. pfFCSFlux() Link a flux to a given pfFCS. pfGetFCSFlux() Get a pointer to the flux linked to a given pfFCS. 63 3: Nodes and Node Types Table 3-4 pfFCS Functions (continued) Function Description pfGetFCSMat() Retrieve the current matrix for a given pfFCS. pfGetFCSMatPtr() Get a pointer to the current matrix for a given pfFCS. pfDoubleSCS Nodes The pfDoubleSCS nodes are double-precision versions of pfSCS nodes. Instead of storing a pfMatrix, they store a pfMatrix4d, a 4x4 matrix of double-precision numbers. See the section “pfDoubleDCS Nodes” for a discussion on using double-precision matrix nodes. pfDoubleDCS Nodes pfDoubleDCS nodes are double-precision versions of pfDCS nodes. Instead of a pfMatrix, they maintain a pfMatrix4d, a 4x4 matrix of double-precision numbers. Double-precision nodes are useful for modeling and rendering objects very far from the origin of the database. The following example demonstrates how double-precision nodes help. Consider a model of the entire Earth and visualize a model of a car moving on the surface of the Earth. Placing the origin of the Earth model in the center of the Earth makes the car object on the surface of the Earth very far from the origin. In Figure 3-4, the distance from the center of the Earth to the car or to the camera is larger than D, and the distance from the viewer to the car is d. D is very large; therefore, single-precision floating point numbers cannot express small changes in the car position. The motion of the car will be shaky and unsmooth. One potential solution for the shaky car motion is to use double-precision matrices in OpenGL. Unfortunately, the underlying hardware implementation does not support double-precision values. All values are converted to single-precision floating point numbers and OpenGL Performer cannot eliminate the shaky motion. 64 007-1680-100 Node Types Eye d Car D Origin (0,0,0) Earth Figure 3-4 A Scenario for Using Double-Precision Nodes In order to solve the shaky motion problem, we observe the following: we usually want to see small translations of an object when the camera is fairly close to that object. If we look at the car from 200 miles away, we do not care to see a 10-inch translation in its position. Therefore, if we could dynamically drag the origin with the camera, then any object will be close enough to the origin when the camera is near it, which is exactly when we want to see its motion smoothly. Double-precision matrix nodes (pfDoubleSCS, pfDoubleDCS, and pfDoubleFCS) allow modeling with a dynamic origin. We start by setting the pfChannel viewing matrix to the identity matrix. This puts the channel eyepoint in the origin. We create a scene graph as in Figure 3-5. Each pfGeode represents a tile of the Earth surface. We model each tile with a local origin somewhere within the tile. Each of the pfDoubleDCS nodes above the pfGeode nodes contains a transformation that sends the node under it to its correct position around the globe. We set the transformation in the pfDoubleDCS node marked EYE to the inverse of the matrix taking an object to the true camera position. This transforms all nodes under EYE to a coordinate system with the eyepoint in the origin. 007-1680-100 65 3: Nodes and Node Types pfScene EYE pfDoubleDCS pfDoubleDCS # 0 pfGeode # 0 Figure 3-5 pfDoubleDCS # 1 pfGeode # 1 .... .... pfDoubleDCS # N pfGeode # N pfDoubleDCS Nodes in a Scene Graph In more practical terms, we set the channel camera position to the origin with the following call: pfChanViewMat(chan, pfIdentMat); The following code fragment loads the EYE pfDoubleDCS node with the correct matrix. We call the function with EYE as the first parameter and the camera position in the second parameter: void loadViewingMatrixOnDoubleDCS (pfDoubleDCS *ddcs, pfCoordd *coord) { pfMatrix4d mat, invMat; pfMakeCoorddMat4d (mat, coord); pfInvertOrthoNMat4d (invMat, mat); pfDoubleDCSMat (ddcs, invMat); } 66 007-1680-100 Node Types pfDoubleFCS Nodes A pfDoubleFCS node is similar to a pfFCS node. Instead of a single-precision matrix, it maintains a pfFlux with a double-precision matrix. See “pfDoubleDCS Nodes” for information on using pfDoubleFCS nodes. pfSwitch Nodes A pfSwitch is a branch node that selects one, all, or none of its children. Use pfNewSwitch() to return a handle to a new pfSwitch. To select all the children, use the PFSWITCH_ON argument to pfSwitchVal(). Deselect all the children (turning the switch off) using PFSWITCH_OFF. To select a single child, give the index of the child from the child list. To find out the current value of a given switch, call pfGetSwitchVal(). Example 3-5 (in the “pfSequence Nodes” section) illustrates a use of pfSwitch nodes to control pfSequence nodes. pfSequence Nodes A pfSequence is a pfGroup that sequences through a range of its children, drawing each child for a specified duration. Each child in a sequence can be thought of as a frame in an animation. A sequence can consist of any number of children, and each child has its own duration. You can control whether an entire sequence repeats from start to end, repeats from end to start, or terminates. Use pfNewSeq() to create and return a handle to a new pfSequence. Once the pfSequence has been created, use the group function pfAddChild() to add the children that you want to animate. Table 3-5 describes the functions for working with pfSequences. Table 3-5 007-1680-100 pfSequence Functions Function Description pfNewSeq() Create a new pfSequence node. pfSeqTime() Set the length of time to display a frame. pfGetSeqTime() Find out the time allotted for a given frame. 67 3: Nodes and Node Types Table 3-5 pfSequence Functions (continued) Function Description pfSeqInterval() Set the range of frames and sequence type. pfGetSeqInterval() Find out interval parameters. pfSeqDuration() Control the speed and number of repetitions of the entire sequence. pfGetSeqDuration() Retrieve speed and repetition information for the sequence. pfSeqMode() Start, stop, pause, and resume the sequence. pfGetSeqMode() Find out the sequence’s current mode. pfGetSeqFrame() Get the current frame. Example 3-5 demonstrates a possible use of both switches and sequences. First, sequences are set up to contain animation sequences for explosions, fire, and smoke; then a switch is used to control which sequences are currently active. Example 3-5 Using pfSwitch and pfSequence Nodes pfSwitch *s; pfSequence *explosion1_seq, *explosion2_seq, *fire_seq, *smoke_seq; ... s = pfNewSwitch(); explosion1_seq = pfNewSeq(); explosion2_seq = pfNewSeq(); fire_seq = pfNewSeq(); smoke_seq = pfNewSeq(); pfAddChild(s, explosion1_seq); pfAddChild(s, explosion2_seq); pfAddChild(s, fire_seq); pfAddChild(s, smoke_seq); pfSwitchVal(s, PFSWITCH_OFF); ... if (direct_hit) { pfSwitchVal(s, PFSWITCH_ON); /* Select all sequences */ /* Set first explosion sequence to go double normal * speed and repeat 3 times. */ 68 007-1680-100 Node Types pfSeqMode(explosion1_seq, PFSEQ_START); pfSeqDuration(explosion1_seq, 2.0f, 3); /* Set second explosion sequence to display first child * of sequence for 2 seconds before continuing. */ pfSeqMode(explosion2_seq, PFSEQ_START); pfSeqTime(explosion2, 0.0f, 2.0f); /* Set fire to wait on first frame of sequence until .3 * seconds after second explosion. */ pfSeqMode(fire_seq, PFSEQ_START); pfSeqTime(fire_seq, 0.0f, 2.3f); /* Set smoke to wait until .1 seconds after fire. */ pfSeqMode(smoke_seq, PFSEQ_START); pfSeqTime(smoke_seq, 0.0f, 2.4f); } else if (explosion && (expl_type == 0)) { pfSeqMode(explosion1_seq, PFSEQ_START); pfSwitchVal(s, 0); } else if (explosion && (expl_type == 1)) { pfSeqMode(explosion2_seq, PFSEQ_START); pfSwitchVal(s, 1); } else if (fire_is_burning) { pfSeqMode(fire_seq, PFSEQ_START); pfSwitchVal(s, 2); } else if (smoking) { pfSeqMode(smoke_seq, PFSEQ_START); pfSwitchVal(s, 3); } else pfSwitchVal(s, PFSWITCH_OFF); ... 007-1680-100 69 3: Nodes and Node Types pfLOD Nodes A pfLOD is a level-of-detail node. Level-of-detail switching is an advanced concept that is discussed in Chapter 5, “Frame and Load Control.” A level-of-detail node specifies how its children are to be displayed, based on the visual range from the channel’s viewpoint. Each child has a defined range, and the entire pfLOD has a defined center. Table 3-6 describes the functions for working with pfLODs. Table 3-6 pfLOD Functions Function Description pfNewLOD() Create a level of detail node. pfLODRange() Set a range at which to use a specified child node. pfGetLODRange() Find out the range for a given node. pfLODCenter() Set the pfLOD center. pfGetLODCenter() Retrieve the pfLOD center. pfLODTransition() Set the width of a specified transition. pfGetLODTransition() Get the width of a specified transition. pfASD Nodes The pfASD nodes handle dynamic generation and morphing of the visible part of a surface based on multiple LODs. pfASD nodes allow for the smooth LOD transition of large and complex surfaces, such as large area terrain. For information on pfASD nodes, see Chapter 20, “Active Surface Definition.” pfLayer Nodes A pfLayer is a leaf node that resolves the visual priority of coplanar geometry. A pfLayer allows the application to define a set of base geometry and a set of layer geometry (sometimes called decal geometry). The base geometry and the decal geometry should be coplanar, and the decal geometry must lie within the extent of the base polygons. 70 007-1680-100 Node Types Table 3-7 describes the functions for working with pfLayers. Table 3-7 pfLayer Functions Function Description pfNewLayer() Create a pfLayer node. pfLayerMode() Specify a hardware mode to use in drawing decals. pfGetLayerMode() Get the current mode. pfLayerBase() Specify the child containing base geometry. pfGetLayerBase() Find out which child contains base geometry. pfLayerDecal() Specify the child containing decal geometry. pfGetLayerDecal() Find out which child contains decal geometry. The pfLayer nodes can be used to overlay any sort of markings on a given polygon and are important to avoid flimmering. Example 3-6 demonstrates how to display runway markings as a decal above a coplanar runway. This example uses the performance mode PFDECAL_BASE_FAST for layering; as described in the pfLayer and pfDecal man pages, other available modes are PFDECAL_BASE_HIGH_QUALITY, PFDECAL_BASE_DISPLACE, and PFDECAL_BASE_STENCIL. Example 3-6 Marking a Runway with a pfLayer Node pfLayer *layer; pfGeode *runway, *runway_markings; ... /* avoid flimmering of runway and runway_markings */ layer = pfNewLayer(); pfLayerBase(layer, runway); pfLayerDecal(layer, runway_markings); pfLayerMode(layer, PFDECAL_BASE_FAST); pfGeode Nodes The pfGeode node is short for geometry node and is the primary node for defining geometry in libpf. A pfGeode contains a list of geometry structures called pfGeoSets, which are part of the OpenGL Performer libpr library. pfGeoSets encapsulate graphics 007-1680-100 71 3: Nodes and Node Types state and geometry and are described in the section, “pfGeoSet (Geometry Set)” in Chapter 8. It is important to understand that pfGeoSets are not nodes but are simply elements of a pfGeode. Table 3-8 describes the functions for working with pfGeodes. pfGeode Functions Table 3-8 Function Description pfNewGeode() Create a pfGeode. pfAddGSet() Add a pfGeoSet. pfRemoveGSet() Remove a pfGeoSet. pfInsertGSet() Insert a pfGeoSet. pfReplaceGSet() Replace a pfGeoSet. pfGetGSet() Supply a pointer to the specified pfGeoSet. pfGetNumGSets() Determine how many pfGeoSets are in the given pfGeode. Example 3-7 shows how to attach several pfGeoSets to a pfGeode. Example 3-7 Adding pfGeoSets to a pfGeode pfGeode *car1; pfGeoSet *muffler, *frame, *windows, *seats, *tires; muffler frame = seats = tires = = read_in_muffler_geometry(); read_in_frame_geometry(); read_in_seat_geometry(); read_in_tire_geometry(); pfAddGSet(car1, pfAddGSet(car1, pfAddGSet(car1, pfAddGSet(car1, ... 72 muffler); frame); seats); tires); 007-1680-100 Node Types pfText Nodes A pfText node is a libpf leaf node that contains a set of libpr pfStrings that should be rendered based on the libpf cull and draw traversals. In this sense, a pfText is similar to a pfGeode except that it renders 3D text through the libpr pfString and pfFont mechanisms rather than rendering standard 3D geometry via libpr pfGeoSet and pfGeoState functionality. pfText nodes are useful for displaying 3D text and other collections of geometry from a fixed index list. Table 3-9 lists the major pfText functions. Table 3-9 pfText Functions Function Description pfNewText() Create a pfText. pfAddString() Add a pfString. pfRemoveString() Remove a pfString. pfInsertString() Insert a pfString. pfReplaceString() Replace a pfString. pfGetString() Supply a pointer to the specified pfString. pfGetNumStrings() Determine how many pfStrings are in the given pfText. Using the pfText facility is easy. Example 3-8 shows how a pfFont is defined, how pfStrings are created that reference that font, and then how those pfStrings are added to a pfText node for display. See the description of pfStrings and pfFonts in Chapter 8, “Geometry,” for information on setting up individual strings to input into a pfText node. Example 3-8 Adding pfStrings to a pfText int nStrings,i; char tmpBuf[8192]; char fontName[128]; pfFont *fnt = NULL; /* Create a new text node pfText *txt = pfNewText(); /* Read in font using libpfdu utility function */ scanf(“%s”,fontName); fnt = pfdLoadFont(“type1”,fontName,PFDFONT_EXTRUDED); 007-1680-100 73 3: Nodes and Node Types /* Cant render pfText or libpr pfString without a pfFont */ if (fnt == NULL) pfNotify(PFNFY_WARN,PFNFY_PRINT, ”No Such Font - %s\n”,fontName); /* Read nStrings text strings from standard input and */ /* Attach them to a pfText */ scanf(“%d”,&nStrings); for(i=0;i<nStrings;i++) { char c; int j=0; int done = 0; pfString *curStr = NULL; while(done < 2) /* READ STRING - END on ‘||’ */ { c = getchar(); if (c == ‘|’) done++; else done = 0; tmpBuf[j++] = c; } tmpBuf[PF_MAX2(j-2,0)] = ‘\0’; /* Create new libpr pfString structure to attach to pfText */ curStr = pfNewString(pfGetSharedArena()); /* Set the font for the libpr pfString */ pfStringFont(curStr, fnt); /* Assign the char string to the pfString */ pfStringString(curStr, tmpBuf); /* Add this libpr pfString to the pfText node */ /* Like adding a libpr pfGeoSet to a pfGeode */ pfAddString(txt, curStr); } pfAddChild(SceneGroup, txt); 74 007-1680-100 Node Types pfBillboard Nodes A pfBillboard is a pfGeode that rotates its children’s geometry to follow the view direction or the eyepoint. Billboards are useful for portraying complex objects that are roughly symmetrical in one or more axes. The billboard rotates to always present the same image to the viewer using far fewer polygons than a solid model uses. In this way, billboards reduce both transformation and pixel fill demands on the graphics subsystem at the expense of some additional host processing. A classic example is a textured billboard of a single quadrilateral representing a tree. Because a pfBillboard is also a pfGeode, you can pass a pfBillboard argument to any pfGeode routine. To add geometry, call pfAddGSet() (see “pfGeode Nodes” on page 71). Each pfGeoSet in the pfBillboard is treated as a separate piece of billboard geometry; each one turns so that it always faces the eyepoint. The pfBillboards can be either constrained to rotate about an axis, as is done for a tree or a lamp post, or constrained only by a point, as when simulating a cloud or a puff of smoke. Specify the rotation mode by calling pfBboardMode(); specify the rotational axis by calling pfBboardAxis(). Since rotating the geometry to the eyepoint does not fully constrain the orientation of a point-rotating billboard, modes are available to use the additional degree of freedom to align the billboard in eye space or world space. Usually the normals of billboards are specified to be parallel to the rotational axis to avoid lighting anomalies. The pfFlatten() function is highly recommended for billboards. If a billboard lies beneath a pfSCS or pfDCS, an additional transformation is done for each billboard. This can have a substantial performance impact on the cull process, where billboards are transformed. Table 3-10 describes the functions for working with pfBillboards. Table 3-10 007-1680-100 pfBillboard Functions Function Description pfNewBboard() Create a pfBillboard node. pfBboardPos() Set a billboard’s position. pfGetBboardPos() Find out a billboard’s position. pfBboardAxis() Specify the rotation or alignment axis. pfGetBboardAxis() Find out the rotation or alignment axis. 75 3: Nodes and Node Types Table 3-10 pfBillboard Functions (continued) Function Description pfBboardMode() Specify a billboard’s rotation type. pfGetBboardMode() Find out a billboard’s rotation type. Example 3-9 demonstrates the construction of a pfBillboard node. The code can be found in /usr/share/Performer/src/pguide/libpf/C/billboard.c for IRIX and Linux and in %PFROOT%\Src\pguide\libpf\C\billboard.c for Microsoft Windows. Example 3-9 Setting Up a pfBillboard static pfVec2 BBTexCoords[] ={{0.0f, {1.0f, {1.0f, {0.0f, 0.0f}, 0.0f}, 1.0f}, 1.0f}}; static pfVec3 BBVertCoords[4] = /* XZ plane for pt bboards */ {{-0.5f, 0.0f, 0.0f}, { 0.5f, 0.0f, 0.0f}, { 0.5f, 0.0f, 1.0f}, {-0.5f, 0.0f, 1.0f}}; static pfVec3 BBAxes[4] = {{1.0f, {0.0f, {0.0f, {0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f}, /* X */ 0.0f}, /* Y */ 1.0f}, /* Z */ 1.0f}}; /*world Zup*/ static int BBPrimLens[] = { 4 }; static pfVec4 BBColors[] = {{1.0, 1.0, 1.0, 1.0}}; /* Convert static data to pfMalloc’ed data */ static void* memdup(void *mem, size_t bytes, void *arena) { void *data = pfMalloc(bytes, arena); memcpy(data, mem, bytes); return data; } 76 007-1680-100 Node Types /* For pedagogical use only. Reasonable performance * requires more then one pfGeoSet per pfBillboard. */ pfBillboard* MakeABill(pfVec3 pos, pfGeoState *gst, long bbType) { pfGeoSet *gset; pfGeoState *gstate; pfBillboard *bill; void *arena = pfGetSharedArena(); gset = pfNewGSet(arena); gstate = pfNewGState(arena); pfGStateMode(gstate, PFSTATE_ENLIGHTING, PF_OFF); pfGStateMode(gstate, PFSTATE_ENTEXTURE, PF_ON); /*.... Create/load texture map for billboard... */ pfGStateAttr(gstate, PFSTATE_TEXTURE, texture); pfGSetGState(gset, gstate); pfGSetAttr(gset, PFGS_COORD3, PFGS_PER_VERTEX, memdup(BBVertCoords, sizeof(BBVertCoords), arena), NULL); pfGSetAttr(gset, PFGS_TEXCOORD2, PFGS_PER_VERTEX, memdup(BBTexCoords, sizeof(BBTexCoords), arena), NULL); pfGSetAttr(gset, PFGS_COLOR4, PFGS_OVERALL, memdup(BBColors, sizeof(BBColors), arena), NULL); pfGSetPrimLengths(gset, (int*)memdup(BBPrimLens, sizeof(BBPrimLens), arena)); pfGSetPrimType(gset, PFGS_QUADS); pfGSetNumPrims(gset, 1); pfGSetGState(gset, gst); bill = pfNewBboard(); switch (bbType) { case PF_X: /* axial rotate */ case PF_Y: case PF_Z: pfBboardAxis(bill, BBAxes[bbType]); 007-1680-100 77 3: Nodes and Node Types pfBboardMode(bill, PFBB_ROT, PFBB_AXIAL_ROT); break; case 3: /* point rotate */ pfBboardAxis(bill, BBAxes[bbType]); pfBboardMode(bill, PFBB_ROT, PFBB_POINT_ROT_WORLD); break; } pfAddGSet(bill, gset); pfBboardPos(bill, 0, pos); return bill; } pfPartition Nodes A pfPartition is a pfGroup that organizes the scene graphs of its children into a static data structure that can be more efficient for intersections. Currently, partitions are only useful for data that lies more or less on an XY plane, such as terrain. Therefore, a pfPartition would be inappropriate for a skyscraper model. Partition construction comes in two phases. After a piece of the scene graph has been placed under the pfPartition, pfBuildPart() examines the spatial arrangement of geometry beneath the pfPartition and determines an appropriate origin and spacing for the grid. Because the search is exhaustive, this examination can be time-consuming the first time through. Once a good partitioning is determined, the search space can be restricted for future database loads using the partition attributes. The second phase is invoked by pfUpdatePart(), which distributes the pfGeoSets under the pfPartition into cells in the spatial partition created by pfBuildPart(). pfUpdatePart() needs to be called if any geometry under the pfPartition node changes. During intersection traversal, the segments in a pfSegSet (see “Intersection Requests: pfSegSets” in Chapter 4) are scan-converted onto the grid, yielding faster access to those pfGeoSets that potentially intersect the segment. A pfPartition can be made to function as a normal pfGroup during intersection traversal by ORing PFTRAV_IS_NO_PART into the intersection traversal mode in the pfSegSet. 78 007-1680-100 Node Types Table 3-11 describes the functions for working with pfPartitions. Table 3-11 pfPartition Functions Function Description pfNewPart() Create a pfPartition. pfPartVal() Set the desired pfPartition value. pfGetPartVal() Find out the attributes of the specified value. pfPartAttr() Set the desired pfPartition attribute. pfGetPartAttr() Find out the attributes of specified the attribute. pfBuildPart() Construct a spatial partitioning based on the attributes. pfUpdatePart() Traverse the partition’s children and incorporate changes. pfGetPartType() Determine what kind of partition is being used. Example 3-10 demonstrates setting up and using a pfPartition node. Example 3-10 Setting Up a pfPartition pfGroup *terrain; pfPartition *partition; pfScene *scene; ... terrain = read_in_grid_aligned_terrain(); ... /* create a default partitioning of a terrain grid */ partition = pfNewPart(); pfAddChild(scene, partition); pfAddChild(partition, terrain); pfBuildPart(partition); ... /* use the partitions to perform efficient intersections * of sets of segments with the terrain */ for(i = 0; i < numVehicles; i++) 007-1680-100 79 3: Nodes and Node Types pfNodeIsectSegs(partition, vehicle_segment_set[i], hit_struct); ... Sample Program The sample program shown in Example 3-11 demonstrates scene graph construction, shared instancing, and transformation inheritance. The program uses OpenGL Performer objects and functions that are described fully in later chapters. This program reads the names of two objects from the command line, although defaults are supplied if file names are not given. These files are loaded and a second instance of each object is created. In each case, this instance is made to orbit the original object, and the second pair are also placed in orbit around the first. This program is inherit.c and is part of the suite of OpenGL Performer Programmer’s Guide example programs. Example 3-11 Inheritance Demonstration Program /* * inherit.c - transform inheritance example */ #include <math.h> #include <Performer/pf.h> #include <Performer/pfdu.h> int main(int argc, char *argv[]) { pfPipe *pipe; pfPipeWindow *pw; pfScene *scene; pfChannel *chan; pfCoord view; float z, s, c; pfNode *model1, *model2; pfDCS *node1, *node2; pfDCS *dcs1, *dcs2, *dcs3, *dcs4; pfSphere sphere; char *file1, *file2; /* choose default objects of none specified */ 80 007-1680-100 Sample Program file1 = (argc > 1) ? argv[1] : “blob.nff”; file2 = (argc > 1) ? argv[1] : “torus.nff”; /* Initialize Performer */ pfInit(); pfFilePathv( “.”, “./data”, “../data”, “../../data”, “../../../data”, “../../../../data”, “/usr/share/Performer/data”, NULL); /* Single thread for simplicity */ pfMultiprocess(PFMP_DEFAULT); /* Load all loader DSO’s before pfConfig() forks */ pfdInitConverter(file1); pfdInitConverter(file2); /* Configure */ pfConfig(); /* Load the files */ if ((model1 = pfdLoadFile(file1)) == NULL) { pfExit(); exit(-1); } if ((model2 = pfdLoadFile(file2)) == NULL) { pfExit(); exit(-1); } /* scale models to unit size */ node1 = pfNewDCS(); pfAddChild(node1, model1); pfGetNodeBSphere(model1, &sphere); if (sphere.radius > 0.0f) pfDCSScale(node1, 1.0f/sphere.radius); 007-1680-100 81 3: Nodes and Node Types node2 = pfNewDCS(); pfAddChild(node2, model2); pfGetNodeBSphere(model2, &sphere); if (sphere.radius > 0.0f) pfDCSScale(node2, 1.0f/sphere.radius); /* Create the hierarchy */ dcs4 = pfNewDCS(); pfAddChild(dcs4, node1); pfDCSScale(dcs4, 0.5f); dcs3 = pfNewDCS(); pfAddChild(dcs3, node1); pfAddChild(dcs3, dcs4); dcs1 = pfNewDCS(); pfAddChild(dcs1, node2); dcs2 = pfNewDCS(); pfAddChild(dcs2, node2); pfDCSScale(dcs2, 0.5f); pfAddChild(dcs1, dcs2); scene = pfNewScene(); pfAddChild(scene, dcs1); pfAddChild(scene, dcs3); pfAddChild(scene, pfNewLSource()); /* Configure and open GL window */ pipe = pfGetPipe(0); pw = pfNewPWin(pipe); pfPWinType(pw, PFPWIN_TYPE_X); pfPWinName(pw, “OpenGL Performer”); pfPWinOriginSize(pw, 0, 0, 500, 500); pfOpenPWin(pw); chan = pfNewChan(pipe); pfChanScene(chan, scene); pfSetVec3(view.xyz, 0.0f, 0.0f, 15.0f); pfSetVec3(view.hpr, 0.0f, -90.0f, 0.0f); pfChanView(chan, view.xyz, view.hpr); /* Loop through various transformations of the DCS’s */ for (z = 0.0f; z < 1084; z += 4.0f) 82 007-1680-100 Sample Program { pfDCSRot(dcs1, (z < 360) ? (int) z % 360 : 0.0f, (z > 360 && z < 720) ? (int) z % 360 : 0.0f, (z > 720) ? (int) z % 360 : 0.0f); pfSinCos(z, &s, &c); pfDCSTrans(dcs2, 1.0f * c, 1.0f * s, 0.0f); pfDCSRot(dcs3, z, 0, 0); pfDCSTrans(dcs3, 4.0f * c, 4.0f * s, 4.0f * s); pfDCSRot(dcs4, 0, 0, z); pfDCSTrans(dcs4, 1.0f * c, 1.0f * s, 0.0f); pfFrame(); } /* show objects static for three seconds */ sleep(3); pfExit(); exit(0); } 007-1680-100 83 Chapter 4 4. Database Traversal Chapter 3, “Nodes and Node Types,” described the node types used by libpf. This chapter describes the operations that can be performed on the run-time database defined by a scene graph. These operations typically work with part or all of a scene graph and are known as traversals because they traverse the database hierarchy. OpenGL Performer supports four major kinds of database traversals: • Application • Cull • Draw • Intersection The application traversal updates the active elements in the scene graph for the next frame. This includes processing active nodes and invoking user-supplied callbacks for animations or other embedded behaviors. Visual processing consists of two basic traversals: culling and drawing. The cull traversal selects the visible portions of the database and puts them into a display list. The draw traversal then runs through that display list and sends rendering commands to the Geometry Pipeline. Once you have set up all the necessary elements, culling and drawing are automatic, although you can customize each traversal for special purposes. The intersection traversal computes the intersection of one or more line segments with the database. The intersection traversal is user-directed. Intersections are used to determine the following: • Height above terrain • Line-of-sight visibility • Collisions with database objects Like other traversals, intersection traversals can be directed by the application through identification masks and function callbacks. Table 4-1 lists the routines and data types 007-1680-100 85 4: Database Traversal relevant to each of the major traversals; more information about the listed traversal attributes can be found later in this chapter and in the appropriate man pages. Table 4-1 Traversal Attributes for the Major Traversals Traversal Attribute Application PFTRAV_APP Cull PFTRAV_CULL Draw PFTRAV_DRAW Intersection PFTRAV_ISECT Controllers pfChannel pfChannel pfChannel pfSegSet Global Activation pfFrame() pfSync() pfAppFrame() pfFrame() pfFrame() pfFrame() pfNodeIsectSegs(), pfChanNodeIsectSegs() Global Callbacks pfChanTravFunc() pfChanTravFunc() pfChanTravFunc() pfIsectFunc() Activation within Callback pfApp() pfCull() pfDraw() pfFrame() pfNodeIsectSegs(), pfChanNodeIsectSegs() Path Activation N/A pfCullPath() N/A N/A Modes pfChanTravMode() pfChanTravMode() pfChanTravMode() pfSegSet (also discriminator callback) Node Callbacks pfNodeTravFuncs() pfNodeTravFuncs() pfNodeTravFuncs() pfNodeTravFuncs() Traverser Masks pfChanTravMask() pfChanTravMask() pfChanTravMask() pfSegSet mask Traversee Masks pfNodeTravMask() pfNodeTravMask() pfNodeTravMask() pfNodeTravMask() pfGSetIsectMask() 86 007-1680-100 Scene Graph Hierarchy Scene Graph Hierarchy A visual database, also known as a scene, contains state information and geometry. A scene is organized into a hierarchical structure known as a graph. The graph is composed of connected database units called nodes. Nodes that are attached below other nodes in the tree are called children. Children belong to their parent node. Nodes with the same parent are called siblings. Database Traversals The scene hierarchy supplies definitions of how items in the database relate to one another. It contains information about the logical and spatial organization of the database. The scene hierarchy is processed by visiting the nodes in depth-first order and operating on them. The process of visiting, or touching, the nodes is called traversing the hierarchy. The tree is traversed from top to bottom and from left to right. OpenGL Performer implements several types of database traversals, including application, clone, cull, delete, draw, flatten, and intersect. These traversals are described in more detail later in this chapter. The principal traversals (application, cull, draw and intersect) all use a similar traversal mechanism that employs traversal masks and callbacks to control the traversal. When a node is visited during the traversal, processing is performed in the following order: 1. Prune the node based on the bitwise AND of the traversal masks of the node and the pfChannel (or pfSegSet). If pruned, traversal continues with the node’s siblings. 2. Invoke the node’s pre-traversal callback, if any, and either prune, continue, or terminate the traversal, depending on the callback’s return value. 3. Traverse, beginning again at step 1, the node’s children or geometry (pfGeoSets). If the node is a pfSwitch, a pfSequence, or a pfLOD, the state of the node affects which children are traversed. 4. Invoke the node’s post-traversal callback, if any. State Inheritance In addition to imposing a logical and spatial ordering of the database, the hierarchy also defines how state is inherited between parent and child nodes during scene graph traversals. For example, a parent node that represents a transformation causes the 007-1680-100 87 4: Database Traversal subsequent transformation of each of its children when it and they are traversed. In other words, the children inherit state, which includes the current coordinate transformation, from their parent node during database traversal. A transformation is a 4x4 homogeneous matrix that defines a 3D transformation of geometry, which typically consist of scaling, rotation, and translation. The node types pfSCS and pfDCS both represent transformations. Transformations are inherited through the scene graph with each new transformation being concatenated onto the ones above it in the scene graph. This allows chained articulations and complex modeling hierarchies. The effects of state are propagated downward only, not from left to right nor upward. This means that only parents can affect their children—siblings have no effect on each other nor on their parents. This behavior results in an easy-to-understand hierarchy that is well suited for high-performance traversals. Graphics states such as textures and materials are not inherited by way of the scene graph but are encapsulated in leaf geometry nodes called pfGeode nodes, which are described in the section “Node Types” in Chapter 3. Database Organization OpenGL Performer uses the spatial organization of the database to increase the performance of certain operations such as drawing and intersections. It is therefore recommended that you consider the spatial arrangement of your database. What you might think of as a logical arrangement of items in the database may not match the spatial arrangement of those items in the visual environment, which can reduce OpenGL Performer’s ability to optimize operations on the database. See “Organizing a Database for Efficient Culling” on page 94 for more information about spatial organization in a visual database and the efficiency of database operations. Application Traversal The application traversal is the first traversal that occurs during the processing of the scene graph in preparation for rendering a frame. It is initiated by calling pfAppFrame(). If pfAppFrame() is not explicitly called, the traversal is automatically invoked by pfSync() or pfFrame(). An application traversal can be invoked for each channel, but usually channels share the same application traversal (see pfChanShare()). 88 007-1680-100 Application Traversal The application traversal updates dynamic elements in the scene graph, such as geometric morphing. The application traversal is also often used for implementing animations or other custom processing when it is desirable to have those behaviors embedded in the scene graph and invoked by OpenGL Performer rather than requiring application code to invoke them every frame. The traversal proceeds as described in “Database Traversals.” The selection of which children to traverse is also affected by the application traversal mode of the channel, in particular the choice of all, none, or one of the children of pfLOD, pfSequence and pfSwitch nodes is possible. By default, the traversal obeys the current selection dictated by these nodes. The following example (this loader reads both Open Inventor and VRML files) shows a simple callback changing the transformation on a pfDCS every frame. Example 4-1 Application Callback to Make a Pendulum int AttachPendulum(pfDCS *dcs, PendulumData *pd) { pfNodeTravFuncs(dcs, PFTRAV_APP, PendulumFunc, NULL); pfNodeTravData(dcs, PFTRAV_APP, pd); } int PendulumFunc(pfTraverser *trav, void *userData) { PendulumData *pd = (PendulumData*)userData; pfDCS *dcs = (pfDCS*)pfGetTravNode(trav); if (pd->on) { pfMatrix mat; double now = pfGetFrameTimeStamp(); float frac, dummy; pd->lastAngle += (now - pd->lastTime)*360.0f*pd->frequency; if (pd->lastAngle > 360.0f) pd->lastAngle -= 360.0f; // using sinusoidally generated angle pfSinCos(pd->lastAngle, &frac, &dummy); frac = 0.5f + 0.5f * frac; frac = (1.0f - frac)*pd->angle0 + frac*pd->angle1; 007-1680-100 89 4: Database Traversal pfMakeRotMat(mat, frac, pd->axis[0], pd->axis[1], pd->axis[2]); pfDCSMat(dcs, mat); pd->lastTime = now; } return PFTRAV_CONT; } Cull Traversal The cull traversal occurs in the cull phase of the libpf rendering pipeline and is initiated by calling pfFrame(). A cull traversal is performed for each pfChannel and determines the portion of the scene to be rendered. The traversal processes the subgraphs of the scene that are both visible and selected by nodes in the scene graph that control traversal (that is, pfLOD, pfSequence, pfSwitch). The visibility culling itself is performed by testing bounding volumes in the scene graph against the channel’s viewing frustum. For customizing the cull traversal, libpf provides traversal masks and function callbacks for each node in the database, as well as a function callback in which the application can do its own culling of custom data structures. Traversal Order The cull is a depth-first, left-to-right traversal of the database hierarchy beginning at a pfScene, which is the hierarchy’s root node. For each node, a series of tests is made to determine whether the traversal should prune the node—that is, eliminate it from further consideration—or continue on to that node’s children. The cull traversal processing is much as described earlier; in particular, the draw traversal masks are compared and the node is checked for visibility before the traversal continues on to the node’s children. Processing proceeds in the following order: 1. Prune the node, based on the channel’s draw traversal mask and the node’s draw mask. 2. Invoke the node’s pre-cull callback and either prune, continue, or terminate the traversal, depending on callback’s return value. 3. Prune the node if its bounding volume is completely outside the viewing frustum. 90 007-1680-100 Cull Traversal 4. Traverse, beginning again at step 1, the node’s children or geometry (pfGeoSets) if the node is completely or partially in the viewing frustum. If the node is a pfSwitch, a pfSequence, or a pfLOD, the state of the node affects which children are traversed. 5. Invoke the node’s post-cull callback. The following sections discuss these steps in more detail. Visibility Culling Culling determines whether a node is within a pfChannel’s viewing frustum for the current frame. Nodes that are not visible are pruned—omitted from the list of objects to be drawn—so that the Geometry Pipeline does not waste time processing primitives that couldn’t possibly appear in the final image. Hierarchical Bounding Volumes Testing a node for visibility compares the bounding volume of each object in the scene against a viewing frustum that is bounded by the near and far clip planes and the four sides of the viewing pyramid. Both nodes (see Chapter 3, “Nodes and Node Types”) and pfGeoSets (see Chapter 8, “Geometry”) have bounding volumes that surround the geometry that they contain. Bounding volumes are simple geometric shapes whose centers and edges are easy to locate. Bounding volumes are organized hierarchically so that the bounding volume of a parent encloses the bounding volumes of all its children. You can specify bounding volumes or let OpenGL Performer generate them for you (see “Bounding Volumes” in Chapter 3). Figure 4-1 shows a frustum and three objects surrounded by bounding boxes. Two of the objects are outside the frustum; one is within it. One of the objects outside the frustum has a bounding box whose edges intersect the frustum, as shown by the shaded area. The visibility test for this object returns TRUE, because its bounding box does intersect the view frustum even though the object itself does not. 007-1680-100 91 4: Database Traversal PFIS_FALSE PFIS_ALL_IN PFIS_TRUE Figure 4-1 92 Culling to the Frustum 007-1680-100 Cull Traversal Visibility Testing The cull traversal begins at the root node of a channel’s scene graph (the pfScene node) and continues downward, directed by the results of the cull test at each node. At each node the cull test determines the relationship of the node’s bounding volume to the viewing frustum. Possible results are that the bounding volume is entirely outside, is entirely within, is partially within, or completely contains the viewing frustum. If the intersection test indicates that the bounding volume is entirely outside the frustum, the traversal prunes that node—that is, it does not consider the children of that node and continues with the node’s next sibling. If the intersection test indicates that the bounding volume is entirely inside the frustum, the node’s children are not cull-tested because the hierarchical nature of bounding volumes implies that the children must also be entirely within the frustum. If the intersection test indicates that the bounding volume is partially within the frustum, or that the bounding volume completely contains the frustum, the testing process continues with the children of that node. Because a bounding volume is larger than the object it surrounds, it is possible for a bounding volume to be partially within a frustum even when none of its enclosed geometry is visible. By default, OpenGL Performer tests bounding volumes all the way down to the pfGeoSet level (see Chapter 8, “Geometry”) to provide fine-grained culling. However, if your application is spending too much time culling, you can stop culling at the pfGeode level by calling pfChanTravMode(). Then if part of a pfGeode is potentially visible, all geometry in that pfGeode is drawn without cull-testing it first. Visibility Culling Example Figure 4-2 portrays a simple database that contains a toy block, train, and car. The block is outside the frustum, the bounding volume of the car is partially inside the frustum, and the train is completely inside the frustum. 007-1680-100 93 4: Database Traversal Figure 4-2 Sample Database Objects and Bounding Volumes Organizing a Database for Efficient Culling Efficient culling depends on having a database whose hierarchy is organized spatially. A good technique is to partition the database into regions, called tiles. Tiling is also required for database paging. Instead of culling the entire database, only the tiles that are within the view frustum need to be traversed. 94 007-1680-100 Cull Traversal The worst case for the cull traversal performance is to have a very flat hierarchy—that is, a pfScene with all the pfGeodes directly under it and many pfGeoSets in each pfGeode— or a hierarchy that is organized by object type (for example, having all trees in the database grouped under one pine tree node, rather than arranged spatially). Figure 4-3 shows a sample database represented by cubes, cones, pyramids, and spheres. Organizing this database spatially, rather than by object type, promotes efficient culling. This type of spatial organization is the most effective control you have over efficient traversal. 007-1680-100 95 4: Database Traversal Board Pyramids Cones Spheres Cubes Board Tile 1 Figure 4-3 96 Tile 2 Tile 3 Tile 4 Tile 5 Tile 6 Tile 7 Tile 8 Tile 9 How to Partition a Database for Maximum Efficiency 007-1680-100 Cull Traversal When modeling a database, you should consider other trade-offs as well. Small amounts of geometry in each culling unit, whether pfGeode or pfGeoSet, provide better culling resolution and result in sending less nonvisible geometry to the pipeline. Small pieces also improve the performance of line-segment intersection inquiries (see “Database Concerns” in Chapter 24). However, using many small pieces of geometry can increase the traversal time and can also reduce drawing performance. The optimal amount of geometry to place in each pfGeoSet depends on the application, database, system CPU, and graphics hardware. Custom Visibility Culling Existence within the frustum is not the only criterion that determines an object’s visibility. The item may be too distant to be seen from the viewpoint, or it may be obscured by other objects between it and the viewer, such as a wall or a hill. Atmospheric conditions can also affect object visibility. An object that is normally visible at a certain distance may not be visible at that same distance in dense fog. Implementing more sophisticated culling requires knowledge of visibility conditions and control over the cull traversal. The cull traversal can be controlled through traversal masks, which are described in the section titled “Controlling and Customizing Traversals” on page 102. Knowing whether an object is visible requires either prior information about the spatial organization of a database, such as cell-to-cell visibilities, or run-time testing such as computing line-of-sight visibility (LOS). You can compute simple LOS visibility by intersecting line segments that start at the eyepoint with the database. See the “Intersection Traversal” on page 120. Sorting the Scene During the cull traversal, a pfChannel can rearrange the order in which pfGeoSets are rendered for improved performance and image quality. It does this by binning and sorting. Binning is the act of placing pfGeoSets into specific bins, which are rendered in a specific order. OpenGL Performer provides two default bins: one for opaque geometry and one for blended, transparent geometry. The opaque bin is drawn before the transparent bin so transparent surfaces are properly blended with the background scene. Applications are free to add new bins and specify arbitrary bin orderings. Bins are often used to group geometry with certain desired characteristics. Sometimes it may be desirable for a pfGeoSet to be in several bins. For this purpose you can create a 007-1680-100 97 4: Database Traversal sub-bin of two existing bins using the function pfChanFindSubBin(bin1,bin2,int). The parameters are the two parent bins and an integer value indicating whether the sub-bin should be created if it does not exist. The function returns a value of –1 if the bin does not exist (and it was not supposed to be created) or if any of the parent bins do not exist. If you need to create a sub-bin of more than two bins, call this function several times. For example, to create a sub-bin of bin 5, 6, and 7, you call pfChanFindSubBin() with parameters 5 and 6. Let us assume that sub-bin of bin 5 and 6 is bin 8. Then you call pfChanFindSubBin() again with parameters 8 and 7 to obtain a sub-bin of bins 5, 6, and 7. It does not matter in what order you call it because all sub-bins are directly linked to their parent root bins (and vice versa); there is no tree hierarchy. See the section “Cull Programs” on page 103 for an example of using sub-bins. The function pfChanFindBinParent(bin,int) returns the first parent of bin bin that is bigger than the value specified as the second parameter. Thus, by calling this method several times (until it returns –1), you can determine all parents of a bin. Sorting is done on a per-bin basis. pfGeoSets within a given bin are sorted by a specific criterion. Two useful criteria provided by OpenGL Performer are sorting by graphics state and sorting by range. When sorting by state, pfGeoSets are sorted first by their pfGeoState, then by an application-specified hierarchy of state modes, values, and attributes which are identified by PFSTATE_* tokens and are described in Chapter 12, “Graphics State”. State sorting can offer a huge performance advantage since it greatly reduces the number of mode changes carried out by the Geometry Pipeline. State sorting is the default sorting configuration for the opaque bin. If a bin has sub-bins, pfGeoSets are ordered in each sub-bin separately, as are pfGeoSets that do not belong to any sub-bin of the bin. Range sorting is required for proper rendering of blended, transparent surfaces, which must be rendered in back-to-front order so that each surface is properly blended with the current background color. Front-to-back sorting is also supported. The default sorting for the transparent bin is back-to-front sorting. Note that the sorting granularity is per-pfGeoSet, not per-triangle so transparency sorting is not perfect. In case of the transparent bin, the order in which pfGeoSets are drawn (back-to-front) is important to avoid visible artifacts. Sub-bins, even if their pfGeoSets were ordered back-to-front, may break that order. For this purpose, you can mark selected bins as non-exclusive. If a pfGeoSet belongs to a sub-bin of a non-exclusive bin, it is added both to the sub-bin and directly to the list of pfGeoSets of the non-exclusive bin. Thus, when pfGeoSets of a non-exclusive bin are sorted, they are all in one list. Any root bin can be marked non-exclusive by setting flag PFBIN_NONEXCLUSIVE_BIN using the function pfChanBinFlags(). The transparent bin is by default non-exclusive. 98 007-1680-100 Cull Traversal The pfChannel bins are given a rendering order and a sorting configuration with pfChanBinOrder() and pfChanBinSort(), respectively. A sub-bin inherits sorting configuration from a parent with the highest sort priority, set by pfChanBinSortPriority(). Sorting configuration of sub-bins cannot be changed using pfChanBinOrder(). A bin’s order is simply an integer identifying its place in the list of bins. An order less than 0 or PFSORT_NO_ORDER means that pfGeoSets that fall into the bin are drawn immediately without any ordering or sorting. Multiple bins may have the same order but the rendering precedence among these bins is undefined. The rendering order of sub-bins is determined by the child-order mask of their parents. This mask can be set by pfChanBinChildOrderMask(). When a sub-bin is created, the mask of all its parents is combined (using a binary OR) and set as a rendering order of the sub-bin. A bin’s sorting configuration is given as a token identifying the major sorting criterion and then an optional list of tokens, terminated with the PFSORT_END token, that defines a state sorting hierarchy. The following tokens control the sort: PFSORT_BY_STATE pfGeoSets are sorted first by pfGeoState then by the state elements found between the PFSORT_STATE_BGN and PFSORT_STATE_END tokens, for example. PFSORT_FRONT_TO_BACK pfGeoSets are sorted by nearest to farthest range from the eyepoint. Range is computed from eyepoint to the center of the pfGeoSet’s bounding volume. PFSORT_BACK_TO_FRONT pfGeoSets are sorted by farthest to nearest range from the eyepoint. Range is computed from eyepoint to the center of the pfGeoSet’s bounding volume. PFSORT_QUICK A special, low-cost sorting technique. pfGeoSets must fall into a bin whose order is 0 in which case they will be sorted by pfGeoState and drawn immediately. This is the default sorting mode for the PFSORT_OPAQUE_BIN bin. For example, the following specification will sort the opaque bin by pfGeoState, then by pfTexture, then by pfMaterial: 007-1680-100 99 4: Database Traversal static int sort[] = {PFSORT_STATE_BGN, PFSTATE_TEXTURE, PFSTATE_FRONTMTL, PFSORT_STATE_END, PFSORT_END}; pfChanBinSort(chan, PFSORT_OPAQUE_BIN, PFSORT_BY_STATE, sort); A pfGeoSet’s draw bin may be set directly by the application with pfGSetDrawBin(). Otherwise, OpenGL Performer automatically determines if the pfGeoSet belongs in the default opaque or transparent bins. Based on the position of a pfGeoSet in the scene, cull programs, if they are enabled, can determine the draw bin of the pfGeoSet. See section “Cull Programs” on page 103 for more details. Paths through the Scene Graph You can define a chain, or path, of nodes in a scene graph using the pfPath data structure. (Note that a pfPath has nothing to do with filesystem paths as specified with the PFPATH environment variable or with specifying a path for a user to travel through a scene.) Once you have specified a pfPath with a call to pfNewPath(), you can traverse and cull that path as a subset of the entire scene graph using pfCullPath(). The function pfCullPath() must only be called from the cull callback function set by pfChanTravFunc()—see “Process Callbacks” on page 116 for details. For more information about the pfPath structure, see the pfPath(3pf) and pfList(3pf) man pages. When OpenGL Performer looks for intersections, it can return a pfPath to the node containing the intersection. This feature is particularly useful when you are using instancing, in which case you cannot use pfGetParent() to find out where in the scene graph the given node is. Finding out the pfPath to a given node is also useful in implementing picking. Draw Traversal For each bin the cull traversal generates a libpr display list of geometry and state commands (see “Display Lists” in Chapter 12), which describes the bin's geometry that is visible from a pfChannel. The draw traversal parses all root bins (bins without a parent bin) in the order given by their rendering order value. For each root bin, it simply traverses the display list and sends commands to the Geometry Pipeline to generate the image. If a bin has sub-bins, objects that are not in any sub-bin of the bin are rendered 100 007-1680-100 Draw Traversal first and are followed by objects of each sub-bin. The order in which sub-bins of the bin are drawn is determined by their rendering order value. Optimizing the Drawing of Sub-bins To avoid drawing sub-bins multiple times (for each of its parents), set the flag PFBIN_DONT_DRAW_BY_DEFAULT for those root bins that share sub-bins with the default opaque or transparent bin. The bin flags can be set using the function pfChanBinFlags(). Individual bins, including sub-bins, may be rendered with the function pfDrawBin() called from the draw callback of pfChannel (see pfChanTravFunc()). The bin –1 is a special pfDrawBin() argument that lets you render the default scene display list, which contains all the objects that did not fall in any defined bin. Note that this default scene display list exists only in PFMP_CULL_DL_DRAW multiprocessing mode. In the case of drawing a sub-bin, all sub-bins that have the same parents as a given sub-bin will be drawn. For example, consider root bins 5, 6, and 7 and sub-bins 8 (child of 5 and 6) and 9 (child of 5, 6, and 7). When pfDrawBin() is called with bin 8, bin 9 will be rendered as well. Traversing a pfDispList is much faster than traversing the database hierarchy because the pfDispList flattens the hierarchy into a simple, efficient structure. In this way, the cull traversal removes much of the processing burden from the draw traversal; throughput greatly increases when both traversals are running in parallel. Bin Draw Callbacks Root bins can have draw callbacks associated with them. Draw callbacks are set by calling function pfChanBinCallBack(). The parameters are the bin number, the type of a callback (PFBIN_CALLBACK_PRE_DRAW or PFBIN_CALLBACK_POST_DRAW), and the callback itself. The callback is a function that has only one parameter, a void pointer that points to the user data. Each bin has one user-data pointer, shared between pre-draw and post-draw callbacks. This pointer can be set using function pfChanBinUserData(). If the callbacks are costly, it makes sense to group sub-bins of a bin with costly callbacks together. To achieve this, ensure that you set a high child-order mask for the bin (see the section “Sorting the Scene” on page 97). 007-1680-100 101 4: Database Traversal Controlling and Customizing Traversals The result of the cull traversal is a display list of geometry to be rendered by the draw traversal. What gets placed in the display list is determined by both visibility and by other user-specified modes and tests. pfChannel Traversal Modes The PFTRAV_CULL argument to pfChanTravMode() modifies the culling traversal. The cull mode is a bitmask that specifies the modes to enable; it is formed by the logical OR of one or more of these tokens: • PFCULL_VIEW • PFCULL_GSET • PFCULL_SORT • PFCULL_IGNORE_LSOURCES • PFCULL_PROGRAM Culling to the view frustum is enabled by the PFCULL_VIEW token. Culling to the pfGeoSet-level is enabled by the PFCULL_GSET token and can produce a tighter cull that improves rendering performance at the expense of culling time. PFCULL_SORT causes the cull to sort geometry by state—for example, by texture or by material, in order to optimize rendering performance. It also causes transparent geometry to be drawn after opaque geometry for proper transparency effects. By default, the enabled culling modes are PFCULL_VIEW | PFCULL_GSET | PFCULL_SORT. It is recommended that these modes be enabled unless the cull traversal becomes a significant bottleneck in the processing pipeline. In this case, try disabling PFCULL_GSET first, then PFCULL_SORT. Normally, a pfChannel’s cull traversal pre-traverses the scene, following all paths from the scene to all pfLightSources in the scene so that light sources can be set up before the normal scene traversal. If you want to disable this pre-traversal, set the PFCULL_IGNORE_LSOURCES cull-enable bit but your pfLightSources will not illuminate the scene. 102 007-1680-100 Controlling and Customizing Traversals When the PFCULL_PROGRAM token is set, a cull program attached to the channel is executed for each pfGeoSet during the cull traversal. See the following section “Cull Programs” for more details. The PFTRAV_DRAW argument to pfChanTravMode() modifies the draw traversal. A mode of PFDRAW_ON is the default and will cause the pfChannel to be rendered. A mode of PFDRAW_OFF indicates that the pfChannel should not be drawn and essentially turns off the pfChannel. Cull Programs This section uses the following topics to describe cull programs: • “The pfCullProgram Class” on page 103 • “Polytopes” on page 104 • “Predefined Cull Program Instructions” on page 105 • “User-Defined Cull Program Instructions” on page 109 • “Cull Traversal” on page 110 • “Occlusion Culling Using Cull Programs” on page 110 • “Small-Feature Culling Using Cull Programs” on page 112 The pfCullProgram Class A pfCullProgram is a class that is used to set up a cull program, a sequence of instructions that are executed for each scene graph node and each pfGeoSet during the cull traversal. There can be two separate cull programs, one for scene graph nodes and one for pfGeosets. The node cull program uses a set of polytopes. Based on the position of the node with respect to each polytope (inside, outside, intersects), it can determine whether the node is culled out (good for occlusion culling) or whether all pfGeoSets under this node are assigned to a specific bin. The pfGeoSet cull program also uses a set of polytopes and assigns each pfGeoSet to a different bin based on the position of the pfGeoSet with respect to each polytope or it culls out the pfGeoSet. The best use of cull programs is for occlusion culling (see section “Occlusion Culling Using Cull Programs” on page 110) or in multipass rendering when in some passes only parts of the scene have to be rendered. Being able to assign these parts to a bin can reduce the rendering time. 007-1680-100 103 4: Database Traversal There is always a default cull program present on a pfChannel. To access it, you can call pfGetChanCullProgram(). Then you can set the program's instructions and the polytopes and can enable the cull program by setting token PFCULL_PROGRAM using the function pfChanTravMode(). See the previous section “pfChannel Traversal Modes” for more details. The following code sequence illustrates the use of a cull program: pfCullProgram *cullPgm = pfGetChanCullProgram(channel); pfCullProgramResetPgm(cullPgm, PFCULLPG_GEOSET_PROGRAM); pfCullProgramAddPgmOpcode(cullPgm, opcode1, data1); ... pfCullProgramAddPgmOpcode(cullPgm, opcodeN, dataN); pfCullProgramResetPgm(cullPgm, PFCULLPG_NODE_PROGRAM); pfCullProgramAddPgmOpcode(cullPgm, opcode1, data1); ... pfCullProgramAddPgmOpcode(cullPgm, opcodeM, dataM); pfCullProgramNumPolytopes(cullPgm, 2); pfCullProgramPolytope(cullPgm, ptope1); pfCullProgramPolytope(cullPgm, ptope2); You can define both a node and a pfGeoSet cull program at once by setting the token in pfCullProgramResetPgm() to PFCULLPG_GEOSET_PROGRAM | PFCULLPG_NODE_PROGRAM. Polytopes Cull program polytopes are standard pfPolytopes. They can be used to define various areas: the area could be some subset of a view frustum in which the geometry is drawn using special attributes, it could be a bounding box around some area of interest, and so on. To initialize cull program polytopes, you set the number of polytopes that are used by the cull programs using pfCullProgramNumPolytopes(). Then create a new pfPolytope in the shared arena and set it using pfCullProgramPolytope(). The polytopes are indexed from 0. Polytopes are shared between the node and the pfGeoset cull program even if the programs are different. 104 007-1680-100 Controlling and Customizing Traversals To modify a polytope of a certain index during the simulation, you get a pointer to the polytope using pfGetCullProgramPolytope(), modify it, and then call pfCullProgramPolytope(). Predefined Cull Program Instructions A cull program is a set of instructions that operate on bins and predefined polytopes. You define instructions one-by-one in the order of their desired execution. First, you use the function pfCullProgramResetPgm() to reset the default program, which consists of a return instruction only. Then you specify each instruction by its opcode (predefined instruction specified with the function pfCullProgramAddPgmOpcode()) or directly by specifying a user-defined instruction using pfCullProgramAddPgmInstruction(). For the details of user-defined instructions, see the later section “User-Defined Cull Program Instructions” on page 109. Each instruction has an associated integer value that is used as a parameter for the instruction. The cull program starts with the bin that is associated with the pfGeoSet. As the cull program executes, it modifies the pfGeoSet. The output is a new bin assignment. The following categories of predefined instructions are available: 007-1680-100 • Test instructions • Assign instructions • Add-bin instructions • Jump instructions • Return instruction 105 4: Database Traversal Test Instructions Table 4-2 describes the test instructions. Table 4-2 Test Instructions Instruction Description PFCULLPG_TEST_POLYTOPE n Tests the bounding box of the pfGeoSet or the bounding sphere of the node against the polytope with index n. The result is one of PFIS_FALSE (all out), PFIS_MAYBE (possible intersection), or PFIS_MAYBE | PFIS_TRUE (all in). PFCULLPG_TEST_IS_SUBBIN_OF b Tests whether the bin that has been determined up to this point is a sub-bin of bin b. The result is 1 or 0. Note that bin b is considered a sub-bin of itself. PFCULLPG_TEST_IS_TRANSPARENT Tests whether the pfGeoSet is transparent. The parameter is ignored. The result is 1 or 0. PFCULLPG_TEST_IS_LIGHT_POINT Tests whether the pfGeoSet belongs to a light point bin. The parameter is ignored. The result is 1 or 0. Assign Instructions Table 4-3 describes the assign instructions. Table 4-3 106 Assign Instructions Instruction Description PFCULLPG_ASSIGN_BIN_MAYBE b Assigns bin b to the pfGeoSet if the result of the last polytope test was PFIS_MAYBE. PFCULLPG_ASSIGN_BIN_TRUE b Assigns bin b to the pfGeoSet if the result of the last binary test was 1. PFCULLPG_ASSIGN_BIN_ALL_IN b Assigns bin b to the pfGeoSet if the result of the last polytope test was PFIS_MAYBE | PFIS_TRUE. PFCULLPG_ASSIGN_BIN_ALL_OUT b Assigns bin b to the pfGeoSet if the result of the last polytope test was PFIS_FALSE. 007-1680-100 Controlling and Customizing Traversals Table 4-3 Assign Instructions (continued) Instruction Description PFCULLPG_ASSIGN_BIN_FALSE b Assigns bin b to the pfGeoSet if the result of the last binary test was 0. PFCULLPG_ASSIGN_BIN b Assigns bin b to the pfGeoSet. Add-Bin Instructions For each PFCULLPG_ASSIGN* instruction, there is an equivalent PFCULLPG_ADD_BIN* instruction, in which the pfGeoSet is assigned a sub-bin of bin b and the existing bin. If the existing bin is –1, the instruction operates as an assign instruction. If the sub-bin does not exist, it is dynamically created. The following are the add-bin instructions: • PFCULLPG_ADD_BIN_MAYBE b • PFCULLPG_ADD_BIN_TRUE b • PFCULLPG_ADD_BIN_ALL_IN b • PFCULLPG_ADD_BIN_ALL_OUT b • PFCULLPG_ADD_BIN_FALSE b • PFCULLPG_ADD_BIN b Jump Instructions Table 4-4 describes the jump instructions. Table 4-4 007-1680-100 Jump Instructions Instruction Description PFCULLPG_JUMP_MAYBE c Skips next c instructions if the result of the last polytope test was PFIS_MAYBE. If c is negative, go back |c|–1 instructions. PFCULLPG_JUMP_TRUE c Skips next c instructions if the result of the last binary test was 1. PFCULLPG_JUMP_ALL_IN c Skips next c instructions if the result of the last polytope test was PFIS_MAYBE | PFIS_TRUE. 107 4: Database Traversal Table 4-4 Jump Instructions (continued) Instruction Description PFCULLPG_JUMP_ALL_OUT c Skips next c instructions if the result of the last polytope test was PFIS_FALSE. PFCULLPG_JUMP_FALSE c Skips next c instructions if the result of the last polytope test was 0. PFCULLPG_JUMP c Skips next c instructions. Return Instruction Each cull program must be terminated by a return instruction. You specify the return instruction with PFCULLPG_RETURN flags. The PFCULLPG_RETURN parameter is a combination of the following binary flags: • PFCULLPG_CULL • PFCULLPG_CULL_ON_ALL_IN • PFCULLPG_CULL_ON_ALL_OUT • PFCULLPG_DONT_TEST_TRANSPARENCY • PFCULLPG_TEST_LPOINTS • PFCULLPG_DONT_TEST_LPOINTS The first three flags determine whether the node or the pfGeoSet is culled out, optionally based on the result of the last polytope test. In that case, any bin assignment made by the cull program is ignored. The last three flags control whether an additional test for the pfGeoSet being transparent or being a light point is performed. These flags affect only the pfGeoset cull program. If the pfGeoSet is transparent or is a light point, the pfGeoSet is assigned the bin resulting from the cull program and either a sub-bin of the transparent bin or the light point bin. If initially the pfGeoSet has no bin assigned to it, both the transparency and light point tests are performed (to follow the operation of a regular cull traversal). If those tests are not needed, you can use the two DONT_TEST flags. If the pfGeoSet has initially assigned a bin, the tests are not performed unless the binary flags specify so. 108 007-1680-100 Controlling and Customizing Traversals If you need to perform any of these two tests earlier—for example, to differentiate bin assignment based on transparency—you can use the instructions PFCULLPG_TEST_IS_TRANSPARENT and PFCULLPG_TEST_IS_LIGHT_POINT. User-Defined Cull Program Instructions You may provide your own cull program instructions. Each instruction must be a function that takes two parameters: a pointer to pfCullProgram and an integer value (the instruction parameter). The instruction has to return a value by which the instruction counter is increased. This value is 1 for all instructions, except jump instructions. Actually, it is possible to write whole cull programs as a single, user-defined instruction. There are two variables that a user-defined instruction can access during the execution of a cull program and there are several useful methods they may use. The following are the two variables: currentResult Result of the last polytope test or a binary test bbox Bounding box for the pfGeoSet Table 4-5 describes the four functions that can be used in instructions. Table 4-5 007-1680-100 Functions Available for User-Defined Cull Program Instructions Function Description pfCullProgramTestPolytope(pgm,n) Tests polytope n using bbox, the bounding box of the pfGeoSet or the bounding sphere of the node. Use this function rather than doing the test directly because the result is often already known by testing the nodes above the current pfGeoSet or the current node and the test can be avoided. pfCullProgramAddBinParent(pgm,b) Adds a new parent b, which could also be a sub-bin. The cull program keeps the list of parents that identify the current bin (to avoid creating many sub-bins that may not be needed). This function adds a new parent b, which can be a sub-bin also. pfCullProgramIsSubbinOf(pgm,b) Tests whether the current bin is a sub-bin of bin b. pfCullProgramResetBinParents(pgm) Resets the list of parents of the current bin. 109 4: Database Traversal For example, the predefined instruction PFCULLPG_ASSIGN_BIN_MAYBE can be implemented as shown in the following code: int MyAssignBinMaybe(pfCullProgram *pgm, int data) { if(pgm->currentResult & PFIS_MAYBE) { pfCullProgramResetBinParents(pgm); pfCullProgramAddBinParent(pgm, data); } return 1; } Cull Traversal To reduce the amount of testing performed for each pfGeoSet, each node of the tree is tested against all cull program polytopes when cull programs are enabled. If the test is conclusive—that is, the bounding sphere of the node is inside or outside of a polytope— children of the node are not tested against the given polytope. Use the node cull program to determine culling and use the pfGeoset cull program to assign bins at the pfGeoset level. If culling to the view frustum is enabled (token PFCULL_VIEW set by pfChanTravMode()), it is done before the cull program is executed. In this case, nodes and pfGeoSets that are not intersecting the view frustum are culled out and the cull program is not executed for them. Sample code illustrating the use of cull programs can be found in the following files in the directory /usr/share/Performer/src/pguide/libpf/C++ on IRIX and Linux and in %PFROOT%\Src\pguide\libpf\C++ on Microsoft Windows: • cullPgmSimple • cullPgmMultipass Occlusion Culling Using Cull Programs In order to use cull programs for occlusion culling you must choose the occluders in the scene—for example, walls in a room or the walls of the nearest buldings in a city. Then you must create a polytope around each occluder. If the occluder is a rectangle, the polytope must have one face for the rectangle and four faces for the edges, four planes each defined by the edge and the eye. You must update the polytope or polytopes every time the eye or the occluder moves. 110 007-1680-100 Controlling and Customizing Traversals For an example of occlusion culling, see the following program: /usr/share/Performer/src/pguide/libpf/C++/occlusionCull.C (IRIX and Linux) %PFROOT%\Src\pguide\libpf\C++\occlusionCull.cxx (Microsoft Windows) The sample program also includes a function that creates a polytope for a polygon defined by four vertices. At present the polytopes must be convex. Consequently, in the case that two occluders are touching but their common shape is concave, they must be defined as two polytopes. In this case, the geometry that is occluded by both occluders and that spans their common boundary is not culled out. To avoid this problem, you can define a convex polytope that contains both shapes (a convex hull) and then define convex cut areas that are not part of the occluders. In this way, you can also add holes in occluders. As long as you start with a convex polytope, you can subtract as many convex polytopes as you need. The following code sequence illustrates the use of a convex hull and cut-out areas: PFCULLPG_TEST_POLYTOPE, 0 // convex hull PFCULLPG_JUMP_ALL_IN, 1 PFCULLPG_RETURN, 0 // no cull PFCULLPG_TEST_POLYTOPE, 1 // first cutout area PFCULLPG_JUMP_ALL_OUT, 1 PFCULLPG_RETURN, 0 // no cull ... PFCULLPG_TEST_POLYTOPE, N // n-th cutout area PFCULLPG_JUMP_ALL_OUT, 1 PFCULLPG_RETURN, 0 // no cull PFCULLPG_RETURN, PFCULLPG_CULL For a complete example, see the following program: /usr/share/Performer/src/pguide/libpf/C++/occlusionCullConcave.C (IRIX and Linux) %PFROOT%\Src\pguide\libpf\C++\occlusionCullConcave.cxx (Microsoft Windows) 007-1680-100 111 4: Database Traversal Small-Feature Culling Using Cull Programs In small-feature culling, geometry that is smaller (in screen space) than a given number of pixels is culled. Since cull programs have access to bounding spheres of nodes and bounding boxes of geosets, the cull programs can be used for small-feature culling. A cull program that performs small-feature culling looks like the following: PFCULLPG_TEST_SMALLER_THAN, 1.5 // smaller than 1.5 pixels PFCULLPG_RETURN, PFCULLPG_CULL_ON_TRUE You must specify this program both for nodes and geosets. You can find an example of small-feature culling in the following file: /usr/share/Performer/src/pguide/libpf/C++/cullSmallFeature.C (IRIX and Linux) %PFROOT%\Src\pguide\libpf\C++\cullSmallFeature.C (Microsoft Windows) This feature can be also accessed in Perfly from the FOV pane. pfNode Draw Mask Each node in the database hierarchy can be assigned a mask that dictates whether the node is added to the display list and thereby whether it is drawn. This mask is called the draw mask (even though it is evaluated in the cull traversal) because it tells the cull process whether the node is drawable or not. The draw mask of a node is set with pfNodeTravMask(). The channel also has a draw mask, which you set with pfChanTravMask(). By default, the masks are all 1’s or 0xffffffff. Before testing a node for visibility, the cull traversal ANDs the two masks together. If the result is zero, the cull prunes the node. If the result is nonzero, the cull proceeds normally. Mask testing occurs before all visibility testing and function callbacks. Masks allow you to draw different subgraphs of the scene on different channels, to turn portions of the scene graph on and off, or to ignore hidden portions of the scene graph while drawing but make them active during intersection testing. 112 007-1680-100 Controlling and Customizing Traversals pfNode Cull and Draw Callbacks One of the primary mechanisms for extending OpenGL Performer is through the use of function callbacks, which can be specified on a per-node basis. OpenGL Performer allows separate cull and draw callbacks, which are invoked both before and after node processing. Node callbacks are set with pfNodeTravFuncs(). Cull callbacks can direct the cull traversal, while draw callbacks are added to the display list and later executed in the draw traversal for custom rendering. There are pre-cull and pre-draw callbacks, invoked before a node is processed, and post-cull and post-draw callbacks, invoked after the node is processed. The cull callbacks return a value indicating how the cull traversal should proceed, as shown in Table 4-6. Table 4-6 Cull Callback Return Values Value Action PFTRAV_CONT Continue and traverse the children of this node. PFTRAV_PRUNE Skip the subgraph rooted at this node and continue. PFTRAV_TERM Terminate the entire traversal. Callbacks are processed by the cull traversal in the following order: 1. If a pre-cull callback is defined, then call the pre-cull callback to get a cull result and find out whether traversal should continue. Possible return values are listed in Table 4-6. 2. If the pre-cull callback returns PFTRAV_PRUNE, the traversal returns to the parent and continues with the node’s siblings, if any. If the callback returns PFTRAV_TERM, the traversal terminates immediately. Otherwise, cull processing continues. 3. If the pre-cull callback does not set the cull result using pfCullResult(), and view-frustum culling is enabled, then perform the standard node-within-frustum test and set the cull result accordingly. 4. If the cull result is PFIS_FALSE, skip the traversal of children. The post-cull callback is invoked and traversal returns so that the parent node can traverse any siblings. 007-1680-100 113 4: Database Traversal 5. If a pre-draw callback is defined, then place a libpr display-list packet in the display list so that the node’s pre-draw callback will be called by the draw process. If running a combined CULLDRAW traversal, invoke the pre-draw callback directly instead. 6. Process the node, continuing the cull traversal with each of the node’s children or adding the node’s geometry to a display list (for pfGeodes). If the cull result was PFIS_ALL_IN, view-frustum culling is disabled during the traversal of the children. 7. If a post-draw callback is defined, then place a libpr display-list packet in the display list so that the node’s post-draw callback will be called by the draw process. If running a combined CULLDRAW traversal, invoke the post-draw callback directly instead. 8. If a post-cull callback is defined, then call the post-cull callback. Draw callbacks are commonly used to place tags or change state while a subgraph is rendered. Note that if the pre-draw callback is called, the post-draw callback is guaranteed to be invoked. This way the callback can restore any state modified by the pre-draw callback. This is useful for state changes such as pfPushMatrix() and pfPopMatrix(), as shown in the environment-mapping code that is part of Example 4-2. For doing customized culling, the pre-cull callback can determine whether a PFIS_ALL_IN has already turned off view-frustum culling using pfGetParentCullResult(), in which case it may not wish to do its own cull testing. It can also find out the result of the standard cull test by calling pfGetCullResult(). Cull callbacks can also be used to render geometry (pfGeoSets) or change graphics state. Any libpr drawing commands are captured in a display list and are later executed during the draw traversal (see “Display Lists” in Chapter 12). However, direct graphics library calls can be made safely only in draw function callbacks, because only the draw process of multiprocess OpenGL Performer configurations is known to be associated with a window. Example 4-2 shows some sample node callbacks. Example 4-2 pfNode Draw Callbacks void LoadScene(char *filename) { pfScene *scene = pfNewScene(); pfGroup *root = pfNewGroup(); pfGroup *reflectiveGeodes = NULL; 114 007-1680-100 Controlling and Customizing Traversals root = pfdLoadFile(filename); ... reflectiveGeodes = ReturnListofGeodesWithReflectiveMaterials(root); /* Use a node callback in the Draw process to turn on * and off graphics library environment mapping before * and after drawing all of the pfGeodes that have * pfGeoStates with reflective materials. */ pfNodeTravFuncs(reflectiveGeodes, PFTRAV_DRAW, pfdPreDrawReflMap, pfdPostDrawReflMap); } /* This callback turns on graphics library environment * mapping. Because it changes graphics state it must be a * Draw process node callback. */ /*ARGSUSED*/ int pfdPreDrawReflMap(pfTraverser *trav, void *data) { glTexGenf(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); glTexGenf(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); return NULL; } /* This callback turns off graphics library environment * mapping. Because it also changes graphics state it also * must be a Draw process node callback. Also notice that * it is important to return the graphics library’s state to * the state at which it was in before the preNode callback * was even made. */ /*ARGSUSED*/ int pfdPostDrawReflMap(pfTraverser *trav, void *data) { glDisable(GL_TEXTURE_GEN_S); glDisable(GL_TEXTURE_GEN_T); return NULL; } 007-1680-100 115 4: Database Traversal Process Callbacks The libpf library processes a visual database with a software-rendering pipeline composed of application, cull, and draw stages. The system of process callbacks allows you to insert your own custom culling and drawing functions into the rendering pipeline. Furthermore, these callbacks are invoked by the proper process when your OpenGL Performer application is configured for multiprocessing. By default, OpenGL Performer culls and draws all active pfChannels when pfFrame() is called. However, you can specify cull and draw function callbacks so that pfFrame() will cause OpenGL Performer to call your custom functions instead. These functions have the option of using the default OpenGL Performer processing in addition to their own custom processing. When multiprocessing is used, the rendering pipeline works on multiple frames at once. For example, when the draw process is rendering frame n, the cull process is working on frame n+1, and the application process is working on frame n+2. This situation requires careful management of data so that data generated by the application is propagated to the cull process and then to the draw process at the right time. OpenGL Performer manages data that is passed to the process callbacks to ensure that the data is frame-coherent and is not corrupted. Example 4-3 illustrates the use of a cull-process callback. Example 4-3 Cull-Process Callbacks InitChannels() { ... /* create and configure all channels*/ ... /* define callbacks for cull and draw processes */ pfChanTravFunc(chan, PFTRAV_CULL, CullFunc); pfChanTravFunc(chan, PFTRAV_DRAW, DrawFunc); ... } /* The Cull callback. Any work that needs to be done in the * Cull process should happen in this function. */ void CullFunc(pfChannel * chan, void *data) { 116 007-1680-100 Process Callbacks static long first = 1; /* Lock down whatever processor the cull is using when * the cull callback is first called. */ if (first) { if ((pfGetMultiprocess() & PFMP_FORK_CULL) && (ViewState->procLock & PFMP_FORK_CULL)) pfuLockDownCull(pfGetChanPipe(chan)); first = 0; } /* User-defined pre-cull processing. Application* specific cull knowledge might be used to provide * things like line-of-sight culling. */ PreCull(chan, data); /* standard Performer culling to the viewing frustum */ pfCull(); /* User-defined post-cull processing; this routine might * be used to do things like record cull state from this * cull to be used in future culls. */ PostCull(chan, data); } /* The draw function callback. * Any graphics library functionality outside * OpenGL Performer must be done here. */ void DrawFunc(pfChannel *chan, void *data) { /* pre-Draw tasks like clearing the viewport */ PreDraw(chan, data); pfDraw(); /* render the frame */ /* draw HUD and so on */ PostDraw(chan, data); } 007-1680-100 117 4: Database Traversal Process Callbacks and Passthrough Data Cull and draw callbacks are specified on a per-pfChannel basis using the functions pfChanTravFunc() with PFTRAV_CULL and PFTRAV_DRAW, respectively. pfAllocChanData() allocates passthrough data, data which is passed down the rendering pipeline to the callbacks. In the cull phase of the rendering pipeline, OpenGL Performer invokes the cull callback with a pointer to the pfChannel that is being culled and a pointer to the pfChannel’s passthrough data buffer. The cull callback may modify data in the buffer. The potentially modified buffer is then copied and passed to the user’s draw callback. Default OpenGL Performer processing is triggered by pfCull() and pfDraw(). By default, pfFrame() calls pfCull() first, then calls pfDraw(). If process callbacks are defined, however, pfCull() and pfDraw() are not invoked automatically and must be called by the callbacks to use OpenGL Performer’s default processing. pfCull() should be called only in the cull callback; it causes OpenGL Performer to cull the current channel and to generate a display list suitable for rendering. Channels culled by pfCull() may be drawn in the draw callback by pfDraw(). It is valid for the draw callback to call pfDraw() more than once. Multipass renderings performed with multiple calls to pfDraw() are typical when you use accumulation buffer techniques. When the draw callback is invoked, the window will have already been properly configured for drawing the pfChannel. Specifically, the viewport, perspective, and viewing matrices are set to their correct values. User modifications of these values are not reset by pfDraw(). If a draw callback is specified, OpenGL Performer does not automatically clear the viewport; it leaves that responsibility to the application. pfClearChan() can be called from the draw callback to clear the channel viewport. If chan has a pfEarthSky(), then the pfEarthSky() is drawn. Otherwise, the viewport is cleared to black and the z-buffer is cleared to its maximum value. You should call pfPassChanData() to indicate that user data should be passed through the rendering pipeline, which propagates the data downstream to cull and draw callbacks. The next call to pfFrame() copies the channel buffer into internal buffers, so that the application is then free to modify data in the buffer without fear of corruption. The pfPassChanData() function should be called only when necessary, since calling it imposes some buffer-copying overhead. In addition, passthrough data should be as small as possible to reduce the time spent copying data. 118 007-1680-100 Process Callbacks The code fragment in Example 4-4 is an example of cull and draw callbacks and the passthrough data that is used to communicate with them. Example 4-4 Using Passthrough Data to Communicate with Callback Routines typedef struct { long val; } PassData; void cullFunc(pfChannel *chan, void *data); void drawFunc(pfChannel *chan, void *data); int main() { PassData *pd; /* allocate passthrough data */ pd = (PassData*)pfAllocChanData(chan,sizeof(PassData)); /* initialize channel callbacks */ pfChanTravFunc(chan, PFTRAV_CULL, cullFunc); pfChanTravFunc(chan, PFTRAV_DRAW, drawFunc); /* main simulation loop */ while (1) { pfSync(); pd->val = 0; pfPassChanData(chan); pfFrame(); } } void cullFunc(pfChannel *chan, void *data) { PassData *pd = (PassData*)data; pd->val++; pfCull(); } void drawFunc(pfChannel *chan, void *data) 007-1680-100 119 4: Database Traversal { PassData *pd = (PassData*)data; fprintf(stderr, "%ld\n", pd->val); pfClearChan(chan); pfDraw(); } This example would, regardless of the multiprocessing mode, have the values 0, 1, and 1 for pd->val at the points where pfFrame(), pfCull(), and pfDraw() are called. In this way, control data can be sent down the pipeline from the application, through the cull, and on to the draw process with frame synchronization without regard to the active multiprocessing mode. When configured as a process separate from the draw, the cull callback should not attempt to send graphics commands to an OpenGL Performer window because only the draw process is attached to the window. Callbacks should not modify the OpenGL Performer database, but they can use pfGet() routines to inquire about database information. The draw callback should not call glXSwapBuffers() because OpenGL Performer must control buffer swapping in order to manage the necessary frame and channel synchronization. However, if you need special control over buffer swapping, use pfPipeSwapFunc() to register a function as the given pipe’s buffer-swapping function. Once your function is registered, it will be called instead of glXSwapBuffers(). Intersection Traversal You can make spatial inquiries in OpenGL Performer by testing the intersection of line segments with geometry in the database. For example, a single line segment pointing straight down from the eyepoint can determine your height above terrain, four such segments can simulate the four tires of a car, and segments swept out by points on a moving object can determine collisions with other objects. Testing Line Segment Intersections The testing of each line segment or group of spatially grouped segments requires a traversal of part or all of a scene graph. You make these inquiries using pfNodeIsectSegs(), which intersects the specified group of segments with the subgraph rooted at the specified node. pfChanNodeIsectSegs() functions similarly, but includes a channel so that the traversal can make decisions based on the level-of-detail specified by pfLOD nodes. 120 007-1680-100 Intersection Traversal Intersection Requests: pfSegSets A pfSegSet is a structure that embodies an intersection request. typedef struct _pfSegSet { long mode; void* userData; pfSeg segs[PFIS_MAX_SEGS]; ulong activeMask; ulong isectMask; void* bound; long (*discFunc)(pfHit*); } pfSegSet; The segs field is an array of line segments making up the query. You tell pfNodeIsectSegs() which segments to test with by setting the corresponding bit in the activeMask field. If your pfSegSet contains many closely-grouped line segments, you can specify a bounding volume using the data structure’s bound field. pfNodeIsectSegs() can use that bounding volume to more quickly test the request against bounding volumes in the scene graph. The userData field is a pointer with which you can point to other information about the request that you might access in a callback. The other fields are described in the following sections. The pfSegSet is not modified during the traversal. Intersection Return Data: pfHit Objects Intersection information is returned in pfHit objects. These can be queried using pfQueryHit() and pfMQueryHit(). Table 4-7 lists the items that can be queried from a pfHit object. Table 4-7 007-1680-100 Intersection-Query Token Names Query Token Description PFQHIT_FLAGS Status and validity information PFQHIT_SEGNUM Index of the segment in a pfSegSet request PFQHIT_SEG Line segment as currently clipped PFQHIT_POINT Intersection point in object coordinates PFQHIT_NORM Geometric normal of an intersected triangle 121 4: Database Traversal Table 4-7 Intersection-Query Token Names (continued) Query Token Description PFQHIT_VERTS Vertices of an intersected triangle PFQHIT_TRI Index of an intersected triangle PFQHIT_PRIM Index of an intersected primitive in pfGeoSet PFQHIT_GSET pfGeoSet of an intersection PFQHIT_NODE pfGeode of an intersection PFQHIT_NAME Name of pfGeode PFQHIT_XFORM Current transformation matrix PFQHIT_PATH Path in scene graph of intersection The PFQHIT_FLAGS field is bit vector with bits that indicate whether an intersection occurred and whether the point, normal, primitive and transformation information is valid. For some types of intersections only some of the information has meaning; for instance, for a pfSegSet bounding volume intersecting a pfNode bounding sphere, the point information may not be valid. Queries can be performed singly by calling pfQueryHit() with a single query token, or several at a time by using pfMQueryHit() with an array of tokens. In the latter case, the return information is placed in the specified order into a return array. Intersection Masks Before using pfNodeIsectSegs() to intersect the geometry in the scene graph, you must set intersection masks for the nodes in the scene graph and correspondingly in your search request. Setting the Intersection Mask The pfNodeTravMask() function sets the intersection masks in a subgraph of the scene down through GeoSets. For example: pfNodeTravMask(root, PFTRAV_ISECT, 0x01, PFTRAV_SELF | PFTRAV_DESCEND, PF_SET) 122 007-1680-100 Intersection Traversal This function sets the intersection mask of all nodes and GeoSets in the scene graph to 0x01. A subsequent intersection request would then use 0x01 as the mask in pfNodeIsectSegs(). A description of how to use this mask follows. Specifying Different Classes of Geometry Databases can contain different classes of objects, and only some of those may be relevant for a particular intersection request. For example, the wheels on a truck follow the ground, even through a small pond; therefore, you only want to test for intersection with the ground and not with the water. For a boat, on the other hand, intersections with both water and the lake bottom have significance. To accommodate distinctions between classes of objects, each node and GeoSet in a scene graph has an intersection mask. This mask allows traversals, such as intersections, to either consider or ignore geometry by class. For example, you could use four classes of geometry to control tests for collision detection of a moving ship, collision detection for a falling bowling ball, and line-of-sight visibility. Table 4-8 matches database classes with the pfNodeTravMask() and pfGSetIsectMask() values used to support the traversal tests listed above. Table 4-8 Database Classes and Corresponding Node Masks Database Class Node Mask Water 0x01 Ground 0x02 Pier 0x04 Clouds 0x08 Once the mask values at nodes in the database have been set, intersection traversals can be directed by them. For example, the line segments for ship collision detection should be sensitive to the water, ground, and pier, while those for a bowling ball would ignore intersections with water and the clouds, testing only against the ground and pier. Line-of-sight ranging should be sensitive to all the geometry in the scene. Table 4-9 lists 007-1680-100 123 4: Database Traversal the traversal mask values and mask representations that would achieve the proper intersection tests. Table 4-9 Representing Traversal Mask Values Intersection Class Mask Value Mask Representation Ship 0x07 (Water | Ground | Pier) Bowling ball 0x06 (Ground | Pier) Line-of-sight ranging 0x0f (Water | Ground | Pier | Clouds) The intersection traversal prunes a node as soon as it gets a zero result from doing a bitwise AND of the node intersection mask and the traversal mask specified by the pfSegSet’s isectMask field. Thus, all nodes in the scene graph should normally be set to be the bitwise OR of the masks of their children. After setting the class-specific masks for different subgraphs of the scene, this can be accomplished by calling this function: pfNodeTravMask(root, PFSET_OR, PFTRAV_SET_FROM_CHILD, 0x0); This function sets each node’s mask by ORing 0x0 with the current mask and the masks of the node’s children. Note that this traversal, like that used to update node bounding volumes, is unusual in that it propagates information up the graph from leaf nodes to root. Discriminator Callbacks If you need to make a more sophisticated discrimination than node masks allow about when an intersection is valid, OpenGL Performer can issue a callback on each successful intersection and let you decide whether the intersection is valid in the current context. If a callback is specified in pfNodeIsectSegs(), then at each level where an intersection occurs—for example, with bounding volumes of libpf pfGeodes (mode PFTRAV_IS_GEODE), libpr GeoSets (mode PFTRAV_IS_GSET), or individual geometric primitives (mode PFTRAV_IS_PRIM)—OpenGL Performer invokes the callback, giving it information about the candidate intersection. The value you return from the callback determines whether the intersection should be ignored and how the intersection traversal should proceed. 124 007-1680-100 Intersection Traversal If the return value includes the bit PFTRAV_IS_IGNORE, the intersection is ignored. The intersection traversal itself can also be influenced by the callback. The traversal is subject to three possible fates, as detailed in Table 4-10. Table 4-10 Possible Traversal Results Set Bits Meaning PFTRAV_CONT Continue the traversal inside this subgraph or GeoSet. PFTRAV_PRUNE Continue the traversal but skip the rest of this subgraph or GeoSet. PFTRAV_TERM Terminate the traversal here. Line Segment Clipping Usually, the intersection point of most interest is the one that is nearest to the beginning of the segment. By default, after each successful intersection, the end of the segment is clipped so that the segment now ends at the intersection point. Upon the final return from the traversal, it contains the closest intersection point. However, if you want to examine all intersections along a segment you can use a discriminator callback to tell OpenGL Performer not to clip segments—simply leave out the PFTRAV_IS_CLIP_END bit in the return value. If you want the farthest intersection point, you can use PFTRAV_IS_CLIP_START so that after each intersection the new segment starts at the intersection point and extends outward. Traversing Special Nodes Level-of-detail nodes are intersected against the model for range zero, which is typically the highest level-of-detail (LOD). If you want to select a different model, you can turn off the intersection mask for the LOD node and place a switch node in parallel (having the same parent and children as the LOD) and set it to the desired model. Sequences and switches intersect using the currently active child or children. Billboards are not intersected, since no eyepoint is defined for intersection traversals. 007-1680-100 125 4: Database Traversal Picking The pfChanPick() function provides a simple interface for intersection testing by enabling the user to move a mouse to select one or more geometries. The method uses pfNodeIsectSegs() and uses the high bit, PFIS_PICK_MASK, of the intersection mask in the scene graph. Setting up picking with pfNodePickSetup() sets this bit in the intersection mask throughout the specified subgraph but does not enable caching inside pfGeoSets. See “Performance” on page 126. The pfChanPick() function has an extra feature: it can either return the closest intersection (PFPK_M_NEAREST) or return all pfHits along the picking ray (PFPK_M_ALL). Performance The intersection traversal uses the hierarchical bounding volumes in the scene graph to allow culling of the database and then processes candidate GeoSets by testing against their internal geometry. For this reason, the hierarchy should reflect the spatial organization of the database. High-performance culling has similar requirements (see Chapter 24, “Performance Tuning and Debugging”). Performance Trade-offs OpenGL Performer currently retains no information about spatial organization of data within GeoSets; so, each triangle in the GeoSet must be tested. Although large GeoSets are good for rendering performance in the absence of culling, spatially localized GeoSets are best for culling (since a GeoSet is the smallest culling unit), and spatially localized GeoSets with few primitives are best for intersections. Front Face/Back Face One way to speed up intersection testing is to turn on PFTRAV_IS_CULL_BACK. When this flag is enabled, only front-facing geometry is tested. Enabling Caching Precomputing information about normals and projections speeds up intersections inside GeoSets. For the best performance, you should enable caching in GeoSets when you set the intersection masks with pfNodeTravMask(). 126 007-1680-100 Intersection Traversal If the geometry within a GeoSet is dynamic, such as waves on a lake, caching can cause incorrect results. However, for geometry that changes only rarely, you can use pfGSetIsectMask() to recompute the cache as needed. Intersection Methods for Segments Normally, when intersecting down to the primitive level each line segment is separately tested against each bounding volume in the scene graph, and after passing those tests is intersected against the pfGeoSet bounding box. Segments that intersect the bounding box are eventually tested against actual geometry. When a pfSegSet has a spatially localized group of at least several line segments, you can speed up the traversal by providing a bounding volume. You can use pfCylAroundSegs() to create a bounding cylinder for the segments, place a pointer to the resulting cylinder in the pfSegSet’s bound field, then OR the PFTRAV_IS_BCYL bit into the pfSegSet’s mode field. If only a rough volume-volume intersection is required, you can specify a bounding cylinder in the pfSegSet without any line segments at all and request discriminator callbacks at the PFTRAV_IS_NODE or PFTRAV_IS_GSET level. Figure 4-4 illustrates some aspects of this process. The portion of the figure labeled A represents a single segment; B is a collection of nonparallel segments, not suitable for tightly bounding with a cylinder; and C shows parallel segments surrounded by a bounding cylinder. In the bottom portion of the figure, the bounding cylinder around the segments intersects the bounding box around the object; each segment in the cylinder, thus, must be tested individually to see if any of them intersect. 007-1680-100 127 4: Database Traversal Figure 4-4 128 Intersection Methods 007-1680-100 Chapter 5 5. Frame and Load Control This chapter describes how to manage the display operations of a visual simulation application to maintain the desired frame rate and visual performance level. In addition this chapter covers advanced topics including multiprocessing and shared memory management. Frame Rate Management A frame is the period of time in which all processing must be completed before updating the display with a new image, for example, a frame rate of 60 Hz means the display is updated 60 times per second and the time extent of a frame is 16.7 milliseconds. The ability to fit all processing within a frame depends on several variables, some of which are the following: • The number of pixels being filled • The number of transformations and modal changes being made • The amount of processing required to create a display list for a single frame • The quantity of information being sent to the graphics subsystem Through intelligent management of SGI CPU and graphics hardware, OpenGL Performer minimizes the above variables in order to achieve the desired frame rate. However, in some cases, peak frame rate is less important than a fixed frame rate. Fixed frame rate means that the display is updated at a consistent, unvarying rate. While a simple step toward achieving a fixed frame rate is to reduce the maximum frame rate to an easily achievable level, we shall explore other (less Draconian) mechanisms in this chapter that do not adversely impact frame rates. As discussed in the following sections, OpenGL Performer lets you select the frame rate and has built-in functionality to maintain that frame rate and control overload situations when the draw time exceeds or grows uncomfortably close to a frame time. While these methods can be effective, they do require some cooperation from the run-time database. 007-1680-100 129 5: Frame and Load Control In particular, databases should be modeled with levels-of-detail and be spatially arranged. Selecting the Frame Rate OpenGL Performer is designed to run at the fixed frame rate as specified by pfFrameRate(). Selecting a fixed frame rate does not in itself guarantee that each frame can be completed within the desired time. It is possible that some frames might require more computation time than is allotted by the frame rate. By taking too long, these frames cause dropped or skipped frames. A situation in which frames are dropped is called an overload or overrun situation. A system that is close to dropping frames is said to be in stress. Achieving the Frame Rate The first step towards achieving a frame rate is to make sure that the scene can be processed in less than a frame’s time—hopefully much less than a frame’s time. Although minimizing the processing time of a frame is a huge effort, rife with tricks and black magic, certain techniques stand out as OpenGL Performer’s main weapons against slothful performance: • Multiprocessing. The use of multiple processes on multi-CPU systems can drastically increase throughput. • View culling. By trivially rejecting portions of the database outside the viewing volume, performance can be increased by orders of magnitude. • State sorting. Many graphics pipelines are sensitive to graphics mode changes. Sorting a scene by graphics state greatly reduces mode changes, increasing the efficiency of the hardware. • Level-of-detail. Objects that are far away project to a relatively small area of the display so fewer polygons can be used to render the object without substantial loss of image quality. The overall result is fewer polygons to draw and improved performance. Multiprocessing and level-of-detail is discussed in this chapter while view culling and state sorting are discussed in Chapter 4, “Database Traversal.” More information on sorting in the context of performance tuning can be found in Chapter 24, “Performance Tuning and Debugging.” 130 007-1680-100 Frame Rate Management Fixing the Frame Rate Frame intervals are fixed periods of time but frame processing is variable in nature. Because things change in a scene, such as when objects come into the field of view, frame processing cannot be fixed. In order to maintain a fixed frame rate, the average frame processing time must be less than the frame time so that fluctuations do not exceed the selected frame rate. Alternately, the scene complexity can be automatically reduced or increased so that the frame rate stays within a user-defined “sweet spot.” This mechanism requires that the scene be modeled with levels of detail (pfLOD nodes). OpenGL Performer calculates the system load for each frame. Load is calculated as the percentage of the frame period it took to process the frame. Then if the default OpenGL Performer fixed frame rate mechanisms are enabled, load is used to calculate system stress, which is in turn used to adjust the level of detail (LOD) of visible models. LOD management is OpenGL Performer’s primary method of managing system load. Table 5-1 shows the OpenGL Performer functions for controlling frame processing. Table 5-1 Frame Control Functions Function Description pfFrameRate() Set the desired frame rate. pfSync() Synchronize processing to frame boundaries. pfFrame() Initiate frame processing. pfPhase() Control frame boundaries. pfChanStressFilter() Control how stress is applied to LOD ranges. pfChanStress() Manually control the stress value. pfGetChanLoad() Determine the current system load. pfChanLODAttr() Control how LOD is performed, including global LOD adjustment and blending (fade). Figure 5-1 shows a frame-timing diagram that illustrates what occurs when frame computations are not completed within the required interval. The solid vertical lines in Figure 5-1 represent frame-display intervals. The dashed vertical lines represent video refresh intervals. 007-1680-100 131 5: Frame and Load Control Refresh count modulo three 0 1 2 0 1 Overrun 2 0 1 2 0 Floating Locked Video refresh interval Frame display interval Time in seconds 1/60TH 1/20TH Figure 5-1 Frame Rate and Phase Control In this example, the video scan rate is 60 Hz and the frame rate is 20 Hz. With the video hardware running at 60 Hz, each of the 20 Hz frames should be scanned to the video display three times, and the system should wait for every third vertical retrace signal before displaying the next image. The numbers across the top of the figure represent the refresh count modulo three. New images are displayed on refreshes whose count modulo three is zero, as shown by the solid lines. In the first frame of this example, the new image is not yet completed when the third vertical retrace signal occurs; therefore, the same image must be displayed again during the next interval. This situation is known as frame overrun, because the frame computation time extends past a refresh boundary. Frame Synchronization Because of the overrun, the frame and refresh interval timing is no longer synchronized; it is out of phase. A decision must be made either to display the same image for the remaining two intervals, or to switch to the next image even though the refresh is not aligned on a frame boundary. The frame-rate control mode, discussed in the next section, determines which choice is selected. Knowing that the situation illustrated in Figure 5-1 is a possibility, you can specify a frame control mode to indicate what you would like the system to do when a frame overrun occurs. 132 007-1680-100 Frame Rate Management To specify a method of frame-rate control, call pfPhase(). There are the following choices: • Free run without phase control (PFPHASE_FREE_RUN) tells the application to run as fast as possible—to display each new frame as soon as it is ready, without attempting to maintain a constant frame rate. • Free run without phase control but with a limit on the maximum frame rate (PFPHASE_LIMIT) tells the application to run no faster than the rate specified by pfFrameRate(). • Fixed frame rate with floating phase (PFPHASE_FLOAT) allows the drawing process to display a new frame (using glXSwapBuffers() at any time, regardless of frame boundaries). • Fixed frame rate with locked phase (PFPHASE_LOCK) requires the draw process to wait for a frame boundary before displaying a new frame. • The draw by default will wait for a new cull result to execute its stage functions. This behavior can be changed by including the token PFPHASE_SPIN_DRAW with the desired mode token from the above choices. This will allow the draw to run every frame, redrawing the previous cull result. This can allow you to make changes of your own in draw callback functions. Objects such as viewing frustum, pfLODs, pfDCSs, and anything else normally processed by the cull or application processes will not be updated until the next full cull result is available. Free-Running Frame-Rate Control The simplest form of frame-rate control, called free-running, is to have no control at all. This uncontrolled mode draws frames as quickly as the hardware is able to process them. In free-running mode, the frame rate may be 60 Hz in the areas of low database complexity, but could drop to a slower rate in views that place greater demand on the system. Use pfPhase(PFPHASE_FREE_RUN) to specify a free-running frame rate. In applications in which real-time graphics provide the majority of visual cues to an observer, the variable frame rates produced by the free-running mode may be undesirable. The variable lag in image update associated with variable frame rate can lead to motion sickness for the simulation participants, especially in motion platform-based trainers or ingressive head-mounted displays. For these and other reasons it is usually preferable to maintain a steady, consistent frame-update rate. 007-1680-100 133 5: Frame and Load Control Fixed Frame-Rate Control Assume that the overrun frame in Figure 5-1 completes processing during the next refresh period, as shown. After the overrun frame, the simulation is still running at the chosen 20-Hz rate and is updating at every third vertical retrace. If a new image is displayed at the next refresh, its start time lags by 1/60th of a second, and therefore it is out of phase by that much. Subsequent images are displayed when the refresh count modulo three is one. As the simulation continues and additional extended frames occur, the phase continues to drift. This mode of operation is called floating phase, as shown by the frame in Figure 5-1 labeled "Floating." Use pfPhase(PFPHASE_FLOAT) to select floating-phase frame control. The alternative to displaying a new image out of phase is to display the old image for the remainder of the current update period, then change to the new image at the normal time. This locked phase extends each frame overrun to an integral multiple of the selected frame time, making the overrun more evident but also maintaining phase throughout the simulation. This timing is shown by the frame in Figure 5-1 labeled Locked. Although this mode is the most restrictive, it is also the most desirable in many cases. Use pfPhase(PFPHASE_LOCK) to select phase-locked frame control. For example, a 20-Hz phase-locked frame rate is selected by specifying the following: pfPhase(PFPHASE_LOCK); pfFrameRate(20.0f); These specifications prevent the system from switching to a newly computed image until a display period of 1/20th second has passed from the time the previous image was displayed. The frame rate remains fixed even when the Geometry Pipeline finishes its work in less time. Fixed frame-rate display, therefore, involves setting the desired frame rate and selecting one of the two fixed-frame-rate control modes. Frame Skipping When multiple frame times elapse during the rendering of a single frame, the system must choose which frame to draw next. If the per-frame display lists are processed in strict succession even after a frame overrun, the visual image slowly recedes in time and the positional correlation between display and simulation is lost. To avoid this problem, only the most recent frame definition received by the draw process is sent to the 134 007-1680-100 Frame Rate Management Geometry Pipeline, and all intervening frame definitions are abandoned. This is known as dropping or skipping frames and is performed in both of the fixed frame-rate modes. Because the effects of variable frame rates, phase variance, and frame dropping are distracting, you should choose a frame rate with care. Steady frame rates are achieved when the frame time allows the worst-case view to be computed without overload. The structure of the visual database, particularly in terms of uniform “complexity density,” can be important in maximizing the system frame rate. See “Organizing a Database for Efficient Culling” in Chapter 4 and Figure 4-3 for examples of the importance of database structure. Maintaining a fixed frame rate involves managing future system load by adjusting graphics display actions to compensate for varying past and present loads. The theory behind load management and suggested methods for dealing with variable load situations are discussed in the “Level-of-Detail Management” on page 136 of this chapter. Sample Code Example 5-1 demonstrates a common approach to frame control. The code is based on part of the main.c source file used in the perfly sample application. Example 5-1 Frame Control Excerpt /* Set the desired frame rate. */ pfFrameRate(ViewState->frameRate); /* Set the MP synchronization phase. */ pfPhase(ViewState->phase); /* Application main loop */ while (!SimDone()) { /* Sleep until next frame */ pfSync(); /* Should do all latency-critical processing between * pfSync() and pfFrame(). Such processing usually * involves changing the viewing position. */ PreFrame(); /* Trigger cull and draw processing for this frame. */ 007-1680-100 135 5: Frame and Load Control pfFrame(); /* Perform non-latency-critical simulation updates. */ PostFrame(); } Level-of-Detail Management All graphics systems have finite capabilities that affect the number of geometric primitives that can be displayed per frame at a specified frame rate. Because of these limitations, maximizing visual cues while minimizing the polygon count in a database is often an important aspect of database development. Level-of-detail (LOD) processing is one of the most beneficial tools available for managing database complexity for the purpose of improving display performance. The basic premise of LOD processing is that objects that are barely visible, either because they are located a great distance from the eyepoint or because atmospheric conditions reduce visibility, do not need to be rendered in great detail in order to be recognizable. This is in stark contrast to mandating that all polygons be rendered regardless of their contribution to the visual scene. Both atmospheric effects and the visual effect of perspective decrease the importance of details as range from the eyepoint increases. The predominant visual effect of distance is the perspective foreshortening of objects, which makes them appear to shrink in size as they recede into the distance. To save rendering time, objects that are visually less important in a frame can be rendered with less detail. The LOD approach to optimizing the display of complex objects is to construct a number of progressively simpler versions of an object and to select one of them for display as a function of range. This requires you to create multiple models of an object with varying levels of detail. You also must supply a rule to determine how much detail is appropriate for a given distance to the eyepoint. The sections that follow describe how to create multiple LOD models and how to control when the changeover to a different LOD occurs. Level-of-Detail Models Most objects comprise smaller objects that become visually insignificant at ranges where the conglomerate object itself is still quite prominent. For example, a complex model of 136 007-1680-100 Level-of-Detail Management an automobile might have door handles, side- and rear-view mirrors, license plates, and other small details. A short distance away, these features may no longer be visible, even though the car itself is still a visually significant element of the scene. It is important to realize that as a group, these small features may contain as many polygons as the larger car itself, and thus have a detrimental effect on rendering speed. You can construct two LOD models simply by providing one model that contains all of the detailed features and another model that contains only the car body itself and none of the detailed features. A more sophisticated scheme uses multiple LOD models that are grouped under an LOD node. Figure 5-2 shows an LOD node with multiple children numbered 1 through n. In this case, the model named LOD 1 is the most detailed model and models LOD 2 through LOD n represent progressively coarser models. Each of these LOD models might contain children that also have LOD components. Associated with the LOD node is a list of ranges that define the distance at which each model is appropriate to display. There is no limit to the number of levels of detail that can be used. Level of Detail Node LOD 1 Figure 5-2 LOD 2 LOD n Level-of-Detail Node Structure The object can be transformed as needed. During the culling phase of frame processing, the distance from the eyepoint to the object is computed and used (with other factors) to select which LOD model to display. 007-1680-100 137 5: Frame and Load Control The OpenGL Performer pfLOD node contains a value known as the center of LOD processing. The LOD center point is an x, y, z location that defines the point used in conjunction with the eyepoint for LOD range-switching calculations, as described in the section “Level-of-Detail Range Processing” on page 141 of this chapter. Figure 5-3 shows an example in which multiple LOD models grouped under a parent LOD node are used to represent a toy race car. 138 007-1680-100 Level-of-Detail Management Blend zones LOD n LOD 2 LOD 1 Switch ranges Figure 5-3 Level-of-Detail Processing Figure 5-3 demonstrates that each car in a row of identical cars placed at increasing range from the eyepoint is drawn using a different child of the tree’s LOD node. 007-1680-100 139 5: Frame and Load Control The double-ended arrows indicate a switch range for each level of detail. When the car is closer to the eyepoint than the first range, nothing is drawn. When the car is between the first and second ranges, LOD 1 is drawn. When the car is between the second and third ranges, LOD 2 is drawn. This range bracketing continues until the final range is passed, at which point nothing is drawn. The pfLOD node’s switch range list contains one more entry than the number of child nodes to allow for this range bracketing. OpenGL Performer provides the ability to specify a blend zone for each switch between LOD models. These blend zones will be discussed in more detail in “Level-of-Detail Transition Blending” on page 145. Level-of-Detail States In addition to standard LOD nodes, OpenGL Performer also supports LOD state—the pfLODState. A pfLODState is in essence a way of creating classes or priorities among LODs. A pfLODState contains eight parameters used to modify four different ways in which OpenGL Performer calculates LOD switch ranges and LOD transition distances. LOD states contain the following parameters: • Scale for LODs switch ranges • Offset for LODs switch ranges • Scale for the effect of Stress of switch ranges • Offset for the effect of Stress on switch ranges • Scale for the transition distances per LOD switch • Offset for the transition distances per LOD switch • Scale for the effect of stress on transition distances • Offset for the effect of stress on transition distances These LOD states can then be attached to either single or multiple LOD nodes such that the LOD behavior of groups or classes of objects can be different and be easily modified. The man pages for pfLODLODState() and pfLODLODStateIndex() contain detailed information on how to attach pfLODStates. 140 007-1680-100 Level-of-Detail Management LOD states are useful because in a particular scene there often exists an object of focus such as a sign, a target, or some other object of particular visual significance that needs to be treated specially with regard to visual importance and thus LOD behavior. It stands to reason that this particular object (or small group of objects) should be at the highest detail possible despite being farther away than other elements in the scene which might not be as visually significant. In fact, it might be feasible to diminish the detail of less important objects (like rocks and trees) in favor of the other more important objects (despite these objects being more distant). In this case one would create two LOD states. The first would be for the important objects and could disable the effect of stress on these nodes as well as scale the switch ranges such that the object(s) would maintain more detail for further ranges. The second LOD state would be used to make the objects of less importance be more responsive to system stress and possibly scale their switch ranges such that they would show even less detail than normal. In this way, LOD states allow biasing among different LODs to maintain desirable rendering speeds while maintaining the visual integrity of various objects depending on their subjective importance (rather than solely on their current visual significance). In some multichannel applications, LOD states are used to control the action of LODs in different viewing channels that have different visual significance criteria—for instance one channel might be a normal channel while a second might represent an infrared display. Rather than simple use of LOD states, it is also possible to specify a list of LOD states to a channel and use indexes from this list for particular LODs (with pfChanLODStateList() and pfLODLODStateIndex()). In this way, in the normal channel a car’s geometry might be particularly important while in the infrared channel, the hot exhaust of the same car might be much more important to observe. This type of channel-dependent LOD can be set up by using two distinct and different LOD states for the same index in the lists of LOD states specified for unique channels. Note that because OpenGL Performer performs LOD calculations in a range squared space as much as possible for efficiency reasons, LOD computation becomes more costly when LOD states contain scales that are not equal to 1.0 or offsets not equal to 0.0 for transitions or switch ranges—these offsets force OpenGL Performer to perform otherwise avoidable square root calculations in order to correctly calculate the effects of scale and offset on the LOD. Level-of-Detail Range Processing The LOD switch ranges present in LOD nodes are processed before being used to make the level of detail selection. The goal of range setting is to switch LODs as objects reach certain levels of perceptibility. The size of a channel in pixels, the field of view used in 007-1680-100 141 5: Frame and Load Control viewing, and the distance from the observer to the display surface all affect object perceptibility. OpenGL Performer uses a channel size of 1024x1024 pixels and a 45-degree field of view as the basis for calculating LOD switching ranges. The screen space size of a channel and the current field of view are used to compute an LOD scale factor that is updated whenever the channel size or the field of view changes. There is an additional global LOD scale factor that can be used to adjust switch ranges based on the relationship between the observer and the display surface. The default global scale factor is 1. Note that LOD switch ranges are also affected by LOD states that have been attached to either a particular LOD or to a channel that contains the LOD. These LOD states provide the mechanism to apply both a scale and an offset for an LODs switch ranges and to the effect of system stress on those switch ranges. See “Level-of-Detail States” on page 140 for more information on pfLODStates. Ultimately, an LOD’s switch range without regard to system stress can be computed as follows: switch_range[i] = (range[i] * LODStateRangeScale * ChannelLODStateRangeScale + LODStateRangeOffset + ChannelLODStateRangeOffset) * ChannelLODScale * ChannelSizeAndFOVFactor; If OpenGL Performer channel stress processing is active, the computed range is modified as follows: switch_range[i] *= (ChannelLODStress * LODStateRangeStressScale * ChannelLODStateRangeStressScale + LODStateRangeStressOffset + ChannelLODStateRangeStressOffset); Example 5-2 illustrates how to set LOD ranges. 142 007-1680-100 Level-of-Detail Management Example 5-2 Setting LOD Ranges /* setLODRanges() -- sets the ranges for the LOD node. The * ranges from 0 to NumLODs are equally spaced between min * and max. The last range, which determines how far you * can get from the object and still see it, is set to * visMax. */ void setLODRanges(pfLOD *lod, float min, float max, float visMax) { int i; float range, rangeInc; rangeInc = (max - min)/(ViewState->shellLOD + 1); for (range = min, i = 0; i < ViewState->shellLOD; i++) { ViewState->range[i] = range; pfLODRange(lod, i, range); range += rangeInc; } ViewState->range[i] = visMax; pfLODRange(lod, i, visMax); } /* generateShellLODs() -- creates shell LOD nodes according * to the parameters specified in the shared data structure. */ void generateShellLODs(void) { int i; pfGroup *grp; pfVec4 clr; long numLOD = ViewState->shellLOD; long numPnts = ViewState->shellPnts; long numPcs = ViewState->shellPcs; for (i = 1; i <= numLOD; i++) { if (ViewState->shellColor == SHELL_COLOR_SING) pfSetVec4(clr, 0.9f, 0.1f, 0.1f, 1.0f); else /* set the color. highest level = RED; * middle LOD = GREEN; lowest LOD = BLUE 007-1680-100 143 5: Frame and Load Control */ pfSetVec4(clr, (i <= (long)floor((double)(numLOD/2.0f)))? (-2.0f/numLOD) * i + 1.0f + 2.0f/numLOD: 0.0f, (i <= (long)floor((double)(numLOD/2)))? (2.0f/numLOD) * (i - 1): (-2.0f/numLOD) * i + 2.0f, (i <= (long)floor((double)(numLOD/2)))? 0.0f: (2.0f/numLOD) * i - 1.0f, 1.0f); /* build a shell GeoSet */ grp = createShell(numPcs, numPnts, ViewState->shellSweep, &clr, ViewState->shellDraw); normalizeNode((pfNode *)grp); /* add geode as another level of detail node */ pfAddChild(ViewState->LOD, grp); /* simplify the geometry, but don’t have less than * 4 points per circle or less than 3 pieces */ numPnts = (numPnts > 7) ? numPnts-4 : 4; numPcs = (numPcs > 6) ? numPcs-4 : 3; } } ... ViewState->LOD = pfNewLOD(); generateShellLODs(); /* get the LOD’s extents */ pfGetNodeBSphere(ViewState->LOD, &(ViewState->bSphere)); pfLODCenter(ViewState->LOD, ViewState->bSphere.center); /* set ranges for LODs; there should be (num LODs + 1) * range entries */ setLODRanges(ViewState->LOD, ViewState->minRange, ViewState->maxRange, ViewState->max); 144 007-1680-100 Level-of-Detail Management Level-of-Detail Transition Blending An undesirable effect called popping occurs when the sudden transition from one LOD to the next LOD is visually noticeable. This distracting image artifact can be ameliorated with a slight modification to the normal LOD-switching process. In this modified method, a transition per LOD switch is established rather than making a sudden substitution of models at the indicated switch range. These transitions specify distances over which to blend between the previous and next LOD. These zones are considered to be centered at the specified LOD switch distance, as shown by the horizontal shaded bars of Figure 5-3. Note that OpenGL Performer limits the transition distances to be equal to the shortest distance between the switch range and the two neighboring switch ranges. For more information, see the pfLODTransition() man page. As the range from eyepoint to LOD center-point transitions the blend zone, each of the neighboring LOD levels is drawn by using transparency-to-composite samples taken from the present LOD model with samples taken from the next LOD model. For example, at the near, center, and far points of the transition blend zone between LOD 1 and LOD 2, samples from both LOD 1 and LOD 2 are composited until the end of the transition zone is reached, where all the samples are obtained from LOD 2. Table 5-2 lists the transparency factors used for transitioning from one LOD range to another LOD range. Table 5-2 LOD Transition Zones Distance LOD 1 LOD 2 Near edge of blend zone 100% opaque 0% opaque Center of blend zone 50% opaque 50% opaque Far edge of blend zone 0% opaque 100% opaque LOD transitions are made smoother and much less noticeable by applying a blending technique rather than making a sudden transition. Blending allows LOD transitions to look good at ranges closer to the eye than LOD popping allows. Decreasing switch ranges in this way improves the ability of LOD processing to maximize the visual impact of each polygon in the scene without creating distracting visual artifacts. The benefits of smooth LOD transition have an associated cost. The expense lies in the fact that when an object is within a blend zone, two versions of that object are drawn. This 007-1680-100 145 5: Frame and Load Control causes blended LOD transitions to increase the scene polygon complexity during the time of transition. For this reason, the blend zone is best kept to the shortest distance that avoids distracting LOD-popping artifacts. Currently, fade level of detail is supported only on RealityEngine and InfiniteReality graphics systems. Note that the actual ‘blend’ or ‘fade’ distance used by OpenGL Performer can also be adjusted by the LOD priority structures called pfLODStates. pfLODStates hold an offset and scale for the size of transition zones as well as an offset and scale for how system stress can affect the size of the transition zones. See “Level-of-Detail States” on page 140 for more information on pfLODStates. Note also, that there exists a global LOD transition scale on a per channel basis that can affect all transition distances uniformly. Thus for an LOD with 5 switch ranges R0, R1, R2, R3, R4 to switch between four models (M0, M1, M2, M3), there are 5 transition zones T0 (fade in M0), T1 (blend between M0 and M1), T2 (blend between M1 and M2), T3 (blend between M2 and M3), T4 (fade out M3). The actual fade distances (without regard to channel stress) are as follows: fadeDistance[i] = (transition[i] * LODStateTransitionScale * ChannelLODStateTransitionScale + LODStateTransitionOffset + ChannelLODStateTransitionOffset) * ChannelLODFadeScale; If OpenGL Performer management of channel stress is turned on then the above fade distance is modified as follows: fadeDistance[i] /= (ChannelStress * LODStateTransitionStressScale * ChannelLODStateTransitionStressScale + LODStateTransitionStressOffset + ChannelLODStateTransitionStressOffset); Run-Time User Control Over LOD Evaluation A pfLOD node provides one last resort for applications that have complex level-of-detail calculations. For example, an application might wish to limit the speed at which different LODs of an object switch. When switching depends on the range from the camera, a very 146 007-1680-100 Level-of-Detail Management fast-moving camera may result in rapid changes of LODs. The application may require an artificial filter to take the simple range-based evaluation and ease it into the display over time. An application may take over the LOD evaluation function using the API pfLODUserEvalFunc() on pfLOD. The user-supplied function must return a floating point number. Similar to the result of pfEvaluateLOD(), this number picks either a single child or a blend of two children of the pfLOD node. Note that the performance of the cull process may decrease if the user function is too slow to execute. Terrain Level-of-Detail In creating LOD models and transitions for objects, it is often safe to assume that the entire model should transition at the same time. It is quite reasonable to make features of an automobile such as door handles disappear from the scene at the same time even when the passenger door is slightly closer than the driver’s door. It is much less clear that this approach would work for very large objects such as an aircraft carrier or a space station, and it is clearly not acceptable for objects that span a large extent, such as a terrain surface. Active Surface Definiton (ASD) Attempts to handle large-extent objects with discrete LOD tools focus on breaking the big object into myriad small objects and treating each small object independently. This works in some cases but often fails at the junction between two or more independent objects where cracks or seams exist when different detail levels apply to the objects. Some terrain processing systems have attempted to provide a hierarchy of crack-filling geometry that is enabled based on the LOD selections of two neighboring terrain patches. This “digital grout” becomes untenable when more than a few patches share a common vertex. You can always make the transitions between LODs smooth by using active surface definition. ASD treats the entire terrain as a single connected surface rather than multiple patches that are loaded into memory as necessary. The surface is modeled with several hierarchical LOD meshes in data structures that allow for the rapid evaluation of smooth LOD transitions, load management on the evaluation itself, and efficient generation of a meshed terrain surface of the visible triangles for the current frame. For more information, refer to the Chapter 20, “Active Surface Definition.” 007-1680-100 147 5: Frame and Load Control Arbitrary Morphing Terrain level of detail using an interpolative active surface definition is a restricted form of the more general notion of object morphing. Morphing of models such as the car in a previous example can simply involve scaling a small detail to a single point and then removing it from the scene. Morphing is possible even when the topologies of neighboring pairs do not match. Both models and terrain can have vertex, normal, color, and appearance information interpolated between two or more representations. The advantages of this approach include: reduced graphics complexity since blending is not used, constant intersection truth for collision and similar tasks, and monotonic database complexity that makes system load management much simpler. Such evaluation might make use of the compute process and pfFlux objects to hold the vertex data and to modify the scene graph control to chose the proper form of the object. pfSwitch nodes can take a pfFlux for holding its value; see the pfSwitchValFlux() man page. pfLOD nodes can take a flux for controlling range with pfLODRangeFlux(). See the pfLOD and pfEngine man pages for more information on morphing. Maintaining Frame Rate Using Dynamic Video Resolution When frame rate is not maintained, some frames display longer than others. If, for example, when the frame rate is 30 frames per second, a frame takes longer than 1/30th of a second to fill the frame buffer, the frame is not displayed. Consequently, the current frame is displayed for two instead of one 1/30ths of a second. The result of inconsistent frame rates is jerky motion within the scene. Note: You have some control over what happens when a frame rate is missed. You can choose, for example, to begin the next frame in the next 1/60th of a second, or wait for the start of the next 1/30th second. For more information about handling frame drawing overruns, see pfPhase in “Free-Running Frame-Rate Control” on page 133. The key to maintaining frame rate is limiting the amount of information to be rendered. OpenGL Performer can take care of this problem automatically for you on InfiniteReality systems when you use the PFPVC_DVR_AUTO token with pfPVChanDVRMode(). In PFPVC_DVR_AUTO mode, OpenGL Performer checks every rendered frame to see if it took too long to render. If it did, OpenGL Performer reduces the size of the image, and correspondingly, the number of pixels in it. Afterwards, the video hardware enlarges the 148 007-1680-100 Maintaining Frame Rate Using Dynamic Video Resolution images to the same size as the pfChannel; in this way, the image is the correct size, but it contains a reduced number of pixels, as suggested in Figure 5-4. Figure 5-4 Real Size of Viewport Rendered Under Increasing Stress Although the viewport is reduced as stress increases, the viewer never sees the image grow smaller because bipolar filtering is used to enlarge the image to the size of the channel. The Channel in DVR When using Dynamic Video Resolution (DVR), the origin and size of a channel are dynamic. For example, a viewport whose lower-left corner is at the center of a pfPipe (with coordinates 0.5, 0.5) would be changed to an origin of (0.25, 0.25) with respect to the full pfPipe window if the DVR settings were scaled by a factors of 0.5 in both X and Y dimensions. If you are doing additional rendering into a pfChannel, you may need to know the size and the actual rendered area of the pfChannel. Use pfGetChanOutputOrigin() and pfGetChanOutputSize() to get the actual rendered origin and size, respectively, of a pfChannel. pfGetChanOrigin() and pfGetChanSize() give the displayed origin and size of the pfChannel and these functions should be used for mapping mouse positions or other window-relative nonrendering positions to the pfChannel area. Additionally, if DVR alters the rendered size of a pfChannel, a corresponding change should be made to the width of points and lines. For example, when a channel is scaled in size by one half, lines and points must be drawn half as wide as well so that when the final image is enlarged, in this case by a factor of two, the lines and points scale correctly. pfChanPixScale() sets the pixel scale factor. pfGetChanPixScale() returns this value for a channel. pfChannels set this pixel scale automatically. 007-1680-100 149 5: Frame and Load Control DVR Scaling DVR scales linearly in response to the most common cause of draw overload: filling the polygons. For example, if the DRAW stage process overran by 50%, to get back in under the frame time, the new scene must draw 30% fewer pixels. We can do this with DVR by rendering to a smaller viewport and letting the video hardware rescale the image to the correct display size. If pfPVChanMode() is set to PFPVC_DVR_AUTO, OpenGL Performer automatically scales each of the pfChannels. pfChannels automatically scale themselves according to the scale set on the pfPipeVideoChannel they are using. If the pfPVChanMode() is PFPVC_DVR_MANUAL, you control scaling according to your own policy by setting the scale and size of the pfPipeVideoChannel in the application process between pfSync() and pfFrame(), as shown in this example: Total pixels drawn last frame = ChanOutX * ChanOutY * Depth Complexity To make the total pixels drawn 30% less, do the following: NewChanOutX = NewChanOutY = .7 * (Chan OutX * ChanOut.) New ChanOut X = sqrt (.7) * ChanOutX New ChanOut X = sqrt (.7) * ChanOut X NewChanOut = sqrt (.7) * ChanOut Customizing DVR Your application has full control over DVR behavior. You can either configure the automatic mode or implement your own response control. Automatic resizing can cause problems when an image has so much information in it the viewport is reduced too drastically, perhaps to only a few hundred pixels, so that when the image is enlarged, the image resolution is unacceptably blurry. To remedy this problem, pfPipeVideoChannel includes the following methods to limit the reduction of a video channel: pfPVChanMaxDecScale() Sets the maximum X and Y decrement scaling that can happen in a single step of automatic dynamic video resizing. A scale value of (-1), the default, removes the upper bound on decremental scales. 150 007-1680-100 Maintaining Frame Rate Using Dynamic Video Resolution pfPVChanMaxIncScale() Sets the maximum X and Y increment scaling that can happen in a single step of automatic dynamic video resizing. A scale value of (-1), the default, removes the upper bound on incremental scales. pfPVChanMinDecScale() Sets the minimum X and Y decrement scaling that can happen in a single step of automatic dynamic video resizing. The default value is 0.0. pfPVChanMinIncScale() Sets the minimum X and Y increment scaling that can happen in a single step of automatic dynamic video resizing. The default value is 0.0. pfPVChanStress() Sets the stress of the pfPipeVideoChannel for the current frame. This call should be made in the application process after pfSync() and before pfFrame() to affect the next immediate draw process frame. pfPVChanStressFilter() Sets the parameters for computing stress if it is not explicitly set for the current frame by pfPVChanStress(). Each of these methods have corresponding Get methods that return the values set by these methods. To resize the video channel manually, use pfPipeVideoChannel sizing methods, such as pfPVChanOutputSize(), pfPVChanAreaScale(), and pfPVChanScale(). The pfPipeVideoChannel associated with a channel is returned by pfGetChanPVChan(). If there is more than one pfPipeVideoChannel associated with a pfPipeWindow, each one is identified by an index number. In the case of multiple pfPipeVideoChannels, the pfPipeVideoChannel index is set using pfChanPWinPVChanIndex() and returned by pfGetChanPWinPVChanIndex(). Understanding the Stress Filter The pfPVChanStressFilter() function sets the parameters for computing stress for a pfPipeVideoChannel when the stress is not explicitly set for the current frame by pfPVChanStress(), as shown in the following example: void pfPipeVideoChannel::setStressFilter(float *frameFrac, float *lowLoad, float *highLoad, float *pipeLoadScale, float *stressScale, float *maxStress); 007-1680-100 151 5: Frame and Load Control The frameFrac argument is the fraction of a frame that pfPipeVideoChannel is expected to take to render the frame; for example, if the rendering time is equal to the period of the frame rate, frameFrac is 1. If there is only one pfPipeVideoChannel, it is best if frameFrac is 1. If there are more than one pfPipeVideoChannels on the pfPipe, by default frameFrac is divided among the pfPipeVideoChannels. You can set frameFrac explicitly for each pfPipeVideoChannel such that a channel rendering visually complex scenes is allocated more time than a channel rendering simple scenes. The pfGetPFChanStressFilter() function returns the stress filter parameters for pfPipeVideoChannel. If stressScale is nonzero, stress is computed for the pfPipeVideoChannel every frame. The parameters low and high define a hysteresis band for system load. When the load is above lowLoad and below highLoad, stress is held constant. When the load falls outside of the lowLoad and highLoad parameters, OpenGL Performer reduces or increases stress respectively by dynamically resizing the output area of the pfPipeVideoChannel until the load stabilizes between lowLoad and highLoad. If pipeStressScale is nonzero, the load of the pfPipe of the pfPipeVideoChannel are considered in computing the stress. The parameter maxStress is the clamping value above which the stress value cannot go. For more information about the stress filter, see the man page for pfPipeVideoChannel. Dynamic Load Management Because the effects of variable image update rates can be objectionable, many simulation applications are designed to operate at a fixed frame rate. One approach to selecting this fixed frame rate is to select an update rate constrained by the most complex portion of the visual database. Although this conservative approach may be acceptable in some cases, OpenGL Performer supports a more sophisticated approach using dynamic LOD scaling. Using multiple LOD models throughout a database provides the traversal system with a parameter that can be used to control the polygonal complexity of models in the scene. The complexity of database objects can be reduced or increased by adjusting a global LOD range multiplier that determines which LOD level is drawn. 152 007-1680-100 Dynamic Load Management Using this facility, a closed-loop control system can be constructed that adjusts the LOD-switching criteria based on the system load, also called stress, in order to maintain a selected frame rate. Figure 5-5 illustrates a stress-processing control system. Desired Frame Time ess Str ers et ram Pa ter Fil D) s s O e Str Set L ( sal ver D) a r T LO e (Us g rin de n Re Frame Buffer Actual Frame Time Figure 5-5 Stress Processing In Figure 5-5, the desired and actual frame times are compared by the stress filter. Based on the user-supplied stress parameters, the stress filter adjusts the global LOD scale factor by increasing it when the system is overloaded and decreasing it when the system is underloaded. In this way, the system load is monitored and adjusted before each frame is generated. The degree of stability for the closed-loop control system is an important issue. The ideal situation is to have a critically damped control system—that is, one in which just the right amount of control is supplied to maintain the frame rate without introducing undesirable effects. The effects of overdamped and underdamped systems are visually 007-1680-100 153 5: Frame and Load Control distracting. An underdamped system oscillates, causing the system to continuously alternate between two different LOD models without reaching equilibrium. Overdamped systems may fail to react within the time required to maintain the desired frame rate. In practice, though, dynamic load management works well, and simple stress functions can handle the slowly changing loads presented by many databases. The default stress function is controlled with user-selectable parameters. These parameters are set using the pfChanStressFilter() function. The default stress function is implemented by the code fragment in Example 5-3. Example 5-3 Default Stress Function /* current load */ curLoad = drawTime * frameRate / frameFrac; /* integrated over time */ if (curLoad < lowLoad) stressLevel -= stressParam * stressLevel; else if (curLoad > highLoad) stressLevel += stressParam * stressLevel; /* limited to desired range */ if (stressLevel < 1.0) stressLevel = 1.0; else if (stressLevel > maxStress) stressLevel = maxStress; The parameters lowLoad and highLoad define a comfort zone for the control system. The first if-test in the code fragment demonstrates that this comfort zone acts as a dead band. Instantaneous system load within the bounds of the dead band does not result in a change in the system stress level. If the size of the comfort zone is too small, oscillatory distress is the probable result. It is often necessary to keep the highLoad level below the 100% point so that blended LOD transitions do not drive the system into overload situations. For those applications in which the default stress function is either inappropriate or insufficient, you can compute the system stress yourself and then set the stress load factor. Your filter function can access the same system measures that the default stress function uses, but it is also free to keep historical data and perform any feedback-transfer processing that application-specific dynamic load management may require. 154 007-1680-100 Successful Multiprocessing with OpenGL Performer The primary limitation of the default stress function is that it has a reactive rather than predictive nature. One of the major advantages of user-written stress filters is their ability to predict future stress levels before increased or decreased load situations reach the pipeline. Often the simulation application knows, for example, when a large number of moving models will soon enter the viewing frustum. If their presence is anticipated, then stress can be artificially increased so that no sudden LOD changes are required as they actually enter the field of view. Successful Multiprocessing with OpenGL Performer Note: This is an advanced topic. This section does not apply to Microsoft Windows. OpenGL Performer 3.0 for Microsoft Windows does not support more than a single processor. This section describes an advanced topic that applies only to systems with more than one CPU. If you do not have a multiple-CPU system, you may want to skip this section. OpenGL Performer uses multiprocessing to increase throughput for both rendering and intersection detection. Multiprocessing can also be used for tasks that run asynchronously from the main application like database management. Although OpenGL Performer hides much of the complexity involved, you need to know something about how multiprocessing works in order to use multiple processors well. Review of Rendering Stages The OpenGL Performer application renders images using one or more pfPipes as independent software-rendering pipelines. The flow through the rendering pipeline can be modeled using these functional stages: 007-1680-100 Intersection Test for intersections between segments and geometry to simulate collision detection or line-of-sight for example. Application Do requisite processing for the visual simulation application, including reading input from control devices, simulating the vehicle dynamics of moving models, updating the visual database, and interacting with other networked simulation stations. 155 5: Frame and Load Control Cull Traverse the visual database and determine which portions of it are potentially visible, perform a level-of-detail selection for models with multiple representations, and a build sorted, optimized display list for the draw stage. Draw Issue graphics library commands to a Geometry Pipeline in order to create an image for subsequent display. You can partition these stages into separate parallel processes in order to distribute the work among multiple CPUs. Depending on your system type and configuration, you can use any of several available multiprocessing models. Choosing a Multiprocessing Model Use pfMultiprocess() to specify which functional stages, if any, should be forked into separate processes. The multiprocessing mode is actually a bitmask where each bit indicates that a particular stage should be configured as a separate process. For example, the bit PFMP_FORK_DRAW means the draw stage should be split into its own process. Table 5-3 lists some convenient tokens that represent common multiprocessing modes. Table 5-3 Multiprocessing Models Model Name Description PFMP_APPCULLDRAW Combine the application, cull, and draw stages into a single process. In this model, all of the stages execute within a single frame period. This is the minimum-latency mode of operation. PFMP_APP_CULLDRAW Combine the cull and draw stages in a process that is separate from the application process. This model provides a full frame period for the application process, while culling and drawing share this same interval. This mode is appropriate when the host’s simulation tasks are extensive but graphic demands are light, as might be the case when complex vehicle dynamics are performed but only a simple dashboard gauge is drawn to indicate the results. or PFMP_FORK_APP 156 007-1680-100 Successful Multiprocessing with OpenGL Performer Table 5-3 Multiprocessing Models (continued) Model Name Description PFMP_APPCULL_DRAW Combine the application and cull stages in a process that is separate from the draw process. This mode is appropriate for many simulation applications when application and culling demands are light. It allocates a full CPU for drawing and has the application and cull stages share a frame period. Like the PFMP_APP_CULLDRAW mode, this mode has a single frame period of pre-draw latency. or PFMP_FORK_DRAW PFMP_APP_CULL_DRAW or PFMP_FORK_CULL | PFMP_FORK_DRAW Perform the application, cull, and draw stages as separate processes. This is the full maximum-throughput multiprocessing mode of OpenGL Performer operation. In this mode, each pipeline stage is allotted a full frame period for its processing. Two frame periods of latency exist when using this high degree of parallelism. You can also use the pfMultiprocess() function to specify the method of communication between the cull and draw stages, using the bitmasks PFMP_CULLoDRAW and PFMP_CULL_DL_DRAW. Cull-Overlap-Draw Mode Setting PFMP_CULLoDRAW specifies that the cull and draw processes for a given frame should overlap—that is, that they should run concurrently. For this to work, the cull and draw stages must be separate processes (PFMP_FORK_DRAW must be true). In this mode the two stages communicate in the classic producer-consumer model, by way of a pfDispList that is configured as a ring (FIFO) buffer; the cull process puts commands on the ring while the draw process simultaneously consumes these commands. The main benefit of using PFMP_CULLoDRAW is reduced latency, since the number of pipeline stages is reduced by one and the resulting latency is reduced by an entire frame time. The main drawback is that the draw process must wait for the cull process to begin filling the ring buffer. Forcing Display List Generation When the cull and draw stages are in separate processes, they communicate through a pfDispList; the cull process generates the display list, and the draw process traverses and renders it. (The display list is configured as a ring buffer when using PFMP_CULLoDRAW mode, as described in the “Cull-Overlap-Draw Mode” section). 007-1680-100 157 5: Frame and Load Control However, when the cull and draw stages are in the same process (as occurs with the PFMP_APPCULLDRAW or PFMP_APP_CULLDRAW multiprocessing models) a display list is not required and by default one will not be used. Leaving out the pfDispList eliminates overhead. When no display list is used, the cull trigger function pfCull() has no effect; the cull traversal takes place when the draw trigger function pfDraw() is invoked. In some cases you may want an intermediate pfDispList between the cull and draw stages even though those stages are in the same process. The most common situation that calls for such a setup is multipass rendering when you want to cull only once but render multiple times. With PFMP_CULL_DL_DRAW enabled, pfCull() generates a pfDispList that can be rendered multiple times by multiple calls to pfDraw(). Intersection Pipeline The intersection pipeline is a two-stage pipeline consisting of the application and the intersection stages. The intersection stage may be configured as a separate process by setting the PFMP_FORK_ISECT bit in the bitmask given to pfMultiprocess(). When configured as such, the intersection process is triggered for the current frame when the application process calls pfFrame(). Then in the special intersection callback set with pfIsectFunc(), you can invoke any number of intersection requests with pfNodeIsectSegs(). To support this operation, the intersection process keeps a copy of the scene graph pfNodes. The intersection process is asynchronous so that if it does not finish within a frame time it does not slow down the rendering pipeline(s). Compute Process The compute process is an asynchronous process provided for doing extensive asynchronous computation. The compute stage is done as part of pfFrame() in the application process unless it is configured to run as separate process by setting the PFMP_FORK_COMPUTE bit in the pfMultiprocess() bitmask. The compute process is asynchronous so that if it does not finish within a frame time, it will not slow down the rendering pipeline. The compute process is intended to work with pfFlux objects by placing the results of asynchronous computation in pfFluxes. pfFlux will automatically manage the needed multibuffering and frame consistency requirements for the data. See Chapter 19, “Dynamic Data,” for more information on pfFlux. Some OpenGL Performer objects, such as pfASD, do their computation in the compute stage so pfCompute() must 158 007-1680-100 Successful Multiprocessing with OpenGL Performer be called from any compute user callback if one has been specified with pfComputeFunc(). Multiple Rendering Pipelines By default, OpenGL Performer uses a single pfPipe, which in turn draws one or more pfChannels into one or more pfPipeWindows. If you want to use multiple rendering pipelines, as on two- or three-Geometry Pipeline Onyx RealityEngine2 and InfiniteReality systems, use pfMultipipe() to specify the number of pfPipes required. When using multiple pipelines, the PFMP_APPCULLDRAW and PFMP_APPCULL_DRAW modes are not supported and OpenGL Performer defaults to the PFMP_APP_CULL_DRAW multiprocessing configuration. Regardless of the number of pfPipes, there is always a single application process that triggers the rendering of all pipes with pfFrame(). Multithreading For additional multiprocessing and attendant increased throughput, the CULL stage of the rendering pipeline may be multithreaded. Multithreading means that a single pipeline stage is split into multiple processes, or threads which concurrently work on the same frame. Use pfMultithread() to allocate a number of threads for the cull stage of a particular rendering pipeline. Cull multithreading takes place on a per-pfChannel basis; that is, each thread does all the culling work for a given pfChannel. Thus, an application with only a single channel will not benefit from multithreading the cull. An application with multiple, equally complex channels will benefit most by allocating a number of cull threads equal to the number of channels. However, it is valid to allocate fewer cull threads if you do not have enough CPUs—in this case the threads are assigned to channels on a need basis. CULL Sidekick Processes The OpenGL Performer CULL process traverses a scene graph and culls out any invisible geometry. Its result is a list of visible pfGeoSets. The OpenGL Performer CULL process does not break pfGeoSets into their visible and invisible parts. This means that a pfGeoSet whose bounding box intersects the viewing frustum will be sent to the graphics pipe even if only one triangle in this pfGeoSet is visible. One way to overcome this problem is to allocate extra processes for cleaning up the pfGeoSet lists that the CULL processes produce. These extra processes are called CULL 007-1680-100 159 5: Frame and Load Control sidekicks. By default, a CULL sidekick process checks all the primitives in all pfGeoSets on the CULL output. It replaces original pfGeoSets with temporary pfGeoSets and populates the temporary pfGeoSets with the visible parts of the original pfGeoSets. By default, CULL sidekick processes test each primitive twice for the following: • For frustum visibility A primitive outside the viewing frustum will be omitted from the temporary pfGeoSet. • For backface culling A primitive facing away from the viewer will be omitted from the temporary pfGeoSet. This test is skipped when a pfGeoSet is drawn without backface testing. Each CULL process can have multiple CULL_SIDEKICK processes. You can use the pfMultithread() call to specify the number of CULL_SIDEKICK processes for each CULL process. The collection of CULL_SIDEKICK processes configured for each CULL process traverse the pfGeoSet list that the CULL process produces in a round-robin manner. The more CULL_SIDEKICK processes (each assigned to a separate CPU), the faster they process the pfGeoSet list that the CULL process produces. For more information about CULL_SIDEKICK processes in the context of CULL optimizations, see section “Cull Sidekick Processes” on page 173. Order of Calls The multiprocessing model set by pfMultiprocess() is used for each of the rendering pipelines. In programs that configures the application stage as a separate process, all OpenGL Performer calls must be made from the process that calls pfConfig() or the results are undefined. Both pfMultiprocess(), pfMultithread(), and pfMultipipe() must be called after pfInit() but before pfConfig(). pfConfig() configures OpenGL Performer according to the required number of pipelines and the desired multiprocessing and multithreading modes, forks the appropriate number of processes, and then returns control to the application. pfConfig() should be called only once during each OpenGL Performer application. Comparative Structure of Models Figure 5-6 shows timing diagrams for each of the process models. The vertical lines are frame boundaries. Five frames of the simulation are shown to allow the system to reach steady-state operation. Only one of these models can be selected at a time, but they are shown together so that you can compare their structures. 160 007-1680-100 Successful Multiprocessing with OpenGL Performer Boxes represent the functional stages and are labeled as follows: 007-1680-100 An Application process for the nth frame Cn Cull process for the nth frame Dn Draw process for the nth frame 161 5: Frame and Load Control APP A CULL C DRAW D Host Simulation Process Cull Process (traversal) Draw Process Period=1/Frame Rate A0 PFMPAPPCULLDRAW P0 C0 A0 D0 C0 A1 C1 A1 D1 C1 A2 C2 A2 D2 C2 A3 C3 A3 D3 C3 A4 C4 A4 D4 C4 PFMPAPPCULL_DRAW P1 P0 A0 D0 D1 D2 D3 A1 A2 A3 A4 PFMP_APP_CULLDRAW C0 P1 P0 A0 PFMP_APP_CULL_DRAW P1 D0 C1 A0 Start D3 C3 A3 A4 C0 C1 C2 C3 D0 D1 D2 A2 A3 A4 C0 P2 D2 A2 A1 PFMP_APP_CULL0DRAW P1 C2 A1 P2 P0 D1 C1 D0 Frame 0 C2 D1 Frame 1 C3 D2 Frame 2 D3 Frame 3 Frame 4 Time Figure 5-6 162 Multiprocessing Models 007-1680-100 Successful Multiprocessing with OpenGL Performer Notice that when a stage is split into its own process, the amount of time available for all stages increases. For example, in the case where the application, cull, and draw stages are three separate processes, it is possible for total system performance to be tripled over the single process configuration. Asynchronous Database Processing Many databases are too large to fit into main memory. A common solution to this problem is called database paging where the database is divided into manageable chunks on disk and loaded into main memory when needed. Usually chunks are paged in just before they come into view and are deleted from the scene when they are comfortably out of viewing range. All this paging from disk and deleting from main memory takes a lot of time and is certainly not amenable to maintaining a fixed frame rate. The solution supported by OpenGL Performer is asynchronous database paging in which a process, completely separate from the main processing pipeline(s), handles all disk I/O and memory allocations and deletions. To facilitate asynchronous database paging, OpenGL Performer provides the pfBuffer structure and the DBASE process. DBASE Process The database (or DBASE) process is forked by pfConfig() if the PFMP_FORK_DBASE bit was set in the mode given to pfMultiprocess(). The database process is triggered when the application process calls pfFrame() and invokes the user-defined callback set with pfDBaseFunc(). The database process is totally asynchronous. If it exceeds a frame time it does not slow down any rendering or intersection pipelines. The DBASE process is intended for asynchronous database management when used with a pfBuffer. pfBuffer A pfBuffer is a logical buffer that isolates database changes to a single process to avoid memory collisions on data from multiple processes. In typical use, a pfBuffer is created with pfNewBuffer(), made current with pfSelectBuffer(), and merged with the main OpenGL Performer buffer with pfMergeBuffer(). While the DBASE process is intended for pfBuffer use, other processes forked by the application may also use different pfBuffers in parallel for multithreaded database management. By ensuring that only a 007-1680-100 163 5: Frame and Load Control single process uses a given pfBuffer at a given time and following a few scoping rules discussed in the following paragraphs, the application can safely and efficiently implement asynchronous database paging A pfNode is said to have buffer scope or be “in” a particular pfBuffer. This is an important concept because it affects what you can do with a given node. A newly created node is automatically “in” the currently active pfBuffer until that pfBuffer is merged using pfMergeBuffer(). At that instant, the pfNode is moved into the main OpenGL Performer buffer, otherwise known as the application buffer. A rule in pfBuffer management is that a process may only access nodes that are in its current pfBuffer. As a result, a database process may not directly add a newly created subgraph of nodes to the main scene graph because all nodes in the main scene graph have application buffer scope only—they are isolated from the database pfBuffer. This may seem inconvenient at first but it eliminates catastrophic errors. For example, the application process traverses a group at the same time you add a child; this changes its child list and causes the traversal to chase a bad pointer. Remedies to the inconveniences stated above are the pfBufferAddChild(), pfBufferRemoveChild(), and pfBufferClone() functions. The first two functions are identical to their non-buffer counterparts pfAddChild() and pfRemoveChild() except the buffer versions do not happen immediately. Other functions, pfBufferAdd(), pfBufferInsert(), pfBufferReplace(), and pfBufferRemove(), perform the buffer-oriented delayed-action versions of the corresponding non-buffer pfList functions. In all cases the add, insert, replace, or removal request is placed on a list in the current pfBuffer and is processed later at pfMergeBuffer() time. The pfBufferClone() function supports the notion of maintaining a library of common objects like trees or houses in a special library pfBuffer. The main database process then clones objects from the library pfBuffer into the database pfBuffer, possibly using the pfFlatten() function for improved rendering performance. pfBufferClone() is identical to pfClone() except the buffer version requires that the source pfBuffer be specified and that all cloned nodes have scope in the source pfBuffer. pfAsyncDelete We have discussed how to create subgraphs for database paging: create and select a current pfBuffer, create nodes and build the subgraph, call pfBufferAddChild() and finally pfMergeBuffer() to incorporate the subgraph into the application’s scene. This section describes how to use the function pfAsyncDelete() to free the memory of old, unwanted subgraphs. 164 007-1680-100 Successful Multiprocessing with OpenGL Performer The pfDelete() function is the normal mechanism for deleting objects and freeing their associated memory. However,the function pfDelete() can be a very expensive since it must traverse, unreference, and register a deletion request for every OpenGL Performer object it encounters which has a 0 reference count. The function pfAsyncDelete() used in conjunction with a forked DBASE process moves the burden of deletion to the asynchronous database process so that all rendering and intersection pipelines are not adversely affected. The pfAsyncDelete() function may be called from any process and places an asynchronous deletion request on a global list that is processed later by the DBASE stage when its trigger function pfDBase() is called. A major difference from pfDelete() is that pfAsyncDelete() does not immediately check the reference count of the object to be deleted and, so, does not return a value indicating whether the deletion was successful. At this time there is no way of querying the result of a pfAsyncDelete() request so care should be taken that the object to be deleted has no reference counts or memory leaks will result. Placing Multiple OpenGL Performer Processes on a Single CPU When placing multiple OpenGL Performer processes on the same CPU, some combinations of processes and priorities may have an effect on the APP process timing even if the APP process runs on its own separate CPU. This happens because the APP process often waits on other processes for completion of various tasks. If these other processes share a CPU with high-priority processes, they may take a long time to finish their task and release the APP process. An application can request that OpenGL Performer upgrade the priority of processes when the APP process waits on them by calling pfProcessPriorityUpgrade(). The APP process upgrades the other process’ priority before it starts waiting for it, and the other process resumes its previous priority as soon as it releases the APP process. In this way, the original settings of priorities is maintained, except when the APP process waits for another process. OpenGL Performer uses the priority 87 as the default priority for upgrading processes. This priority is the default because it is close to the highest priority that any application-level process should ever have (89). The application may change this priority by using pfProcessHighestPriority(). The priority-upgrade mode is turned off by default. An OpenGL Performer application that does not try to place multiple processes on the same processor or a non-realtime application does not have to set this flag. 007-1680-100 165 5: Frame and Load Control Rules for Invoking Functions While Multiprocessing There are some restrictions on which functions can be called from an OpenGL Performer process while multiple processes are running. Some specialized processes (such as the process handling the draw stage) can call only a few specific OpenGL Performer functions and cannot call any other kinds of functions. This section lists general and specific rules concerning function invocation in the various OpenGL Performer and user processes. In this section, the phrase “the draw process” refers to whichever process is handling the draw stage, regardless of whether that process is also handling other stages. Similarly, “the cull process” and “the application process” refer to the processes handling the cull and application stages, respectively. This is a general list of the kinds of routines you can call from each process: application Configuration routines, creation and deletion routines, set and get routines, and trigger routines such as pfAppFrame(), pfSync(), and pfFrame() database Creation and deletion routines, set and get routines, pfDBase(), and pfMergeBuffer() cull pfCull(), pfCullPath(), OpenGL Performer graphics routines draw pfClearChan(), pfDraw(), pfDrawChanStats(), OpenGL Performer graphics routines, and graphics library routines More specific elaborations: • 166 You should call configuration routines only from the application process, and only after pfInit() and before pfConfig(). pfInit() must be the first OpenGL Performer call, except for those routines that configure shared memory (see “Memory Allocation” in Chapter 18). Configuration routines do not take effect until pfConfig() is called. These are the configuration routines: – pfMultipipe() – pfMultiprocess() – pfMultithread() – pfHyperpipe() 007-1680-100 Successful Multiprocessing with OpenGL Performer • You should call creation routines, such as pfNewChan(), pfNewScene(), and pfAllocIsectData(), only in the application process after calling pfConfig() or in a process that has an active pfBuffer. There is no restriction on creating libpr objects like pfGeoSets and pfTextures. • The pfDelete() function should only be called from the application or database processes while pfAsyncDelete() may be called from any process. • Read-only routines—that is, the pfGet*() functions—can be called from any OpenGL Performer process. However, if a forked draw process queries a pfNode, the data returned will not be frame-accurate. (See “Multiprocessing and Memory” on page 168.) • Write routines—functions that set parameters—should be called only from the application process or a process with an active pfBuffer. It is possible to call a write routine from the cull process, but it is not recommended since any modifications to the database will not be visible to the application process if it is separate from the cull (as when using PFMP_APP_CULLDRAW or PFMP_APP_CULL_DRAW). However, for transient modifications like custom level-of-detail switching, it is reasonable for the cull process to modify the database. The draw process should never modify any pfNode. • OpenGL Performer graphics routines should be called only from the cull or draw processes. These routines may modify the hardware graphics state. They are the routines that can be captured by an open pfDispList. (See “Display Lists” in Chapter 12.) If invoked in the cull process, these routines are captured by an internal pfDispList and later invoked in the draw process; but if they are invoked in the draw process, they immediately affect the current window. These graphics routines can be roughly partitioned into those that do the following: • 007-1680-100 – Apply a graphics entity: pfApplyMtl(), pfApplyTex(), and pfLightOn(). – Enable or disable a graphics mode: pfEnable() and pfDisable(). – Set or a modify graphics state: pfTransparency(), pfPushState(), and pfMultMatrix(). – Draw geometry or modify the screen: pfDrawGSet(), pfDrawString(), and pfClear(). Graphics library routines should be called only from the draw process. Since there is no open display list to capture these commands, an open window is required to accept them. 167 5: Frame and Load Control • “Trigger” routines should be called only from the appropriate processes (see Table 5-4). Table 5-4 Trigger Routines and Associated Processes Trigger Routine Process/Context pfAppFrame() pfSync() pfFrame() APP/main loop pfPassChanData() pfPassIsectData() APP/main loop pfApp() APP/channel APP callback pfCull() pfCullPath() CULL/channel CULL callback pfDraw() pfDrawBin() DRAW/channel DRAW callback pfNodeIsectSegs() pfChanNodeIsectSegs() ISECT/callback or APP/main loop pfDBase() DBASE/callback • User-spawned processes created with sproc() can trigger parallel intersection traversals through multiple calls to pfNodeIsectSegs() and pfChanNodeIsectSegs(). • Functions pfApp(), pfCull(), pfDraw(), and pfDBase() are only called from within the corresponding callback specified by pfChanTravFunc() or pfDBaseFunc(). Multiprocessing and Memory In OpenGL Performer, as is often true of multiprocessing systems, memory management is the most difficult aspect of multiprocessing. Most data management problems in an OpenGL Performer application can be partitioned into three categories: • 168 Memory visibility. OpenGL Performer uses fork(), which—unlike sproc()— generates processes that do not share the same address space. The processes also cannot share global variables that are modified after the fork() call. After calling fork(), processes must communicate through explicit shared memory. 007-1680-100 Successful Multiprocessing with OpenGL Performer • Memory exclusion. If multiple processes read or write the same chunk of data at the same time, consequences can be dire. For example, one process might read the data while in an inconsistent state and end up dumping core while dereferencing a NULL pointer. • Memory synchronization. OpenGL Performer is configured as a pipeline where different processes are working on different frames at the same time. This pipelined nature is illustrated in Figure 5-6 on page 162, which shows that, for instance, in the PFMP_APP_CULL_DRAW configuration the application process is working on frame n while the draw process is working on frame n–2. If, in this case, if we have only a single memory location representing the viewpoint, then it is possible for the application to set the viewpoint to that of frame n and the draw process to incorrectly use that same viewpoint for frame n–2. Properly synchronized data is called frame accurate. Fortunately, OpenGL Performer transparently solves all of the problems just described for most OpenGL Performer data structures and also provides powerful tools and mechanisms that the application can use to manage its own memory. Shared Memory and pfInit() The pfInit() function creates a shared memory arena that is shared by all processes spawned by OpenGL Performer and all user processes that are spawned from any OpenGL Performer process. A handle to this arena is returned by pfGetSharedArena() and should be used as the arena argument to routines that create data that must be visible to all processes. Routines that accept an arena argument are the pfNew*() routines found in the libpr library and the OpenGL Performer memory allocator, pfMalloc(). In practice, it is usually safest to create libpr objects like pfGeoSets and pfMaterials in shared memory. libpf objects like pfNodes are always created in shared memory. Allocating shared memory does not by itself solve the memory visibility problem discussed above. You must also make sure that the pointer that references the memory is visible to all processes. OpenGL Performer objects, once incorporated into the database through routines like pfAddGSet(), pfAddChild(), and pfChanScene(), automatically ensure that the object pointers are visible to all OpenGL Performer processes. However, pointers to application data must be explicitly shared. A common way of doing this is to allocate the shared memory after pfInit() but before pfConfig() and to reference the memory with a global pointer. Since the pointer is set before pfConfig() forks any processes, these processes will all share the pointer’s value and can thereby 007-1680-100 169 5: Frame and Load Control access the same shared memory region. However, if this pointer value changes in a process, its value will not change in any other process, since forked processes do not share the same address space. Even with data visible to all processes, data exclusion is still a problem. The usual solution is to use hardware spin locks so that a process can lock the data segment while reading or writing data. If all processes must acquire the lock before accessing the data, then a process is guaranteed that no other processes will be accessing the data at the same time. All processes must adhere to this locking protocol, however, or exclusion is not guaranteed. In addition to a shared memory arena, pfInit() creates a semaphore arena whose handle is returned by pfGetSemaArena(). Locks can be allocated from this semaphore arena by usnewlock() and can be set and unset by ussetlock() and usunsetlock(), respectively. pfDataPools The pfDataPools—named shared memory arenas with named allocation blocks— provide a complete solution to the memory visibility and memory exclusion problems, thereby obviating the need to set global pointers between pfInit() and pfConfig(). For more information about pfDataPools, see the pfDataPools man page. Passthrough Data The techniques discussed thus far do not solve the memory synchronization problem. OpenGL Performer’s libpf library provides a solution in the form of passthrough data. When using pipelined multiprocessing, data must be passed through the processing pipeline so that data modifications reach the appropriate pipeline stage at the appropriate time. Passthrough data is implemented by allocating a data buffer for each stage in the processing pipeline. Then, at well-defined points in time, the passthrough data is copied from its buffer into the next buffer along the pipeline. This copying guarantees memory exclusion, but you should minimize the amount of passthrough data to reduce the time spent copying. Allocate a passthrough data buffer for the rendering pipeline using pfAllocChanData(); for data to be passed down the intersection pipeline, call pfAllocIsectData(). Data returned from pfAllocChanData() is passed to the channel cull and draw callbacks that 170 007-1680-100 CULL Process Optimizations are set by pfChanTravFunc(). Data returned from pfAllocIsectData() is passed to the intersection callback specified by pfIsectFunc(). Passthrough data is not automatically passed through the processing pipeline. You must first call pfPassChanData() or pfPassIsectData() to indicate that the data should be copied downstream. This requirement allows you to copy only when necessary—if your data has not changed in a given frame, simply do not call a pfPass*() routine, and you will avoid the copy overhead. When you do call a pfPass*() routine, the data is not immediately copied but is delayed until the next call to pfFrame(). The data is then copied into internal OpenGL Performer memory and you are free to modify your passthrough data segment for the next frame. Modifications to all libpf objects—such as pfNodes and pfChannels—are automatically passed through the processing pipeline, so frame-accurate behavior is guaranteed for these objects. However, in order to save substantial amounts of memory, libpr objects such as pfGeoSets and pfGeoStates do not have frame-accurate behavior; modifications to such objects are immediately visible to all processes. If you want frame-accurate modifications to libpr objects you must use the passthrough data mechanism, use a frame-accurate pfSwitch to select among multiple copies of the objects you want to change, or use the pfCycleBuffer memory type. CULL Process Optimizations The OpenGL Performer CULL process traverses a scene graph and culls out invisible geometry. Its result is a list (pfDispList) of visible pfGeoSets. The OpenGL Performer CULL process treats pfGeoSets as rendering atoms: It does not break them into their visible and invisible parts. If the bounding box of a pfGeoSet intersects the viewing frustum, OpenGL Performer draws the entire pfGeoSet even if only one of its triangles is visible. Figure 5-7 demonstrates this problem using a triangle strip. 007-1680-100 171 5: Frame and Load Control Figure 5-7 Loose Culling of pfGeosets The figure shows a triangle strip starting inside the viewing frustum, leaving the viewing frustum, and then returning into the viewing frustum. Only the shaded triangles of the strip are visible, but OpenGL Performer renders the entire strip. In this figure, OpenGL Performer sends five superfluous vertices to the graphics pipe. This problem is important in applications with one of the following bottlenecks: • Geometry processing Applications that render large numbers of relatively small triangles—for example, CAD visualization or detailed terrain visualization. • Host-Pipe interface bandwidth Applications that saturate the interface between the host CPU and the graphics pipe either by rendering too many triangles or by downloading too many texture maps each frame. This problem is not important in raster-limited applications that render very large triangles (in screen space). These application saturate the raster portion of the graphics pipe but leave the geometry portion idle. Therefore, speeding up the geometry portion of the graphic pipe does not speed up the overall application frame rate. 172 007-1680-100 CULL Process Optimizations Cull Sidekick Processes You can overcome the loose-culling problem by allocating extra processes for cleaning up the pfGeoSet lists that the CULL processes produce. These extra processes are called CULL sidekicks. By default, a CULL sidekick process checks all the primitives in all pfGeoSets on the CULL output. It replaces original pfGeoSets with temporary pfGeoSets and populates the temporary pfGeoSets with the visible parts of the original pfGeoSets. By default, CULL sidekick processes test each primitive twice for the following: • For frustum visibility A primitive outside the viewing frustum will be omitted from the temporary pfGeoSet. • For backface culling A primitive facing away from the viewer will be omitted from the temporary pfGeoSet. This test is especially powerful when rendering enclosed objects (for example—vehicles, houses, or machine parts) because about half of the triangles in such models face away from the viewer. This test is skipped when a pfGeoSet is drawn without backface testing. CULL sidekick processes run side-by-side with their CULL process. They do not interact with the CULL process during its frame, but they merely patch the visible pfGeoSet list as the CULL process populates it. This means that configuring CULL sidekick processes does not add any latency to the application. Figure 5-8 shows how CULL_SIDEKICK optimizes visible pfGeoSet lists while CULL is writing them. The figure shows three CULL_SIDEKICK processes working on the visible pfGeoSet list that a CULL process produces. Visible pfGeoSet#1 is replaced by Temporary pfGeoSet#1. Visible pfGeoSet#2 contains no visible primitives and is skipped entirely. 007-1680-100 173 5: Frame and Load Control Temporary pfGeoSet #1 CULL_SIDEKICK Skipped Test and replace visible pfGeoSets Visible pfGeoSet list Write visible pfGeoSets visible pfGeoSet #1 Figure 5-8 visible pfGeoSet #2 visible pfGeoSet #3 CULL CULL_SIDEKICK Processing Configuring CULL_SIDEKICK Processes Each CULL process can have multiple CULL_SIDEKICK processes. You can use the pfMultithread() call to specify the number of CULL_SIDEKICK processes for each CULL process. The collection of CULL_SIDEKICK processes configured for each CULL process traverses in a round-robin manner the pfGeoSet list that the CULL process produces. The more CULL_SIDEKICK processes (each assigned to a separate CPU), the faster they process the pfGeoSet list that the CULL process produces. CULL Sidekick Optimization Mask Using the function pfMultithreadParami() and the parameter PFSK_OPTIMIZATION, an application can specify a bit-wise OR of the constants PFSK_BACKFACE_CULL and PFSK_FRUSTUM_CULL. Specifying the PFSK_BACKFACE_CULL flag instructs CULL_SIDEKICK to run a backface test on each primitive and to remove backfacing primitives. This mode is aware of the pfGeoState setting for each pfGeoSet and correctly ignores pfGeoSets that do not require this test. Specifying the PFSK_FRUSTUM_CULL flag instructs CULL_SIDEKICK to run a frustum test on each primitive and to remove primitives outside the viewing frustum. Both of these tests break triangle strips, line strips, and triangle fans if portions of these are invisible. 174 007-1680-100 CULL Process Optimizations Note: It is safe to change the CULL_SIDEKICK optimization mask on the fly. CULL Sidekick Synchronization Policy Since traversing the visible pfGeoSet list that CULL produces may take longer than a single frame, you can specify a policy for the behavior of CULL_SIDEKICK processes. Using the function pfMultithreadParami() and the parameter PFSK_POLICY, you can specify one of three options: • PFSK_CULL_DONE All CULL_SIDEKICK processes stop processing pfGeoSet lists as soon as their CULL process finishes its frame. This means that the CULL_SIDEKICK process is likely to skip the optimization of many pfGeoSets on the visible pfGeoSet list. • PFSK_CULL_FRAME_DONE All CULL_SIDEKICK processes continue processing until the end of the expected CULL frame time. If the CULL process finishes its frame early in the PFSK_CULL_DONE mode, the CULL_SIDEKICK processes cannot use the remainder of the time to complete their own processing. The PFSK_CULL_FRAME_DONE mode allows the CULL_SIDEKICK processes to use all of the available frame time for processing. Use the parameter PFSK_SAFETY_MARGIN to specify a floating number of seconds. This sets a margin before the end of the frame where CULL_SIDEKICK stops processing. This is a safety measure. If CULL_SIDEKICK does not complete early enough, it can make CULL miss its frame. The default value is 1.0 millisecond. The more sensitive to frame drops your application is, the larger this margin should be. • PFSK_CULL_SIDEKICK_DONE All CULL_SIDEKICK processes finish processing all the visible pfGeoSet lists that the CULL process produces. If this takes longer than the desired CULL frame rate, the CULL process waits for its CULL_SIDEKICK helpers and may miss a frame. Note: It is safe to change the CULL_SIDEKICK synchronization policy on the fly. 007-1680-100 175 5: Frame and Load Control CULL Sidekick User Functions Use the function pfMultithreadParami() with parameters PFSK_USER_FUNC and PFSK_USER_FUNC_DATA to register a callback function for the CULL_SIDEKICK pfGeoSet optimization. When specified, a CULL_SIDEKICK calls the callback function instead of running the default optimization. The CULL_SIDEKICK provides the callback function with a target pfGeoSet. The callback function can clone the target pfGeoSet, modify the cloned pfGeoSet, and return it as a replacement for the target pfGeoSet. The callback function should return a pfGeoSet pointer. It can return one of the following values: • The original pfGeoSet pointer CULL_SIDEKICK does not optimize this pfGeoSet and leaves it on the visible pfGeoSet list. • A new pfGeoSet pointer CULL_SIDEKICK replaces the pfGeoSet in the visible pfGeoSet list with the returned value. • A NULL pointer CULL_SIDEKICK removes this pfGeoSet from the visible pfGeoSet list. The callback function receives as a parameter a pointer to a pfDispListOptimizer class. The callback function can use this pointer in order to do the following: 176 • Retrieve the projection/modelview matrix that will be loaded when this pfGeoSet is rendered. • Allocate temporary pfGeoSets. • Allocate temporary memory buffers. • Clone a pfGeoSet onto a temporary pfGeoSet. • Invoke the default optimization on a pfGeoSet. • Get a pointer to the pfChannel in which this pfGeoSet was found visible. • Get the number of CULL_SIDEKICK processes working for the CULL process and get the index of the calling CULL_SIDEKICK process. • Get the optimization mask of this CULL_SIDEKICK process. 007-1680-100 CULL Process Optimizations The following is a sample callback function. This function clones the incoming pfGeoSet, jitters all its coordinates by a random amount, and replaces all its colors by random colors: pfGeoSet * userFunction(pfGeoSet *gset, pfDispListOptimizer *op, void *userData) { pfGeoSet *new_gset; ushort *ilist; int *len; float *c; float *v; int numVerts, numPrims, numColors; int i; /* Modify geosets with line-strip/tri-strip primitives only. */ /* When not modifying a pfGeoSet, return its original pointer. */ if ((pfGetGSetPrimType(gset) != PFGS_LINESTRIPS) && (pfGetGSetPrimType(gset) != PFGS_TRISTRIPS)) return (gset); /* Clone geoset. We can modify the cloned geoset because it */ /* is temporary for this CULL process for this frame. */ new_gset = pfDLOptimizerCloneGSet(op, gset, PFSK_COORD3 |PFSK_NORMAL3 | PFSK_TEXCOORD2 |PFSK_ATTR_LENGTHS); /* Get pointers to cloned geoset attributes */ pfGetGSetAttrLists(new_gset, PFGS_COLOR4, &c, &ilist); if (ilist) return gset; /* ignore indexed gsets */ pfGetGSetAttrLists(new_gset, PFGS_COORD3, &v, &ilist); if (ilist) return gset; /* ignore indexed gsets */ len = pfGetGSetPrimLengths(new_gset); numPrims = pfGetGSetNumPrims(new_gset); /* Count how many vertex entries in the COORD3 attribute. */ numVerts = 0; for (i = 0 ; i < numPrims ; i ++) numVerts += len[i]; /* Count how many color entries in the COLOR4 attribute. */ switch (pfGetGSetAttrBind(gset, PFGS_COLOR4)) { case PFGS_PER_VERTEX: numColors = numVerts; 007-1680-100 177 5: Frame and Load Control break; case PFGS_PER_PRIM: numColors = numPrims; break; case PFGS_OVERALL: numColors = 1; break; case PFGS_OFF: numColors = 0; break; } /* Pick a random color for each color entry in the cloned */ /* color attribute array. */ for (i = 0 ; i < numColors ; i ++) { *(c++) = getRand(); *(c++) = getRand(); *(c++) = getRand(); *(c++) = 1.0; } /* Pick a random perturbation for each coordinate */ for (i = 0 ; i < numVerts ; i ++) { *(v++) += vertex_jitter_amount * getRand(); *(v++) += vertex_jitter_amount * getRand(); *(v++) += vertex_jitter_amount * getRand(); } /* Send new geoset for default frustum/backface culling. */ return pfDLOptimizerOptimize(op, new_gset); } Modifying Attributes of Cloned pfGeoSets When cloning a pfGeoSet from within a CULL_SIDEKICK callback function, you may wish to modify the pointers to the attribute arrays of the cloned pfGeoSet. Cloned pfGeoSets are temporary and do not require reference counting. Use the following quick methods on the pfGeoSet in order to manipulate its attributes: • pfQuickCopyGSet() Copies the contents of one pfGeoSet onto another with no reference count considerations. • pfGSetQuickAttr() Sets an attribute of a pfGeoSet. 178 007-1680-100 CULL Process Optimizations • pfGSetQuickMultiAttr() Sets a multi-value attribute of a pfGeoSet (for example, multitexture) • pfGSetQuickPrimLengths() Sets the primitive length array of a pfGeoSet. • pfQuickResetGSet() Sets all attribute arrays to NULL. No reference counting. Note: If you wish to replace the attribute binding of cloned pfGeoSet attributes, you must use the standard pfGeoSet API (as opposed to the quick API). Changing anything other than the pointers to attribute arrays requires internal pfGeoSet state changes and, therefore, cannot happen through the quick API. Marking pfGeoSets for Optimization Use the function pfGSetOptimize() to mark any single pfGeoSet for optimization by the CULL_SIDEKICK process. By default, all pfGeoSets under a pfGeode node undergo optimization. All pfGeoSetCBs are not optimized by default but can be optimized using this function. No pfGeoSet under a pfBillboard node is ever optimized (regardless of the optimization flag setting). 007-1680-100 179 Chapter 6 6. Creating Visual Effects This chapter describes how to use environmental, atmospheric, lighting, and other visual effects to enhance the realism of your application. The following sections appear: • “Using pfEarthSky” on page 181 • “Atmospheric Effects” on page 182 • “Patchy Fog and Layered Fog” on page 186 • “Real-Time Shadows” on page 198 • “Image-Based Rendering” on page 204 Using pfEarthSky A pfEarthSky is a special set of functions that clears a pfChannel’s viewport efficiently and implements various atmospheric effects. A pfEarthSky is attached to a pfChannel with pfChanESky(). Several pfEarthSky definitions can be created, but only one can be in effect for any given channel at a time. A pfEarthSky can be used to draw a sky and horizon, to draw sky, horizon, and ground, or just to clear the entire screen to a specific color and depth. The colors of the sky, horizon, and ground can be changed in real time to simulate a specific time of day. At the horizon boundary, the ground and sky share a common color, so that there is a smooth transition from sky to horizon color. The width of the horizon band can be defined in degrees. A pfChannel’s earth-sky model is automatically drawn by OpenGL Performer before the scene is drawn unless the pfChannel has a draw callback set with pfChanTravFunc(). In this case it is the application’s responsibility to clear the viewport. Within the callback pfClearChan() draws the channel’s pfEarthSky. 007-1680-100 181 6: Creating Visual Effects Example 6-1 shows how to set up a pfEarthSky(). Example 6-1 How to Configure a pfEarthSky pfEarthSky *esky; pfChannel *chan; sky = pfNewESky(); pfESkyMode(esky, PFES_BUFFER_CLEAR, PFES_SKY_GRND); pfESkyAttr(esky, PFES_GRND_HT, -1.0f); pfESkyColor(esky, PFES_GRND_FAR, 0.3f, 0.1f, 0.0f, 1.0f); pfESkyColor(esky, PFES_GRND_NEAR, 0.5f, 0.3f, 0.1f,1.0f); pfChanESky(chan, esky); Atmospheric Effects The complexities of atmospheric effects on visibility are approximated within OpenGL Performer using a multiple-layer sky model, set up as part of the pfEarthSky function. In this design, individual layers are used to represent the effects of ground fog, clear sky, and clouds. Figure 6-1 shows the identity and arrangement of these layers. 182 007-1680-100 Atmospheric Effects General visibility Upper transition zone Clouds Lower transition zone General visibility Groung fog Figure 6-1 Layered Atmosphere Model The lowest layer consists of ground fog, extending from the ground up to a user-selected altitude. The fog thins out with increasing altitude, disappearing entirely at the bottom of the general visibility layer. This layer extends from the top of the ground fog layer to the bottom of the cloud layer’s lower transition zone, if such a zone exists. The transition 007-1680-100 183 6: Creating Visual Effects zone provides a smooth transition between general visibility and the cloud layer. (If there is no cloud layer, then general visibility extends upward forever.) The cloud layer is defined as an opaque region of near-zero visibility; you can set its upper and lower boundaries. You can also place another transition zone above the cloud layer to make the clouds gradually thin out into clear air. Set up the atmospheric simulation with the commands listed in Table 6-1 Table 6-1 pfEarthSky Functions Function Action pfNewESky() Create a pfEarthSky. pfESkyMode() Set the render mode. pfESkyAttr() Set the attributes of the earth and sky models. pfESkyColor() Set the colors for earth and sky and clear. pfESkyFog() Set the fog functions. You can set any pfEarthSky attribute, mode, or color in real time. Selecting the active pfFog definition can also be done in real time. However, changing the parameters of a pfFog once they are set is not advised when in multiprocessing mode. The default characteristics of a pfEarthSky are listed in Table 6-2. Table 6-2 184 pfEarthSky Attributes Attribute Default Clear method PFES_FAST (full screen clear) Clear color 0.0 0.0 0.0 Sky top color 0.0 0.0 0.44 Sky bottom color 0.0 0.4 0.7 Ground near color 0.5 0.3 0.0 Ground far color 0.4 0.2 0.0 Horizon color 0.8 0.8 1.0 007-1680-100 Atmospheric Effects Table 6-2 pfEarthSky Attributes (continued) Attribute Default Ground fog NULL (no fog) General visibility NULL (no fog) Cloud top 20000.0 Cloud bottom 20000.0 Cloud bottom color 0.8 0.8 0.8 Cloud top color 0.8 0.8 0.8 Transition zone bottom 15000.0 Transition zone top 25000.0 Ground height 0 Horizon angle 10 degrees By default, an earth-sky model is not drawn. Instead, the channel is simply cleared to black and the Z-buffer is set to its maximum value. This default action also disables all other atmospheric attributes. To enable atmospheric effects, select PFES_SKY, PFES_SKY_GRND, or PFES_SKY_CLEAR when turning on the earth-sky model. Clouds are disabled when the cloud top is less than or equal to the cloud bottom. Cloud transition zones are disabled when clouds are disabled. Fog is enabled when either the general or ground fog is set to a valid pfFog. If ground fog is not enabled, no ground fog layer will be present and fog will be used to support general visibility. Setting a fog attribute to NULL disables it. See “Atmospheric Effects” on page 182 for further information on fog parameters and operation. The earth-sky model is an attribute of the channel and thus accesses information about the viewer’s position, current field of view, and other pertinent information directly from pfChannel. To set the pfEarthSky in a channel, use pfChanESky(). 007-1680-100 185 6: Creating Visual Effects Patchy Fog and Layered Fog A pfVolFog is a class that uses a multi-pass algorithm to draw the scene with a fog that has different densities at different locations. It extends the basic layered fog provided by pfEarthSky and introduces a new type of fog: a patchy fog. A patchy fog has a constant density in a given area. The boundaries of this area can be defined by an arbitrary three-dimensional object or by a set of objects. A layered fog changes only with elevation; its density and color is uniform at a given height. It is defined by a set of elevation points, each specifying a fog density and, optionally, also a fog color at the point’s elevation. The density and the color between two neighboring points is linearly interpolated. Figure 6-2 illustrates the basic difference between patchy fog and layered fog. P1 P2 color 2 P3 P4 P5 node 1 color 1 P6 node 2 Layered fog Patchy fog Figure 6-2 Patchy Fog Versus Layered Fog Compared to a layered fog in pfEarthSky, a layered fog in pfVolFog has distinct advantages: 186 • It can be specified by an arbitrary number of elevation points. • Each elevation point can have a different color associated with it. • A layered fog in pfVolFog is not dependent on an InfiniteReality-specific texgen. It can also be drawn using only 2D textures to simulate the 3D texture. Thus, a layered fog in pfVolFog can virtually be used on any machine. 007-1680-100 Patchy Fog and Layered Fog Creating Layered Fog A pfVolFog is not part of the scene graph; it is created separately by the application process. Once created, elevation points of a layered fog can be specified by calling pfVolFogAddPoint() or pfVolFogAddColoredPoint() repeatedly. The fog initialization is completed by calling pfApplyVolFog(). Example 6-2 Fog initialization Using pfVolFogAddPoint() pfVolFog *lfog; lfog = pfNewVolFog(arena); pfVolFogAddPoint(lfog, elev1, density1); pfVolFogAddPoint(lfog, elev2, density2); pfVolFogAddPoint(lfog, elev2, density2); pfApplyVolFog(lfog); Creating Patchy Fog The boundary of a patchy fog is specified by pfVolFogAddNode(pfog,node),where node contains the surfaces enclosing the foggy areas. It is possible to define several disjoint areas in the same tree or by adding several different nodes. Note that each area has to be completely enclosed, and the vertices of the surfaces have to be ordered so that the front face of each surface faces outside the foggy area. The node has to be part of the scene graph for the rendering to work properly. Example 6-3 Specifying Patchy Fog Boundaries Using pfVolFogAddNode() pfVolFog *pfog; pfNode *fogNode; pfog = pfNewVolFog(arena); fogNode = pfdLoadFile(filename); pfVolFogAddNode(pfog, fogNode); pfAddChild(scene, fogNode); pfApplyVolFog(pfog); Patchy and layered fog can be combined but only if layered fog has a uniform color; that is, it is specified using pfVolFogAddPoint() only. 007-1680-100 187 6: Creating Visual Effects Initializing a pfVolFog The function pfApplyVolFog() initializes a pfVolFog. If at least two elevation points were defined, it initializes data structures necessary for rendering of a layered fog, including a 3D texture. Any control points defined afterward are ignored. If a node containing patchy fog boundaries has been added prior to calling pfApplyVolFog(), a patchy fog is initialized. Since function pfVolFogAddNode() only marks the parts of the scene graph that specifies the texture, it is possible to add additional patchy fog nodes, even after pfApplyVolFog() has been called. Table 6-3 summarizes routines for initialization and drawing of a pfVolFog. Table 6-3 188 pfVolFog Functions Function Action pfNewVolFog() Create a pfVolFog. pfVolFogAddChannel() Add a channel on which pfVolFog is used. pfVolFogAddPoint() Add a point specifying fog density at a certain elevation. pfVolFogAddColoredPoint() Add a point specifying fog density and color at a certain elevation. pfVolFogAddNode() Add a node defining the boundary of a patchy fog. pfVolFogSetColor() Set color of a layered fog or patchy fog. pfVolFogSetDensity() Set density of a patchy fog. pfVolFogSetFlags() Set binary flags. pfVolFogSetVal() Set a single attribute. pfVolFogSetAttr() Set an array of attributes. pfApplyVolFog() Initialize data structures necessary for rendering fog. pfVolFogAddChannel() Add a channel on which pfVolFog is used. pfVolFogUpdateView() Update the current view for all stored channels. pfDrawVolFog() Draw the scene with fog. pfGetVolFogTexture() Return the texture used by layered fog. 007-1680-100 Patchy Fog and Layered Fog The attributes of a pfVolFog are listed in Table 6-4. Table 6-4 pfVolFog Attributes Attribute Identifier Default Color PFVFOG_COLOR 0.9, 0.9, 1 Density PFVFOG_DENSITY 1.0 Density bias PFVFOG_DENSITY_BIAS 0 Maximum distance PFVFOG_MAX_DISTANCE 2000 Mode PFVFOG_MODE PFVFOG_LINEAR Layered fog mode PFVFOG_LAYERED_MODE PFVFOG_LINEAR Texture size PFVFOG_3D_TEX_SIZE 64 x 64 x 64 Resolution PFVFOG_RESOLUTION 0.2 Patchy fog mode PFVFOG_PATCHY_MODE PFVFOG_LINEAR Texture bottom PFVFOG_PATCHY_TEXTURE_BOTTOM 0.3 Texture top PFVFOG_PATCHY_TEXTURE_TOP 0.1.5 PFVFOG_ROTATE_NODE Identity Attenuation scale PFVFOG_LIGHT_SHAFT_ATTEN_SCALE 0.04 Attenuation shift PFVFOG_LIGHT_SHAFT_ATTEN_TRANSLATE 6 Darken factor PFVFOG_LIGHT_SHAFT_DARKEN_FACTOR General Layered fog Patchy fog Layered patchy fog Rotation matrix Light shafts 007-1680-100 0.3 189 6: Creating Visual Effects The flags of a pfVolFog are listed in Table 6-5. Table 6-5 pfVolFog Flags Flag Identifier Default Close surfaces PFVFOG_FLAG_CLOSE_SURFACES 1 Use 2D texture PFVFOG_FLAG_FORCE_2D_TEXTURE 0 Force patchy fog passes PFVFOG_FLAG_FORCE_PATCHY_PASS 0 Self-shadowing PFVFOG_FLAG_SELF_SHADOWING 0 Darken objects PFVFOG_FLAG_DARKEN_OBJECTS 0 Filter color PFVFOG_FLAG_FOG_FILTER 0 Faster patchy fog PFVFOG_FLAG_FASTER_PATCHY_FOG 0 No object in fog PFVFOG_FLAG_NO_OBJECT_IN_FOG 0 1D texture on surface PFVFOG_FLAG_PATCHY_FOG_1DTEXTURE 0 Separate node bins PFVFOG_FLAG_SEPARATE_NODE_BINS 0 Screen-bounding rectangle PFVFOG_FLAG_SCREEN_BOUNDING_RECT 1 Draw nodes separately PFVFOG_FLAG_DRAW_NODES_SEPARATELY 0 User-defined texture PFVFOG_FLAG_USER_PATCHY_FOG_TEXTURE 0 Use cull programs PFVFOG_FLAG_USE_CULL_PROGRAM 0 PFVFOG_FLAG_LAYERED_PATCHY_FOG 0 PFVFOG_FLAG_LIGHT_SHAFT 0 General Layered fog Patchy fog Layered patchy fog Use layered patchy fog Light shafts Light shaft 190 007-1680-100 Patchy Fog and Layered Fog Updating the View A pfVolFog needs information about the current eye position and view direction. Since this information is not directly accessible in a draw process, it is necessary to call pfVolFogAddChannel() for each channel at the beginning of the application. Whenever the view changes, the application process has to call pfVolFogUpdateView(). See programs in /usr/share/Performer/src/sample/apps/C/fogfly or /usr/share/Performer/src/sample/apps/C++/volfog on IRIX and Linux or %PFROOT%\Src\sample\apps\C\fogfly or %PFROOT%\Src\sample\apps\C++\volfog on Microsoft Windows for an example. If you do not update the view, the fog will not be rendered. If the application changes the position of the patchy fog boundaries (for example, by inserting a pfSCS, pfDCS, or pfFCS node above the fog node) or the orientation of the whole scene with respect to the up vector (for example, the use of a trackball in Perfly), the fog may not be drawn correctly. Drawing a Scene with Fog To draw the scene with a fog, the draw process has to call pfDrawVolFog() instead of pfDraw(). This function takes care of drawing the whole scene graph with the specified fog. Expect the draw time to increase because the scene is drawn twice (three times if both patchy and layered fog are specified). In case of a patchy fog there may also be several full-screen polygons being drawn. You can easily disable the fog by not calling pfDrawVolFog(). Since boundaries of patchy fog are in the scene graph, do not use pfDraw() to draw the scene without fog; instead, use pfDrawBin() with PFSORT_DEFAULT_BIN, PFSORT_OPAQUE_BIN, and PFSORT_TRANSP_BIN. A patchy fog needs as deep a color buffer as possible (optimally 12 bits per color component) and a stencil buffer. Use at least a 4-bit stencil buffer (1-bit is sufficient only for very simple fog objects). It may be necessary to modify your application so that it asks for such a visual. Deleting a pfVolFog A pfVolFog can be deleted using pfDelete(). In case of a layered fog it is necessary to delete the texture handle in a draw process. The texture is returned by 007-1680-100 191 6: Creating Visual Effects pfGetVolFogTexture(). See the example in /usr/share/Performer/src/sample/apps/C/fogfly on IRIX and Linux and in %PFROOT%\Src\sample\apps\C\fogfly on Microsoft Windows. Specifying Fog Parameters This section describes how to manage the various parameters for both layered and patchy fog. Layered Fog As mentioned earlier, a layered fog of a uniform color is specified by function pfVolFogAddPoint(), which sets the fog density at a given elevation. The density is scaled so that if the fog has a density of 1, the nearest object inside the fog that has full fog color is at a distance equal to 1/10 of the diagonal of the scene bounding box. The layered fog color is set by function pfVolFogSetColor() or by calling pfVolFogSetAttr() with parameter PFVFOG_COLOR and a pointer to an array of three floats. A layered fog of nonuniform color is specified by function pfVolFogAddColoredPoint(), which sets the fog density and the fog color at a given elevation. The color set by pfVolFogSetColor() is then ignored. The layered fog mode is set by function pfVolFogSetVal() with parameter PFVFOG_LAYERED_MODE and one of PFVFOG_LINEAR, PFVFOG_EXP, or PFVFOG_EXP2. It is also possible to set the mode both for a layered and patchy fog at once by using parameter PFVFOG_MODE. The default mode is PFVFOG_LINEAR. The function of the mode parameter is equivalent to the function of the fog mode parameter of the OpenGL function glFog(). The size of a 3D texture used by a layered fog can be modified by calling pfVolFogSetAttr() with parameter PFVFOG_3D_TEX_SIZE and an array of three integer values. The default texture size is 64x64x64, but reasonable results can be achieved with even smaller sizes. The sizes are automatically rounded up to the closest power of 2. The second value should be equal to or greater than the third value. If 3D textures are not supported, a set of 2D textures is used instead of a 3D texture (the number of 2D textures is equal to the third dimension of the 3D texture). Every time the r coordinate changes more than 0.1, a new texture is computed by interpolating between two neighboring 192 007-1680-100 Patchy Fog and Layered Fog slices, and the texture is reloaded. The use of 2D textures can be forced by calling: pfVolFogSetFlags() with flag PFVFOG_FLAG_FORCE_2D_TEXTURE set to 1. Note: Once a layered fog is initialized by calling the pfApplyVolFog(), changing any of the parameters described here will not affect rendering of the layered fog. Patchy Fog The density of a patchy fog is controlled by function pfVolFogSetDensity() or by using pfVolFogSetVal() with parameter PFVFOG_FOG_DENSITY. As in the case of a layered fog, the density of a patchy fog is scaled by 1/10 of the diagonal of the scene bounding box. You can specify an additional density value that is added to every pixel inside or behind a patchy fog boundary using the function pfVolFogSetVal() with parameter PFVFOG_FOG_DENSITY_BIAS. This value makes a patchy fog appear denser but it may create unrealistically sharp boundaries. The patchy fog color is set by function pfVolFogSetColor() or by calling pfVolFogSetAttr() with parameter PFVFOG_COLOR and a pointer to an array of three floats. If the blend_color extension is not available, patchy fog will be white. The patchy fog mode is set by function pfVolFogSetVal() with parameter PFVFOG_PATCHY_MODE and one of PFVFOG_LINEAR, PFVFOG_EXP, or PFVFOG_EXP2. It is also possible to set the mode both for a patchy and layered fog at once by using parameter PFVFOG_MODE. The default mode is PFVFOG_LINEAR. Note: The parameters of a patchy fog can be modified at any time and they will affect the rendering of the subsequent frame. Advanced Features of Layered Fog and Patchy Fog This section describes the following topics: • 007-1680-100 “Enabling Self-Shadowing of a Layered Fog and Scene Darkening” 193 6: Creating Visual Effects • “Animating Patchy Fog” • “Selecting a Different Type of Patchy Fog Algorithm” • “Simulating Self-Shadowing in Patchy Fog” • “Layered Patchy Fog” • “Light Shafts” The example in /usr/share/Performer/src/sample/C++/volfog on IRIX and Linux and in %PFROOT%\Src\sample\C++\volfog on Microsoft Windows illustrates the use of all these advanced features. Enabling Self-Shadowing of a Layered Fog and Scene Darkening A layered fog can be self-shadowed—that is, the lower parts of a dense fog appear darker. Self-shadowing is enabled by setting the flag PFVFOG_FLAG_SELF_SHADOWING to 1. The fog mode should be set to PFVFOG_EXP. When the fog has different colors at different elevations and the flag PFVFOG_FLAG_FOG_FILTER is set to 1, a secondary scattering is approximated. In this case, the color of a higher layer may affect the color of a lower layer. If the flag PFVFOG_FLAG_DARKEN_OBJECTS is set, even the objects below a dense fog become darker. The light is assumed to come from the top. Animating Patchy Fog A patchy fog can be animated by modifying the geometry of the fog nodes. When changing the content of geosets specifying the fog boundary, make sure that the geosets are fluxed and that the bounding box of each geoset is updated. In addition, function pfVolFogAddNode() has to be called every time the fog bounding box changes. Selecting a Different Type of Patchy Fog Algorithm It is possible to use a different algorithm for rendering patchy fog that can handle semi-transparent surfaces better. To use this algorithm, set the flag PFVFOG_FASTER_PATCHY_FOG to 1. Some advanced features of patchy fog described in the following subsections are supported only in one of the two algorithms. In such cases, this limitation is noted. 194 007-1680-100 Patchy Fog and Layered Fog Simulating Self-Shadowing in Patchy Fog If the flag PFVFOG_FASTER_PATCHY_FOG is set to 1, the algorithm also allows the color of the patchy fog boundary to be modified using a texture. Either a built-in 1D texture expressing the attenuation between two elevations is used or you can provide a 1D or a 3D texture for each volume object. This can be used to simulate self-shadowing of dense gases, such as clouds. The built-in 1D texture is enabled by setting the flag PFVFOG_FLAG_PATCHY_FOG_1DTEXTURE. The texture is mapped to the range of elevations between the bottom and top of the fog bounding box. The texture value at the bottom (default of 0.3) can be modified by calling pfVolFogSetVal() with parameter PFVFOG_PATCHY_TEXTURE_BOTTOM and the value at the top (default of 1.5) using parameter PFVFOG_PATCHY_TEXTURE_TOP. To use a different scale for objects of different sizes, you must specify the fog objects separately. When the flag PFVFOG_FLAG_SEPARATE_NODE_BINS is set, all calls to pfVolFogAddNode() define fog nodes that are drawn separately, and the predefined texture is scaled according to the bounding box of each node. If both the flag PFVFOG_FLAG_PATCHY_FOG_1DTEXTURE and the flag PFVFOG_FLAG_USER_PATCHY_FOG_TEXTURE are set, textures associated with the fog nodes are used to modify the surface color of a patchy fog. To avoid artifacts on overlapping colored patchy fog objects the flag PFVFOG_FLAG_DRAW_NODES_SEPARATELY forces the algorithm to be applied to each node separately in the back-to-front order with respect to the viewpoint. Currently, this mode does not work well when scene objects intersect fog objects. Layered Patchy Fog If the flag PFVFOG_FLAG_LAYERED_PATCHY_FOG is set, the layered fog is used to define the density of a patchy fog. The layered fog is then present only in areas enclosed by the patchy fog boundaries. Since layered fog is computed for the whole scene, it is important to set fog parameter PFVFOG_MAX_DISTANCE to a value that corresponds to the size of the patchy fog area (for example, a diameter of its bounding sphere). Use function pfVolFogSetVal() to modify the maximum distance parameter. Layered patchy fog nodes can be moved and rotated by specifying a matrix for each fog node, identified by its index (the order in which nodes were specified). The function pfVolFogSetAttr() with three parameters specified can be used for this purpose. The first 007-1680-100 195 6: Creating Visual Effects parameter is PFVFOG_ROTATE_NODE, the second parameter specifies the node index, and the last one is a pointer to a pfMatrix. Light Shafts Light shafts are a special application of a layered patchy fog. The fog boundary specifies a cone of light with decreasing intensity (density) along the cone axis. Additional rendering passes darken the objects outside the cone of light and lighten the objects inside the light shaft based on their distance from the light. To enable these additional passes, set flag PFVFOG_FLAG_LIGHT_SHAFT to 1. To ensure that these passes are applied even if the light shaft is not in the field of view, you must also set flag PFVFOG_FLAG_FORCE_PATCHY_PASS to 1. To control the additional passes, the parameter PFVFOG_LIGHT_SHAFT_DARKEN_FACTOR (set using pfVolFogSetAttr()) can change the factor by which all objects outside the light shaft are darkened. The default value is 0.3. Parameters PFVFOG_LIGHT_SHAFT_ATTEN_SCALE and PFVFOG_LIGHT_SHAFT_ATTEN_TRANSLATE set the translate and scaling of a built-in, one-dimensional texture that is used to reduce the color of objects lit by the light. Set the translate to a small value—for example, 10 to 20% of the shaft length—and the scale to the inverse of the shaft length. Performance Considerations and Limitations The quality and speed of patchy fog rendering can be controlled by calling pfVolFogSetVal() with the parameter PFVFOG_RESOLUTION. The resolution is a value between 0 and 1. Higher values will reduce banding and speed up the drawing. On the other hand, high values may cause corruption in areas of many overlapping fog surfaces. The default value is 0.2, but you may use values higher than that if your fog boundaries do not overlap much. The following are other performance considerations: • 196 The multipass algorithms used for rendering layered and patchy fog may produce incorrect results if the scene graph contains polygons that have equal depth values. To avoid such problems, a stencil buffer is used during rendering of the second pass. You can disable this function by setting the flag PFVFOG_FLAG_CLOSE_SURFACES to 0. 007-1680-100 Patchy Fog and Layered Fog 007-1680-100 • By default, the multipass algorithm is applied only when boundaries of a patchy fog are visible. This may cause undesirable changes of semi-transparent edges of scene objects when fog objects move into or away from the view. To force the use of the multipass algorithm, set the flag PFVFOG_FLAG_FORCE_PATCHY_PASS to 1. • Cull programs (see “Cull Programs” in Chapter 4) can speed up rendering of patchy fog because in some draw passes only the part of the scene intersecting the fog boundary is rendered. To enable cull programs, set the flag PFVFOG_FLAG_USE_CULL_PROGRAM to 1. • A layered fog is faster to render than a patchy fog; use a layered fog instead of a patchy fog whenever possible. Rendering of both types of fog together is even slower; so, you may try to define only one type. • Changing the fog mode does not affect the rendering speed in the case of a layered fog but rendering of a patchy fog is slower for fog modes PFVFOG_EXP and PFVFOG_EXP2. If you prefer using non-linear modes, try to use them only for layered fog and not for patchy fog. • You can speed up drawing of a patchy fog by reducing the size of the fog boundaries. In case of several disjoint fog areas, the size of a bounding box containing all boundaries will affect the draw time and quality. Try to avoid defining a patchy fog in two opposite parts of your scene. Try also to increase the value of resolution (if there are not too many overlapping fog boundaries) or reduce the patchy fog density. • If there is a lot of banding visible in the fog, try to choose a visual with as many bits per color component as possible. Keep in mind that a patchy fog needs a stencil buffer. You can also try to apply all techniques mentioned in the previous item— reducing the size of patchy fog boundaries, increasing resolution, or decreasing density. • If a patchy fog looks incorrect (the fog appears outside the specified boundaries) make sure that the vertices of the fog boundaries are specified in the correct order so that front faces always face outside the foggy area. • If you see a darker band in a layered fog at eye level, make sure the texture size is set so that the second value is equal to or greater than the third value. • Since light shafts are using a combination of layered and patchy fog and the density is decreasing to 0 at the end of the light cone, the quality of results is very sensitive to the depth of color buffers. 12-bit visuals are required and the light shaft should not be too large. Also, ensure that PFVFOG_MAX_DISTANCE is set as small as possible. 197 6: Creating Visual Effects OpenGL Performer has the following limitations in regards to fog management: Layered fog • The values of a layered fog are determined at each vertex and interpolated across a polygon. Consequently, an object located on top of a large ground polygon may be fogged a bit more or less than the part of the polygon just under the object. • A layered fog works fast with a 3D texture. Reloading of 2D textures during the animation can be slow. Patchy fog • The method does not work well for semitransparent surfaces. If your scene contains objects that are semitransparent or that have semitransparent edges, (for example, tree billboards or mountains in Performer Town), these objects or edges may be cut or may be fogged more than the neighboring pixels. Even if a semitransparent edge of a billboard is outside the fog, it will not be smooth. • A layered patchy fog is extremely sensitive to the size of the fog area and the density of the layered fog. Specifically, the fog values accumulated along an arbitrary line crossing the bounding box of the fog area should not reach 1. • A patchy fog needs a stencil buffer and the deepest color buffers possible.The rendering quality on a visual with less than 12 bits per color component is low unless the fogged area is very small compared to the size of the whole scene. • If the blend_color extension is not available, the patchy fog color will be white. Real-Time Shadows You can create real-time shadows using the class pfShadow. You specify a set of light sources and a set of objects that cast shadows on all other objects in the scene. The class manages the drawing and renders shadows for each combination of a shadow caster and a light source. Shadows are rendered by projecting the objects as seen from the light source into a texture and projecting the texture onto a scene. To avoid computing the texture for each frame, a set of textures is precomputed at the first frame, then for each frame the best representative is chosen and warped to approximate the correct shadow. The following sections further describe real-time shadows: 198 • “Creating a pfShadow” • “Drawing a Scene with Shadows” 007-1680-100 Real-Time Shadows • “Specifying Shadow Parameters” • “Assigning Data with Directions” • “Limitations of Real-Time Shadows” Creating a pfShadow A pfShadow is not part of the scene graph; it is created separately by the application process. Once the pfShadow is created, you can specify the number of shadow casters by calling function pfShadowNumCasters() and then set each caster using the function pfShadowShadowCasters(). Each shadow caster is specified by a scene graph node and a matrix that contains the transformation of the node with respect to the scene graph root. Shadow casters are indexed from 0 to the number of casters minus 1. Similarly, the number of light sources is set by function pfShadowNumSources(). A light source is defined by its position or direction, set by pfShadowSourcePos() or pfShadowLight(). A pfShadow needs information about the current eye position and view direction. Since this information is not directly accessible in a draw process, it is necessary to call pfShadowAddChannel() for each channel at the beginning of the application. Whenever the view changes, the application process has to call pfShadowUpdateView(). Even if the view does not change, this function must be called at least once in single-process mode or as many times as the number of buffers in a pfFlux in multiprocess mode. Without updating the view, the shadow is not rendered correctly. The class initialization is completed by calling the function pfShadowApply() as shown in the following creation example: pfShadow *shd = pfNewShadow(); pfShadowNumCasters(shd, 2); pfShadowShadowCaster(shd, 0, node1, matrix1); pfShadowShadowCaster(shd, 1, node2, matrix2); pfShadowNumSources(shd, 1); pfShadowSourcePos(shd, 0, x1, y1, z1, w1); pfShadowAddChannel(channel); pfShadowApply(shd); 007-1680-100 199 6: Creating Visual Effects Table 6-6 summarizes the functions for the initialization and drawing of a pfShadow. Table 6-6 200 pfShadow Functions Function Action pfNewShadow() Create a pfShadow. pfShadowNumCasters() Set number of shadow casters. pfShadowShadowCaster() Set a shadow caster and its rotation matrix. pfShadowAdjustCasterCenter() Specify the translation of caster's center. pfShadowNumSources() Set number of light sources. pfShadowSourcePos() Specify light source position. pfShadowLight() Specify light source. pfShadowAmbientFactor() Set ambient factor. pfShadowShadowTexture() Set a user-defined shadow texture for a given caster and light source. pfShadowTextureBlendFunc() Set a function used when blending closest shadows. pfShadowAddChannel() Add a channel on which pfShadow is used. pfShadowUpdateView() Update the current view for all stored channels. pfShadowUpdateCaster() Update rotation matrix of a caster. pfShadowFlags() Set binary flags. pfShadowVal() Set a single attribute. pfGetShadowDirData() Get a pfDirData associated with the pfShadow. pfShadowApply() Initialize a pfShadow. pfShadowDraw() Draw the scene and shadows. 007-1680-100 Real-Time Shadows The attributes of a pfShadow are listed in Table 6-7. Table 6-7 pfShadow Attributes Attribute Identifier Default Size of shadow texture PFSHD_PARAM_TEXTURE_SIZE 512 x 512 Number of shadow textures PFSHD_PARAM_NUM_TEXTURES 1 There is only one pfShadow flag, PFSHD_BLEND_TEXTURES. This blend-textures flag has a default of 0. Drawing a Scene with Shadows To draw a scene with real-time shadows, the draw process has to call the draw function provided by the pfShadow class: pfShadowDraw(). Before the first frame is rendered, all required shadow textures are precomputed. A warning is printed if the window size is smaller than the texture dimensions. Ensure that the window is not obscured; otherwise, the textures will not be correct. By default, only the closest shadow texture is selected for any direction and it is skewed so that it approximates the correct shadow. Optionally, the flag PFSHD_BLEND_TEXTURES can be set using the function pfShadowFlags(). In this case, the two closest textures are selected and blended together, resulting in smoother transitions. Also, instead of a linear blend between the textures, you can define a blend function, mapping values 0–1 to the interval 0–1. The blend function can be set using the function pfShadowTextureBlendFunc(). Every time the caster changes its position or orientation with respect to the light source, it is necessary to update its matrix using pfShadowUpdateCaster() (the caster is identified by its index). When the caster's matrix changes, the shadow of the caster changes as well. In this case, the set of precomputed shadow textures is searched to find the one or two closest representatives. 007-1680-100 201 6: Creating Visual Effects Specifying Shadow Parameters The shadow texture is used to darken the scene pixels when the texture texel is set to 1. The amount by which the scene pixel is darkened can be set by the function pfShadowAmbientFactor(). The default value is 0.6 As the caster is projected into a shadow texture, the center of the projection corresponds with the center of the bounding box of the caster's node. When the shadow texture is skewed to approximate shadows from a slightly different direction, it is best if the center of the projection corresponds with the center of the object. The bounding box center may not coincide with the center of the object (in the case of some long protruding parts) and you can use the function pfShadowAdjustCasterCenter() to shift the bounding box center toward the center of the object. For each combination of a shadow caster and a light source, it is possible to specify the number of shadow textures used, their sizes, and a set of directions for which the textures are precomputed. The number of textures and their sizes can be set by the function pfShadowVal(), where the first parameter is PFSHD_PARAM_TEXTURE_SIZE or PFSHD_PARAM_NUM_TEXTURES. The set of directions can be controlled by using the function pfGetShadowDirData() to get the pointer to the corresponding pfDirData, a class that stores data associated with a set of directions. Then you can either select the default mode or specify the directions directly. See following section “Assigning Data with Directions” for more details. By default, there is one texture of size 512 x 512 and the direction corresponds to the light direction (or a vector from a point light source to the object's center). If there are more textures, the original light direction is rotated around a horizontal direction, assuming that the object will primarily keep its horizontal position (for example, a helicopter or a plane). A sample implementation of shadows is in the file perf/samples/pguide/libpf/C++/shadowsNew. Assigning Data with Directions The pfDirData class is used to store directional data—that is, data that depend on direction. A pfDirData stores an array of directions and an array of (void *) pointers representing the data associated with each direction. 202 007-1680-100 Real-Time Shadows The directions and data can be set using the function pfDirDataData(). Optionally, you can set only the directions using the function pfDirDataDirections() in the case that the associated data are defined later or generated internally by another OpenGL Performer class (such as pfShadow). You can also generate directions automatically using the function pfDirDataGenerateDirections(). The first parameter defines one of the default sets of directions and the second parameter is used to specify additional values. At present only type PFDD_2D_ROTATE_AROUND_UP is supported, in which case the second parameter points to a 3D vector that is rotated around the up vector, creating a number of directions. The data can be queried using the pfDirDataFindData() or pfDirDataFindData2() function. In the first case, the function finds the closest direction to the direction specified as the first parameter, copies it to the second parameter, and returns the pointer to the data associated with it. The input direction has to be normalized. The second function finds the two closest directions to the specified direction. It copies the two directions to the second parameter (which should point to an array of two vectors). The two pointers to the data associated with the two directions are copied to the array of two (void *) pointers specified as the third parameter. In addition, two weights associated with each direction are copied to the array of two floats. These weights are determined based on the distance of the end point of the input direction and each of the two closest directions. Limitations of Real-Time Shadows The following are limitations of real-time shadows in OpenGL Performer: • When projecting a caster into a shadow texture, pfSwitch children are selected according to switch value. In the case of pfLOD, the finest level is chosen. Also, pfSequences are ignored—which can be useful in the case of helicopter rotors, for example. • The pfShadow class uses cull programs to cull out geometry that is not affected by the shadow to make the multipass drawing more efficient. At present, though, the cull program used by the pfShadow class overwrites any other cull program you specify. Note: Ensure that you do not overwrite TravMode in your application by setting it to PFCULL_ALL. The mode is set by pfShadow when pfShadowApply() is called. 007-1680-100 203 6: Creating Visual Effects • When projecting a caster into a shadow texture, pfSwitch and pfLOD may not be handled properly. Also, pfSequences are ignored—which can be useful in case of helicopter rotors, for example. Image-Based Rendering The image-based rendering approach is used for very complex objects. Such an object is represented by a set of images taken from many directions around it. When the object is rendered for each view direction, several closest views are blended together. In OpenGL Performer, you can use the pfIBRnode class to represent complex objects. Unlike a pfBillboard, a parent class of pfIBRnode, the texture on pfGeoSets of a pfIBRnode is not static, but it changes based on the view direction for each pfGeoSet. The following sections further describe image-based rendering: • “Creating a pfIBRnode” on page 204 • “Creating a pfIBRnode Using a Proxy” on page 205 • “Creating a pfIBRtexture” on page 206 • “Parameters Controlling Drawing of a pfIBRnode” on page 208 • “The Simplify Application” on page 209 • “Creating Images of an Object with makeProxyImages” on page 215 • “Creating Images of an Object with makeIBRimages” on page 219 • “Limitations” on page 220 Creating a pfIBRnode A pfIBRnode is a child class of pfBillboard. You create a pfIRRnode in a fashion similar to that of a pfBillboard. Compared to a pfBillboard, a pfIBRnode has two additional parameters: a pfIBRtexture and an array of angles defining the initial rotation of the objects. Each pfIBRnode has associated with it a single pfIBRtexture, which stores a set of images of the complex object as viewed from different directions. Each pfGeoSet is then rendered 204 007-1680-100 Image-Based Rendering with a texture representing the view of the object from the given direction. A pfIBRtexture is specified using the function pfIBRnodeIBRtexture(). Using the function pfIBRnodeAngles(), you control the initial orientation of the complex object by specifying the rotation from the horizontal and vertical planes for each pfGeoSet. These angles are very useful in case of trees, for example, because you can use a different vertical angle for each instance of the tree. The trees then appear different, although they all use the same pfIBRtexture. The first value is ignored in the case that only one ring of views around the object is used. You must set up a pfIBRnode so that the pfIBRtexture applied to it can modify properly the image at each frame. You do so in the following manner: 1. Set the texture of the pfGeoState associated with each pfGeoSet of the pfIBRnode to the texture returned by the function pfGetIBRtextureDefaultTexture(). 2. If the pfIBRtexture has the flag PFIBR_USE_REG_COMBINERS set, enable multitexturing and specify texture coordinates for additional texture units. 3. If the pfIBRtexture has the flag PFIBR_3D_VIEWS enabled, set the billboard rotation (PFBB_ROT) to PFBB_AXIAL_ROT. On IRIX and Linux, see the example in the following file: /usr/share/Performer/src/sample/pguide/C++/IBRnode.C On Microsoft Windows, see the example in the following file: %PFROOT%\Src\sample\pguide\C++\IBRnode.C Creating a pfIBRnode Using a Proxy By default, it is assumed that the geosets of the pfIBRnode specify rectangles that are always facing the viewer (like billboards). This approach is very fast but it requires a large number of views to limit the artifacts due to the differences between the neighboring views. To reduce the number of views required to obtain a reasonable image of the complex object from any direction, we can use a shape that approximates the surface of the complex object instead of a billboard. This shape is called a proxy. The closer the proxy is to the original surface, the fewer views of the objects are required. Optimally, you create a proxy that contains a relatively small number of primitives and that is very close 007-1680-100 205 6: Creating Visual Effects to the original surface. The proxy can be created using the new tool Simply. See section “The Simplify Application” on page 209 for the details. Compared to default mapping of views on a billboard there are only minor changes. Instead of a billboard, the node's geosets contain the proxy geometry. The pfIBRtexture associated with the node has the flag PFIBR_USE_PROXY set. There is an array of texture coordinates indexed by the view index and the geoset index. These texture coordinates can be defined and queried by pfIBRnodeProxyTexCoords() and pfGetIBRnodeProxyTexCoords(). Note that it is more efficient to store the proxy in one geoset. Optionally, it is possible to specify different geosets for each view (if the PFIBR_NEAREST flag is set in the pfIBRtexture assigned to the pfIBRnode) or for each group of views if the views are blended. In this case, you must set the flag PFIBRN_VARY_PROXY_GEOSETS using pfIBRnodeFlags(). This can be useful for removing the invisible parts of the proxy (invisible from the range of views in the group) or for sorting the proxy triangles to avoid artifacts when edges of the proxy textures are transparent. The array of texture coordinates is then organized as follows: • The first index is the view index or the group index (if the views are blended). • The second index is the geoset index multiplied by the number of views in a group (1 for the nearest view). • The coordinates are grouped by geosets. Thus, there are texture coordinates for the geoset 0 for all views in the group, then for geoset 1, and so on. The geosets are organized as follows: if the proxy has n geosets and there are v views or groups of views, the pfIBRnode has n*v geosets, and each group of n geosets belongs to one view. To create views of a complex object from various directions and to compute the texture coordinates of its proxy, you can use the makeProxyImages tool described in section “Creating Images of an Object with makeProxyImages” on page 215. Creating a pfIBRtexture A pfIBRtexture stores a set of images of a complex object as viewed from different directions. The directions are specified using pfIBRtextureIBRdirections(). Internally, 206 007-1680-100 Image-Based Rendering pfIBRtexture uses pfDirData to store the views. A pfDirData determines the type of view distribution. It could be a set of views around the object with all views perpendicular to the vertical axis, or the views can be from a set of rings and each ring contains an array of evenly spaced views that have the same angle from the horizontal plane. Otherwise, the views are assumed to be uniformly or randomly distributed around the sphere of directions. You must specify the directions before the images are set. Once you specify the directions, you set the images using pfIBRtextureIBRtextures(). The parameters are an array of pointers to the textures containing the views and the number of the textures in this array. If views are organized in rings, you can load the images directly from a set of files using pfIBRtextureLoadIBRtexture() without the need to specify the directions first. The parameter format specifies the path where the images are stored as well as how they are indexed—for example, images/view%03d.rgb. The other two parameters specify the number of images and the increment between two loaded images. The increment specification is useful when the texture memory is limited; for instance, specifying step=2 causes every second image to be skipped. Optionally, you can specify the views using the function pfIBRtextureIBRtextures(). The parameters are an array of pointers to the textures containing the views and the number of the textures in this array. If the views are organized in rings, the textures, by default, represent views around the object, all perpendicular to the vertical axis. In this case, specified textures form a single ring of views that are evenly spaced. If the flag PFIBR_3D_VIEWS is specified by the function pfIBRtextureFlags(), the textures form a set of rings. Each ring contains an array of evenly spaced views that have the same angle from the horizontal plane. If the flag PFIBR_3D_VIEWS is not set, both functions pfIBRtextureLoadIBRtexture() and pfIBRtextureIBRtextures() will set one ring with the specified number of textures and a horizontal angle of 0. If the flag PFIBR_3D_VIEWS is set, the class checks whether a file info is present in the image directory. If it is, the information about rings is loaded from that file. The file contains two values on each line: the horizontal angle and the number of textures at each ring. If the file is not present in the image directory, you must specify the rings before the images are loaded by calling the functions pfIBRtextureNumRings() and pfIBRtextureRing(). Rings are indexed from 0 and should be ordered by the horizontal angle, with the lowest angle at index 0. Each ring can have a different number of textures associated with it. When 3D views are used, the image files read by function pfIBRtextureLoadIBRtexture() should be indexed by the ring index and the index of the 007-1680-100 207 6: Creating Visual Effects image in a given ring. Specify the format string in the manner shown in the following example: images/view%02d_%03d.rgb If you specify the textures using the function pfIBRtextureIBRtextures(), the texture pointers are all stored in a single array, starting with textures of the first ring, followed by textures of the second ring, and so on. It is assumed that the views in each ring are uniformly spaced and they are ordered clockwise with respect to the vertical axis. If the views are ordered in the opposite direction, use the function pfIBRtextureDirection() to set the direction to –1. When using pfIBRnodes and pfIBRtextures in Perfly, you need an alpha buffer. If the pfIBRnode is rendered as an opaque rectangle, try the command-line parameter –9, in which case Perfly requests a visual with an alpha buffer. For more details about associating a pfIBRtexture with a pfIBRnode, see the pfIBRnode man page and the following program: /usr/share/Performer/src/sample/pguide/C++/IBRnode (IRIX and Linux) %PFROOT%\Src\sample\pguide\C++\IBRnode (Microsoft Windows) Parameters Controlling Drawing of a pfIBRnode At present, the pfIBRtexture class is used only by the pfIBRnode class. The pfIBRtexture class provides a draw function for pfGeoSets that belong to the pfIBRnode, but the draw process is transparent to you. You can control the drawing by setting flags using the function pfIBRtextureFlags(). If the flag PFIBR_NEAREST is set, the closest view from the closest ring is selected and applied as a texture of the pfGeoSet. This approach is fast on all platforms, but it results in visible jumps when the texture is changed. Thus, by default, the flag PFIBR_NEAREST is not set and the two or, in case of 3D views, four closest views are blended together. If the graphics hardware supports register combiners, flags PFIBR_USE_REG_COMBINERS and PFIBR_USE_2D_TEXTURES are automatically set by the class constructor and blending of textures can be done in one pass. The flag PFIBR_USE_PROXY is used when the views are mapped on an approximation of the complex object (a proxy) and a different draw function is applied. You can read more about proxies in section “Creating a pfIBRnode Using a Proxy” on page 205. 208 007-1680-100 Image-Based Rendering By default on IRIX, the flag PFIBR_USE_2D_TEXTURES is not set and a 3D texture is used for fast blending between the two closest views. To avoid flickering when the object is viewed from a distance, additional 3D textures are used to store additional mipmap levels. This feature is available on machines with multisampling only (InfiniteReality systems). To disable the mipmapping, set flag PFIBR_MIPMAP_3DTEXTURES to zero. In case of several rings, the nearest ring is selected and the views inside this ring are blended using the 3D texture. 3D texture is not compatible with other distributions of the views. Hence, in this case, ensure that you set flag PFIBR_USE_2D_TEXTURES. The Simplify Application The Simplify application is an interactive tool that is used to simplify a complex object. It has the following two main functions: • Create a regular simplification of an object • Create a proxy of an object In a regular simplification of an object, the resulting geometry does not cross the inner and outer boundaries of the original object. The distance of these boundaries from the original object controls the coarseness of the resulting geometry. All vertex parameters, such as the normal or texture coordinates, are preserved. A simplified version of the object can be used to create a pfLOD node (see section “pfLOD Nodes” on page 70). A proxy is a simplified version of the object where the original object is fully inside the proxy. This property is important because the proxy is used in image-based rendering where the images of a complex object from various directions are projected onto the proxy. In this way, it is possible to render a very complex object using a simplified version (a proxy) and store the surface detail, including the associated lighting, in multiple textures. See section “Creating Images of an Object with makeProxyImages” on page 215 for the process of making the textures that are projected on the proxy. The Simplify Graphical User Interface (GUI) The Simplify application is based on the Perfly application and they share many command-line parameters and key commands (see the man page for perfly). The syntax for the command-line invocation is as follows: simplify [ perfly-options ] infile outfile [ simplification-options ] 007-1680-100 209 6: Creating Visual Effects You can get the list of the simplication options by running simplify with no option or with only the option –h. When you start the Simplify application, the menu is similar to that of Perfly. There is an additional pane of buttons and sliders, called the Simplify pane, which can be enabled and disabled using the Simplify pane button. Figure 6-3 shows the Simplify pane, which is enabled by default. Most of the buttons and sliders on the Simplify pane have command-line equivalents. Figure 6-3 210 The Default Simplify Pane 007-1680-100 Image-Based Rendering Making a Proxy of an Object Computing a proxy with Simplify requires two basic decisions: • Where to position the initial proxy and an outer boundary for the original object • What algorithm to use for creating the initial proxy and the outer boundary Since these decisions may be difficult to make in an analytical fashion initially, the Simplify GUI allows you to make some guesses and refine them in an iterative fashion. The following procedure for making a proxy assumes that you have invoked the Simplify application using the default simplification options. 1. Ensure that the Simplify into proxy button is selected (the default). 2. Specify the initial distance of the proxy from the object and an outer boundary. Use the sliders Initial distance and Outer boundary to do this. Distances are specified as a percentage of the object diameter (more precisely, the diameter of the object's bounding sphere). Initially, you might want to use the defaults, 2% for Initial distance and 5% for Outer boundary. 3. Select the algorithm for creating the initial proxy. Simplify provides two algorithms: the marching cubes algorithm and the deplace-along-normals algorithm. The first button on the Simplify pane is the Do marching cubes button, which is selected by default. If the Do marching cubes button is not selected, Simplify uses the deplace-along-normals algorithm. The marching cubes algorithm creates an isosurface at a certain distance (slider Iso distance) from the original object. The isosurface is later moved to the distance of the outer boundary (slider Outer boundary) and a copy of the isosurface is moved to the distance of the initial proxy (slider Initial distance). The marching cubes algorithm has the following additional controls: • Grid Size X slider • Grid Size Y slider • Grid Size Z slider • Iso distance slider With these controls, you can set the grid size at each axis and the distance of the isosurface from the object (using the slider Iso distance). The finer the grid, the longer the algorithm takes and the more complex the initial proxy. On the other hand, if the grid is too coarse, many details may be missed. 007-1680-100 211 6: Creating Visual Effects In general, the algorithm does not work very well if the desired isosurface distance is too small compared to the size of a grid voxel. For this reason, it is possible to specify the isosurface distance separately from the outer boundary distance and the initial proxy distance. Often it is possible to specify the isosurface distance large enough so that the isosurface does not miss any part of the object and then move it closer as needed. It is also possible to preview the isosurface by clicking the button Get isosurface while the button Show boundary is selected. If you select the deplace-along-normals algorithm, the outer boundary and the initial proxy are created by displacing the original surface along its normals. This approach works better in the case where distances are very small. Unfortunately, some areas of the object may not be simplified. For example, if two parts of the object are touching, displacing along the normals will create a self-intersecting boundary that will not allow any room for simplification in the area of intersection. With the deplace-along-normals algorithm, the grid is used to accelerate the intersection test of the simplified proxy with the boundary surfaces. Thus, do not reduce the grid resolution too much. 4. Click the Run simplify proxy button to start the simplification. The simplification algorithm starts by moving the isosurface or the original surface to create the outer boundary and the initial proxy. The initial proxy is simplified by removing vertices and edges as long as the surface is within the surfaces defined by the object and the outer boundary. At the end, the vertices of the proxy are moved as close to the original object as possible. After completing the computation, the proxy is saved in the file specified on the command line. The simplification algorithm can be stopped or paused by clicking the Stop simplify or Pause button, respectively. When the algorithm is paused, it is possible to save the current proxy by clicking the Save mesh button. The file name contains the index of the current step so that several meshes can be output during the simplification. Simplifying an Object The procedure for a regular simplification is very similar to the procedure for making a proxy, as described in the preceding section. In contrast to making a proxy, however, Simplify uses two boundary surfaces, an outer boundary (set by the slider Outer boundary) and an inner boundary (set by the slider Inner boundary) to create a regular simplication. 212 007-1680-100 Image-Based Rendering To simplify an object requires two basic decisions: • Where to place an outer boundary and inner boundary • What algorithm to use for creating the boundaries The following procedure for making a proxy assumes that you have invoked the Simplify application using the default simplification options. 1. Ensure that the Simplify into proxy button is not selected. This is not the default. Figure 6-4 shows the resulting Simplify pane. Figure 6-4 007-1680-100 The Simplify Pane for Simplifying an Object 213 6: Creating Visual Effects 2. Specify inner and outer boundaries. Use the sliders Inner boundary and Outer boundary to do this. Distances are specified as a percentage of the object diameter (more precisely, the diameter of the object's bounding sphere). Initially, you might want to use the defaults, 2.5% for Inner boundary and 5% for Outer boundary. 3. Select the algorithm for creating the boundaries. Simplify provides two algorithms: the marching cubes algorithm and the deplace-along-normals algorithm. The first button on the Simplify pane is the Do marching cubes button, which is selected by default. If the Do marching cubes button is not selected, Simplify uses the deplace-along-normals algorithm. See the preceding section for a description of the algorithms. If you select the marching cubes algorithm, the distance of both boundaries from the original surface is the same (in absolute value) and it is controlled by the slider Iso distance. As in the case of making a proxy, the isosurface can be previewed by clicking the Get isosurface button. If you select the deplace-along-normals algorithm, the boundaries are created by moving the original surface along its normals to distances specified by the sliders Outer boundary and Inner boundary. Note that the distance for the inner boundary is specified as a negative number. 4. Click the Run simplify button to start the simplication. The computation can be paused or stopped by clicking the Pause or Stop simplify button, respectively. When the algorithm is paused, it is possible to save the intermediate result by clicking the Save mesh button or to toggle the visibility of the boundary by clicking the Show boundary button. After the simplification is finished you can display the original object by clicking the Restore object button. You can restart the algorithm without restoring the original object. As you may realize, this procedure could be used to create an object proxy if you select the displace-along-normals algorithm and the inner boundary is set to zero. The result may be different, though, because the algorithm is trying to preserve seams between pfGeoSets with different pfGeoStates; the seam preservation is not necessary for the proxy. 214 007-1680-100 Image-Based Rendering Creating Images of an Object with makeProxyImages You can use the program makeProxyImages to create images (views) of the specified object from a set of directions. Since the images are being projected on a proxy, a simplification of the original object, additional processing may be required to add views of parts of the proxy that are partially or fully obstructed by other parts. These additional texture pieces are important because as the proxy is rotated away from the view at which the texture was computed, some parts of the proxy that were not directly visible from the view may become visible. Thus, each image consists of the view of the object and a collection of texture pieces for obstructed parts of the proxy. It is necessary to store texture coordinates for each proxy triangle so that the texture pieces are correctly mapped. Consequently, the program makeProxyImages outputs not only textures storing the views but also a pfIBRnode that contains the texture coordinates and the proxy geometry. You can create the proxy of an object using the program Simplify. Command-Line Options for makeProxyImages The input to the makeProxyImages program is the file containing the original complex object. Table 6-8 and the following sections describe other key, command-line options: • “Packing Additional Textures Pieces” on page 216 • “Fine Tuning Texture Rendering” on page 217 • “Potential Problems” on page 218 Table 6-8 007-1680-100 Key Command-Line Options of makeProxyImages Command Option Description –pf Specifies the file containing the proxy. –f Specifies the files where the images are stored. A view number and the extension rgb is added automatically. –pfb Specifies the file where the resulting pfIBRnode is stored. –W Specifies the size of the texture (–W xsize ysize). It is important to specify the size. –o Specifies the oversampling factor. Specify this option when the hardware does not support antialiasing. 215 6: Creating Visual Effects Table 6-8 Command Option Key Command-Line Options of makeProxyImages (continued) Description –rv Specifies the text file with ring information to determine the view directions. Each line of the ring file contains two values: the angle from the horizontal plane and how many views are created for that angle. –n Specifies that only views around the object are used. –nv Specifies that uniformly distributed 3D views are used. –sk Enables skipping a certain number of views in each ring. –s Scales up the object. By default, the program uses orthographic projection. The center of the projection is the center of the bounding sphere around the object and the object is scaled so that the bounding sphere fits the window. If the bounding sphere is too large you may try to upscale the object using the –s option to make better use of the texture. You can use perspective projection by defining the distance of the camera from the center of the bounding sphere. Unless there are reasons for doing otherwise, use the orthographic projection. –l Specifies non-default lighting. In image-based rendering the lighting is captured in the textures. Thus, it is important you specify the lights in the same way as in your scene. By default, the default Perfly lighting is selected. You can specify your own lights using the –l option: –l posx posy posz posw r g b You can use multiple –l specifications to define multiple lights. To obtain the full set of options, run the program makeProxyImages without any parameters. Packing Additional Textures Pieces By default, the program makeProxyImages renders only the view of the object without the extra texture pieces for obstructed triangles of the proxy. To enable this feature you have to add the option –ev. The process has two steps. First, the number and size of texture pieces is determined and a packing algorithm determines their position around the primary view. Second, for each view the texture pieces are rendered in place. The packing algorithm operates on the pixel level and there are several options that affect its speed and the quality of the results. To speed up the packing algorithm, you can 216 007-1680-100 Image-Based Rendering downsample the textures before packing using the option –evd. The drawback is that there may be more wasted space between texture pieces. You can also reduce the number of neighboring pixels the texture packing algorithm checks when finding the optimal place for texture pieces by using the option –evp. In general, the texture pieces are not aligned with their neighboring pieces. Thus, when the view texture is mipmapped, the gaps between the textures may become visible. For this purpose, you can add the option –evmp to set the number of mipmapping levels that will not have cracks. Each edge of the texture piece that is not a silhouette edge is extended to contain more pixels from neighboring triangles. Setting the value too high may cause the packing algorithm to fail. If the packing algorithm fails to place the texture pieces around the primary view, the object is scaled down a little (for the given view) and the algorithm is restarted. This process repeats until all the texture pieces fit. Similarly, as obstructed triangles may come into full view, backfacing triangles may become visible as the proxy is rotated away from the view. Thus, it is possible to add texture pieces for backfacing triangles into the view texture using the option –bf. Not all backfacing triangles are added but only those that may be visible from neighboring views. Since additional texture pieces that are used for backfacing triangles of the proxy can be found in neighboring views, it is advantageous to combine several views into a single texture. This reduces the number of texture pieces packed into a texture for one view. You can use the option –tm to control this. Do not exceed 2Kx2K when combining several views into one texture. Fine Tuning Texture Rendering When rendering additional texture pieces, you can control how far before and after the proxy triangle the clip planes are being set. This option affects triangles around the silhouette of the object. This is view-dependent: for each view, there are different triangles that contain the silhouette of the object. Since the proxy fully contains the original object, parts of the silhouette triangles may be transparent. This may cause visible cracks when the object is rotated. Moving the clip planes reduces some of the cracks. If you move the plane that is behind the triangle farther away (using option –evlb), some of the geometry that is behind the silhouette is included in the texture. When you move the front clip plane closer to the cameras (using option –evlf), some of the geometry that is in front of the silhouette is included in the texture. Because some proxy triangles may have a texture with transparent edges, it may be desirable to sort the proxy triangles. Because the proxy can be viewed from any direction, 007-1680-100 217 6: Creating Visual Effects it is necessary to determine how the triangles are sorted. If the proxy is rendered with only the nearest views selected, the triangles are ordered for each view differently. You must set that mode using the option –nr. By default, three or four of the nearest views are blended together. In that case, the proxy triangles are sorted for each group of views. Sometimes it may be possible to see changes in transparency as the view moves from one group of views to another. If this becomes too obvious, you can disable the sorting using the option –evns. Potential Problems Generally, you can drastically reduce problems if you place the proxy very close to the original object (especially around visible sharp edges) or if you increase the number of views. The following are some potential problems you might encounter: • The images are missing an alpha channel. If your machine does not support a single-buffered visual with at least 8 bits per red, green, blue, and alpha component, the images may be missing an alpha channel. Note the number of alpha bits printed at the beginning of the makeProxyImages output. On some SGI systems with multisampling, you may try to use the option –nms to request a visual without multisampling to improve the probability of getting a visual with an alpha channel. • Your textures are not antialiased. Do not forget to oversample the textures on machines with no antialiasing (using option –o). • The processing time is very long. The process may take a very long time if the proxy is very fine and many texture pieces have to be added to each view. Since the rendering is done into a window, ensure that you do not overlap the window during the process or that the screen saver does not start. If some of the textures are corrupt, you may restart the program with the same parameters and add the option –sfr, which skips the rendering of the specified number of textures. It is also a good idea to increase shared arena size (use environment variable PFSHAREDSIZE) to avoid memory overflow when the pfIBRnode is saved at the end. • Texture pieces intersect the image of the object. Inspecting the view textures, you may notice that sometimes the additional texture pieces may intersect the image of the object. This is fine because those triangles that are overlapped are assigned one of the additional texture pieces packed around the object. 218 007-1680-100 Image-Based Rendering Creating Images of an Object with makeIBRimages You can use the program makeIBRimages from the directory /usr/share/Performer/src/conv/ on IRIX and Linux and %PFROOT%\Src\conv on Microsoft Windows to create images (views) of a specified object from a set of directions. The input is a file that can be read by OpenGL Performer and the output is a set of images of that object that can be directly used as an input for a pfIBRtexture. The images are stored in a directory specified using the option -f. If a text file info is present in the output directory, a set of 3D views is rendered. The file has the same syntax as described in section “Creating a pfIBRtexture” on page 206. Each line of the file info contains two values: the angle from the horizontal plane and how many views are created for that angle. The images are then indexed by two integer values that are appended to the name specified by the option -f. The first value is the ring index of the views and the second one indexes the views within the ring. If the file info is not present, a set of N views (set by the option -n) is computed around the object using the horizontal angle of 0. In this case, only one index is appended to the image name. If you specify the option -pfb, the program outputs a pfb file in the specified directory. The file contains a single pfIBRnode that uses the created images. Note: Before loading perfly, ensure that PFPATH is set to the directory that contains the images. If your machine does not support a single-buffered visual with at least 8 bits per red, green, blue, and alpha component, the images may be missing the alpha channel. Note the number of alpha bits printed when makeIBRimages begins. When using pfIBRnodes and pfIBRtextures in perfly, you also need an alpha buffer. If the pfIBRnode is rendered as a full rectangle, try the command-line parameter –9, in which case perfly requests a visual with alpha. To obtain the full set of command-line options, run the program makeIBRimages without any parameters. 007-1680-100 219 6: Creating Visual Effects Limitations The following are current limitations of image-based rendering in OpenGL Performer: 220 • A pfIBRtexture applied to a pfIBRnode is not properly rotated when the pfIBRnode is viewed from the top. This may result in visible rotation of the texture with respect to the ground. • When the flag PFIBR_3D_VIEWS is set in a pfIBRtexture, do not use 3D textures. This mode is not implemented. 007-1680-100 Chapter 7 7. Importing Databases Once you have learned how to create visual simulation applications with OpenGL Performer, your next task is to import visual databases into those applications. OpenGL Performer provides import and export functions for numerous popular database formats to ease this effort. This chapter describes the following: • The steps involved in creating custom loaders for other data formats • Pre-existing file loading utilities • Several utility functions in the OpenGL Performer database utility library that can make the process of database conversion easier for you • Supported database formats • The Maya database exporter Overview of OpenGL Performer Database Creation and Conversion Source code is provided for most of the tools discussed in this chapter. In most cases the loaders are short, easy to understand, and easy to modify. Table 7-1 lists the subdirectories of /usr/share/Performer/src/lib on IRIX and Linux and %PFROOT%\Src\lib on Microsoft Windows where you can find the source code for the database processing tools. Table 7-1 007-1680-100 Database-Importer Source Directories Directory Name Directory Contents libpfdu General database processing tools and utilities. 221 7: Importing Databases Table 7-1 Database-Importer Source Directories (continued) Directory Name Directory Contents libpfdb Load, convert, and store specific database formats.. libpfutil Additional utility functions. Before you can import a database, you must create it. Some simulation applications create data procedurally; for examples of this approach, see the “SGI PHD Format” on page 268 or the “Sierpinski Sponge Loader” on page 280” sections of this chapter. In most cases, however, you must create visual databases manually. Several software packages are available to help with this task, and most such systems facilitate geometric modeling, texture creation, and interactive specification of colors and material properties. Some advanced systems support level-of-detail specification, animation sequences, motion planning for jointed objects, automated roadway and terrain generation, and other specialized functions. - Utilities for Creating Efficient OpenGL Performer Run-Time Structures libpfdu There are several layers of support in OpenGL Performer for loading 3D models and 3D environments into OpenGL Performer run-time scene graphs. OpenGL Performer contains the libpfdu library devoted to the import of data into (and export of data from) OpenGL Performer run-time structures. Note that two database exporters have already been written for the Medit and DWB database formats. At the top level of the API, OpenGL Performer provides a standard set of functions to read in files and convert databases of unknown type. This functionality is centered around the notion of a database converter. A database converter is an abstract entity that knows how to perform some or all of a set of database format conversion functions with a particular database format. Moreover, converters must follow certain API guidelines for standard functionality such that they can be easily integrated into OpenGL Performer in a run-time environment without OpenGL Performer needing any prior knowledge of a particular converter’s existence. This run-time integration is done through the use of dynamic shared object (DSO) libraries on IRIX and Linux. On Microsoft Windows this is accomplished using Dynamic-link Libraries (DLL). 222 007-1680-100 libpfdu - Utilities for Creating Efficient OpenGL Performer Run-Time Structures pfdLoadFile - Loading Arbitrary Databases into OpenGL Performer Table 7-2 describes the general routines for 3D databases provided by libpfdu. Table 7-2 libpfdu Database Converter Functions Function Name Description pfdInitConverter() Initialize the library and its classes for the desired format. pfdLoadFile() Load a database file into an OpenGL Performer scene graph. pfdStoreFile() Store a run-time scene graph into a database file. pfdConvertFrom() Convert an external run-time format into an OpenGL Performer scene graph. pfdConvertTo() Convert an OpenGL Performer scene graph into an external run-time format. The database loader utility library, libpfdu, provides a convenient function, named pfdLoadFile(), that imports database files stored in any of the supported formats listed in Table 7-6 on page 242. Loading database files with pfdLoadFile() is easy. The function prototype is pfNode *pfdLoadFile(char *fileName); pfdLoadFile() tests the filename-extension portion of fileName (the substring starting at the last period in fileName, if any) for one of the format-name codes listed in Table 7-6 on page 242, then calls the appropriate importer. The file-format selection process is implemented using dynamic loading of DSOs, dynamic shared objects, for IRIX and Linux and DLLs, dynamic link libraries, for Microsoft Windows. This process allows new loaders that are developed as database formats change to be used with OpenGL Performer-based applications without requiring recompilation of the OpenGL Performer application. Note: Subsequent general references in this manual to DSOs also pertain to DLLs unless otherwise noted. If at all possible, pfdInitConverter() should be called before pfConfig() for the potential formats that may be loaded. This will preload the DSO and allow it to initialize any of its own data structures and classes. This is required if the loader DSO extends 007-1680-100 223 7: Importing Databases OpenGL Performer classes or uses any node traversal callbacks so that if multiprocessing these data elements will all have been precreated and be valid in all potential processes. pfdInitConverter() automatically calls pfdLoadNeededDSOs_EXT() to preload additional DSOs needed by the loader if the given loader has defined that routine. These routines take a filename so that the loader has the option to search through the file for possible DSO references in the file. Loading Process Internals The details of the loading process internal to pfdLoadFile() include the following: 1. Searching for the named file using the current OpenGL Performer file path. 2. Extraction of the file-type extension. 3. Translation of the extension using a registered alias facility, formation of the DSO or DLL name. 4. Formation of a loader function name. 5. Finding that function within the DSO using either dlsym() on IRIX and Linux or GetProcAddress() on Microsoft Windows. 6. Searching first the current executable and loaded DSOs for the proper load function and then searching through a list of user-defined and standard directories for that DSO. Dynamic loading of the indicated DSO using dlopen() on IRIX and Linux and using LoadLibrary() on Microsoft Windows. 7. Invocation of the loader function. Loader Name The loader function name is constructed from two components: • A prefix always consisting of pfdLoadFile_. • Loader suffix, which is the file extension string. Note: The loader function pfdLoadFile_ must be exported using _declspec(dllexport) on Microsoft Windows only. 224 007-1680-100 libpfdu - Utilities for Creating Efficient OpenGL Performer Run-Time Structures Examples of several complete loader function names are shown in Table 7-3. Table 7-3 Loader Name Composition File Extension Loader Function Name dwb pfdLoadFile_dwb() flt pfdLoadFile_flt() medit pfdLoadFile_medit() obj pfdLoadFile_obj() pfb pfdLoadFile_pfb() Shell Environment Variables Several shell environment variables are used in the loader location process. These are PFLD_LIBRARY{N32,64}_PATH, LD_LIBRARY{N32,64}_PATH, and PFHOME. Confusion about loader locations can be resolved by consulting the sources mentioned earlier in this chapter to understand the use of these directory lists and reading the following section, “Database Loading Details” on page 225. When the pfNotifyLevel is set to the value for PFNFY_DEBUG (5) or greater, the DSO and loader function names are printed as databases are loaded, as is the name of each directory that is searched for the DSO. The OpenGL Performer sample programs, including perfly, use pfdLoadFile() for database importing. This allows them to simultaneously load and display databases in many disparate formats. As you develop your own database loaders, follow the source code examples in any of the libpfdb loaders. Then you will be able to load your data into any OpenGL Performer application. You will not need to rebuild perfly or other applications to view your databases. Database Loading Details Details about the database loading process are described further in this section, the pfdLoadFile man page, and the source code which is in /usr/share/Performer/src/lib/libpfdu/pfdLoadFile.c on IRIX and Linux and in %PFROOT%\Src\lib\libpfdu\pfdLoadFile.c on Microsoft Windows. 007-1680-100 225 7: Importing Databases The routines pfdInitConverter(), pfdLoadFile(), pfdStoreFile(), pfdConvertFrom(), and pfdConvertTo() exist only as a level of indirection to allow you to manipulate all databases regardless of format through a central API. They are in fact merely a mechanism for creating an open environment for data sharing among the multitudes of three-dimensional database formats. Each of these routines determines, using file-type extensions, which database converter to load as a run-time DSO. The routine then calls the appropriate functionality from that converter’s DSO. All converters must provide API that is exactly the same as the corresponding libpfdu API with _EXT added to the routine names (for example, for .medit files, the suffix is _medit). Note that multiple physical extensions can be mapped to one converter extension with calls to pfdAddExtAlias(). Several aliases are predefined upon initialization of libpfdu. It is also important to note that because each of these converters is a unique entity that they each may have state that is important to their proper function. Moreover, their database formats may allow for multiple OpenGL Performer interpretations; so, there exist APIs, shown in Table 7-4, not only to initialize and exit database converters, but also to set and get modes, attributes, and values that might affect the converter’s methodology. Table 7-4 libpfdu Database Converter Management Functions Function Name Description pfdInitConverter() Initialize a database conversion DSO. pfdExitConverter() Exit a database conversion DSO. pfdConverterMode() Specify a mode for a specific conversion DSO. pfdGetConverterMode() Get a mode setting from a specific conversion DSO. pfdConverterAttr() Specify an attribute for a conversion DSO. pfdGetConverterAttr() Get an attribute setting from a conversion DSO. pfdConverterVal() Specify a value for a conversion DSO. pfdGetConverterVal() Get a value setting from a conversion DSO. Once again each converter provides the equivalent routines with _EXT added to the function name. For example, the converter for the Open Inventor format would define the function pfdInitConverter_iv() if it needed to be initialized before it was used. Likewise, it would 226 007-1680-100 libpfdu - Utilities for Creating Efficient OpenGL Performer Run-Time Structures define the function pfdLoadFile_iv() to read an Open Inventor “.iv” file into an OpenGL Performer scene graph. Note: Because each converter is an individual entity (DSO) and deals with a particular type of database, it may be the case that a converter will not provide all of the functionality listed above, but rather only a subset. For instance, most converters that come with OpenGL Performer only implement their version of pfdLoadFile but not pfdStoreFile, pfdConvertFrom, or pfdConvertTo. However, users are free to add this functionality to the converters using compliant APIs and OpenGL Performer’s libpfdu will immediately recognize this functionality. Also, libpfdu traps access to nonexistent converter functionality and returns gracefully to the calling code while notifying the user that the functionality could not be found. Finding and initializing a Converter When one of the general database converter functions is called, it in turn calls the corresponding routine provided by the converter, passing on the arguments it was given. But the first time a converter is called, a search occurs to identify the converter and the functions it provides. This is accomplished as follows. • Parse the extension—what appears after the final “.” in the filename. This is referred to as EXT in the following bulleted items. • Check to see if any alias was created for the EXT extension with pfdAddExtAlias(). If a translation is defined, EXT is replaced with that extension. • Check the current executable to see if the symbol pfdLoadFile_EXT is already defined, that is. if the loader was statically linked into the executable or a DSO was previously loaded by some other mechanism. If not, the search continues. For IRIX and Linux: – Generate a DSO library name to search for using the extension prototype “libpfEXT_{-g,}.so”. This means the following strings will be constructed: libpfEXT_.so for the optimized OpenGL loader libpfEXT_-g.so for the debug OpenGL loader – Look for the DSO in several places, including the following: . $PFLD_LIBRARY_PATH 007-1680-100 227 7: Importing Databases $LD_LIBRARY_PATH $PFHOME/usr/lib{,32,64}/libpfdb $PFHOME/usr/share/Performer/lib/libpfdb – Open the DSO using dlopen(). For Microsoft Windows: – Generate a DLL library name to search for using the extension prototype “libpfEXT_{-g,}.so”. This means the following strings will be constructed: libpfEXT_.so for the optimized OpenGL loader libpfEXT_-g.so for the debug OpenGL loader – Look for the DLL in several places, including the following: . $PFLD_LIBRARY_PATH $LD_LIBRARY_PATH $PFHOME/Lib/libpfdb $PFHOME/Lib/Debug/libpfdb – • Open the DLL using LoadLibrary(). Once the object has been found, processing continues. – Query all libpfdu converter functionality from the symbol table of the DSO using dlsym() on IRIX and Linux and of the DLL using GetProcAddress() on Microsoft Windows with function names generated by appending _EXT to the name of the corresponding pfd routine name. This symbol dictionary is retained for future use. – Invoke the converter’s initialization function, pfdInitConverter_EXT(), if it exists. – Invoke pfdLoadNeededDSOs_EXT() if it exists. This routine can then recursively call pfdInitConverter_EXT(), as needed. Developing Custom Importers Having fully described how database converters can be integrated into OpenGL Performer and the types of functionality they provide, the next undertaking is actually implementing a converter from scratch. OpenGL Performer makes a great effort at allowing the quick and easy development of effective and efficient database converters. 228 007-1680-100 Developing Custom Importers While creating a new file loader for OpenGL Performer is not inherently difficult, it does require a solid understanding of the following issues: • The structure and interpretation of the data file to be read • The scene graph concepts and nodes of libpf • The geometry and attribute definition objects of libpr Structure and Interpretation of the Database File Format In order to effectively convert a database into an OpenGL Performer scene graph, it is important to have a substantial understanding of several concepts related to the original database format: • The parsing of the file based on the database format • The data types represented in the format and their OpenGL Performer correspondence • The scene graph structure of the file (if any) • The method of graphics state definition and inheritance defined in the format Before trying to convert sophisticated 3D database formats into OpenGL Performer it is important to have a thorough grasp of how every structure in the format needs to affect how OpenGL Performer performs its run-time management of a scene graph. However, although it requires a great deal of understanding to convert complex behaviors of external formats into OpenGL Performer, it is still very straight forward to migrate basic structure, geometry, and graphics state into efficient OpenGL Performer run-time structures using the functionality provided in the OpenGL Performer database builder, pfdBuilder. Scene Graph Creation Using Nodes as Defined in libpf Creating an OpenGL Performer scene graph requires a definite knowledge of the following OpenGL Performer libpf node types: pfScene, pfGroup, and pfGeode. These nodes can be used to define a minimally functional OpenGL Performer scene graph. See “Nodes” in Chapter 3 for more details on libpf and OpenGL Performer scene graphs and node types. 007-1680-100 229 7: Importing Databases Defining Geometry and Graphics State for libpr In order to input geometry and graphics into OpenGL Performer, it is important to have an understanding of how OpenGL Performer’s low-level rendering objects work in libpr, OpenGL Performer’s performance rendering library. The main libpr rendering primitives are a pfGeoSet and a pfGeoState. A pfGeoSet is a collection of like geometric primitives that can all be rendered in exactly the same way in one large continuous chunk. A pfGeoState is a complete definition of graphics mode settings for the rendering hardware and software. It contains many attributes such as texture and material. Given a pfGeoSet and a corresponding pfGeoState, libpr can completely and efficiently render all of the geometry in the pfGeoSet. For a more detailed description of pfGeoSets and pfGeoStates, see “pfGeoSets and pfGeoStates” in Chapter 12, which goes into detail on all libpr primitives and how OpenGL Performer will use them. However, realizing that OpenGL Performer’s structuring of geometry and graphics state is optimized for rendering speed and not for modeling ease or general conceptual partitioning, OpenGL Performer now contains a new mechanism for translating external graphics state and geometry into efficient libpr structures. This new mechanism is the pfdBuilder that exists in libpfdu. The pfdBuilder allows the immediate mode input of graphics state and primitives through very simple and exposed data structures. After having received all of the relevant information, the pfdBuilder builds efficient and somewhat optimized libpr data structures and returns a low-level libpf node that can be attached to an OpenGL Performer scene graph. The pfdBuilder is the recommended method of importing data from non-OpenGL Performer-based formats into OpenGL Performer. Creating an OpenGL Performer Database Converter using libpfdu Creating a new format converter is very simple process. More than thirty database loaders are shipped with OpenGL Performer in source code form to serve as practical examples of this process. The loaders read formats that range from trivial to complex and should serve as an instructive starting point for those developing loaders for other formats. These loaders can be found in the directory /usr/share/Performer/src/lib/libpfdb/libpf* on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpf* on Microsoft Windows. This section describes the libpfdu framework for creating a 3D database format converter. Consider writing a converter for a simple ASCII format that is called the Imaginary Immediate Mode format with the file type extension .iim. This format is 230 007-1680-100 Developing Custom Importers much like the more elaborate .im format loader used at SGI for the purposes of testing basic OpenGL Performer functionality. The first thing to do is set up the routine that pfdLoadFile() will call when it attempts to load a file with the extension .iim. #ifdef WIN32 #define PFDB_DLLEXPORT __declspec(dllexport) #else #define PFDB_DLLEXPORT /* no-op */ #endif extern PFDB_DLLEXPORT pfNode *pfdLoadFile_iim(char *fileName) { } This function needs to perform several basic actions: 1. Find and open the given file. 2. Reset the libpfdu pfdBuilder for input of new geometry and state. 3. Set up any pfdBuilder modes that the converter needs enabled. 4. Set up local data structures that can be used to communicate geometry and graphics state with the pfdBuilder. 5. Set up a libpf pfGroup which can hold all of the logical partitions of geometry in the file (or hold a subordinate collection of nodes as a general scene graph if the format supports it). 6. Optionally set up a default state to use for geometry with unspecified graphics state. 7. Parse the file, which entails the following: 007-1680-100 • Filling in the local geometry and graphics state data structures • Passing them to the pfdBuilder as inputted from the file • Asking the pfdBuilder to build the data structures into OpenGL Performer data structures when a logical partition of the file has ended • Attaching the OpenGL Performer node returned by the build to the higher-level group which will hold the entire OpenGL Performer representation of this file. Note that this step becomes more complex if the format supports the notion of hierarchy only in that the appropriate libpf nodes must be created and attached to each other using pfAddChild() to build the hierarchy. In this case 231 7: Importing Databases requests are made for the builder to build after inputting all of the geometry and state found in a particular leaf node in the database. 8. Delete local data structures used to input geometry and graphics state. 9. Close the file. 10. Perform any optional optimization of the OpenGL Performer scene graph. Optimizations might include calls to pfdFreezeTransforms(), pfFlatten() or pfdCleanTree(). 11. Return the pfGroup containing the entire OpenGL Performer representation of the database file. Steps 1-8 expand the function outline to the following: extern PFDB_DLLEXPORT pfNode *pfdLoadFile_iim(char *fileName) { FILE* iimFile; pfdGeom* polygon; pfGroup* root; /* Performer has utility for finding and opening file */ if ((iimFile = pfdOpenFile(fileName)) == NULL) return NULL; /* Clear builder from previous converter invocations */ pfdResetBldrGeometry(); pfdResetBldrState(); /* Call pfdBldrMode for any needed modes here */ /* Create polygon structure */ /* holds one N-sided polygon where N is < 300 */ polygon = pfdNewGeom(300); /* Create pfGroup to hold entire database */ /* loaded from this file */ root = pfNewGroup(); /* Specify state for geometry with no graphics state */ /* As well as default enables, etc. This routine */ /* should invoke pfdCaptureDefaultBldrState()*/ SetupDefaultGraphicsStateIfThereIsOne(); /* Do all the real work in parsing the file and */ 232 007-1680-100 Developing Custom Importers /* converting into Performer */ ParseIIMFile(iimFile, root, polygon); /* Delete local polygon struct */ pfdDelGeom(polygon); /* Close File */ fclose(iimFile); /* Optimize OpenGL Performer scene graph */ /* via use of pfFlatten, pfdCleanTree, etc. */ OptimizeGraph(root); return (pfNode*)root; } At the heart of the file loader lies the ParseIIMFile() function. The specifics of parsing a file are completely dependent on the format; so, the parsing will be left as an exercise to you. However, the following code fragments should show a framework for what goes into integrating the parser with the pfdBuilder framework for geometry and graphics state data conversion. Note that several possible graphics state inheritance models might be used in external formats and that the pfdBuilder is designed to support all of them: • The default pfdBuilder state inheritance is that of immediate mode graphics state. Immediate mode state is specified through calls to pfdBldrStateMode(), pfdBldrStateAttr(), and pfdBldrStateVal(). • There also exists a pfdBuilder state stack for hierarchical state application to geometry. This is accomplished through the use of pfdPushBldrState() and pfdPopBldrState() in conjunction with the normal use of the immediate mode pfdBuilder state API. • Lastly, there is a pfdBuilder named state list that can be used to define a number of "named materials" or "named state definitions" that can then be recalled in one API called (for instance, you might define a "brick" state with a red material and a brick texture. Later you might just want to say "brick" is the current state and then input the walls of several buildings). This type of state naming is accomplished by fully specifying the state to be named using the immediate mode API and then calling pfdSaveBldrState(). This state can then be recalled using pfdLoadBldrState(). ParseIIMFile(FILE *iimFile, pfGroup *root, pfdGeom *poly) { while((op = GetNextOp(iimFile)) != NULL) { switch(op) 007-1680-100 233 7: Importing Databases { case GEOMETRY_POLYGON: polygon->numVerts = GetNumVerts(iimFile); /* Determine if polygon has Texture Coords */ if (pfdGetBldrStateMode(PFSTATE_ENTEXTURE)==PF_ON) polygon->tbind = PFGS_PER_VERTEX; else polygon->tbind = PFGS_OFF; /* Determine if Polygon has normals */ if (AreThereNormalsPerVertex() == TRUE) polygon->nbind = PFGS_PER_VERTEX; else if (pfdGetBldrStateMode(PFSTATE_ENLIGHTING)==PF_ON) polygon->nbind = PFGS_PER_PRIM; else polygon->nbind = PFGS_OFF; /* Determine if Polygon has colors */ if (AreThereColorsPerVertex() == TRUE) polygon->cbind = PFGS_PER_VERTEX; else if (AreThereColorsPerPrim() == TRUE) polygon->cbind = PFGS_PER_PRIM; else polygon->cbind = PFGS_OFF; for(i=0;i<polygon->numVerts;i++) { /* Read ith Vertex into local data structure */ polygon->coords[i][0] = GetNextVertexFloat(); polygon->coords[i][1] = GetNextVertexFloat(); polygon->coords[i][2] = GetNextVertexFloat(); /* Read texture coord for ith vertex if any */ if (polygon->tbind == PFGS_PER_VERTEX) { polygon->texCoords[i][0] = GetNextTexFloat(); polygon->texCoords[i][1] = GetNextTexFloat(); } /* Read normal for ith Vertex if normals bound*/ if (polygon->nbind == PFGS_PER_VERTEX) { polygon->norms[i][0] = GetNextNormFloat(); polygon->norms[i][1] = GetNextNormFloat(); 234 007-1680-100 Developing Custom Importers polygon->norms[i][2] = GetNextNormFloat(); } /* Read only one normal per prim if necessary */ else if ((polygon->nbind == PFGS_PER_PRIM) && (i == 0)) { polygon->norms[0][0] = GetNextNormFloat(); polygon->norms[0][1] = GetNextNormFloat(); polygon->norms[0][2] = GetNextNormFloat(); } /* Get Color for the ith Vertex if color bound*/ if (polygon->cbind == PFGS_PER_VERTEX) { polygon->colors[i][0] = GetNextColorFloat(); polygon->colors[i][1] = GetNextColorFloat(); polygon->colors[i][2] = GetNextColorFloat(); } /* Get one color per prim if necessary */ else if ((polygon->cbind == PFGS_PER_PRIM) && (i == 0)) { polygon->colors[0][0] = GetNextColorFloat(); polygon->colors[0][1] = GetNextColorFloat(); polygon->colors[0][2] = GetNextColorFloat(); } } /* Add this polygon to pfdBuilder */ /* Because it is a single poly, 1 */ /* is specified here */ pfdAddBldrGeom(1); break; case GRAPHICS_STATE_TEXTURE: { char *texName; pfTexture *tex; texName = ReadTextureName(iimFile); if (texName != NULL) { 007-1680-100 235 7: Importing Databases /* Get prototype tex from pfdBuilder*/ tex = pfdGetTemplateObject(pfGetTexClassType()); /* This clears that object to default */ pfdResetObject(tex); /* If just the name of a pfTexture is */ /* set, pfdBuilder will auto find & Load */ /* the texture*/ pfTexName(tex,texName); /* This is the current pfdBuilder */ /* texture and texturing is on */ pfdBldrStateAttr(PFSTATE_TEXTURE,tex); pfdBldrStateMode(PFSTATE_ENTEXTURE, PF_ON); } else { /* No texture means disable texturing */ /* And set current texture to NULL */ pfdBldrStateMode(PFSTATE_ENTEXTURE,PF_OFF); pfdBldrStateAttr(PFSTATE_TEXTURE, NULL); } } break; case GRAPHICS_STATE_MATERIAL: { pfMaterial *mtl; mtl = pfdGetTemplateObject(pfGetMtlClassType()); pfdResetObject(mtl); pfMtlColor(mtl, PFMTL_AMBIENT, GetAmRed(), GetAmGreen(), GetAmBlue()); pfMtlColor(mtl, PFMTL_DIFFUSE, GetDfRed(), GetDfGreen(), GetDfBlue()); pfMtlColor(mtl, PFMTL_SPECULAR, GetSpRed(), GetSpGreen(), GetSpBlue()); pfMtlShininess(mtl, GetMtlShininess()); pfMtlAlpha(mtl, GetMtlAlpha()); pfdBldrStateAttr(PFSTATE_FRONTMTL, mtl); pfdBldrStateAttr(PFSTATE_BACKMTL, mtl); } break; case GRAPHICS_STATE_STORE: pfdSaveBldrState(GetStateName()); 236 007-1680-100 Developing Custom Importers break; case GRAPHICS_STATE_LOAD: pfdLoadBldrState(GetStateName()); break; case GRAPHICS_STATE_PUSH: pfdPushBldrState(); break; case GRAPHICS_STATE_POP: pfdPopBldrState(); break; case GRAPHICS_STATE_RESET: pfdResetBldrState(); break; case GRAPHICS_STATE_CAPTURE_DEFAULT: pfdCaptureDefaultBldrState(); break; case BEGIN_LEAF_NODE: /* Not really necessary because it is */ /* destroyed on build*/ pfdResetBldrGeometry(); break; case END_LEAF_NODE: { pfNode *nd = pfdBuild(); if (nd != NULL) pfAddChild(root,nd); } break; } } } One of the fundamental structures involved in the above routine outline is the pfdGeom structure which you fill in with information about a single primitive, or a single strip of primitives. The pfdGeom structure is essential in communicating with the pfdBuilder and is defined as follows: typedef struct _pfdGeom { int flags; int nbind, cbind, tbind[PF_MAX_TEXTURES]; int short float 007-1680-100 numVerts; primtype; pixelsize; 237 7: Importing Databases /* Non-indexed attributes - do not set if poly is indexed */ pfVec3 *coords; pfVec3 *norms; pfVec4 *colors; pfVec2 *texCoords[PF_MAX_TEXTURES]; /* Indexed attributes - do not set if poly is non-indexed */ pfVec3 *coordList; pfVec3 *normList; pfVec4 *colorList; pfVec2 *texCoordList[PF_MAX_TEXTURES]; /* Index lists - do not set if poly is non-indexed */ ushort *icoords; ushort *inorms; ushort *icolors; ushort *itexCoords[PF_MAX_TEXTURES]; int numTextures; struct _pfdGeom *next; } pfdGeom; See the pfdGeoBuilder(3pf) man pages for more information on using this structure along with its sister structure, the pfdPrim. The above should provide a well-defined framework for creating a database converter that can be used with any OpenGL Performer applications using the pfdLoadFile() functionality. However, it is also important to note that there are a multitude of pfdBuilder modes and attributes that can be used to affect some of the basic methods that the builder actually uses: 238 007-1680-100 Developing Custom Importers Table 7-5 pfdBuilder Modes and Attributes Function Name Token Description pfd{Get}BldrMode() PFDBLDR_MESH_ENABLE PFDBLDR_MESH_SHOW_TSTRIPS PFDBLDR_MESH_INDEXED PFDBLDR_MESH_MAX_TRIS PFDBLDR_MESH_RETESSELLATE PFDBLDR_MESH_LOCAL_LIGHTING PFDBLDR_AUTO_COLORS PFDBLDR_AUTO_NORMALS PFDBLDR_AUTO_ORIENT PFDBLDR_AUTO_ENABLES PFDBLDR_AUTO_CMODE PFDBLDR_AUTO_DISABLE_TCOORDS_BY_STATE PFDBLDR_AUTO_DISABLE_NCOORDS_BY_STATE PFDBLDR_AUTO_LIGHTING_STATE_BY_NCOORDS PFDBLDR_AUTO_LIGHTING_STATE_BY_MATERIALS PFDBLDR_AUTO_TEXTURE_STATE_BY_TEXTURES PFDBLDR_AUTO_TEXTURE_STATE_BY_TCOORDS PFDBLDR_BREAKUP PFDBLDR_BREAKUP_SIZE PFDBLDR_BREAKUP_BRANCH PFDBLDR_BREAKUP_STRIP_LENGTH PFDBLDR_SHARE_MASK PFDBLDR_ATTACH_NODE_NAMES 007-1680-100 239 7: Importing Databases Table 7-5 pfdBuilder Modes and Attributes (continued) Function Name Token Description PFDBLDR_DESTROY_DATA_UPON_BUILD PFDBLDR_PF12_STATE_COMPATIBLE PFDBLDR_BUILD_LIMIT PFDBLDR_GEN_OPENGL_CLAMPED_TEXTURE_COORDS PFDBLDR_OPTIMIZE_COUNTS_NULL_ATTRS pfd{Get}BldrAttr() PFDBLDR_NODE_NAME_COMPARE PFDBLDR_STATE_NAME_COMPARE Because the pfdBuilder is released as source code, it is easy to add further functionality and more modes and attributes to even further customize this central functionality. In fact, because the pfdBuilder acts as a “data funnel” in converting data into OpenGL Performer run-time structures, it is easy to control the behavior of many standard conversion tasks through merely globally setting builder modes which will subsequently affect all converters that use the pfdBuilder to process their data. Maximizing Database Loading and Paging Performance with PFB and PFI Formats “Description of Supported Formats” on page 244 describes all of the file formats supported by OpenGL Performer. Although you can use files in these formats directly, you can dramatically reduce database loading time by preconverting databases into the PFB format and images into the PFI format. To convert to the PFB file format or the PFI image format, use the pfconv and pficonv utilities. pfconv The pfconv utility converts from any format for which a pfdLoadFile...() function exists into any format for which a pfdStoreFile...() exists. The most common format to convert 240 007-1680-100 Supported Database Formats to is the PFB format. For example, to convert cow.obj into the PFB format, use the following command: % pfconv cow.obj cow.pfb By default, pfconv optimizes the scene graph when doing the conversion. The optimizations are controlled with the -o and -O command line options. Builder options are controlled with the -b and -B command line options. Converter modes are controlled with the -m and -M command line options. Refer to the help page for more specific information about the command line options by entering: % pfconv -h Example Conversion When converting to the PFB format, texture files can be converted to the PFI format using the following command line options: % pfconv -M pfb, 5, 1 5 means PFPFB_SAVE_TEXTURE_PFI. 1 means convert .rgb texture images to .pfi. pficonv The pficonv utility converts from IRIS libimage format to PFI format image files. For example, to convert cafe.rgb into the PFI format, use the following command: % pficonv cafe.rgb cafe.pfi MIPmaps can be automatically generated and stored in the resulting PFI files by adding -m to the command line. Supported Database Formats Vendors of several leading database construction and processing tools have provided database-loading software for you to use with OpenGL Performer. This section describes these loaders, the loaders developed by the OpenGL Performer engineering team and 007-1680-100 241 7: Importing Databases several loaders developed in the OpenGL Performer user community for other database formats. Importing your databases is simple if they are in formats for which OpenGL Performer database loaders have already been written. Each of the loaders listed in Table 7-6 is included with OpenGL Performer. If you want to import or export databases in any of these formats, refer to the appropriate section of this chapter for specific details about the individual loaders. 242 Table 7-6 Supported Database Formats Name Description 3ds AutoDesk 3DStudio binary data bin SGI format used by powerflip bpoly Side Effects Software PRISMS binary data byu Brigham Young University CAD/FEA data csb OpenGL Optimizer Format ct Cliptexture config file loader - auto-generates viewing geometry dwb Coryphaeus Software Designer’s Workbench data dxf AutoDesk AutoCAD ASCII format flt11 MultiGen public domain Flight v11 format flt MultiGen OpenFlight format provided by MultiGen gds McDonnell-Douglas GDS things data gfo Old SGI radiosity data format im Simple OpenGL Performer data format irtp AAI/Graphicon Interactive Real-Time PHIGS iv SGI Open Inventor format (VRML 1.0 superset) lsa Lightscape Technologies ASCII radiosity data lsb Lightscape Technologies binary radiosity data 007-1680-100 Supported Database Formats 007-1680-100 Table 7-6 Supported Database Formats (continued) Name Description medit Medit Productions medit modeling data nff Eric Haines’ ray tracing test data pfb OpenGL Performer fast binary format obj Wavefront Technologies data format pegg Radiosity research data format phd SGI polyhedron data format poly Side Effects Software PRISMS ASCII data ptu Simple OpenGL Performer terrain data format rpc ArchVision rich photorealistic content sgf US Naval Academy standard graphics format sgo Paul Haeberli’s graphics data format spf US Naval Academy simple polygon format sponge Sierpinski sponge 3D fractal generator star Astronomical data from Yale University star chart stla 3D Structures ASCII stereolithography data stlb 3D Structures binary stereolithography data stm Michael Garland’s terrain data format sv John Kichury’s i3dm modeler format tri University of Minnesota Geometry Center data unc University of North Carolina walkthrough data wrl OpenWorlds VMRL 2.0 provided by DRaW Computing 243 7: Importing Databases Description of Supported Formats This section describes the different database file formats that OpenGL Performer supports. AutoDesk 3DS Format The AutoDesk 3DS format is used by the 3DStudio program and by a number of 3D file-interchange tools. The OpenGL Performer loader for 3DS files is located in the directory /usr/share/Performer/src/lib/libpfdb/libpf3ds on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpf3ds on Microsoft Windows. This loader uses an auxiliary library, 3dsftk.a, to parse and interpret the 3ds file. pfdLoadFile() uses the function pfdLoadFile_3ds() to import data from 3DStudio files into OpenGL Performer run-time data structures. SGI BIN Format The SGI BIN format is supported by both Showcase and the powerflip demonstration program. BIN files are in a simple format that specifies only independent quadrilaterals. The image in Figure 7-1 shows several of the BIN-format objects provided in the OpenGL Performer sample data directory. 244 007-1680-100 Description of Supported Formats Figure 7-1 BIN-Format Data Objects The source code for the BIN-format importer pfdLoadFile_bin() is provided in the file pfbin.c. This code shows how easy it can be to implement an importer. Since pfdLoadFile_bin() is based on the pfdBuilder() utility function, it will build efficient triangle-strip pfGeoSets from the quadrilaterals of a given BIN file. The BIN format has the following structure: 1. A 4-byte magic number, 0x5432, which identifies the file as a BIN file. 2. A 4-byte number that contains the number of vertices, which is four times the number of quadrilaterals. 3. Four bytes of zero. 4. A list of polygon data for each vertex in the object. The data consists of three floating-point words of information about normals followed by three floating-point words of vertex information. The BIN format uses these data structures: typedef struct { float normal[3]; float coordinate[3]; 007-1680-100 245 7: Importing Databases } Vertex; typedef struct { long magic; long vertices; long zero; Vertex vertex[1]; } BinFile; pfdLoadFile() uses the function pfdLoadFile_bin() to import data from BIN format files into OpenGL Performer run-time data structures: The pfdLoadFile_bin() function composes a random color for each file it reads. The chosen color has red, green, and blue components uniformly distributed within the range 0.2 to 0.7 and is fully opaque. Side Effects POLY Format The Side Effects software PRISMS database modeler format supports both ASCII and binary forms of the POLY format. The OpenGL Performer loader for ASCII “.poly” files is located in the directory /usr/share/Performer/src/lib/libpfdb/libpfpoly for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfpoly for Microsoft Windows. The binary format “.bpoly” loader is located in the directory /usr/share/Performer/src/lib/libpfdb/libpfbpoly for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfbpoly for Microsoft Windows. These formats are equivalent in content and differ only in representation. The POLY format is an easy to understand ASCII data representation with the following structure: 1. A text line containing the keyword “POINTS” 2. One text line for each vertex in the file. Each line begins with a vertex number, followed by a colon, followed by the X, Y, and Z axis coordinates of the vertex, optional additional information, and a new-line character. The optional information includes color specification in the form “c(R,G,B,A)”, a normal vector of the form “n(NX,NY,NZ)”, or a texture coordinate in the form “uv(S,T)” where each of the values shown are floating point numbers. 3. A text line containing the keyword “POLYS” 246 007-1680-100 Description of Supported Formats 4. One text line for each polygon in the file. Each line begins with a polygon number, followed by a colon, followed by a series of vertex indices, optional additional information, an optional “<“character, and a new-line. The optional information includes color specification in the form “c(R,G,B,A)”, a normal vector of the form “n(NX,NY,NZ)”, or a texture coordinate in the form “uv(S,T)” where the values in parentheses are floating point numbers. Here is a sample POLY format file for a cube with colors, texture coordinates, and normals specified at each vertex: POINTS 1: -0.5 -0.5 -0.5 c(0, 0, 0, 1) uv(0, 0) n(0, -1, 0) 2: -0.5 -0.5 0.5 c(0, 0, 1, 1) uv(0, 0) n(0, -1, 0) 3: 0.5 -0.5 0.5 c(1, 0, 1, 1) uv(1, 0) n(0, -1, 0) 4: 0.5 -0.5 -0.5 c(1, 0, 0, 1) uv(1, 0) n(0, -1, 0) 5: -0.5 -0.5 0.5 c(0, 0, 1, 1) uv(0, 0) n(0, 0, 1) 6: -0.5 0.5 0.5 c(0, 1, 1, 1) uv(0, 1) n(0, 0, 1) 7: 0.5 0.5 0.5 c(1, 1, 1, 1) uv(1, 1) n(0, 0, 1) 8: 0.5 -0.5 0.5 c(1, 0, 1, 1) uv(1, 0) n(0, 0, 1) 9: -0.5 0.5 0.5 c(0, 1, 1, 1) uv(0, 1) n(0, 1, 0) 10: -0.5 0.5 -0.5 c(0, 1, 0, 1) uv(0, 1) n(0, 1, 0) 11: 0.5 0.5 -0.5 c(1, 1, 0, 1) uv(1, 1) n(0, 1, 0) 12: 0.5 0.5 0.5 c(1, 1, 1, 1) uv(1, 1) n(0, 1, 0) 13: -0.5 -0.5 -0.5 c(0, 0, 0, 1) uv(0, 0) n(0, 0, -1) 14: 0.5 -0.5 -0.5 c(1, 0, 0, 1) uv(1, 0) n(0, 0, -1) 15: 0.5 0.5 -0.5 c(1, 1, 0, 1) uv(1, 1) n(0, 0, -1) 16: -0.5 0.5 -0.5 c(0, 1, 0, 1) uv(0, 1) n(0, 0, -1) 17: -0.5 -0.5 -0.5 c(0, 0, 0, 1) uv(0, 0) n(-1, 0, 0) 18: -0.5 0.5 -0.5 c(0, 1, 0, 1) uv(0, 1) n(-1, 0, 0) 19: -0.5 0.5 0.5 c(0, 1, 1, 1) uv(0, 1) n(-1, 0, 0) 20: -0.5 -0.5 0.5 c(0, 0, 1, 1) uv(0, 0) n(-1, 0, 0) 21: 0.5 0.5 0.5 c(1, 1, 1, 1) uv(1, 1) n(1, 0, 0) 22: 0.5 0.5 -0.5 c(1, 1, 0, 1) uv(1, 1) n(1, 0, 0) 23: 0.5 -0.5 -0.5 c(1, 0, 0, 1) uv(1, 0) n(1, 0, 0) 24: 0.5 -0.5 0.5 c(1, 0, 1, 1) uv(1, 0) n(1, 0, 0) POLYS 1: 1 2 3 4 < 2: 5 6 7 8 < 3: 9 10 11 12 < 4: 13 14 15 16 < 5: 17 18 19 20 < 6: 21 22 23 24 < 007-1680-100 247 7: Importing Databases pfdLoadFile() uses the functions pfdLoadFile_poly() and pfdLoadFile_bpoly() to import data from “.poly” and “.bpoly” format files into OpenGL Performer run-time data structures. Brigham Young University BYU Format The Brigham Young University “.byu” format is used as an interchange format by some finite element analysis packages. The OpenGL Performer loader for “.byu” files is located in the directory /usr/share/Performer/src/lib/libpfdb/libpfbyu for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfbyu for Microsoft Windows. The format of a BYU file consists of four parts as defined below: 1. A text line containing four counts: the number of parts, the number of vertices, the number of polygons, and the number of elements in the connectivity array. 2. The part definition list, containing the starting polygon number and ending polygon number (one pair per line) for parts lines. 3. The vertex list, which has the X, Y, Z coordinates of each vertex in the database packed two per line. This means that vertices 1 and 2 are on the first line, 3 and 4 are on the second, and so on for (vertices + 1)/2 lines of text in the file. 4. The connectivity array, with an entry for each polygon. These entries may span multiple lines in the input file and each consists of three or more vertex indices with the last negated as an end of list flag. For example, if the first polygon were a quad, the connectivity array might start with “1 2 3 -4” to define a polygon that connects the first four vertices in order. The following BYU format file defines two adjoining quads: 2 6 2 1 1 2 2 0 0 0 10 10 10 10 1 2 3 4 3 5 0 10 0 0 0 0 10 0 10 0 10 10 -4 -6 pfdLoadFile() uses the function pfdLoadFile_byu() to import data from “.byu” format files into OpenGL Performer run-time data structures. 248 007-1680-100 Description of Supported Formats Optimizer CSB Format OpenGL Performer can load native OpenGL Optimizer format files using this loader. OpenGL Optimizer can also load OpenGL Performer’s PFB native format files, providing full database interoperability. This allows you to use OpenGL Optimizer database simplification and optimization tools on OpenGL Performer databases. Virtual Cliptexture CT Loader The OpenGL Performer CT loader allows you to create and configure cliptextures and virtual cliptextures, complete with a scene graph containing simple geometry and callbacks. See the Cliptexture chapter for more details. Designer’s Workbench DWB Format The binary DWB format is used for input and output by the Designer’s Workbench, EasyT, and EasyScene database modeling tools produced by Coryphaeus Software. DWB is an advanced database format that directly represents many of OpenGL Performer’s attribute and hierarchical scene graph concepts. An importer for this format, named pfdLoadFile_dwb(), has been provided by Coryphaeus Software for your use. The loader code and its associated documentation are in the directory /usr/share/Performer/src/lib/libpfdb/libpfdwb for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfdwb for Microsoft Windows.The image in Figure 7-2 shows a model of the Soma Cube puzzle invented by Piet Hein. The model was created using Designer’s Workbench. Each of the pieces is stored as an individual DWB-format file. Do you see how to form the 3 x 3 cube at the lower left from the seven individual pieces? 007-1680-100 249 7: Importing Databases Figure 7-2 Soma Cube Puzzle in DWB Form pfdLoadFile() uses the function pfdLoadFile_dwb() to load Designer’s Workbench files into OpenGL Performer run-time data structures. AutoCAD DXF Format The DXF format originated with Autodesk’s AutoCAD database modeling system. The version recognized by the pfdLoadFile_dxf() database importer is a subset of ASCII Drawing Interchange Format (DXF) Release 12. The binary version of the DXF format, also known as DXF, is not supported. Source code for the importer is in the file /usr/share/Performer/src/lib/libpfdb/libpfdxf/pfdxf.c for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfdxf\pfdxf.c for Microsoft Windows. pfdLoadFile_dxf() was derived from the DXF-to-DKB data file converter developed and placed in the public domain by Aaron A. Collins. The image in Figure 7-3 shows a DXF model of the famous Utah teapot. This model was loaded from DXF format using the pfdLoadFile_dxf() database importer. 250 007-1680-100 Description of Supported Formats Figure 7-3 The Famous Teapot in DXF Form The DXF format has an unusual though well-documented structure. The general organization of a DXF file is the following: 1. HEADER section with general information about the file 2. TABLES section to provide definitions for named items, including: 007-1680-100 ■ LTYPE, the line-type table ■ LAYER, the layer table ■ STYLE, the text-style table ■ VIEW, the view table ■ UCS, the user coordinate-system table ■ VPORT, the viewport configuration table ■ DIMSTYLE, the dimension style table ■ APPID, the application identification table 251 7: Importing Databases 3. BLOCKS section containing block definition entities 4. ENTITIES section containing entities and block references 5. END-OF-FILE Within each section are groups of values, where each value is defined by a two-line pair of tokens. The first token is a numeric code indicating how to interpret the information on the next line. For example, the sequence 10 1.000 20 5.000 30 3.000 defines a “start point” at the XYZ location (1, 5, 3). The codes 10, 20, and 30 indicate, respectively, that the primary X, Y, and Z values follow. All data values are retained in a set of numbered registers (10, 20, and 30 in this example), which allows values to be reused. This simple state-machine type of run-length coding makes DXF files space-efficient at the cost of making them harder to interpret. pfdLoadFile() uses the function pfdLoadFile_dxf() to load DXF format files into OpenGL Performer run-time data structures. Several widely available technical books provide full details of this format if you need more information. Chief among these are AutoCAD Programming, 2nd Edition, by Dennis N. Jump, Windcrest Books, 1991, and AutoCAD: The Complete Reference, Second Edition, by Nelson Johnson, Osborne McGraw-Hill, 1991. MultiGen OpenFlight Format The OpenFlight format is a binary format used for input and output by the MultiGen and ModelGen database modeling tools produced by MultiGen. It is a comprehensive format that can represent nearly all of OpenGL Performer’s advanced concepts, including object hierarchy, instancing, level-of-detail selection, light-point specification, texture mapping, and material property specification. MultiGen has provided an OpenFlight-format importer, pfdLoadFile_flt(), for your use. The loaders and associated documentation are in the directories /usr/share/Performer/src/lib/libpfdb/libpfflt11 and libpfflt for 252 007-1680-100 Description of Supported Formats IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfflt11 and libpfllt for Microsoft Windows. Refer to the Readme files in these directories for important information about the loaders and for help in contacting MultiGen for information about pfdLoadFile_flt() or the OpenFlight format. The image in Figure 7-4 shows a model of a spacecraft created by Viewpoint Animation Engineering using MultiGen. This OpenFlight format model was loaded into OpenGL Performer using pfdLoadFile_flt(). Figure 7-4 Spacecraft Model in OpenFlight Format pfdLoadFile() uses the function pfdLoadFile_flt() to load OpenFlight format files into OpenGL Performer run-time data structures. Files in the OpenFlight format are structured as a linear sequence of records. The first few bytes of each record are a header containing an op-code, the length of the record, and possibly an ASCII name for the record. The first record in the file is a special “database header” record whose op-code, stored as a 2-byte short integer, has the value 1. This op-code header can be used to identify OpenFlight-format files. By convention, these files have a “.flt” filename extension. 007-1680-100 253 7: Importing Databases pfdLoadFile_flt() makes use of several environment variables when locating data and texture files. These variables and several additional functions, including pfdConverterMode_flt(), pfdGetConverterMode_flt(), and pfdConverterAttr_flt() assist in OpenFlight file processing. McDonnell-Douglas GDS Format The “.gds” format (also known as the “Things” format) is used in at least one CAD system, and a minimal loader for this format has been developed for OpenGL Performer users. The OpenGL Performer loader for “.gds” files is located in the directory /usr/share/Performer/src/lib/libpfdb/libpfgds for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfgds for Microsoft Windows. The GDS format subset accepted by the pfdLoadFile_gds() function is easy to describe. It consists of the following five sequential sections in an ASCII file: 1. The number of vertices, which is given following a “YIN” tag 2. The vertices, with one X, Y, Z triple per line for vertices lines 3. The number zero on a line by itself 4. The number of polygons on a line by itself 5. A series of polygon definitions, each of which is represented on two or more lines. The first line contains the number one and the name of a material to use for the polygon. The next line or lines contain the indices for the polygons vertices. The first number on the first line is the number of vertices. This is followed by that number of vertex indices on that and possibly subsequent lines. pfdLoadFile() uses the function pfdLoadFile_gds() to load “.gds” format files into IRIS Performer. SGI GFO Format The GFO format is the simple ASCII format of the barcelona database that is provided in the OpenGL Performer sample database directory. This database represents the famous German Pavilion at the Barcelona Exhibition of 1929, which was designed by Ludwig Mies van der Rohe and is shown in Figure 7-5. 254 007-1680-100 Description of Supported Formats Figure 7-5 GFO Database of Mies van der Rohe’s German Pavilion The source code for the GFO-format loader is provided in the file /usr/share/Performer/src/lib/libpfdb/libpfgfo/pfgfo.c for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfgfo\pfgfo.c for Microsoft Windows. pfdLoadFile() uses the function pfdLoadFile_gfo() to load GFO format files into OpenGL Performer run-time data-structures. When working with GFO files, remember that hardware lighting is not used since all illumination effects have already been accounted for with the ambient color at each vertex. The GFO format defines polygons with a color at every vertex. It is the output format of an early radiosity system. Files in this format have a simple ASCII structure, as indicated by the following abbreviated GFO file: 007-1680-100 255 7: Importing Databases scope { v3f {42.9632 8.7500 0.9374} cpack {0x8785a9} v3f {42.9632 8.0000 0.9374} cpack {0x8785a9} ... v3f {-1.0000 -6.5858 10.0000} cpack {0xffffff} polygon {cpack[0] v3f[0] cpack[1] v3f[1] cpack[2] v3f[2] cpack[3] v3f[3] } polygon {cpack[4] v3f[4] cpack[5] v3f[5] cpack[6] v3f[6] cpack[7] v3f[7] } ... polygon {cpack[7330] v3f[7330] cpack[7331] v3f[7331] cpack[7332] v3f[7332] cpack[7333] v3f[7333] } instance { polygon[0] polygon[1] ... polygon[2675] } } This example is taken from the file barcelona-l.gfo, one of only two known databases in the GFO format. The importer uses functions from the libpfdu library (such as those from the pfdBuilder) to generate efficient shared triangle strips. This increases the speed with which GFO databases can be drawn and reduces the size and complexity of the loader, since the builder’s functions hide the details of the pfGeoSet construction process. SGI IM Format The “.im” format is a simple format developed for test purposes by the OpenGL Performer engineering team. As new features are added to OpenGL Performer, the “.im” loader is extended to allow experimentation and testing. A recent example of this is support for pfText, pfString, and pfFont objects which can be seen by running Perfly on the sample data file fontsample.im. The OpenGL Performer “.im” loader is in the directory /usr/share/Performer/src/lib/libpfdb/libpfim for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfim for Microsoft Windows. Here is an example IM format file that creates an extruded 3D text string. Copy this to a file ending in the extension “.im” and load it into Perfly. For a complete example of how text is handled in OpenGL Performer, use Perfly to examine the file 256 007-1680-100 Description of Supported Formats /usr/share/Performer/data/fontsample2.im on IRIX and Linux and in %PFROOT%\Data\fontsample2.im on Microsoft Windows. breakup 0 0.0 0 0 new root top end_root new font mistr-extruded Mistr 3 end_font new str_text textnode mistr-extruded 1 Hello World|| end_text attach top textnode pfdLoadFile() uses the function pfdLoadFile_im() to load “.im” format files into OpenGL Performer run-time data structures: pfdLoadFile_im() searches the current OpenGL Performer file path for the named file and returns a pointer to the pfNode parenting the imported scene graph, or NULL if the file is not readable or does not contain a valid database. AAI/Graphicon IRTP Format The AAI/Graphicon “.irtp” format is used by the TopGen database modeling system and by the Graphicon-2000 image generator. The name IRTP is an acronym for Interactive Real-Time PHIGS. The OpenGL Performer “.irtp” loader is in the directory /usr/share/Performer/src/lib/libpfdb/libpfirtp for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfirtp for Microsoft Windows. Though loader does not support the more arcane IRTP features, such as binary separating planes or a global matrix table, it has served as a basis for porting applications to OpenGL Performer and the RealityEngine. pfdLoadFile() uses the function pfdLoadFile_irtp() to load IRTP format files into OpenGL Performer run-time data structures. 007-1680-100 257 7: Importing Databases SGI Open Inventor Format The Open Inventor object-oriented 3D-graphics toolkit defines a persistent data format that is also a superset of the VRML networked graphics data format. The image in Figure 7-6 shows a sample Open Inventor data file. Figure 7-6 Aircar Database in IRIS Inventor Format The model in Figure 7-6 represents one design for the perennial “personal aircar of the future” concept. It was created, using Imagine, by Mike Halvorson of Impulse, and was modeled after the Moller 400 as described in Popular Mechanics. The Open Inventor data-file loader provided with OpenGL Performer reads both binary and ASCII format Open Inventor data files. Open Inventor scene graph description files in both formats have the suffix “.iv” appended to their file names. Here is a simple Open Inventor file that defines a cone: #Inventor V2.1 ascii Separator { Cone { } } 258 007-1680-100 Description of Supported Formats The source code for the Open Inventor format importer is provided in the libpfdb/libpfiv source directory. pfdLoadFile() uses the function pfdLoadFile_iv() to load Open Inventor format files into OpenGL Performer run-time data-structures. OpenGL Performer also comes with an Inventor loader that works with Open Inventor 2.0, if Open Inventor 2.1 is not installed. Lightscape Technologies LSA and LSB Formats The Lightscape Visualization system is a product of Lightscape Technologies, Inc., and is designed to compute accurate simulations of global illumination within complex 3D environments. The output files created with Lightscape Visualization can be read into OpenGL Performer for real-time visual exploration. Lightscape Technologies provides importers for two of their database formats, the simple ASCII LSA format and the comprehensive binary LSB format. These loaders are in the files pflsa.c and pflsb.c in the directories /usr/share/Performer/src/lib/libpfdb/libpflsa and libpflsb for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpflsa and libpflsb for Microsoft Windows. Files in the LSA format are in ASCII and have the following components: 1. A 4x4 view matrix representing a default transformation 2. Counts of the number of independent triangles, independent quadrilaterals, triangle meshes, and quadrilateral meshes in the file 3. Geometric data definitions There are four types of geometric definitions in LSA files. The formats of these definitions are as shown in Table 7-7. Table 7-7 Geometric Definitions in LSA Files Geometric Type Format Triangle t X1 Y1 Z1 C1 X2 Y2 Z2 C2 X3 Y3 Z3 C3 Triangle mesh tm n X1 Y1 Z1 C1 X2 Y2 Z2 C2 ... 007-1680-100 259 7: Importing Databases Geometric Definitions in LSA Files (continued) Table 7-7 Geometric Type Format Quadrilateral q X1 Y1 Z1 C1 X2 Y2 Z2 C2 X3 Y3 Z3 C3 X4 Y4 Z4 C4 Quadrilateral mesh qm n X1 Y1 Z1 C1 X2 Y2 Z2 C2 ... The Cn values in Table 7-7 refer to colors in the format accepted by the OpenGL function glColor(); these colors should be provided in decimal form. The X, Y, and Z values are vertex coordinates. Polygon vertex ordering in LSA files is consistently counterclockwise, and polygon normals are not specified. The first few lines of the LSA sample file chamber.0.lsa provide an example of the format: 0.486911 -1.665110 0.000000 0.240398 0.03228900 0.00944197 1.92730000 -5.54670000 0.979046 0.286293 -0.017805 13.021200 0.9596590 0.2806240 -0.0174524 13.4945000 1782 4751 0 0 t 4.35 -7.3677 2.57 6188666 6.5 -9.3 2.57 5663353 4.35 -9.3 2.57 5728890 t 6.5 -9.3 2.57 5663353 4.35 -7.3677 2.57 6188666 6.5 -8.2463 2.57 6057596 The count line indicates that the file contains 1782 independent triangles and 4751 independent quadrilaterals, which together represent 11,284 triangles. The image in Figure 7-7 shows this database, the New Jerusalem City Hall. This was produced by A.J. Diamond of Donald Schmitt and Company, Toronto, Canada, using the Lightscape Visualization system. 260 007-1680-100 Description of Supported Formats Figure 7-7 LSA-Format City Hall Database pfdLoadFile() uses the function pfdLoadFile_lsa() to load LSA format files into OpenGL Performer run-time data structures. Files in the LSB binary format have a very different structure from LSA files. Representing not just polygon data, they contain much of the structural information present in the “.ls” files used by the Lightscape Visualization system, including material, layer, and texture definitions as well as a hierarchical mesh definition for geometry. This information is structured as a series of data sections, which include the following: 007-1680-100 • The signature, a text string that identifies the file • The header, which contains global file information • The material table, defining material properties • The layer table, defining grouping and association • The texture table, referencing texture images • Geometry in the form of clusters 261 7: Importing Databases The format of the geometric clusters is somewhat complicated. A cluster is a group of coplanar surfaces called patches that share a common material, layer, and normal. Each patch shares at least one edge with another patch in the cluster. Each patch defines either a convex quadrilateral or a triangle, and patches represent quad-trees called nodes. Each node points to its corner vertices and its children. The leaf nodes point to their corner vertices and the child pointers can optionally point to the vertices that split an edge of the node. Only the locations of vertices that are corners of the patches are stored in the file; other vertices are created by subdividing nodes of the quad-tree as the LSB file is loaded. The color information for each vertex is unique and is specified in the file. The image in Figure 7-8 shows an LSB-format database developed during the design of a hospital operating room. This database was produced by the DeWolff Partnership of Rochester, New York, using the Lightscape Visualization system. Figure 7-8 LSB-Format Operating Room Database pfdLoadFile() uses the function pfdLoadFile_lsb() to load LSB format files into OpenGL Performer run-time data structures. 262 007-1680-100 Description of Supported Formats When working with Lightscape Technologies files, remember that hardware lighting is not needed because all illumination effects have already been accounted for with the ambient color at each vertex. Medit Productions MEDIT Format The “.medit” format is used by the Medit database modeling system produced by Medit Productions. The OpenGL Performer “.medit” loader is in the directory /usr/share/Performer/src/lib/libpfdb/libpfmedit for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfmedit for Microsoft Windows. pfdLoadFile() uses the function pfdLoadFile_medit() to load MEDIT format files into OpenGL Performer run-time data structures. NFF Neutral File Format The “.nff” format was developed by Eric Haines as a way to provide standard procedural databases for evaluating ray tracing software. OpenGL Performer includes an extended NFF loader with superquadric torus support, a named build keyword, and numerous small bug fixes. The “.nff” loader is located in the directory /usr/share/Performer/src/lib/libpfdb/libpfnff for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfnff for Microsoft Windows. The file /usr/share/Performer/data/sampler.nff on IRIX and Linux and %PFROOT%\Data\sampler.nff on Microsoft Windows uses each of the NFF data types. It is an excellent way to explore the “Show Tree”, “Draw Style”, and “Highlight Mode” features of Perfly. It is included here: #-- torus f .75 .00 .25 .6 .8 20 0 t 5 5 0 0 0 1 2 1 build torus #-- cylinder f .00 .75 .25 .6 .8 20 0 c 15 5 -3 2 15 5 3 2 #-- put a disc on the top and bottom of the cylinder d 15 5 -3 0 0 -1 0 2 007-1680-100 263 7: Importing Databases d 15 5 3 0 0 1 0 2 build cylinder #-- cone f .00 .25 .75 .6 .8 20 0 c 25 5 -3 3 25 5 3 0 #-- put a disc on the bottom of the cone d 25 5 -3 0 0 -1 0 3 build cone #-- sphere f .75 .00 .75 .6 .8 20 0 s 5 15 0 3 build sphere #-- hexahedron f .25 .25 .50 .6 .8 20 0 h 13 13 -2 17 17 2 build hexahedron #-- superquadric sphere f .80 .10 .30 .6 .8 20 0 ss 25 15 0 2 2 2 .1 .4 build superquadric_sphere #-- disc (washer shape) f .20 .20 .90 .6 .8 20 0 d 5 25 0 0 0 1 1 2.5 build disc #-- grid (height field) f .80 .80 .10 .6 .8 20 0 g 4 4 12 18 22 28 0 4 0 0 0 0 0 1 0 0 0 0 -1 0 0 0 0 0 build grid #-- superquadric torid f .40 .20 .60 .6 .8 20 0 st 25 25 0 0.5 0.5 0.5 .33 .33 3 build superquadric_torid 264 007-1680-100 Description of Supported Formats #-- polygon with no normals f .20 .20 .20 .6 .8 20 0 p 4 -5 -5 -10 35 -5 -10 35 35 -10 -5 35 -10 build polygon pfdLoadFile() uses the function pfdLoadFile_nff() to load NFF format files into OpenGL Performer run-time data structures. Wavefront Technology OBJ Format The OBJ format is an ASCII data representation read and written by the Wavefront Technology Model program. A number of database models in this format have been placed in the public domain, making this a useful format to have available. OpenGL Performer provides the function pfdLoadFile_obj() to import OBJ files. The source code for pfdLoadFile_obj() is in the file pfobj.c in the loader source directory /usr/share/Performer/src/lib/libpfdb/libpfobj for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfobj for Microsoft Windows. The OBJ-format database shown in Figure 7-9 models an office building that is part of the SGI corporate campus in Mountain View, California. 007-1680-100 265 7: Importing Databases Figure 7-9 SGI Office Building as OBJ Database Files in the OBJ format have a flexible all-ASCII structure, with simple keywords to direct the parsing of the data. This format is best illustrated with a short example that defines a texture-mapped square: #-- ‘v’ defines a vertex; here are four vertices v -5.000000 5.000000 0.000000 v -5.000000 -5.000000 0.000000 v 5.000000 -5.000000 0.000000 v 5.000000 5.000000 0.000000 #-- ‘vt’ defines a vertex texture coordinate; four are given vt 0.000000 1.000000 0.000000 vt 0.000000 0.000000 0.000000 vt 1.000000 0.000000 0.000000 vt 1.000000 1.000000 0.000000 #-- ‘usemtl’ means select the material definition defined #-- by the name MaterialName usemtl MaterialName 266 007-1680-100 Description of Supported Formats #-- ‘usemap’ means select the texturing definition defined #-- by the name TextureName usemap TextureName #-- ‘f’ defines a face. This face has four vertices ordered #-- counterclockwise from the upper left in both geometric #-- and texture coordinates. Each pair of numbers separated #-- by a slash indicates vertex and texture indices, #-- respectively, for a polygon vertex. f 1/1 2/2 3/3 4/4 pfdLoadFile() uses the function pfdLoadFile_obj() to load Wavefront OBJ files into OpenGL Performer run-time data structures. SGI PFB Format Note: The PFB format is undocumented and is subject to change. Although OpenGL Performer has no true native database format, the PFB format is designed to exactly replicate the OpenGL Performer scene graph; this design increases loading speed. A file in the PFB format has the following advantages: • PFB files often load in one tenth (or less) of the time it takes an equivalent file in another format to load. • PFB files are often half the size of equivalent files in another format. You can think of the PFB format as being a cache. You can convert your files into PFB for fast and efficient loading or paging, but you should always keep your original files in case you wish to modify them. Converting to the PFB Format You can convert files into the PFB format in one of the following ways: 007-1680-100 • Use the function pfdStoreFile_pfb() in libpfpfb. • Use pfconv. 267 7: Importing Databases SGI PFI Format The PFI image file format is designed for fast loading of images into pfTextures. pfLoadTexFile() can load PFI files as the image of a pfTexture. Since the format of the image in a PFI file matches that of a pfTexture, data is not reformatted at load time. Eliminating the reformatting often cuts the load time of textures to half of the load time of the same image in the IRIS RGB image format. PFI files can contain the mipmaps of the image. This feature saves significant time in the OpenGL Performer DRAW process since it does not have to generate the mipmaps. Creating PFI Files PFI files are created in the following ways: • pfSaveTexFile() creates a PFI file from a pfTexture. • The pfdImage methods in libpfdu create PFI files. • pficonv converts IRIS RGB image files into PFI files. • pfconv converts all referenced image files into PFI files when the setting PFPFB_SAVE_TEXTURE_PFI mode is PF_ON. The command line options to do this with pfconv is -Mpfb,5. SGI PHD Format The PHD format was created to describe the geometric polyhedron definitions derived mathematically by Andrew Hume and by the Kaleido program of Zvi Har’El. This format describes only the geometric shape of polyhedra; it provides no specification for color, texture, or appearance attributes such as specularity. The OpenGL Performer sample data directories contain numerous polyhedra in the PHD format. The image in Figure 7-10 shows many of the polyhedron definitions laboriously computed by Andrew Hume. 268 007-1680-100 Description of Supported Formats Figure 7-10 Plethora of Polyhedra in PHD Format The source code for the PHD-format importer is in the file /usr/share/Performer/src/lib/libpfdb/libpfpoly/pfphd.c on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfpoly\pfdhd.c on Microsoft Windows. PHD format files have a line-structured ASCII form; an initial keyword defines the contents of each line of data. The file format consists of a filename definition (introduced by the keyword file) followed by one or more object definitions. Object definitions are bracketed by the keywords object.begin and object.end and contain one or more polygon definitions. Objects can have a name in quotes following the object.begin keyword; such a name is used by the loader for the name of the corresponding OpenGL Performer node. Polygon definitions are bracketed by the keywords polygon.begin and polygon.end and contain three or more vertex definitions. 007-1680-100 269 7: Importing Databases Vertex definitions are introduced by the vertex keyword and define the X, Y, and Z coordinates of a single vertex. The following is a PHD-format definition of a unit-radius tetrahedron centered at the origin of the coordinate axes. It is derived from the database developed by Andrew Hume but has since been translated, scaled, and reformatted. file 000.phd object.begin "tetrahedron" polygon.begin vertex -0.090722 -0.366647 vertex 0.544331 -0.628540 vertex 0.453608 0.890430 polygon.end polygon.begin vertex -0.907218 0.104757 vertex -0.090722 -0.366647 vertex 0.453608 0.890430 polygon.end polygon.begin vertex -0.090722 -0.366647 vertex -0.907218 0.104757 vertex 0.544331 -0.628540 polygon.end polygon.begin vertex 0.453608 0.890430 vertex 0.544331 -0.628540 vertex -0.907218 0.104757 polygon.end object.end 0.925925 -0.555555 0.037037 -0.407407 0.925925 0.037037 0.925925 -0.407407 -0.555555 0.037037 -0.555555 -0.407407 pfdLoadFile() uses the function pfdLoadFile_phd() to load PHD format files into OpenGL Performer run-time data structures. The pfdLoadFile_phd() function composes a color with red, green, and blue components uniformly distributed within the range 0.2 to 0.7 that is consistent for each polygon with the same number of vertices within a single polyhedron. SGI PTU Format The PTU format is named for the OpenGL Performer Terrain Utilities, of which the pfdLoadFile_ptu() function is the sole example at the present time. This function accepts 270 007-1680-100 Description of Supported Formats as input the name of a control file (the file with the “.ptu” filename extension) that defines the desired terrain parameters and references additional data files. The database shown in Figure 7-11 represents a portion of the Yellowstone National Park. This terrain database was generated completely by the OpenGL Performer Terrain Utility data generator from digital terrain elevation data and satellite photographic images. Image manipulation is performed using the SGI ImageVision Library functions. Figure 7-11 Terrain Database Generated by PTU Tools The PTU control file has a fixed format that does not use keywords. The contents of this file are simply ASCII values representing the following data items: 1. The name to be assigned to the top-level pfNode built by pfdLoadFile_ptu(). 2. The number of desired levels-of-detail (LOD) for the resulting terrain surface. The pfdLoadFile_ptu() function will construct this many versions of the terrain, each representing the whole surface but with exponentially fewer numbers of polygons in each version. 007-1680-100 271 7: Importing Databases 3. The number of highest-LOD tiles that will tessellate the entire terrain surface in the X and Y axis directions. 4. Two numeric values that define the mapping of texture image pixels to world-coordinate terrain geometry. These values are the number of meters per texel (texture pixel) of filtered grid post data in the X and Y axis dimensions. 5. The name of an image file that represents terrain height at regularly spaced sample points in the form of a monochrome image whose brightness at each pixel indicates the height at that sample point. Additional arguments are the number of samples in the input image in the X and Y directions, as well as the desired number of samples in these directions. The pfdLoadFile_ptu() function resamples the grid posts from the original to the desired resolution by filtering the height image using SGI ImageVision Library functions. 6. The name of an image file that represents the terrain texture image at regularly spaced sample points. Subsequent arguments are the number of samples in the image in the X and Y directions as well as the desired number of samples in these directions. This image will be applied to the terrain geometry. The scale values provided in the PTU file allow the terrain grid and texture image to be adjusted to create an orthographic alignment. 7. An optional second texture-image filename that serves as a detail texture when the terrain is viewed on RealityEngine systems. This texture is used in addition to the base texture image. 8. An optional detail-texture spline-table definition. The blending of the primary texture image and the secondary detail texture is controlled by a blend table defined by this spline function. The spline table is optional even when a detail texture is specified. Detail texture and its associated blend functions are applicable only on RealityEngine systems. The source code for the PTU-format importer is provided in the file /usr/share/Performer/src/lib/libpfdb/libpfptu/pfptu.c on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfptu\pfptu.c on Microsoft Windows. pfdLoadFile() uses the function pfdLoadFile_ptu() to load PTU format files into OpenGL Performer run-time data structures. 272 007-1680-100 Description of Supported Formats ArchVision RPC Format ArchVision provides the rich photorealistic content (RPC) loader. The RPC loader loads in images from an ArchVision RPC file. The images represent views of an object from a set of directions around the object. If you provide an existing pfIBRnode, the images are loaded into a pfIBRtexture of the node. Otherwise, the function creates a new pfIBRnode with a single pfGeoSet and a pfIBRtexture containing the images. In the case of new content with meshes, the pfGeoSet contains the mesh, which becomes the proxy in the pfIBRnode. The following functions allow you to access and alter the modes, values, and attributes of the RPC loader: • pfdConverterMode_rpc(), pfdGetConverterMode_rpc() • pfdConverterVal_rpc(), pfdGetConverterVal_rpc() • pfdConverterAttr_rpc(), pfdGetConverterAttr_rpc() You control the RPC converter modes with the token PFRPC_USE_USER_IBRNODE. By default, the loader creates a pfIBRnode with a single pfGeoSet and a pfIBRtexture that contains the loaded images. If this mode is set to PF_ON and you supply a pfIBRnode using pfdConverterAttr_rpc(), the images are loaded into the pfIBRtexture of that node. Table 7-8 describes the RPC converter values. Table 7-8 007-1680-100 RPC Converter Values Converter Value Description PFRPC_SKIP_TEXTURES Skips every n images. ArchVision RPC files often contain hundreds of images. A pfIBRtexture containing so many images would be too large. The default is set to 2. If you want to use all images in the file, set it to 0. PFRPC_CROP_LEFT Crops the loaded images by the specified number of pixels on the left. PFRPC_CROP_RIGHT Crops the loaded images by the specified number of pixels on the right. Note that the resulting image width should be a power of 2. PFRPC_CROP_TOP Crops the loaded images by the specified number of pixels on the top. 273 7: Importing Databases Table 7-8 274 RPC Converter Values (continued) Converter Value Description PFRPC_CROP_BOTTOM Crops the loaded images by the specified number of pixels on the bottom. Note that the resulting image height should be a power of 2. PFRPC_SCALE_WIDTH Scales the billboard width in the case of a pfIBRnode without a proxy. PFRPC_SCALE_HEIGHT Scales the billboard height in the case of a pfIBRnode without a proxy. PFRPC_NEAREST Sets flag PFIBR_NEAREST on the pfIBRnode created by the loader. PFRPC_USE_NEAREST_RING In the case of content with a proxy and having more than one ring of views, forces the mode in which views are selected from the nearest ring rather than having the views blended between the two nearest rings. PFRPC_COMBINED_TEXTURE_SIZE Combines textures into a square texture of the specified size (should be a power of 2). By default, if the texture size is not a power of 2, textures are combined into a texture of size 2048x2048. If the texture size is power of 2, textures are not combined into a bigger texture unless the value PFRPC_COMBINED_TEXTURE_SIZE is explicitly specified. You can also set it to 0 to disable combining. 007-1680-100 Description of Supported Formats Table 7-9 describes the converter attributes. Table 7-9 RPC Converter Attributes Converter Attribute Description PFRPC_USER_IBRNODE Specifies a pfIBRnode. The images from the RPC file are loaded into the pfIBRtexture of the node. PFRPC_RING_FILE Specifies the path to ring files that define the rings of views where proxies are used. There is one file for each component of the input RPC file, indexed by extension .0, .1, .2, and so on. Each line of the ring file contains the angle of the ring from the horizon and the number of views in that ring. If no ring file is specified, each component has only one ring of 16 views at horizontal angle 0. You can use an environment variable of the same name to set this attribute. PFRPC_SKIP_TEXTURES PFRPC_SCALE_WIDTH PFRPC_FLIP_TEXTURES PFRPC_NEAREST PFRPC_USE_NEAREST_RING PFRPC_COMBINED_TEXTURE_SIZE See Table 7-8 for the descriptions of these attributes. You can use an environment variable of the same name to set this attribute. Setting attribute values through the use of environment variables allows you to affect the loading of the files without the necessity of changing your application. Note: The loader is using a relatively slow, third-party routine for decompressing images. For a faster load time, you may want to convert your RPC files into PFB files using pfconv. Two sample RPC files can be found in directory /usr/share/Performer/data/ibr/rpc for IRIX and Linux and in %PFROOT%\Data\ibr\rpc for MicroSoft Windows. You can download other files from the ArchVision webpage at www.archvision.com. USNA Standard Graphics Format The SGF format is used at the United States Naval Academy as a standard graphics format for geometric data. The loader was developed based on the description of the 007-1680-100 275 7: Importing Databases standard graphics format as described by David F. Rogers and J. Alan Adams in the book Mathematical Elements for Computer Graphics. The OpenGL Performer “.sgf” format loader is located in the directory /usr/share/Performer/src/lib/libpfdb/libpfsgf for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfsgf for Microsoft Windows Here is the vector definition for four stacked squares in SGF form: 0, 0, 0 1, 0, 0 1, 1, 0 0, 1, 0 0, 0, 0 1.0e37, 0, 0, 1 1, 0, 1 1, 1, 1 0, 1, 1 0, 0, 1 1.0e37, 0, 0, 2 1, 0, 2 1, 1, 2 0, 1, 2 0, 0, 2 1.0e37, 0, 0, 3 1, 0, 3 1, 1, 3 0, 1, 3 0, 0, 3 1.0e37, 1.0e37, 1.0e37 1.0e37, 1.0e37 1.0e37, 1.0e37 1.0e37, 1.0e37 pfdLoadFile() uses the function pfdLoadFile_sgf() to load SGF format files into OpenGL Performer run-time data-structures. SGI SGO Format The SGI Object format is used by several utility programs and was one of the first database formats supported by OpenGL Performer. The image in Figure 7-12 shows a model generated by Paul Haeberli and loaded into Perfly by the pfdLoadFile_sgo() database importer. 276 007-1680-100 Description of Supported Formats Figure 7-12 Model in SGO Format Objects in the SGO format have per-vertex color specification and multiple data formats. Objects contained in SGO files are constructed from three data types: • Lists of quadrilaterals • Lists of triangles • Triangle meshes Objects of different types can be included as data within one SGO file. The SGO format has the following structure: 1. A magic number, 0x5424, which identifies the file as an SGO file. 2. A set of data for each object. Each object definition begins with an identifying token, followed by geometric data. There can be multiple object definitions in a single file. An end-of-data token terminates the file. 007-1680-100 277 7: Importing Databases The layout of an SGO file is the following: <SGO-file magic number> <data-type token for object #1> <data for object #1> <data-type token for object #2> <data for object #2> ... <data-type token for object #n> <data for object #n> <end-of-data token> Each of the identifying tokens is 4 bytes long. Table 7-10 lists the symbol, value, and meaning for each token. Table 7-10 Object Tokens in the SGO Format Symbol Value Meaning OBJ_QUADLIST 1 Independent quadrilaterals OBJ_TRILIST 2 Independent triangles OBJ_TRIMESH 3 Triangle mesh OBJ_END 4 End-of-data token The next word following any of the three object types is the number of 4-byte words of data for that object. The format of this data varies depending on the object type. For quadrilateral list (OBJ_QUADLIST) and triangle list (OBJ_TRILIST) objects, there are nine words of floating-point data for each vertex, as follows: 1. Three words that specify the components of the normal vector at the vertex 2. Three words that specify the red, green, and blue color components, scaled to the range 0.0 to 1.0 3. Three words that specify the X, Y, and Z coordinates of the vertex itself In quadrilateral lists, vertices are in groups of four; so, there are 4 × 9 = 36 words of data for each quadrilateral. In triangle lists, vertices are in groups of three, for 3 x 9 = 27 words per triangle. 278 007-1680-100 Description of Supported Formats The triangle mesh, OBJ_TRIMESH, is the most complicated of the three object data types. Triangle mesh data consists of a set of vertices followed by a set of mesh-control commands. Triangle mesh data has the following format: 1. A long word that contains the number of words in the complete triangle mesh data packet 2. A long word that contains the number of floating-point words required by the vertex data, at nine words per vertex 3. The data for each vertex, consisting of nine floating-point words representing normal, color, and coordinate data 4. A list of triangle mesh controls The triangle mesh controls, each of which is one word in length, are listed in Table 7-11. Table 7-11 Mesh Control Tokens in the SGO Format Symbol Value Meaning OP_BGNTMESH 1 Begin a triangle strip. OP_SWAPTMESH 2 Exchange old vertices. OP_ENDBGNTMESH 3 End then begin a strip. OP_ENDTMESH 4 Terminate triangle mesh. The triangle-mesh controls are interpreted sequentially. The first control must always be OP_BGNTMESH, which initiates the mesh-decoding logic. After each mesh control is a word (of type long integer) that indicates how many vertex indices follow. The vertex indices are in byte offsets, so to access vertex n, you must use the byte offset n x 9 x 4. pfdLoadFile() uses the function pfdLoadFile_sgo() to load SGO format files into OpenGL Performer run-time data structures. You can find the source code for the SGO-format importer in the file pfsgo.c. This importer does not attempt to decode any triangle meshes present in input files; instead, it terminates the file conversion process as soon as an OBJ_TRIMESH data-type token is encountered. If you use SGO-format files containing triangle meshes you will need to extend the conversion support to include the triangle mesh data type. 007-1680-100 279 7: Importing Databases USNA Simple Polygon File Format The “.spf” format is used at the United States Naval Academy as a simple polygon file format for geometric data. The loader was developed based on the description in the book Mathematical Elements for Computer Graphics. The OpenGL Performer “.spf” loader is in the directory /usr/share/Performer/src/lib/libpfdb/libpfspf on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfspf on Microsoft Windows. The following “.spf” format file is defined in that book. polygon with a hole 14,2 4,4 4,26 20,26 28,18 28,4 21,4 21,8 10,8 10,4 10,12 10,20 17,20 21,16 21,12 9,1,2,3,4,5,6,7,8,9 5,10,11,12,13,14 If you look at this file in Perfly, you will see that the hole is not cut out of the letter “A” as might be desired. Such computational geometry computations are not considered the province of simple database loaders. pfdLoadFile() uses the function pfdLoadFile_spf() to load SPF format files into OpenGL Performer run-time data structures. Sierpinski Sponge Loader The Sierpinski Sponge (also known as Menger Sponge) loader is not based on a data format but rather is a procedural data generator. The loader interprets the portion of the user-provided filename before the period and extension as an integer which specifies the number of recursive subdivisions desired in data generation. For example, providing the 280 007-1680-100 Description of Supported Formats pseudo filename “3.sponge” to Perfly will result in the Sponge loader being invoked and generating a sponge object using three levels of recursion, resulting in a 35712 polygon database object. The OpenGL Performer “.sponge” loader can be found in the directory /usr/share/Performer/src/lib/libpfdb/libpfsponge on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfsponge on Microsoft Windows. pfdLoadFile() uses the function pfdLoadFile_sponge() to load Sponge format files into OpenGL Performer run-time data structures. Star Chart Format The “.star” format is a distillation of astronomical data from the Yale Compact Star Chart (YCSC). The sample data file /usr/share/Performer/data/3010.star for IRIX and Linux and %PFROOT%\Data\3010.star for Microsoft Windows contains data from the YCSC that has been reduced to a list of the 3010 brightest stars as seen from Earth and positioned as 3010 points of light on a unit-radius sphere. The OpenGL Performer “.star” loader can read this data and is provided as a convenience for making dusk, dawn, and night-time scenes. The loader is in the directory /usr/share/Performer/src/lib/libpfdb/libpfstar on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfstar on Microsoft Windows. Data in a “.star” file is simply a series of ASCII lines with the “s” (for star) keyword followed by X, Y, and Z coordinates, brightness, and an optional name. Here are the 10 brightest stars (excluding Sol) in the “.star” format: s s s s s s s s s s -0.18746032 0.93921369 -0.28763914 1.00 Sirius -0.06323564 0.60291260 -0.79529721 1.00 Canopus -0.78377002 -0.52700269 0.32859191 1.00 Arcturus 0.18718566 0.73014212 0.65715599 1.00 Capella 0.12507832 -0.76942003 0.62637711 0.99 Vega 0.13051330 0.68228769 0.71933979 0.99 Capella 0.19507207 0.97036278 -0.14262892 0.98 Rigel -0.37387931 -0.31261155 -0.87320572 0.94 Rigil Kentaurus -0.41809806 0.90381104 0.09121194 0.94 Procyon 0.49255905 0.22369388 -0.84103900 0.92 Achernar pfdLoadFile() uses the function pfdLoadFile_star() to load Star format files into OpenGL Performer run-time data structures. 007-1680-100 281 7: Importing Databases 3D Lithography STL Format The STL format is used to define 3D solids to be imaged by 3D lithography systems. STL defines objects as collections of triangular facets, each with an associated face normal. The ASCII version of this format is known as STLA and has a very simple structure. The image in Figure 7-13 shows a typical STLA mechanical CAD database. This model is defined in the bendix.stla sample data file. Figure 7-13 Sample STLA Database The source code for the STLA-format loader is in the files /usr/share/Performer/src/lib/libpfdb/libpfstla/pfstla.c on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfstla\pfstla.c on Microsoft Windows. STLA-format files have a line-structured ASCII form; an initial keyword defines the contents of each line of data. An STLA file consists of one or more facet definitions, each of which contains the following: 1. The facet normal, indicated with the facet normal keyword 2. The facet vertices, bracketed by outer loop and endloop keywords 282 007-1680-100 Description of Supported Formats 3. The endloop keyword Here is an excerpt from nut.stla, one of the STLA files provided in the OpenGL Performer sample data directories. These are the first two polygons of a 524-triangle hex-nut object: facet normal 0 -1 0 outer loop vertex 0.180666 -7.62 2.70757 vertex -4.78652 -7.62 1.76185 vertex -4.436 -7.62 0 endloop endfacet facet normal -0.381579 -0.921214 -0.075915 outer loop vertex -4.48833 -7.59833 0 vertex -4.436 -7.62 0 vertex -4.78652 -7.62 1.76185 endloop endfacet Use this function to import data from STLA-format files into OpenGL Performer run-time data structures: pfNode *pfdLoadFile_stla(char *fileName); pfdLoadFile_stla() searches the current OpenGL Performer file path for the file named by the fileName argument and returns a pointer to the pfNode that parents the imported scene graph, or NULL if the file is not readable or does not contain recognizable STLA format data. SuperViewer SV Format The SuperViewer (SV) format is one of the several database formats that the I3DM database modeling tool can read and write. The I3DM modeler was developed by John Kichury of SGI and is provided with OpenGL Performer. The source code for the SV format importer is in the file pfsv.c. The passenger vehicle database shown in Figure 7-14 was modeled using I3DM and is stored in the SV database format. 007-1680-100 283 7: Importing Databases Figure 7-14 Early Automobile in SuperViewer SV Format Within SV files, object geometry and attributes are described between text lines that contain the keywords model and endmodel. For example: model wing geometry and attributes endmodel Any number of models can appear within a SuperViewer file. The geometry and attribute data mentioned above each consist of one of the following types: • 3D Polygon with vertex normals and optional texture coordinates: poly3dn <num_vertices> [textured] x1 y1 z1 nx1 ny1 nz1 [s1 t1] x2 y2 z2 nx2 ny2 nz2 [s2 t2] ... where the coordinates and normals are defined as follows: 284 – Xn Yn Zn are the nth vertex coordinates – Nxn Nyn Nzn are the nth vertex normals 007-1680-100 Description of Supported Formats – • Sn Tn are the nth texture coordinates 3D Triangle mesh with vertex normals and optional texture coordinates tmeshn <num_vertices> [textured] x1 y1 z1 nx1 ny1 nz1 [s1 t1] x2 y2 z2 nx2 ny2 nz2 [s2 t2] ... where the coordinates and normals are defined as follows: • – Xn Yn Zn are the nth vertex coordinates – Nxn Nyn Nzn are the nth vertex normals – Sn Tn are the nth texture coordinates Material definition. If the material directive exists before a model definition, it is taken as a new material specification. Its format is the following: material n Ar Ag Ab Dr Dg Db Sr Sg Sb Shine Er Eg Eb where the variables represent the following: – n is an integer specifying a material number – Ar Ag Ab is the ambient color. – Dr Dg Db is the diffuse color. – Sr Sg Sb is the specular color. – Shine is the material shininess. – Er Eg Eb is the emissive color. If the material directive exists within a model description, the format is the following: material n where n is an integer specifying which material (as defined by the material description above) is to be assigned to subsequent data. • Texture definition. If the texture directive exists before a model definition it is taken as a new texture specification. Its format is the following: texture n TextureFileName If the texture directive exists within a model description, the format is the following: texture n 007-1680-100 285 7: Importing Databases where n is an integer specifying which texture (as defined by the texture description above) is to be assigned to subsequent data. • Backface polygon display mode. The backface directive is specified within model definitions to control backface polygon culling: backface mode where a mode of “on” allows the display of backfacing polygons and a mode of “off” suppresses their display. In actual use the SV format is somewhat self-documenting. Here is part of the SV file apple.sv from the directory /usr/share/Performer/data on IRIX and Linux and in %PFROOT%\Data on Microsoft Windows: material 20 0.0 0.0 0 0.400000 0.000000 0 0.333333 0.000000 0.0 10.0000 0 0 0 material 42 0.2 0.2 0 0.666667 0.666667 0 0.800000 0.800000 0.8 94.1606 0 0 0 material 44 0.0 0.2 0 0.000000 0.200000 0 0.000000 0.266667 0.0 5.0000 0 0 0 texture 4 prchmnt.rgb texture 6 wood.rgb model LEAF material 44 texture 4 backface on poly3dn 4 textured 1.35265 1.35761 13.8338 0.88243 0.96366 14.0329 -4.44467 1.24026 13.5669 -2.37938 2.17479 13.3626 poly3dn 4 textured -2.37938 2.17479 13.3626 -4.44467 1.24026 13.5669 -9.23775 2.34664 13.1475 -6.69592 3.94535 12.6716 0.0686595 0.0502096 0.0363863 0.0363863 -0.234553 -0.376701 -0.337291 -0.337291 -0.969676 -0.924973 -0.940697 -0.940697 0 1 0 0.75 0.0909091 0.75 0.0909091 1 0.0363863 0.0363863 0.0344832 0.0344832 -0.337291 -0.337291 -0.284369 -0.284369 -0.940697 -0.940697 -0.958095 -0.958095 0.0909091 1 0.0909091 0.75 0.181818 0.75 0.181818 1 This excerpt specifies material properties and references texture images stored in the files prchmnt.rgb and wood.rgb, and then defines two polygons. pfdLoadFile() uses the function pfdLoadFile_sv() to load SuperViewer files into OpenGL Performer run-time data structures. 286 007-1680-100 Description of Supported Formats Geometry Center Triangle Format The “.tri” format is used at the University of Minnesota’s Geometry Center as a simple geometric data representation. The loader was developed by inspection of a few sample files. The OpenGL Performer “.tri” loader is in the directory /usr/share/Performer/src/lib/libpfdb/libpftri on IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpftri on Microsoft Windows. These files have a very simple format: a line per vertex with position and normal given on each line as 6 ASCII numeric values. The file is simply a series of these triangle definitions. Here are the first two triangles from the data file /usr/share/Performer/data/mobrect.tri on IRIX and Linux and in %PFROOT%\Data\mobrect.tri on Microsoft Windows: 1.788180 1.000870 0.135214 0.076169 -0.085488 0.993423 1.574000 0.925908 0.146652 0.089015 -0.086072 0.992304 1.793360 0.634711 0.099409 0.076402 -0.111845 0.990784 0.836848 -0.595230 0.197960 0.156677 0.044503 0.986647 0.709638 -0.345676 0.210010 0.157642 0.021968 0.987252 0.581200 -0.535321 0.234807 0.145068 0.030985 0.988936 pfdLoadFile() uses the function pfdLoadFile_tri() to load “.tri” format files into OpenGL Performer run-time data structures. UNC Walkthrough Format The “.unc” format was once used at the University of North Carolina as a format for geometric data in an architectural walkthrough application. The loader was developed based on inspection of a few sample files. The OpenGL Performer “.unc” loader is in the directory /usr/share/Performer/src/lib/libpfdb/libpfunc for IRIX and Linux and in %PFROOT%\Src\lib\libpfdb\libpfunc for Microsoft Windows. pfdLoadFile() uses the function pfdLoadFile_unc() to load UNC format files into OpenGL Performer run-time data structures. WRL Format The VRML 2.0 format for OpenGL Performer, wrl, is made by DRaW Computing Associates. It accepts geometry and texture only. Basic geometry nodes like Sphere, Cone, Cylinder, Box and related nodes like Shape, Material, Appearance, 007-1680-100 287 7: Importing Databases TextureTransform, ImageTexture, and ElevationGrid are supported. Also, complex geometries can be obtained using the IndexedFaceSet node. You can do geometric manipulations to nodes using Group nodes and Transform nodes. You can also make very complex structures using PROTOs, where you group many geometry nodes. Database Operators with Pseudo Loaders The OpenGL Performer dynamic database loading mechanism provides additional DSOs that operate on the resulting scene graph from a file or set of files after the file(s) are loaded. This mechanism, called “pseudo loaders,” enables the desired operator DSO to be specified as additional suffixes to the filename. The DSO matching the last suffix is loaded first and provided the entire filename. That pseudo loader then can parse the arbitrary filename and invoke the next operator or loader and then operate on the results. This process allows additional arguments to be buried in the specified filename for the pseudo loader to detect and parse. One set of pseudo loaders included with OpenGL Performer are the rot, trans, and scale loaders. These loaders take hpr and xyz arguments in addition to their Filename and can be invoked from any program using pfdLoadFile(), as shown in this example: % perfly cow.obj.-90,90,0.rot -90, 90, and 0 are the h, p, and r values, respectively. If you are using a shell with argument expansion, such as csh, you can create interesting cow art. Try out the following example: % perfly cow.obj.{0,1},0,0.trans cow.obj.{0,1,2,3,4},0,-5.trans Specifying a base filename is only needed if the specified pseudo loader expects a file to process. Loaders can generate their scene graphics procedurally based on simple parameters specified in the command string. 288 007-1680-100 Database Operators with Pseudo Loaders The pseudo loaders in the OpenGL Performer distribution are described in Table 7-12. Table 7-12 OpenGL Performer Pseudo Loaders Pseudo Loaders Description libpfrot Add pfSCS at root to rotate scene graph by specified h,p,r. libpftrans Add pfSCS at root to translate scene graph by specified x,y,z. libpfscale Add pfSCS at root to sale scene graph by specified x,y,z. libpfclosest Adds run-time application callback to highlight closest point each frame. libpfcliptile Adds callback to compute for the specified tilename, minS ,minT, maxS, and maxT, the proper virtual cliptexture viewing parameters. libpfsphere Generates a sphere database with morphing LOD starting from an n-gon for specified n, power of 2. libpfvct Convert normal cliptexture .ct file to virtual cliptexture. libpfsubdiv Subdivide an arbitrary file using Loop or Catmull-Clark subdivision. libpfgeoa Convert geometry from pfGeoSets to pfGeoArrays. libpfgopt Optimize the geometry of pfGeoSets or pfGeoArrays. libpfbreakup Create an artificial hierarchy from unstructured input. Pseudo loaders should define pfdLoadNeededDSOs_EXT() for the following: • Preinitializing DSOs • Loading other special files • Performing additional initialization, such as class initialization, that should happen before pfConfig() The libpfbreakup pseudo loader, which creates an artificial hierarchy from unstructured input, uses the pfdBreakup() function, whose syntax follows: pfNode * pfdBreakup(pfGeode *geode, float geodeSize, int stripLength, int geodeChild); The function accepts a pfGeode that contains pfGeoSets of type PFGS_TRISTRIPS and builds a new scene graph with the same geometric content but with a spatial subdivision structure designed for efficient processing. The function returns the root of the new scene graph (a pfGroup) or, if the subdivision was not done, a copy of the original pfGeode. 007-1680-100 289 7: Importing Databases The first triangle strips of all pfGeoSets in geode are split into strips no longer than stripLength. If the pfGeoSets do not contain triangle strips, they are left untouched and geode is subdivided based on the geometrical centers of the split pfGeoSets using an octree. The degree of recursive partitioning desired is specified in the function arguments. The resulting scene graph is a pfGroup that contains more pfGroups, recursively. The recursion stops if the resulting pfGeode is smaller than geodeSize (geodeSize is the maximum size of the leaf octants in world coordinates) or the number of its pfGeoSets is smaller than geodeChild. Note: The input pfGeode is not deleted. See also “The libpfsubdiv Pseudo Loader” on page 424. The Maya Database Exporter OpenGL Performer provides the PFBexport plug-in for Maya. This Maya exporter converts a Maya scene into OpenGL Performer data structures and saves them in an OpenGL Performer binary (.pfb) or ASCII (.pfa) file. These output files can be displayed with the OpenGL Performer viewer or imported into an OpenGL Performer application. The exporter produces a log file that describes the Maya objects converted and flags any errors or unsupported features. This section describes the following topics: • “Installation Requirements” • “Exporting a Scene Using the Graphical Interface” • “Exporting a Scene Using the Maya Embedded Language (MEL)” • “Translation Details” Installation Requirements The Maya exporter should be installed for you automatically as part of the OpenGL Performer installation process. You must have a licensed copy of Maya 4.5 or later installed on every machine that runs the exporter because it is a Maya plug-in and can only run within the Maya environment. This differs from previous OpenGL Performer 290 007-1680-100 The Maya Database Exporter file loaders that run within the OpenGL Performer environment. The Maya exporter is available for both IRIX and Windows, but not for Linux. The .pfa and .pfb files produced by the Maya exporter can be viewed on any machine with OpenGL Performer 3.1 or later installed. These files are not compatible with previous versions of OpenGL Performer. Maya has capabilities like subdivision surfaces and non-uniform rational B-splines (NURBS) that require the OpenGL Performer 3.1 or later run-time environment. The exporter can optimize geometry to take advantage of the new OpenGL extensions available on Onyx4 systems. These extensions also need OpenGL Performer 3.1 or later support. If you launch Maya and it cannot find the plug-in, you can troubleshoot by checking the MAYA_PLUG_IN_PATH and MAYA_SCRIPT_PATH environment variables. Both variables should be defined at installation to reference the OpenGL Performer directory containing the Maya extensions. Table 7-13 shows the default path for IRIX and Microsoft Windows. Table 7-13 Default Path for the Maya Export Plug-in Platform Default Path IRIX /usr/share/OpenGL Performer/bin Microsoft Windows c:\SGI\OpenGL Performer\Bin Exporting a Scene Using the Graphical Interface Any Maya scene can be exported to OpenGL Performer format. Unsupported features will be flagged in the export log file and may result in objects missing from the scene when viewed with OpenGL Performer. To run the plug-in from the Maya graphical user interface, use the Export All or Export Selection items from the File menu. As shown in Figure 7-15, you should be able to select PFBexport as one of the file types. The only supported extensions are .pfb and .pfa. 007-1680-100 291 7: Importing Databases Figure 7-15 Maya Export Screen There are a number of parameters that control how Maya content is translated to OpenGL Performer format. You can produce exported files that take advantage of OpenGL extensions for multitexturing and vertex buffer objects but you must be running Maya on a platform which has these features. In general, OpenGL Performer .pfa and .pfb files are cross-platform and can be successfully imported on machines that are less-capable than where they originated with some loss of fidelity. For example, displaying a multitextured object on an Octane will drop all textures but the first. As shown in Figure 7-16, you can optimize for earlier versions of SGI hardware by choosing Optimize for OpenGL 1.0. If you have have an Onyx 4 system, do not check this option or you will not get the best graphics performance. If your graphics hardware does not have multitexturing capabilities, do check this option. 292 007-1680-100 The Maya Database Exporter Figure 7-16 007-1680-100 Maya Export Options 293 7: Importing Databases Table 7-14 describes the export options. Table 7-14 Maya Export Options Category Option Description Export Options Export geometry only If checked, only geometry will be included in the are also exported. This can result in poor performance from using too many light sources. Optimize for OpenGL 1.0 If checked, the exporter optimizes for earlier versions of SGI hardware without multitexturing capabilities. Do not check this option for an Onyx4 system. Visual preview If checked, you get a visual preview. Indexed meshes If checked, polygon mesh geometry will be exported as indexed, allowing vertices to be shared. Non-indexed meshes are faster on OpenGL 1.0. Triangle strips If checked, the exporter will convert polygon meshes to triangle lists or triangle strips if possible. This option is faster on all platforms but should not be checked if you are producing subdivision surfaces (see the option Force subdivision surfaces). If not checked, the Maya polygonal structure is preserved in the OpenGL Performer geometry. (Triangle stripping is not performed on indexed meshes.) Force subdivision surfaces If checked, the exporter will convert polygon meshes to subdivision surfaces in the OpenGL Performer file. This works best with low-count polygon meshes. Using complex meshes can exhaust memory or produce large models that are slow to display. Geometry Options 294 007-1680-100 The Maya Database Exporter Table 7-14 Maya Export Options (continued) Category Texture Options 007-1680-100 Option Description Force polygon meshes If checked, the exporter will convert subdivision surfaces and NURBS models to polygon meshes. The format of the output meshes is controlled by the mesh export options (see category Export geometry). If not checked, subdivision surfaces and NURBS from Maya are preserved in OpenGL Performer. Export textures If checked, texturing and UV mapping is enabled. Objects that are textured in Maya will be output as textured in the OpenGL Performer file. If you uncheck this box, the exporter ignores all Maya texturing and produces OpenGL Performer geometry with materials only. Make RGB texture files If checked, all textures encountered in Maya are converted to RGB format as part of the export process. The Width and Height sliders control the size of the RGB texture images. This option should be checked if you are using procedural textures in your Maya scene or if you plan to use IRIX to display your OpenGL Performer files. 295 7: Importing Databases Exporting a Scene Using the Maya Embedded Language (MEL) If you are familiar with MEL, the Maya scripting language, you can invoke the OpenGL Performer export plug-in from within a script. This allows you to do batch translation of Maya files to OpenGL Performer. The following sample MEL script converts all of the Maya binary (.mb) files in a given directory to OpenGL Performer binary (.pfb) format: global proc int ExportAsPFB(string $indir, string $opts) { string $infile; string $outfile; string $files[]; string $s; int $succeed = 1; if ($opts == "") $opts = "textures=1;width=256;height=256;indexed=1;tristrip=1;" "gl1=1;preview=0;"; if ($indir == "") $indir = "./"; setProject $indir; $files=`getFileList -folder $indir -filespec "*.mb"`; for ($infile in $files) { $outfile = $indir + substring($infile, 1, size($infile) - 3) + ".pfb"; $infile = $indir + $infile; file -open -force $infile; $s = `file -exportAll -type "PFBexport" -options $opts -force $outfile`; $succeed = `file -query -exists $s`; if (!$succeed) $s = "ERROR: failed to export " + $outfile + "\n"; else $s = "exported " + $s + "\n"; print $s; } return $succeed; } 296 007-1680-100 The Maya Database Exporter Translation Details This section describes the translation of Maya scenes to OpenGL Performer using the following topics: • “Supported Maya Features” • “Hierarchy” • “Geometry” • “Shaders” Supported Maya Features Maya has many capabilities that cannot be represented in OpenGL Performer, particularly in the area of shaders. Some of these features are simply ignored while others may suffer quality loss. OpenGL Performer uses the OpenGL graphics pipeline, which only allows light sources to illuminate objects directly. Light reflected from objects does not illuminate other objects in the scene as it does with Maya. Consequently, high-quality shading effects are often lost in the conversion. Table 7-15 lists the Maya features that are supported by the exporter (column 1) and those that are not supported (column 2). Table 7-15 007-1680-100 Maya Features Supported by the Exporter Supported Maya Features Unsupported Maya Features Polygonal meshes Layered shaders Subdivision surfaces Ramp shaders NURBS Shading maps Multitexturing Anisotropic materials Lambert, Blinn and Phong materials Volumetric materials Layered textures Translucence Procedural textures Displacement mapping Filtering, mipmapping Quadratic, quartic, and Gaussian filtering Reflection mapping 3D or cube map textures 297 7: Importing Databases Table 7-15 Maya Features Supported by the Exporter (continued) Supported Maya Features Unsupported Maya Features Cameras Animation and skinning Ambient, point, directional, and spot light sources Area and volume light sources Node instancing Ray tracing Shadow mapping Custom shaders and mental ray Bump mapping Glow, fog, and motion blur Vertex colors Dynamics and fluids Per-object light list Switches, expressions, and scripting Hierarchy The exporter attempts to preserve the structure of the Maya scene graph as much as possible in the OpenGL Performer output. Nodes that are instanced in Maya are also shared in the OpenGL Performer hierarchy. Maya and OpenGL Performer use different internal coordinate systems. This is not a issue when importing a Maya scene into an OpenGL Performer application if you use the whole scene because the root transformation corrects for the coordinate system differences. However, if you take part of a scene from a .pfb file exported from Maya and use it within another OpenGL Performer scene, the Maya objects may be oriented incorrectly. The following subsections describe translation details surrounding the scene graph hierarchy: 298 • Naming • Cameras • Light Sources • Sprites and Billboards 007-1680-100 The Maya Database Exporter Naming Node names are preserved so that Maya objects can be found by name in an OpenGL Performer application. The name of an OpenGL Performer scene graph node is composed of the file base name and the Maya object name. This is to permit multiple Maya files to be imported without naming conflicts. For example, the Maya object pCubeShape exported to file /usr/people/nola/myscene.pfb would be called myscene.pCubeShape in the OpenGL Performer scene graph. Cameras Maya cameras are kept in the scene graph and any camera node may be used to view the scene. In OpenGL Performer, the camera functionality is provided by the channel, which is the root of the scene hierarchy. Maya cameras are exported as OpenGL Performer DCS (dynamic coordinate system) nodes and keep their relative position within the hierarchy so that they are accessible in OpenGL Performer applications. The root of the exported scene graph includes the appropriate viewing transformation to display the scene from the viewpoint of the current Maya viewport camera. The Maya background color associated with the camera is not preserved in the OpenGL Performer scene. Light Sources OpenGL Performer has equivalents for ambient, directional, point, and spot light sources because they are supported by the OpenGL pipeline. Area and volume lights are ignored by the exporter. The exporter honors Maya per-object lighting. If you specify that only certain lights should affect an object in Maya, this light list is associated with the OpenGL Performer object. Because adding a light source in a real-time system can be computationally costly, it is a good idea to use as few lights as possible when exporting to OpenGL Performer. Sprites and Billboards A Maya sprite is an animated particle type that maps an image sequence onto a flat rectangle. Sprites always face the camera and maintain their size when moved in the Z direction. The sprite texture can be a single image or a sequence of images (texture animation). Sprites are supported by the exporter. Maya does not have an equivalent to the OpenGL Performer billboard that makes arbitrary geometry face the camera at run time. Billboards are exported as DCS nodes. 007-1680-100 299 7: Importing Databases Geometry Maya has three ways to construct and represent geometry: • Indexed triangle meshes • Subdivision surfaces • NURBS OpenGL Performer supports all three of these surface description methods directly. So, Maya geometry can be displayed without a loss of fidelity. The following subsections further describe geometry translation: • Polygonal Meshes • Subdivision Surfaces • UV Mapping Polygonal Meshes Polygonal meshes in Maya are indexed and share vertex, normal, and texture coordinate data whenever possible. In Maya, different faces in the mesh can use different shaders. Therefore, a single mesh can be associated with multiple appearances. In this case, one mesh in Maya can produce several OpenGL Performer meshes, one for each appearance. This is true of surfaces as well. There are several ways to export Maya meshes to OpenGL Performer depending on how you plan to use the geometry and the platform. Static geometry that is not modified at run time can be highly optimized to display quickly. Meshes that will be dynamically modified preserve the original Maya structure to make vertex manipulation at run time easier and more efficient. All exported meshes have locations and either normals or vertex colors depending on how they were constructed in Maya. Texture coordinate sets are only included if texture output is enabled and the mesh is textured. Per-vertex colors are not supported by the exporter and will be ignored. Subdivision Surfaces Both Maya and OpenGL Performer subdivision surfaces use the Catmull-Clark algorithm. Therefore, run-time tesselation should produce results similar to Maya. 300 007-1680-100 The Maya Database Exporter OpenGL Performer subdivision surfaces allow multiple materials on a single mesh and will preserve connectivity. Although Maya NURBS shapes can use multiple shaders, the OpenGL Performer NURBS geode only supports one material. The exporter does not support the use of multiple shaders across a NURBS surface. You can export a surface as a polygonal mesh and the exporter will use Maya to tesselate the surface. UV Mapping The UV mapping process transforms or generates texture coordinates for a mesh. Static UV mapping is applied to the Maya texture coordinates at export time. Environment mapping dynamically generates texture coordinates at run time based on the location of the viewer. Texture coordinate animation is not supported by the exporter. Table 7-16 describes how the exporter supports the various UV mapping methods. Table 7-16 Maya Exporter Support for UV Mapping Methods UV Map Method Exporter Support place2dTexture 2D translation, scaling and rotation of the texture coordinates done at export time. envSphere Spherical environment mapping. Texture coordinates are generated by OpenGL Performer at run time. envCube Cubic environment mapping. Texture coordinates are generated by OpenGL Performer at run time. Place3dTexture 3D matrix applied to texture coordinates at run time. envSky Ignored. envChrome Ignored. envBall Ignored. Shaders The following subsections describe how the exporter supports Maya shaders: 007-1680-100 301 7: Importing Databases • Materials • Textures • Texture Effects • Procedural Textures • Layered Textures Materials Lambert, Blinn, and Phong materials are supported by the exporter but not all of the material properties are honored, as shown in Table 7-17. Table 7-17 302 Maya Exporter Support for Material Properties Material Property Exporter Support Color Becomes diffuse material color. Transparency Becomes material transparency. You cannot specify a map here to be used as an alpha channel. The texture alpha channel must be included in the texture file. Ambient color Becomes ambient material color. If a texture map is used here, it is added to the overall color and light sources do not affect it. Incandescence Becomes emissive material color. If a texture map is used here, it is added to the overall color. Diffuse Is used as a multiplier for diffuse material color. Translucence Ignored. Glow intensity Ignored. Hide source Ignored. Matte opacity Ignored. Ray trace options Ignored. 007-1680-100 The Maya Database Exporter Textures All textures used with OpenGL Performer must come from bitmap files. Because OpenGL Performer does not support all texture formats on every platform, use the .rgb format for textures if you plan to use your exported files on multiple platforms. OpenGL Performer multitexturing support is hardware-based and varies across platforms. On some older IRIX systems, multitexturing is not supported. Newer hardware can have two or four texture units. The Maya exporter supports a maximum of eight textures per object. It is possible to produce .pfb files that will not look the same on all platforms. If you use more than eight textures on a single mesh, the exporter will ignore the extra textures and produce an error message in the log file. Texture Effects Maya allows you to correctly color your textures by adding and/or multiplying the texture by a color before applying it (using the color gain and color offset attributes). You can also invert or filter the texels. These texture effects are only preserved if you convert your textures to RGB. The exporter captures the texture after these computations have been performed. Table 7-18 describes how the exporter supports the various the Maya texture properties. Table 7-18 007-1680-100 Maya Exporter Support for Texture Properties Texture Property Exporter Support Image name Name of bitmap file containing texture data. Color gain Ignored unless the Make RGB option is enabled. Color offset Ignored unless the Make RGB option is enabled. Invert Ignored the unless Make RGB option is enabled. Wrap Becomes texture wrap mode. Filter Only mipmap and box filters may be used. Hardware cycling If animation export is enabled, a texture animation is produced. Otherwise, it is ignored. Filter offset Ignored. Blend Ignored. 303 7: Importing Databases Table 7-18 Maya Exporter Support for Texture Properties (continued) Texture Property Exporter Support Alpha gain Ignored. Alpha offset Ignored. Alpha is luminance Ignored. Default color Ignored. Color remap Ignored. Procedural Textures Maya has a rich library of procedural textures that can dynamically create texel patterns without requiring bitmap files. When the exporter encounters a procedural texture, it produces a reference to a bitmap file containing a snapshot of the procedural texture from Maya. If the Make RGB option is enabled, the exporter creates a separate RGB bitmap file for every texture used in the scene. This step only needs to be done when you add new textures or change the construction options on an existing procedural texture.The name of the RGB file is constructed by using the name of the Maya texture object with a .rgb suffix. Consequently, if you export multiple files that use the same object names, the exporter will use the same filename for multiple textures. Consequently, the same filenames could cause problems if the textures are not the same. Procedural textures are emulated in OpenGL Performer using 2D UV coordinates and the texture matrix. It is not always possible to get the same results as Maya, particularly with 3D textures. The exporter uses the first UV set on the mesh to map the snapshot of the procedural texture. By manipulating these UVs, you can often improve the real-time appearance of procedural textures. Layered Textures Layered textures in Maya allow the artist to blend several different textures together to produce an output color. The exporter supports layered textures but OpenGL Performer cannot implement all of the blend modes. Only the None, Over, In, Add and Multiply blend modes are honored. All other modes default to Multiply. 304 007-1680-100 Chapter 8 8. Geometry All libpr geometry is defined by modular units of prmitives that employ a flexible specification method. The following two classes allow you to group these basic primitives: • “pfGeoSet (Geometry Set)” on page 305 • “pfGeoArray (Geometry Array)” on page 319 This chapter describes these two classes and the following two topics: • “Optimizing Geometry for Rendering” on page 329 • “Rendering 3D Text” on page 335 pfGeoSet (Geometry Set) A pfGeoSet is a collection of geometry that shares certain characteristics. All items in a pfGeoSet must be of the same primitive type (whether they are points, lines, or triangles) and share the same set of attribute bindings (you cannot specify colors-per-vertex for some items and colors-per-primitive for others in the same pfGeoSet). A pfGeoSet forms primitives out of lists of attributes that may be either indexed or nonindexed. An indexed pfGeoSet uses a list of unsigned short integers to index an attribute list. (See “Attributes” on page 313 for information about attributes and bindings.) Indexing provides a more general mechanism for specifying geometry than hard-wired attribute lists and also has the potential for substantial memory savings as a result of shared attributes. Nonindexed pfGeoSets are sometimes easier to construct, usually a bit faster to render, and may save memory (since no extra space is needed for index lists) in situations where vertex sharing is not possible. A pfGeoSet must be either completely indexed or completely nonindexed; it is not valid to have some attributes indexed and others nonindexed. 007-1680-100 305 8: Geometry Note: libpf applications can include pfGeoSets in the scene graph with the pfGeode (Geometry Node). Table 8-1 lists a subset of the routines that manipulate pfGeoSets. Table 8-1 306 pfGeoSet Routines Routine Description pfNewGSet() Create a new pfGeoSet. pfDelete() Delete a pfGeoSet. pfCopy() Copy a pfGeoSet. pfGSetGState() Specify the pfGeoState to be used. pfGSetGStateIndex() Specify the pfGeoState index to be used. pfGSetNumPrims() Specify the number of primitive items. pfGSetPrimType() Specify the type of primitive. pfGSetPrimLengths() Set the lengths array for strip primitives. pfGetGSetPrimLength() Get the length for the specified strip primitive. pfGSetAttr() Set the attribute bindings. pfGSetMultiAttr() Set multi-value attributes (for example, multi-texture coordinates). pfGSetDrawMode() Specify draw mode (for example, flat shading or wireframe). pfGSetLineWidth() Set the line width for line primitives. pfGSetPntSize() Set the point size for point primitives. pfGSetHlight() Specify highlighting type for drawing. pfDrawGSet() Draw a pfGeoSet. pfGSetBBox() Specify a bounding box for the geometry. pfGSetIsectMask() Specify an intersection mask for pfGSetIsectSegs(). pfGSetIsectSegs() Intersect line segments with pfGeoSet geometry. 007-1680-100 pfGeoSet (Geometry Set) Table 8-1 pfGeoSet Routines (continued) Routine Description pfQueryGSet() Determine the number of triangles or vertices. pfPrint() Print the pfGeoSet contents. Primitive Types All primitives within a given pfGeoSet must be of the same type. To set the type of all primitives in a pfGeoSet named gset, call pfGSetPrimType(gset, type). Table 8-2 lists the primitive type tokens, the primitive types that they represent, and the number of vertices in a coordinate list for that type of primitive. Table 8-2 Geometry Primitives Token Primitive Type Number of Vertices PFGS_POINTS Points numPrims PFGS_LINES Independent line segments 2 * numPrims PFGS_LINESTRIPS Strips of connected lines Sum of lengths array PFGS_FLAT_LINESTRIPS Strips of flat-shaded lines Sum of lengths array PFGS_TRIS Independent triangles 3 * numPrims PFGS_TRISTRIPS Strips of connected triangles Sum of lengths array PFGS_FLAT_TRISTRIPS Strips of flat-shaded triangles Sum of lengths array PFGS_TRIFANS Fan of conected triangles Sum of lengths array PFGS_FLAT_TRIFANS Fan of flat-shaded triangles Sum of lengths array PFGS_QUADS Independent quadrilaterals 4 * numPrims PFGS_POLYS Independent polygons Sum of lengths array The parameters in the last column denote the following: numPrims 007-1680-100 The number of primitive items in the pfGeoSet, as set by pfGSetNumPrims(). 307 8: Geometry lengths The array of strip lengths in the pfGeoSet, as set by pfGSetPrimLengths() (note that length is measured here in terms of number of vertices). Connected primitive types (line strips, triangle strips, and polygons) require a separate array that specifies the number of vertices in each primitive. Length is defined as the number of vertices in a strip for STRIP primitives and is the number of vertices in a polygon for the POLYS primitive type. The number of line segments in a line strip is numVerts – 1, while the number of triangles in a triangle strip and polygon is numVerts – 2. Use pfGSetPrimLengths() to set the length array for strip primitives. The number of primitives in a pfGeoSet is specified by pfGSetNumPrims(gset, num). For strip and polygon primitives, num is the number of strips or polygons in gset. pfGeoSet Draw Mode In addition to the primitive type, pfGSetDrawMode() further defines how a primitive is drawn. Triangles, triangle strips, quadrilaterals, and polygons can be specified as either filled or as wireframe, where only the outline of the primitive is drawn. Use the PFGS_WIREFRAME argument to enable or disable wireframe mode. Another argument, PFGS_FLATSHADE, specifies that primitives should be shaded. If flat shading is enabled, each primitive or element in a strip is shaded with a single color. PFGS_COMPILE_GL At the next draw for each pfState, compile gset’s geometry into a GL display list and subsequently render the display list. PFGS_DRAW_GLOBJ Select the rendering of an already created display list but do not force a recompile. PFGS_PACKED_ATTRS Use the gset’s packed attribute arrays, set with the PFGS_PACKED_ATTRS to pfGSetAttr, to render geometry with GL vertex arrays. The pfGeoSets are normally processed in immediate mode, which means that pfDrawGSet() sends attributes from the user-supplied attribute arrays to the Graphics Pipeline for rendering. However, this kind of processing is subject to some overhead, particularly if the pfGeoSet contains few primitives. In some cases it may help to use GL display lists (this is different from the libpr display list type pfDispList) or compiled 308 007-1680-100 pfGeoSet (Geometry Set) mode. In compiled mode, pfGeoSet attributes are copied from the attribute lists into a special data structure called a display list during a compilation stage. This data structure is highly optimized for efficient transfer to the graphics hardware. However, compiled mode has some major disadvantages: • Compilation is usually costly. • A GL display list must be recompiled whenever its pfGeoSet’s attributes change. • The GL display list uses a significant amount of extra host memory. In general, immediate mode will offer excellent performance with minimal memory usage and no restrictions on attribute volatility, which is a key aspect in may advanced applications. Despite this, experimentation may show databases or machines where compiled mode offers a performance benefit. To enable or disable compiled mode, call pfGSetDrawMode() with the PFGS_COMPILE_GL token. When enabled, compilation is delayed until the next time the pfGeoSet is drawn with pfDrawGSet(). Subsequent calls to pfDrawGSet() will then send the compiled pfGeoSet to the graphics hardware. To select a display list to render, without recompiling it, use pfGSetDrawMode() with the token PFGS_DRAW_GLOBJ. Packed Attributes Packed attributes is an optimized way of sending formatted data to the graphics pipeline under OpenGL that does not incur the same memory overead or recompilation burden as GL display lists. To render geometry with packed attributes, use the pfGSetDrawMode(PFGS_PACKED_ATTRS) method when using OpenGL. This pfGSetAttr list includes the currently bound PER_VERTEX vertex attribute data packed into a single nonindexed array. When specifying a packed attribute array, the optional vertex attributes, colors, normals, and texture coordinates, can be NULL. This array, like the other attribute arrays, is then shared betweenOpenGL Performer, the GL, and accessible by the user. Optionally, you can put your vertex coordinates in this packed array but in this case the vertices must be duplicated in the normal coordinate array because vertex coordinate data is used internally for other nondrawing operations such as intersections and computation of bounding geometry. Packed attribute arrays also allow OpenGL Performer to extend the vertex attribute types accepted by pfGeoSets. There are several base formats that expect all currently bound attributes of specified data type (unsigned byte, short, or float) to be in the attribute array. Attributes specified by the format but not bound to vertices are assumed to not be present and the present data is 007-1680-100 309 8: Geometry packed with the data for each vertex starting on a 32-bit word-aligned boundary. Then, there are several derived formats that let you put some attribute data in the packed array while leaving the rest in the normal individual coordinate attribute arrays. Table 8-3 shows the different base formats supported. Table 8-3 pfGeoSet PACKED_ATTR Formats Format Description PFGS_PA_C4UBN3ST2FV3F Accepts all currently bound coordinate attributes; colors are unsigned bytes; normals are shorts. Vertices are duplicated in the packed attribute array. PFGS_PA_C4UBN3ST2F Vertices are in the normal coordinate array. PFGS_PA_C4UBT2F Normals and vertices are in the normal coordinate array. PFGS_PA_C4UBN3ST2SV3F All bound coordinate attributes are in the packed attribute array. Colors are unsigned bytes, normals are shorts, and texture coordinates are unsigned shorts. PFGS_PA_C4UBN3ST3FV3F Texture coordinates are 3D floats. PFGS_PA_C4UBN3ST3SV3F Texture coordinates are 2D shorts. To create packed attributes, you can use the utility pfuTravCreatePackedAttrs(), which traverses a scene graph to create packed attributes for pfGeoSets and, optionally, pfDelete redundant attribute arrays. This utility packs the pfGeoSet attributes using pfuFillGSetPackedAttrs(). Examples of packed attribute usage can be seen in /usr/share/Performer/src/pguide/libpr/C/packedattrs.c and /usr/share/Performer/src/sample/C/perfly.c and /usr/share/Performer/src/sample/C++/perfly.C for IRIX and Linux and in %PFROOT%\Src\pguide\libpr\C\packedattrs.c, %PFROOT%\Src\sample\C\perfly.c, and %PFROOT%\Src\sample\C++\perfly.C for Microsoft Windows. Primitive Connectivity A pfGeoSet requires a coordinate array that specifies the world coordinate positions of primitive vertices. This array is either indexed or not, depending on whether a coordinate index list is supplied. If the index list is supplied, it is used to index the coordinate array; if not, the coordinate array is interpreted in a sequential order. 310 007-1680-100 pfGeoSet (Geometry Set) A pfGeoSet’s primitive type dictates the connectivity from vertex to vertex to define geometry. Figure 8-1 shows a coordinate array consisting of four coordinates, A, B, C, and D, and the geometry resulting from different primitive types. This example uses index lists that index the coordinate array. 007-1680-100 311 8: Geometry Note: Flat-shaded line strip and flat-shaded triangle strip primitives have the vertices listed in the same order as for the smooth-shaded varieties. O 1 2 3 ... n D C A B Primitive type Points Vertex list XA, YA, ZA XB, YB, ZB XC, YC, ZC XD, YD, ZD XN, YN, ZN Line strips Line segments Geometry Index list Primitive type 0 0 0 2 3 1 3 1 3 2 2 1 3 0 1 3 2 2 1 0 Independent Quadrilaterals triangles Triangle strips Polygons Geometry Index list Figure 8-1 312 0 0 0 0 1 1 1 1 3 2 3 2 3 3 2 3 1 ... ... 2 n n Primitives and Connectivity 007-1680-100 pfGeoSet (Geometry Set) Attributes The definition of a primitive is not complete without attributes. In addition to a primitive type and count, a pfGeoSet references four attribute arrays (see Figure 8-2): • Colors (red, green, blue, alpha) • Normals (Nx, Ny, Nz) • Texture coordinates (S, T)—multiple arrays for multitexture. • Vertex coordinates (X, Y, Z) (A pfGeoState is also associated with each pfGeoSet; see Chapter 12, “Graphics State” for details.) The four components listed above can be specified with pfGSetAttr(). Multivalue attributes (texture coordinates) can be specified using pfGSetMultiAttr() or pfGSetAttr(). Using zero as the index parameter for pfGSetMultiAttr() is equivalent to calling pfGSetAttr(). Attributes may be set in two ways: by indexed specification—using a pointer to an array of components and a pointer to an array of indices; or by direct specification—providing a NULL pointer for the indices, which indicates that the indices are sequential from the initial value of zero. The choice of indexed or direct components applies to an entire pfGeoSet; that is, all of the supplied components within one pfGeoSet must use the same method. However, you can emulate partially indexed pfGeoSets by using indexed specification and making each nonindexed attribute’s index list be a singly shared “identity mapping” index array whose elements are 0, 1, 2, 3,…, N–1, where N is the largest number of attributes in any referencing pfGeoSet. (You can share the same array for all such emulated pfGeoSets.) The direct method avoids one level of indirection and may have a performance advantage compared with indexed specification for some combinations of CPUs and graphics subsystems. Note: Use pfMalloc() to allocate your arrays of attribute data. This allows OpenGL Performer to reference-count the arrays and delete them when appropriate. It also allows you to easily put your attribute data into shared memory for multiprocessing by specifying an arena such as pfGetSharedArena() to pfMalloc(). While perhaps convenient, it is very dangerous to specify pointers to static data for pfGeoSet attributes. Early versions of OpenGL Performer permitted this but it is strongly discouraged and may have undefined and unfortunate consequences. Attribute arrays can be created through pfFlux to support the multiprocessed generation of the vertex data for a dynamic object, such as ocean waves, or morphing geometry. pfFlux will automatically keep separate copies of data for separate proceses so that one 007-1680-100 313 8: Geometry process can generate data while another draws it. The pfFluxed buffer can be handed directly to pfGSetAttr() or pfGSetMultiAttr(). In fact, the entire pfGeoSet can be contained in a pfFlux. Index lists cannot be pfFluxed. See Chapter 19, “Dynamic Data,” for more information on pfFlux. 0: R G 1: R B A G 2: R B A GB A lor Co ay r ar Ge o,1 ,3,2 ,8,2 lor Co ex ind ... rd coo ord T e x r r a y e x c oy ord a T r r a e x c oy d 0: S a T rra oor x ca y a T 0: S e T 0: S 1: S T arr T T 1: 2: S T ST 2: S T 1: S T 2: S T 0: S T 1: S T 2: S T dxc oor 314 p et 0:n xn 1:n y nz x 2:n ny nz xn yn z 3,1 ,8,3 ,2.. 0: X Y 1: X Z Y 2: X Z YZ 11, 4,8 ,2,6 l a rm Nodex in l . d 0,1 d c o o rT e n d e x ,r2 x i 0,1 d x c o o T,3e,4n,5d.e x ,r2T,3e e x i .. o 0,1 o ,2,3e x c x ,i4n,d5... T ,4n,d5 e 0,1 ,2,3 i ... ,4,5 ... Figure 8-2 oSt at prim e itiv e ty pe prim itiv ec oun t col or a rra y col or i nde x nor ma l ar ray nor ma l in dex tex tur ec oor da tex rra tur y ec oor d in ver dex tex coo rd a ver rra tex y coo rd i nde x oS fGe a rm No ray ar x rte Ve ray ar x rte Ve ex ind ... pfGeoSet Structure 007-1680-100 pfGeoSet (Geometry Set) Note: When using multiple texture-coordinate arrays, pfGeoSet recognizes texture-coordinate arrays starting at the first array (index of 0) and ending immediately before the first index with a NULL array. In other words, specifying texture-coordinate arrays using pfGSetMultiAttr() for indices 0, 1, and 3 is equivalent to specifying texture-coordinate arrays for only indices 0 and 1. When using pfTexGen to automatically generate texture coordinates for some texture units, the application should not interleave texture units with texture coordinates and texture units with pfTexGen. Texture units with texture coordinates should come before texture units with pfTexGen. This is an implementation limitation and may be removed in future releases. Attribute Bindings Attribute bindings specify where in the definition of a primitive an attribute has effect. You can leave a given attribute unspecified; otherwise, its binding location is one of the following: • Overall (one value for the entire pfGeoSet) • Per primitive • Per vertex Only certain binding types are supported for some attribute types. Table 8-4 shows the attribute bindings that are valid for each type of attribute. Table 8-4 Attribute Bindings Binding Token Color Normal Texture Coordinate Coordinate PFGS_OVERALL Yes Yes No No PFGS_PER_PRIM Yes Yes No No PFGS_PER_VERTEX Yes Yes Yes Yes PFGS_OFF Yes Yes Yes No Attribute lists, index lists, and binding types are all set by pfGSetAttr(). 007-1680-100 315 8: Geometry For FLAT primitives (PFGS_FLAT_TRISTRIPS,PFGS_FLAT_TRIFANS, PFGS_FLAT_LINESTRIPS), the PFGS_PER_VERTEX binding for normals and colors has slightly different meaning. In these cases, per-vertex colors and normals should not be specified for the first vertex in each line strip or for the first two vertices in each triangle strip since FLAT primitives use the last vertex of each line segment or triangle to compute shading. Indexed Arrays A cube has six sides; together those sides have 24 vertices. In a vertex array, you could specify the primitives in the cube using 24 vertices. However, most of those vertices overlap. If more than one primitive can refer to the same vertex, the number of vertices can be streamlined to 8. The way to get more than one primitive to refer to the same vertex is to use an index; three vertices of three primitives use the same index which points to the same vertex information. Adding the index array adds an extra step in the determination of the attribute, as shown in Figure 8-3. 316 007-1680-100 pfGeoSet (Geometry Set) pfGeoSet StripLengths PrimCoords ColorBind NormalBind TexCoordBind n1 n2 n3 . . . CoordSet ColorSet NormalSet TexCoordSet CoordIndexSet ColorIndexSet NormalIndexSet TextCoordIndexSet < x, y, z > . . . n1 n2 n3 . . . Figure 8-3 < r, g, b > . . . n1 n2 n3 . . . < nx, ny, nz > . . . n1 n2 n3 . . . < x, y, z > . . . n1 n2 n3 . . . Indexing Arrays Indexing can save system memory, but rendering performance is often lost. When to Index Attributes The choice of using indexed or sequential attributes applies to all of the primitives in a pfGeoSet; that is, all of the primitives within one pfGeoSet must be referenced sequentially or by index; you cannot mix the two. The governing principle for whether to index attributes is how many vertices in a geometry are shared. Consider the following two examples in Figure 8-4, where each dot marks a vertex. 007-1680-100 317 8: Geometry Figure 8-4 Deciding Whether to Index Attributes In the triangle strip, each vertex is shared by two adjoining triangles. In the square, the same vertex is shared by eight triangles. Consider the task that is required to move these vertices when, for example, morphing the object. If the vertices were not indexed, in the square, the application would have to look up and alter eight triangles to change one vertex. In the case of the square, it is much more efficient to index the attributes. On the other hand, if the attributes in the triangle strip were indexed, since each vertex is shared by only two triangles, the index look-up time would exceed the time it would take to simply update the vertices sequentially. In the case of the triangle strip, rendering is improved by handling the attributes sequentially. The deciding factor governing whether to index attributes relates to the number of primitives that share the same attribute: if attributes are shared by many primitives, the attributes should be indexed; if attributes are not shared by many primitives, the attributes should be handled sequentially. pfGeoSet Operations There are many operations you can perform on pfGeoSets. pfDrawGSet() “draws “ the indicated pfGeoSet by sending commands and data to the Geometry Pipeline, unless OpenGL Performer’s display-list mode is in effect. In display-list mode, rather than sending the data to the pipeline, the current pfDispList “captures” the pfDrawGSet() command. The given pfGeoSet is then drawn along with the rest of the pfDispList with the pfDrawDList() command. When the PFGS_COMPILE_GL mode of a pfGeoSet is not active (pfGSetDrawMode()), pfDrawGSet() uses rendering loops tuned for each primitive type and attribute binding combination to reduce CPU overhead in transferring the geometry data to the hardware pipeline. Otherwise, pfDrawGSet() sends a special, compiled data structure. 318 007-1680-100 pfGeoArray (Geometry Array) Table 8-1 on page 306 lists other operations that you can perform on pfGeoSets. pfCopy() does a shallow copy, copying the source pfGeoSet’s attribute arrays by reference and incrementing their reference counts. pfDelete() frees the memory of a pfGeoSet and its attribute arrays (if those arrays were allocated with pfMalloc() and provided their reference counts reach zero). pfPrint() is strictly a debugging utility and will print a pfGeoSet’s contents to a specified destination. pfGSetIsectSegs() allows intersection testing of line segments against the geometry in a pfGeoSet; see “Intersecting with pfGeoSets” in Chapter 22 for more information on that function. pfGeoArray (Geometry Array) The pfGeoArray is a new OpenGL Performer data structure aimed at replacing the class pfGeoSet. Conceptually, pfGeoArrays are very similar to pfGeoSets, but they allow you to define new sets of attributes in addition to the standard vertex coordinates, normals, texture coordinates, and colors. These attributes can be used by vertex or fragment programs applied to the primitives (see “Using OpenGL Performer with GPUs” on page 532). Also, pfGeoArrays are rendered using vertex arrays and vertex objects, making the rendering much more efficient. pfGeoArrays can be up to 10 times faster than pfGeoSets on Onyx4 or Prism systems. Each pfGeoArray is a collection of geometry with one primitive type, such as points, lines, or triangles. Vertex coordinates, normals, colors, texture coordinates, and user-defined attributes are used to specify the primitives. There are two ways to specify the attributes. First, each attribute is specified per vertex, there is no concept of an attribute per primitive or an overall attribute. Second, you can use a single list of unsigned integers to index all attributes of a pfGeoArray. Indexing provides a more general mechanism for specifying geometry than hardwired attribute lists and, in many cases, provides substantial memory savings due to shared attributes. Nonindexed pfGeoArrays are sometimes easier to construct and may exhibit better caching behavior. Indexing is often a desirable approach especially when your primitives are sharing many attributes (such as having the same normal for each face). Also, if you have a primitive with many triangle strips, it is better to create a single pfGeoArray containing indexed triangles than to have a set of short pfGeoArrays, each containing one triangle strip. 007-1680-100 319 8: Geometry This section contains the following topics: • “Creating pfGeoArrays” on page 320 • “pfGeoArray Attributes” on page 320 • “pfGeoArray Attribute Types” on page 323 • “pfGeoArray Primitive Types” on page 323 • “Example Code” on page 324 • “Converting pfGeoSets to pfGeoArrays” on page 328 Creating pfGeoArrays The function pfNewGArray() creates and returns a handle to a pfGeoArray. The parameter arena specifies a malloc() arena out of which the pfGeoArray is allocated or NULL for allocation off the process heap. pfGeoArrays can be deleted with pfDelete(). The call new(arena) allocates a pfGeoArray from the specified memory arena, or from the process heap if arena is NULL. The new() call allocates a pfGeoArray from the default memory arena (see the man page for pfGetSharedArena). Like other pfObjects, pfGeoArrays cannot be automatically created statically on the stack or in arrays. Delete pfGeoArrays with pfDelete() rather than with the delete operator. pfGeoArray Attributes The function pfGArrayAddAttr() adds a new attribute to the list of attributes of a pfGeoArray. This list is initially empty. An attribute is specified by its attribute type and the following parameters: 320 size Specifies the number of coordinates per vertex; It must be 2, 3, or 4. type Specifies the type of each component in the attribute data. It is one of GL_DOUBLE, GL_FLOAT, GL_INT, GL_UNSIGNED_INT, GL_UNSIGNED_SHORT, GL_SHORT, GL_UNSIGNED_BYTE, and GL_BYTE. stride Specifies the byte offset between consecutive vertex data. It is usually 0. pointer Specifies a pointer to the attribute data. 007-1680-100 pfGeoArray (Geometry Array) You can modify the name, size, the data type, the stride, and the data pointer for any existing attribute using the following functions: • pfVAttrName() • pfVAttrPtr() • pfVAttrStride() • pfVAttrDataType() • pfVAttrSize() You can also remove an attribute using the function pfGArrayRemoveAttr(). Multitexturing is supported by adding multiple PFGA_TEX_ARRAY vertex attributes and specifying different stages, as shown in the following example: pfGeoArray *gArray = pfNewGArray(); pfGArrayMultiAttr(gArray, PFGA_TEX_ARRAY, 0, 2, GL_FLOAT, 0, baseCoords); pfGArrayMultiAttr(gArray, PFGA_TEX_ARRAY, 1, 2, GL_FLOAT, 0, bumpCoords); /* set name for the two sets of tex coords just assigned */ pfVArrayName( pfGArrayQueryAttrTypeStage(gArray, PFGA_TEX_ARRAY, 0), "base texture coords"); pfVArrayName( pfGArrayQueryAttrTypeStage(gArray, PFGA_TEX_ARRAY, 1), "bump texture coords"); Note that since the pfGeoArray attributes are rendered in the order they were added, it is possible to interleave the attributes with your own callbacks. To do so, create a special "callback" type with a function mask 0 (no callback data) or a function mask 0x1 (callback data is used). It is possible to index the attributes, although (in contrast to pfGeoSets) a single index list is used for all attributes. The optional attribute index list is a list of unsigned short integers. The index list is specified using the function pfGArrayIndexArray(). If attribute and index lists are allocated from the pfMalloc routines, pfGArrayAddAttr() and pfGArrayAttrPtr() will correctly update the reference counts of the lists. Specifically, they will decrement the reference counts of the old lists and increment the reference counts of the new lists. It will not free any lists whose reference counts reach 0. When a 007-1680-100 321 8: Geometry pfGeoArray is deleted with pfDelete(), all pfMalloc'ed lists will have their reference counts decremented by one and will be freed if their count reaches 0. When pfGeoArrays are copied with pfCopy(), all pfMalloc'ed lists of the source pfGeoArray will have their reference counts incremented by one and those pfMalloc'ed lists of the destination pfGeoArray will have their reference counts decremented by one. The function pfCopy() copies lists only by reference (only the pointer is copied) and will not free any lists whose reference counts reach 0. Attribute data may be any of the following types of memory: • Data allocated with pfMalloc This is the usual and recommended memory type for pfGeoArray index and attribute arrays. • Static, malloc(), amalloc(), usmalloc(), and similar data (non-pfMalloc'ed data) This type of memory is not generally recommended since it does not support reference counting or other features provided by pfMalloc. In particular, do not use static data because it may result in segmentation violations. • pfFlux memory In a pipelined, multiprocessing environment, a pfFlux provides multiple data buffers, which allow frame-accurate data modifications to pfGeoArray attribute arrays like coordinates (facial animation) and texture coordinates (ocean waves, surf). The functions pfGArrayAddAttr() and pfGArrayAttrPtr() will accept a pfFlux* or pfFluxMemory* for the attribute list (index lists do not support pfFlux) and the pfGeoArray will select the appropriate buffer when rendered or intersected. See the man page for pfFlux for more details. Since pfGeoArrays are cached using vertex array objects, if you want to animate some attributes, you need to either disable caching using the function pfGArrayAllowCache() or call the function pfGArrayUpdateData() each time you change any of the attribute data. • pfCycleBuffer and pfCycleMemory Note that pfCycleBuffer has been obsoleted by pfFlux. See the man page for pfCycleBuffer for more details. OpenGL Performer allows mixing pfMalloc'ed, pfFlux, and pfCycleBuffer attributes on a single pfGeoArray. 322 007-1680-100 pfGeoArray (Geometry Array) pfGeoArray Attribute Types When a new pfGeoArray is created, it has no default attribute. When adding a new attribute, you must specify the type of the attribute— that is, whether it specifies one of the following: • A vertex coordinates (PFGA_COORD_ARRAY) • A normal vector (PFGA_NORMAL_ARRAY) • A color (PFGA_COLOR_ARRAY) • A texture coordinate (PFGA_TEX_ARRAY) • A generic user-defined attribute (PFGA_GENERIC_ARRAY) Attribute types are identified by their type, their name (a string), and the associated texture stage (if applicable). A new attribute type can be added using the function pfGArrayAddAttrType(). The parameter type is one of tokens just cited. The name can be any arbitrary string and if one is not set, then depending on the array type, a default name will be used ("vertex", "normal", "color", "texture coord" or "generic"). The parameter stage defines the associated texture stage. In the case of attributes of type PFGA_GENERIC_ARRAY, the attributes are applied using the function glVertexAttribPointerARB(). The following example code adds two sets of texture coordinates and one set of vertices to a pfGeoArray: pfVertexAttr *vAttr, tAttrs[2]; pfGeoArray *gArray = ...; vAttr = pfGArrayAddAttrType(gArray, PFGA_COORD_ARRAY, "vertices", 0); tAttr[0] = pfGArrayAddAttrType(gArray, PFGA_TEX_ARRAY, "texCoord0", 0); tAttr[1] = pfGArrayAddAttrType(gArray, PFGA_TEX_ARRAY, "texCoord1", 1); pfGeoArray Primitive Types A primitive is a single point, line segment, line strip, triangle, triangle strip, quad, or polygon depending on the primitive type. The primitive type dictates how the coordinate and coordinate index lists are interpreted to form geometry. The function pfGSetPrimType() specifies the type of primitives found in a pfGeoArray. The following example shows how to set up a nonindexed, TRISTRIP pfGeoArray: 007-1680-100 323 8: Geometry /* Set up a nonindexed, TRISTRIP pfGeoArray */ garray = pfNewGArray(NULL); pfGSetPrimType(garray, PFGS_TRISTRIPS); pfGSetNumPrims(garray, 1); lengths[0] = 4; pfGSetPrimLengths(gset, lengths); coords = (pfVec3*) pfMalloc(sizeof(pfVec3) * 4, NULL); colors = (pfVec4*) pfMalloc(sizeof(pfVec4) * 4, NULL); pfGArraySetAttr(garray, PFGA_COORD_ARRAY, 3, GL_FLOAT, 0, coords); pfGArraySetAttr(garray, PFGA_COLOR_ARRAY, 4, GL_FLOAT, 0, colors); The function pfGetGSetClassType() returns the pfType* for the class pfShaderProgram. The pfType* returned by pfGetGSetClassType() is the same as the pfType* returned by invoking pfGetType(), the virtual function getType() on any instance of class pfShaderProgram. Because OpenGL Performer allows subclassing of built-in types when decisions are made based on the type of an object, use pfIsOfType() the member function isOfType() to test if an object is of a type derived from an OpenGL -Performer type rather than to test for strict equality of the pfType*s. Example Code The following example shows one way to create a pfGeoArray defining a hexahedron (cube). static pfVec3 coords[] = { {-1.0, -1.0, 1.0}, /* front */ { 1.0, -1.0, 1.0}, { 1.0, 1.0, 1.0}, {-1.0, 1.0, 1.0}, {-1.0, -1.0, 1.0}, /* left */ {-1.0, 1.0, 1.0}, {-1.0, 1.0, -1.0}, {-1.0, -1.0, -1.0}, {-1.0, -1.0, -1.0}, /* back */ {-1.0, 1.0, -1.0}, { 1.0, 1.0, -1.0}, { 1.0, -1.0, -1.0}, 324 007-1680-100 pfGeoArray (Geometry Array) { { { { 1.0, -1.0, 1.0}, /* right */ 1.0, -1.0, -1.0}, 1.0, 1.0, -1.0}, 1.0, 1.0, 1.0}, {-1.0, { 1.0, { 1.0, {-1.0, {-1.0, {-1.0, { 1.0, { 1.0, 1.0, 1.0}, /* top */ 1.0, 1.0}, 1.0, -1.0}, 1.0, -1.0}, -1.0, 1.0}, /* bottom */ -1.0, -1.0}, -1.0, -1.0}, -1.0, 1.0} }; static pfVec3 norms[] = { { 0.0, 0.0, 1.0}, { 0.0, 0.0, 1.0}, { 0.0, 0.0, 1.0}, { 0.0, 0.0, 1.0}, 007-1680-100 {-1.0, {-1.0, {-1.0, {-1.0, 0.0, 0.0, 0.0, 0.0, 0.0}, 0.0}, 0.0}, 0.0}, { { { { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0}, -1.0}, -1.0}, -1.0}, { { { { 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0}, 0.0}, 0.0}, 0.0}, { { { { 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0}, 0.0}, 0.0}, 0.0}, { 0.0, -1.0, { 0.0, -1.0, 0.0}, 0.0}, 325 8: Geometry { 0.0, -1.0, { 0.0, -1.0, 0.0}, 0.0} }; /* Convert static data to pfMalloc'ed data */ static void* memdup(void *mem, size_t bytes, void *arena) { void *data = pfMalloc(bytes, arena); memcpy(data, mem, bytes); return data; } /* Set up a PFGS_QUADS pfGeoArray */ garray = pfNewGArray(NULL); pfGSetPrimType(garray, PFGS_QUADS); pfGSetNumPrims(garray, 6); pfGArraySetAttr(garray, PFGA_COORD_ARRAY, 3, GL_FLOAT, 0, memdup(coords, sizeof(coords), NULL)); pfGArraySetAttr(garray, PFGA_NORMAL_ARRAY, 3, GL_FLOAT, 0, memdup(norms, sizeof(norms), NULL)); static pfVec3 coords[] = { {-1.0, -1.0, 1.0}, /* front */ { 1.0, -1.0, 1.0}, { 1.0, 1.0, 1.0}, {-1.0, 1.0, 1.0}, {-1.0, -1.0, 1.0}, /* left */ {-1.0, 1.0, 1.0}, {-1.0, 1.0, -1.0}, {-1.0, -1.0, -1.0}, {-1.0, -1.0, -1.0}, /* back */ {-1.0, 1.0, -1.0}, { 1.0, 1.0, -1.0}, { 1.0, -1.0, -1.0}, { 1.0, -1.0, 1.0}, /* right */ { 1.0, -1.0, -1.0}, { 1.0, 1.0, -1.0}, 326 007-1680-100 pfGeoArray (Geometry Array) { 1.0, 1.0, {-1.0, { 1.0, { 1.0, {-1.0, 1.0, 1.0}, /* top */ 1.0, 1.0}, 1.0, -1.0}, 1.0, -1.0}, {-1.0, {-1.0, { 1.0, { 1.0, 1.0}, -1.0, 1.0}, /* bottom */ -1.0, -1.0}, -1.0, -1.0}, -1.0, 1.0} }; static pfVec3 norms[] = { { 0.0, 0.0, 1.0}, { 0.0, 0.0, 1.0}, { 0.0, 0.0, 1.0}, { 0.0, 0.0, 1.0}, {-1.0, {-1.0, {-1.0, {-1.0, 0.0, 0.0, 0.0, 0.0, 0.0}, 0.0}, 0.0}, 0.0}, { { { { { { { { 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0}, -1.0}, -1.0}, -1.0}, 0.0}, 0.0}, 0.0}, 0.0}, { { { { 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0}, 0.0}, 0.0}, 0.0}, { { { { 0.0, 0.0, 0.0, 0.0, -1.0, -1.0, -1.0, -1.0, 0.0}, 0.0}, 0.0}, 0.0} }; 007-1680-100 327 8: Geometry // Convert static data to pfMalloc'ed data static void* memdup(void *mem, size_t bytes, void *arena) { void *data = pfMalloc(bytes, arena); memcpy(data, mem, bytes); return data; } /* Set up a PFGS_QUADS pfGeoArray */ garray = new pfGeoArray; garray->setPrimType(PFGS_QUADS); garray->setNumPrims(6); garray->setAttr(PFGA_COORD_ARRAY, 3, GL_FLOAT, 0, memdup(coords, sizeof(coords), NULL)); garray->setAttr(PFGA_NORMAL_ARRAY, 3, GL_FLOAT, 0, memdup(norms, sizeof(norms), NULL)); With pfGeoArrays, unlike with pfGeoSets, you cannot index vertex coordinates and normals separately. This results in bigger memory requirements. The extra storage is worth the reduced rendering times, though. Another example of creating pfGeoArrays can be found in following files: (IRIX and Linux) /usr/share/Performer/src/pguide/libpfdu/pfdConvertToGeoArrays.C /usr/share/Performer/src/pguide/libpr/C++/geoArray.C (Microsoft Windows) %PFROOT%\Src\pguide\libpfdu\pfdConvertToGeoArrays.cxx %PFROOT%\Src\pguide\libpr\C++\geoArray.cxx Converting pfGeoSets to pfGeoArrays Since using pfGeoArrays can be much faster on some platforms, such as Onyx4 or Prism systems, you can convert your geometry from pfGeoSets to pfGeoArrays using the following two functions: 328 • pfdConvertGeoSetToGeoArray() • pfdConvertNodeGeoSetsToGeoArrays() 007-1680-100 Optimizing Geometry for Rendering The first function converts an individial pfGeoSet into a pfGeoArray. The second function traverses a pfNode and replaces all its pfGeoSets with pfGeoArrays. The parameter flags can be set to 0 or to PFD_CONVERT_TO_INDEXED_GEOARRAYS. In the second case the loader tries to avoid the use of lengths array and it converts strips to indexed lines or triangles. Also, it is possible to use the pseudo loader libpfgeoa to convert the geometry from pfGeoSets to pfGeoArrays during loading. The pseudo loader is used as follows: perfly file.ext.geoa The pseudo loader calls pfdConvertNodeGeoSetsToGeoArrays() with the flag PFD_CONVERT_TO_INDEXED_GEOARRAYS set. You can overwrite this default by setting the environment variable PFD_CONVERT_TO_INDEXED_GEOARRAYS to 0. Optimizing Geometry for Rendering This section describes how you can use three functions to optimize your pfGeosets or pfGeoArrays for rendering. The following topics are described: • “Function pfdMergeGraph()” on page 329 • “Function pfdStripGraph()” on page 330 • “Function pfdSpatializeGraph()” on page 331 • “The Optimization Pipeline” on page 332 • “Using the libpfgopt Pseudo Loader” on page 333 Function pfdMergeGraph() The function pfdMergeGraph() gathers all pfGeoSets referenced in the each static subgraph rooted at a node. It then creates a new subgraph for each that has a minimum number of pfGeoSets by grouping pfGeoSets that share state and attribute data. The function only operates on static subgraphs and, thus, will not destroy pfLOD, pfSequence, or other dynamic structures in a hierarchy. The new graph, which is not spatialized, is returned to the caller. All functions described in this section can output geometry as either pfGeoSets or pfGeoArrays and as either indexed or nonindexed. The default behavior is to output nonindexed pfGeoArrays if any are present and 007-1680-100 329 8: Geometry nonindexed pfGeoSets, otherwise. This can be controlled by passing in the following as the flags parameters: • PFD_OUTPUT_GEOSETS • PFD_OUTPUT_GEOARRAYS • PFD_OUTPUT_INDEXED These flags force the output format to be of the type and format indicated. The function pfdMergeGraph() returns the root to a new scene graph and does not modify the graph that is rooted at node. The calling application must delete the previous graph if it is no longer needed. Function pfdStripGraph() The function pfdStripGraph() collects all pfGeoSets in the graph rooted by the node and modifies each in several possible ways in order to increase performance. This can include stripping (converting from separate primitives, such as triangles, to their stripped form, triangle strips) or unstripping geometry, merging stripped geometry, and reordering primitives. On certain hardware, such as Onyx4 or Prism systems, reordering primitives can improve performance by taking advantage of hardware vertex caches to decrease the number of transformed vertices. If you specify an integer length for cacheLength OpenGL Performer uses the given value as the length of the cache on the target hardware. If you do not know the cache length, which varies on some platforms, using a value of 0 triggers a more generic algorithm to provide better results on machines of varying cache lengths. Controlling the behavior of the operation can be done by passing in the OR result of the following tokens as the flags parameter: PFD_STRIP_UNSTRIPPED_PRIMITIVES This token indicates that any primitives that are not stripped should be stripped. PFD_STRIP_STRIPPED_PRIMITIVES This token indicates that any primitives that are already stripped should be restripped. PFD_UNSTRIP_STRIPPED_PRIMITIVES This token indicates that any primitives that are already stripped should be unstripped or converted back to a separated form. If this token and PFD_STRIP_STRIPPED_PRIMITIVES are both passed, the input geometry is first unstripped and then restripped. 330 007-1680-100 Optimizing Geometry for Rendering PFD_MERGE_TRISTRIPS This token indicates that triangle strips should be joined with degenerate triangles to construct longer strips. This will reduce the number of primitives in a pfGeoSet and may improve performance by reducing the number of draw calls needed to render the geometry. PFD_REORDER_CACHE_REUSE This token indicates that the primitives should be reordered to improve cache reuse. On systems with a hardware vertex cache, this may improve performance and is typically the most effective reordering strategy. PFD_REORDER_REDUCE_DEGENERATES This token indicates that the reordering of primitives should seek to reduce the number of mergings of degenerate triangles. This is not typically as effective as reordering to improve cache reuse. The function pfdStripGraph() uses the same output flags as pfdMergeGraph(). Note: There is no default behavior for pfdStripGraph(); that is, if flags is set to 0, then the processing options just described are disabled. In this case, the result will be no change to the input geometry. Function pfdSpatializeGraph() The function pfdSpatializeGraph() operates similarily to pfdSpatialize() and pfdBreakup(). The function pfdSpatializeGraph() first breaks the geometry of the graph rooted at a node into smaller chunks to improve spatialization and, in effect, culling. This breakup turns each geode into a subgraph rooted by a pfGroup node with several pfGeode children. Next, each static subgraph is spatialized using pfSpatialize(). Similar to pfdMergeGraph(), this operation does not change the dynamic aspects of a scene graph. Graphs rooted by nodes such as pfSwitch and pfSequence will not be changed by the operation. The maxStripLength parameter controls the strip length threshold for the breakup. If this value is set to 0, then no maximum will be used. The octreeLevels parameter controls the target number of octree levels for the breakup and spatialization operations. However, depending upon the number of pfGeoSets in the graph, there may be slightly more or fewer levels in the actual output graph. Like pfdStripGraph(), the flags parameter is used to pass in the OR result of the following tokens to control the behavior of the operation: 007-1680-100 331 8: Geometry PFD_BREAKUP_GEOSETS This token indicates that the breakup operation should be performed. PFD_SPATIALIZE_GRAPH This token indicates that the spatialization operation should be performed. As with pfdStripGraph(), pfdSpatializeGraph() has no default behavior and will perform no graph modification if no flags are passed in. Like both pfdStripGraph() and pfdMergeGraph(), the output can be controlled by the same tokens passed through the flags parameter, and pfdSpatializeGraph() returns the root to a new scene graph. The Optimization Pipeline Collectively, pfdMergeGraph(), pfdStripGraph(), and pfdSpatializeGraph() can be run as a pipeline on an input scene graph to reformat and optimize the graph for best performance on a given hardware. Additionally, the following compund tokens are available for setting the flags which can be passed to all three functions to modify behavior: PFD_OPTIMIZE_AGGRESSIVE This token indicates that the operations should perform their most aggressive optimizations. This includes restripping all primitives, reordering to improve cache reuse, merging strips, and performing all spatialization steps. While this may not always be the best set of operations, it should provide an excellent starting point, especially for optimizing on newer hardware, such as Onyx4 or Prism systems. PFD_OPTIMIZE_CONSERVATIVE This token is used for performing only the safest optimizations. These optimizations are unlikely to decrease performance and may be useful for optimizing across several different platforms and generations of hardware. PFD_OPTIMIZE_INDEXED_TRIS On some hardware, indexed triangles may be the fastest geometry format. For these systems, the PFD_OPTIMIZE_INDEXED_TRIS token will use aggressive methods but output to an indexed, unstripped, but reordered format. These tokens are combinations of the previous tokens and may be used with any of the previously mention tokens (most notably, the output tokens). All three compound tokens include PFD_OUTPUT_INDEXED; therefore, if that mode is not desired, an application 332 007-1680-100 Optimizing Geometry for Rendering could remove it by performing an AND of token and the negation of PFD_OUTPUT_INDEXED. The following code sample illustrates using the optimization functions in a pipeline on an input graph and saving the output to a file: root = pfdLoadFile(inputFile); if (root == NULL) { pfNotify(PFNFY_FATAL, PFNFY_USAGE, "Input graph was not found. Quitting."); } // First, call merge graph root = pfdMergeGraph(root, PFD_OPTIMIZE_AGGRESSIVE); // Now strip each of the merged geoSets // length of the cache is 12 vertices root = pfdStripGraph(root, 12, PFD_OPTIMIZE_AGGRESSIVE); // Finally, spatialize the graph // Have 3 octree levels, and no maximum strip length root = pfdSpatializeGraph(root, 3, 0, PFD_OPTIMIZE_AGGRESSIVE); // Save new scene graph pfdStoreFile(root, outputFile); A sample application is located in the following file: (IRIX and Linux) /usr/share/Performer/src/pguide/libpf/C++/optimizeGraph.C (Microsoft Windows) %PFROOT%\Src\lib\pguide\libpf\C++\optimizeGraph.cxx Using the libpfgopt Pseudo Loader As an alternative to the functions described in this chapter, you can use the libpfgopt pseudo loader to optimize geometry while loading a database file. You can specify the filename in one of two formats to call the pseudo loader: 007-1680-100 333 8: Geometry file.ext.gopt file.ext.parameters.gopt If the first format is specified, then the optimizer uses a default operation mode, which consists of aggressive optimization but with no breakup and with output to indexed geoarrays. This is equivalent to the following specification in the second format: file.ext.aggressive,nobreakup,geoarray.gopt In the second format, parameters is a comma separated list of keywords or keyword=value pairs. The following are valid keywords: • aggressive • conservative • indexedtris • join • nojoin • cachereuse • reducedegens • noreorder • breakup • nobreakup • spatialize • nospatialize • geoset • geoarray • indexed • unindexed Each of these keywords enables or disables a related operation or mode of the optimization pipeline, which consists of pfdMergeGraph(), pfdStripGraph(), and pfdSpatializeGraph(). 334 007-1680-100 Rendering 3D Text The following are valid keyword=value entries for parameters: • cachelength=int • octree=int • striplength=int They set the related values of the corresponding functions to the specified value. For more information about all the keywords and their functionality, see the pfdOptimizeGraph man page. If at least one parameter is specified, then there is no default operations by the optimizer, except those explicitly enabled. For example, for the following filename specification, the optimizer will not join triangle strips, breakup geosets, or spatialize the graph: foo.pfb.cachereuse.gopt Additionally, since some keywords have opposing behavior, the order they are specified matters. For example, if the file is specified in the following manner, then the resulting geometry will be nonindexed: foo.pfb.indexed,unindexed.gopt Rendering 3D Text In addition to the pfGeoSet and pfGeoArray, libpr offers two other primitives which together are useful for rendering a specific type of geometry—3D characters. See Chapter 3, “Nodes and Node Types” and the description for pfText nodes for an example of how to set up the 3D text within the context of libpf. pfFont The basic primitive supporting text rendering is the libpr pfFont primitive. A pfFont is essentially a collection of pfGeoSets in which each pfGeoSet represents one character of a particular font. pfFont also contain metric data, such as a per-character spacing, the 3D escapement offset used to increment a text ‘cursor’ after the character has been drawn. Thus, pfFont maintains all of the information that is necessary to draw any and all valid characters of a font. However, note that pfFonts are passive and have little functionality on their own; for example, you cannot draw a pfFont—it simply provides the character set for the next higher-level text data object, the pfString. 007-1680-100 335 8: Geometry Table 8-5 lists some routines that are used with a pfFont. Table 8-5 pfFont Routines Routine Description pfNewFont() Create a new pfFont. pfDelete() Delete a pfFont. pfFontCharGSet() Set the pfGeoSet to be used for a specific character of this pfFont. pfFontCharSpacing() Set the 3D spacing to be used to update a text cursor after this character has been rendered. pfFontMode() Specify a particular mode for this pfFont. Valid modes: PFFONT_CHAR_SPACING—Specify whether to use fixed or variable spacings for all characters of a pfFont. Possible values are PFFONT_CHAR_SPACING_FIXED and PFFONT_CHAR_SPACING_VARIABLE, the latter being the default. PFFONT_NUM_CHARS—Specify how many characters are in this font. PFFONT_RETURN_CHAR—Specify the index of the character that is considered a ‘return’ character and thus relevant to line justification. pfFontAttr() Specify a particular attribute of this pfFont. Valid attributes: PFFONT_NAME—Name of this font. PFFONT_GSTATE—pfGeoState to be used when rendering this font. PFFONT_BBOX—Bounding box that bounds each individual character. PFFONT_SPACING—Set the overall character spacing if this is a fixed width font (also the spacing used if one has not been set for a particular character). Example 8-1 Loading Characters into a pfFont /* Setting up a pfFont */ pfFont *ReadFont(void) { pfFont *fnt = pfNewFont(pfGetSharedArena()); for(i=0;i<numCharacters;i++) { pfGeoSet* gset = getCharGSet(i); pfVec3* spacing = getCharSpacing(i); 336 007-1680-100 Rendering 3D Text pfFontCharGSet(fnt, i, gset); pfFontCharSpacing(fnt, i, spacing); } } pfString Simple rendering of 3D text can be done using a pfString. A pfString is an array of font indices stored as 8-bit bytes, 16-bit shorts, or 32-bit integers. Each element of the array contains an index to a particular character of a pfFont structure. A pfString can not be drawn until it has been associated with a pfFont object with a call to pfStringFont(). To render a pfString once it references a pfFont, call the function pfDrawString(). The pfString class supports the notion of ‘flattening’ to trade off memory for faster processing time. This causes individual, noninstanced geometry to be used for each character, eliminating the cost of translating the text cursor between each character when drawing the pfString. Example 8-2 illustrates how to set up and draw a pfString. Example 8-2 Setting Up and Drawing a pfString /* Create a string a rotate it for 2.5 seconds */ void LoadAndDrawString(const char *text) { pfFont *myfont = ReadMyFont(); pfString *str = pfNewString(NULL); pfMatrix mat; float start,t; /* Use myfont as the 3-d font for this string */ pfStringFont(str, fnt); /* Center String */ pfStringMode(str, PFSTR_JUSTIFY, PFSTR_MIDDLE); /* Color String is Red */ pfStringColor(str, 1.0f, 0.0f, 0.0f, 1.0f); /* Set the text of the string */ pfStringString(str, text); 007-1680-100 337 8: Geometry /* Obtain a transform matrix to place this string */ GetTheMatrixToPlaceTheString(mat); pfStringMat(str, &mat); /* optimize for draw time by flattening the transforms */ pfFlattenString(str); /* Twirl text for 2.5 seconds */ start = pfGetTime(); do { pfVec4 clr; pfSetVec4(clr, 0.0f, 0.0f, 0.0f, 1.0f); /* Clear the screen to black */ pfClear(PFCL_COLOR|PFCL_DEPTH, clr); t = (pfGetTime() - start)/2.5f; t = PF_MIN2(t, 1.0f); pfMakeRotMat(mat, t * 315.0f, 1.0f, 0.0f, 0.0f); pfPostRotMat(mat, mat, t * 720.0f, 0.0f, 1.0f, 0.0f); t *= t; pfPostTransMat(mat, mat, 0.0f, 150.0f * t + (1.0f - t) * 800.0f, 0.0f); pfPushMatrix(); pfMultMatrix(mat); /* DRAW THE INPUT STRING */ pfDrawString(str); pfPopMatrix(); pfSwapWinBuffers(pfGetCurWin()); } while(t < 2.5f); } 338 007-1680-100 Rendering 3D Text Table 8-6 lists the key routines used to manage pfStrings. Table 8-6 pfString Routines Routine Description pfNewString() Create a new pfString. pfDelete() Delete a pfString. pfStringFont() Set the pfFont to use when drawing this pfString. pfStringString() Set the character array that this pfString will represent or render. pfDrawString() Draw this pfString. pfFlattenString() Flatten all positional translations and the current specification matrix into individual pfGeoSets so that more memory is used, but no matrix transforms or translates have to be done between each character of the pfString. pfStringColor() Set the color of the pfString. pfStringMode() Specify a particular mode for this pfString. Valid modes: PFSTR_JUSTIFY — Sets the line justification and has the following possible values: PFSTR_FIRST or PFSTR_LEFT, PFSTR_MIDDLE or PFSTR_CENTER, and PFSTR_LAST or PFSTR_RIGHT. PFSTR_CHAR_SIZE — Sets the number of bytes per character in the input string and has the following possible values: PFSTR_CHAR, PFSTR_SHORT, PFSTR_INT. 007-1680-100 pfStringMat() Specify a transform matrix that affects the entire character string when the pfString is drawn. pfStringSpacingScale() Specify a scale factor for the escapement translations that happen after each character is drawn. This routine is useful for changing the spacing between characters and even between lines. 339 Chapter 9 9. Higher-Order Geometric Primitives OpenGL Performer also supports a large set of higher-order primitives that you can include in a scene graph. These higher-order primitives extend the simpler geometric primitives that can be specified using pfGeodes. “Higher-order” means objects other than sets of triangles, and typically implies an object that is defined mathematically. Designs produced by CAD systems are defined by mathematically defined surface representations. By providing direct support for them, OpenGL Performer expands possible applications from simple walkthrough ability to direct interaction with the design data base. OpenGL Performer also provides classes to define discrete curves and discrete surfaces. The objects are discussed in the following sections: 007-1680-100 • “Features and Uses of Higher-Order Geometric Primitives” on page 342 • “Objects Required by Reps” on page 342 • “Geometric Primitives: The Base Class pfRep and the Application repTest” on page 346 • “Planar Curves” on page 348 • “Spatial Curves” on page 371 • “Parametric Surfaces” on page 375 • “Meshes” on page 413 • “Subdivision Surfaces” on page 420 341 9: Higher-Order Geometric Primitives Features and Uses of Higher-Order Geometric Primitives Higher-order geometric primitives, called representations or simply reps, facilitate the design process by providing a library of useful curves and surfaces that ease interactive flexibility, accelerate scene-graph transformations, and reduce the memory footprint of the scene graph. Reps yield these advantages by using parameters to describe objects. Instead of a collection of vertices, which must be manipulated independently to change a surface, reps define surfaces in terms of a relatively small set of control parameters; they are more like pure mathematical objects. Reps and the Rendering Process OpenGL Performer allows you to interact with an abstract object (a representation or rep) and treat rendering as a separate operation. A simple example of a rep is a sphere, defined by a radius and a center. After defining a sphere, you can implement how it is rendered in several ways: by tessellating, by a sphere-specific draw routine, or conceivably by hardware. Member functions of geometric-primitive classes allow you to implement the most appropriate way of rendering. The fundamental rendering step of tessellating a representation is discussed in Chapter 11, “Rendering Higher-Order Primitives: Tessellators”. Trimmed NURBS NURBS curves and surfaces are included in the set of OpenGL Performer reps. OpenGL also has these, but OpenGL Performer NURBS have two advantages: • You can maintain topology, so cracks do not appear at the boundaries of adjacent tessellations when they are drawn. • You have better control over tessellation. See Chapter 10, “Creating and Maintaining Surface Topology”. Objects Required by Reps To use reps effectively, you have to understand the OpenGL Performer representations of geometric points and the transformation matrices that are used by the methods of the rep classes. 342 007-1680-100 Objects Required by Reps New Types Required for Reps The classes pfRVec2, pfRVec3, and pfRVec4 define two-, three-, and four-dimensional vectors, and include common operations of vector algebra such as addition, scalar multiplication, cross products, and so on. See the header file Performer/pr/pfLinMath.h for a list of operations defined for each vector. The classes pfRVec2, pfRVec3, pfRVec4 and pfRMatrix are composed of either single- or double-precision values based on the PF_REAL_IS_DOUBLE #define. Currently, PF_REAL_IS_DOUBLE is set to 0. Hence, all pfRVecs as well as pfRMatrix and the pfReal type are defined as single-precision elements. It is important to use these new types (which are essentially #define statemtents) so that you may change to double- or arbitrary-precision versions of these elements when this functionality is enabled in OpenGL Performer. One more type, pfBool, has also been added to the repertoire of types for OpenGL Performer and it always maps to the value of a 32-bit integer, regardless of the value of the PF_REAL_IS_DOUBLE #define, which can be found in Performer/pf.h. In addition, pfLoop has been defined as a 32-bit integer and can take on one of the following values: • PFLOOP_OPEN • PFLOOP_CLOSED • PFLOOP_PERIODIC • PFLOOP_UNRESOLVED Classes for Scalar Functions The pfScalar class is the base class for defining scalar functions; it allows you to conveniently read and write functions. The class provides a virtual evaluation method. 007-1680-100 343 9: Higher-Order Geometric Primitives Class Declaration for pfScalar The class has the following main methods: class pfScalar : public pfObject { public: // Creating and destroying pfScalar(); virtual ~pfScalar(); virtual pfReal eval(pfReal u) = 0; }; The class pfCompositeScalar allows you to define the functional composition of two pfScalars. Class Declaration for pfCompositeScalar The class has the following main methods: class pfCompositeScalar : public pfScalar { public: // Creating and destroying pfCompositeScalar( ); pfCompositeScalar(pfScalar *outFun, pfScalar *inFun); virtual ~pfCompositeScalar(); // Accessor functions pfScalar *getOutF() pfScalar *getInF() void setOutF(pfScalar *outF); void setInF (pfScalar *inF); pfReal eval(pfReal t); Main Features of the Methods in pfCompositeScalar eval() Returns the value of outF(inF(t)). Trigonometric Functions OpenGL Performer provides classes for two trigonometric functions, pfCosScalar and pfSinScalar. The class declarations are similar to that of pfScalar. 344 007-1680-100 Objects Required by Reps Polynomials Polynomials of arbitrary degree are defined by the class pfPolyScalar. Class Declaration for pfPolyScalar The class has the following main methods: class pfPolyScalar : public pfScalar { public: // Creating and destroying pfPolyScalar( void ); pfPolyScalar( int degree, pfReal* coef); virtual ~pfPolyScalar(); // Accessor functions void set( int degree, pfReal* coef); int getDegree() pfReal getCoef( int i) // Evaluators pfReal eval(pfReal u); }; Matrix Class: pfRMatrix Each geometric primitive is defined with respect to its own coordinate system. The elementary definition of an object gives a particular orientation and location with respect to the origin. This reference frame can, in turn, be manipulated by a pfDCS to place it in a scene or manipulate it. The base class for higher-order primitives has methods that allow you to locate and orient a primitive with respect to its own reference frame. These methods make insertion of pfDCS nodes whenever you want to define the location or orientation of an object or to change the shape of an object unnecessary. The location is defined by an pfRVec2 or pfRVec3, and the orientation is controlled by a 3 x 3 matrix, held in the class pfRMatrix. If the matrix is not a rotation matrix, you can change the shape of an object, for example, you can distort a sphere into an ellipsoid. 007-1680-100 345 9: Higher-Order Geometric Primitives Geometric Primitives: The Base Class pfRep and the Application repTest pfRep is the base class for higher-order geometric primitives that are stored in an OpenGL Performer scene graph. A pfRep is derived from a pfGeode and is therefore always a leaf node. Figure 9-1 shows the hierarchy of classes derived from pfRep. The following sections discuss the subclasses of pfRep: • “Planar Curves” on page 348 • “Spatial Curves” on page 371 • “Parametric Surfaces” on page 375 • “Meshes” on page 413 • “Subdivision Surfaces” on page 420 To experiment with pfReps, you can use and modify the application repTest in /usr/share/Performer/src/pguide/libpf/C++/repTest.C on IRIX and Linux and in %PFROOT%\Src\pguide\libpf\C++\repTest.cxx on Microsoft Windows. This code provides sample instances of several geometric representations, as well as the tessellation and OpenGL Performer calls that render the objects. Sample code from repTest is included with discussions of several of the classes derived from pfRep. pfRep has methods to orient the object in space, so you do not have to place a pfDCS node above each pfRep to move it from its default position. 346 007-1680-100 Geometric Primitives: The Base Class pfRep and the Application repTest pfPieceWisePolyCurve2d pfLine2d pfCircle2d pfCurve2d pfSuperQuadCurve2d pfHsplineCurve2d pfDisCurve2d pfNurbCurve2d pfPieceWisePolyCurve3d pfLine3d pfOrientedLine3d pfCurve3d pfCircle3d pfSuperQuadCurve3d pfHsplineCurve3d pfGeode pfNurbCurve3d pfRep pfDisCurve3d pfCompositeCurve3d pfCuboid pfPieceWisePolySurface pfPlane pfSphere pfCylinder pfTorus pfCone pfParaSurface pfSweptSurface pfRuled pfCoons pfNurbSurface pfHsplineSurface Figure 9-1 007-1680-100 Class Hierarchy for Higher-Order Primitives 347 9: Higher-Order Geometric Primitives Class Declaration for pfRep The class has the following main methods: class pfRep : public pfGeode { public: pfRep( ); virtual ~pfRep( ); // Accessor functions void setOrigin( const pfRVec3& org ); void setOrient( const pfRMatrix& mat ); void getOrigin(pfRMatrix& mat ); void getOrient(pfRVec3& org ); }; Main Features of the Methods in pfRep setOrient() Sets the orientation of the representation with respect to the origin using matrix multiplication. setOrigin() Sets the location of the representation with respect to the origin. For example, supplying the vector (1,0,0) shifts the location of the object 1 unit in the direction of the positive X axis. pfRep’s subclasses typically include evaluator methods to determine coordinates of points, tangents, and normals. If you do not want the values corresponding to the default position, do not call these methods before you use setOrient() and setOrigin() to locate an pfRep. Thus, for example, when defining points on a circle, first set the center and the radius, then call setOrient() to set the orientation, and then evaluate points. Planar Curves A parametric curve in the plane can be thought of as the result of taking a piece of the real number line, twisting it, stretching it, maybe gluing the ends together, and laying it down on the plane. The base class for parametric curves that lie in the XY plane is the class pfCurve2d. 348 007-1680-100 Planar Curves An important use of pfCurve2d is to specify trim curves, which define boundaries for surfaces. Surfaces are parameterized by part of a plane, which in OpenGL Performer is referred to as the uv plane. When an pfCurve2d is used to define a trim curve, it is treated as a curve in the UV plane. This topic is discussed further in the section “Parametric Surfaces” on page 375. Another important use of pfCurve2d is for specifying cross sections for swept surfaces. See “Swept Surfaces” on page 395. OpenGL Performer also provides a class to create discrete curves, pfDisCurve2d. The following sections discuss planar curve classes, most of which are derived from pfCurve2d: • “Mathematical Description of a Planar Curve” on page 349 • “Lines in the Plane” on page 353 • “Circles in the Plane” on page 354 • “Superquadric Curves: pfSuperQuadCurve2d” on page 356 • “Hermite-Spline Curves in the Plane” on page 359 • “NURBS Overview” on page 360 • “NURBS Curves in the Plane” on page 365 • “Piecewise Polynomial Curves: pfPieceWisePolyCurve2d” on page 367 • “Discrete Curves in the Plane” on page 368 Mathematical Description of a Planar Curve Planar curves consist of sets of points, described by two-dimensional vectors, pfRVec2s. They are parameterized by the pfReal variable t; as t varies, a point “moves” along the curve. t can be thought of as the amount of time that has passed as a point moves along the curve. Or, t can measure the distance traveled. More precisely, each component of a point on the curve is a function of t, which lies in the parameter interval (t0, t1) on the real line. Points on the curve are described by a pair of functions of t: (x(t), y(t)). 007-1680-100 349 9: Higher-Order Geometric Primitives y t=1.0 t=0.0 x Object space 0.0 1.0 t Parameter space Figure 9-2 Parametric Curve: Parameter Interval (0,1). Classes derived from pfCurve2d inherit a set of evaluator functions which, for a given value of t, evaluate a point on the curve, the tangent and normal vectors at the point, and the curvature. Naturally, the base-class evaluator that locates points on the curve is a pure virtual function. To evaluate tangent and normal vectors at a point, pfCurve2d provides virtual functions that, by default, use finite-central-difference calculations. To compute the tangent to the curve at p[t], a point on the curve, the tangent evaluator function takes the vector connecting two “nearby” points on the curve, p[t+∆t] − p[t−∆t] where ∆t is “small,” and divides by 2∆t. Similarly, a finite-central-difference calculation of the normal vector uses the difference between two nearby tangent vectors: n[t] = (t[t+∆t] −t[t−∆t])/2∆t. 350 007-1680-100 Planar Curves Class Declaration for pfCurve2d The class has the following main methods: class pfCurve2d : public pfRep { public: // Creating and destroying pfCurve2d( ); pfCurve2d( pfReal beginT, pfReal endT ); virtual ~pfCurve2d(); // Accessor functions void setBeginT( const pfReal beginT ); void setEndT( const pfReal endT ); pfReal getBeginT(); pfReal getEndT(); pfRVec2 getBeginPt(); pfRVec2 getEndPt(); pfRVec2 getBeginTan(); pfRVec2 getEndTan(); void setClosed( const pfLoop loopVal ); pfLoop getClosed(); void setClosedTol( const pfReal tol ); pfReal getClosedTol() const; // Evaluators virtual void evalPt( virtual void evalTan( virtual void evalNorm( virtual void evalCurv( virtual void eval( pfReal pfReal pfReal pfReal pfReal t, t, t, t, t, pfRVec2 &pnt ) = 0; pfRVec2 &tan ); pfRVec2 &norm ); pfReal *curv ); pfRVec2 &pnt, pfRVec2 &tan, pfReal *curv, pfRVec2 &norm ); }; 007-1680-100 351 9: Higher-Order Geometric Primitives Main Features of the Methods in pfCurve2d pfCurve2d(beginT, endT) Creates an instance of pfCurve2d(). If you do not specify any arguments, then the parametric range of the curve is [0.0,1.0]. eval() For a given t, returns the position, tangent, curvature, and normal vectors. evalPt() Is a pure virtual function to evaluate position on the curve. evalTan(), evalCurv(), and evalNorm() Evaluate the curve’s tangent, curvature, and normal vectors, respectively. The default methods approximate the computation using central differences taken about a small ∆t, given by (endT - beginT) * functionTol. functionTol is a static data element specified in the file pfRep.h. setBeginT() and setEndT(), getBeginPt() and getEndPt() Set and get the parameter range for the curve. Whenever you set one of these values, the endpoint of the curve changes. Therefore, each of these methods also recomputes the endpoint, which is cached because it is frequently used. Also, the methods recompute the ∆t used to approximate derivatives. Note that all planar curve classes derived from pfCurve2d reuse setBeginT() and setEndT() to define the extents of their curves. setClosed() and getClosed() Set and get whether a curve is closed. A closed curve is one for which the endpoints match. OpenGL Performer determines automatically whether curves are closed, but you can override this with setClosed(). setClosedTol() and getClosedTol() Set and get the mismatch between endpoints that is allowed when calculating whether curves are closed. To specify the origin used to locate an pfCurve2d, use the first two components set by the inherited method pfRep::setOrigin(). 352 007-1680-100 Planar Curves Lines in the Plane Parametric lines in the plane are defined by beginning and ending points. The parameterization is such that as t varies from t1 to t2, a point on the line “moves,” at a uniform rate, from the beginning to the ending point. y (x2,y2) t=t2 (x1,y1) t=t1 x Figure 9-3 Line in the Plane Parameterization Class Declaration for pfLine2d The class has the following main methods: class pfLine2d : public pfCurve2d { public: // Creating and destroying pfLine2d(); pfLine2d( pfReal x1, pfReal y1, pfReal t1, pfReal x2, pfReal y2, pfReal t2 ); virtual ~pfLine2d(); // Accessor functions void setPoint1( pfReal x1, pfReal y1, pfReal t1 ) ; void setPoint2( pfReal x2, pfReal y2, pfReal t2 ) ; void getPoint1( pfReal *x1, pfReal *y1, pfReal *t1 ) const; void getPoint2( pfReal *x2, pfReal *y2, pfReal *t2 ) const; // Evaluators void evalPt( pfReal t, pfRVec2 &pnt ); }; 007-1680-100 353 9: Higher-Order Geometric Primitives Main Features of Methods in pfLine2d Creates a parametric line with end points (0,0) and (1,0), and parameter interval (0,1). pfLine2d() pfLine2d(x1, y1, t1, x2, y2, t2) Creates a parametric line starting at the point (x1, y1) and ending at (x2,y2). The line is parameterized so that t = t1 corresponds to (x1, y1) and t = t2 corresponds to (x2,y2). Is the only evaluator function defined for this object. The tangent vector is (x2-x1, y2-y1) and the curvature is zero. evalPt() setPoint*() and getPoint*() Set and get the end points of the line. Circles in the Plane Use the class pfCircle2d to define a parametric circle in the plane. The parameterization is such that t is the angular displacement, in radians, in a counterclockwise direction from the X axis. Figure 9-4 illustrates the parameterization of the circle. y s diu Ra t x Origin Figure 9-4 354 Circle in the Plane Parameterization 007-1680-100 Planar Curves Class Declaration for pfCircle2d The class has the following main methods: class pfCircle2d : public pfCurve2d { public: // Creating amd destroying pfCircle2d(); pfCircle2d( pfReal rad, pfRVec2 *org ); virtual ~pfCircle2d(); // Accessor functions void setRadius( pfReal rad ) ; pfReal getRadius() const; // Evaluator void evalPt( pfReal t, pfRVec2 &pnt ); void evalTan( pfReal t, pfRVec2 &tan ); void evalCurv( pfReal t, pfReal *curv ); void evalNorm( pfReal t, pfRVec2 &norm ); void eval( pfReal t, pfRVec2 &pnt, pfRVec2 &tan, pfReal *curv, pfRVec2& norm ); }; Main Features of the Methods in pfCircle2d pfCircle2d inherits methods to set the range of parameter values from pfCurve2d. pfCircle2d(rad, org) Creates an instance of a two-dimensional circle with radius rad centered at org. The default circle has unit radius and origin (0,0). To change the default position, use the methods setOrigin() and setOrient() inherited from pfRep. setRadius() and getRadius() Set and get the circle’s radius. pfCircle2d provides exact calculations for the evaluator functions inherited from pfCurve2d. 007-1680-100 355 9: Higher-Order Geometric Primitives Superquadric Curves: pfSuperQuadCurve2d The class pfSuperQuadCurve2d provides methods to define a generalization of a circle that, when used for constructing a swept surface, is convenient for generating rounded, nearly square surfaces, or surfaces with sharp cusps (see “Swept Surfaces” on page 395). Two examples of superquadrics appear in repTest. The position along the curve is specified by an angle from the x axis, in the same as for an pfCircle2d. The shape of the curve is controlled by a second parameter. A superquadric is the set of points (x,y) given by the following equation that clearly expresses the relationship to the equation of a circle: ( x2 )1 / α + ( y2 )1 / α = ( r2 )1 / α The above equation can be written in a parametric form: α x ( t ) = r cos [ t ] sign [ cos [ t ] ] α y ( t ) = r sin [ t ] sign [ sin [ t ] ] The family of curves generated by these equations as the quantity α varies can be described as follows (see Figure 9-5). Four points are always on the curve for any value of α: (±r, 0) and (0, ±r). 356 • If α is 1, the curve is a circle of radius r. • As α approaches zero, the circle expands to fill a square of side 2r as if you were inflating a balloon in a box. • As α approaches infinity, the circle contracts towards the two diameters along the x and y axes, approaching two orthogonal lines as if you deflated a balloon with two rigid orthogonal sticks inside it. 007-1680-100 Planar Curves y x Figure 9-5 Superquadric Curve’s Dependence on the Parameter α. Class Declaration for pfSuperQuadCurve2d The class has the following main methods: class pfSuperQuadCurve2d : public pfCurve2d { public: // Creating and destroying pfSuperQuadCurve2d(); pfSuperQuadCurve2d( pfReal radius, pfRVec2 *origin, pfReal exponent ); virtual ~pfSuperQuadCurve2d(); // Accessor functions void setRadius( pfReal _radius ); 007-1680-100 357 9: Higher-Order Geometric Primitives pfReal getRadius() const; void setExponent( pfReal _exponent ); pfReal getExponent() const; // Evaluator void evalPt( }; pfReal t, pfRVec2 &pnt ); Main Features of the Methods in pfSuperQuadCurve2d The accessor functions allow you to control the radius r and exponent α of the curve. To change the default position, use the methods setOrigin() and setOrient() inherited from pfRep. 358 007-1680-100 Planar Curves Hermite-Spline Curves in the Plane A spline is a mathematical technique for generating a single geometric object from pieces. An advantage of breaking a curve into pieces is greater flexibility when you have many points controlling the shape: changes to one piece of the curve do not have significant effects on remote pieces. To define a spline curve for a range of values for the parameter t, say from 0 to 3, you “tie” together pieces of curves defined over intervals of values for t. For example, you might assign curve pieces to the three intervals 0 to 1, 1 to 2, and 2 to 3. The four points in the set of parameters, 0, 1, 2, and 3, define the endpoints of the intervals and are called knots. A Hermite-spline curve is a curve whose segments are cubic polynomials of the parameter t, where the coefficients of the polynomials are determined by the position and tangent to the curve at each knot point. Thus the curve passes through each of a set of specified points with a specified tangent vector. The set of knot points must be increasing values of the parameter t. y p3 tng0 p1 t=t1 p0 t=t0 tng1 p2 t=t2 t=t3 tng3 tng2 x Figure 9-6 Hermite Spline Curve Parameterization Class Declaration for pfHsplineCurve2d The class for creating Hermite spline curves is pfHsplineCurve2d. The class has the following main methods: class pfHsplineCurve2d : public pfCurve2d { public: // Creating and destroying pfHsplineCurve2d(); virtual ~pfHsplineCurve2d(); 007-1680-100 359 9: Higher-Order Geometric Primitives // Accessor functions void setPoint( int i, const pfRVec2 &p ); void setTangent( int i, const pfRVec2 &tng ); void setKnot( int i, pfReal t ); int getKnotCount() const; pfRVec2* getPoint( int i ); pfRVec2* getTangent( int i ); pfReal getKnot( int i ); // Evaluator virtual void evalPt( pfReal t, pfRVec2 &pnt ); }; NURBS Overview The acronym NURBS stands for “nonuniform rational B-splines.” NURBS define a set of curves and surfaces that generalizes Bezier curves. Both NURBS curves and Bezier curves are “smooth” curves that are well suited for CAD design work. They are essentially determined by a set of points that controls the shape of the curves, although the points do not lie on the curves. Because NURBS properties are not widely known, a discussion of their features precedes details of how to create instances of them. The discussion is necessarily brief and is intended to provide the minimum amount of information needed to start using OpenGL Performer NURBS classes. This general discussion of NURBS is presented in the following sections: 360 • “OpenGL Performer NURBS Classes” on page 361 • “NURBS Elements That Determine the Control Parameters” on page 361 • “Knot Points” on page 362 • “Control Hull” on page 362 • “Features of NURBS and Bezier Curves” on page 363 • “Weights for Control Points” on page 362 007-1680-100 Planar Curves OpenGL Performer NURBS Classes The OpenGL Performer classes allow you to treat a NURBS object as a black box that takes a set of control parameters and generates a geometric shape. A NURBS object’s essential properties are rather straightforward, although the underlying mathematics are complex. Unlike lines and circles, NURBS can represent a large set of distinct complex shapes. Because of this flexibility, developing a NURBS object is often best done interactively. For example, you could allow a user to design a curve using an interface in which control parameters are changed by clicking and dragging and by using sliders. There are three classes: • The pfNurbCurve2d class generates curves in the plane, the simplest NURBS object provided by OpenGL Performer. • The pfNurbCurve3d class generates NURBS curves in three-dimensional space. • The pfNurbSurface class generates NURBS surfaces, which extend the ideas underlying NURBS curves to two-dimensional objects. The principles for controlling the shapes of these objects are all essentially the same. NURBS Elements That Determine the Control Parameters This section provides some theoretical background information on NURBS elements. If you already understand NURBS, continue with “NURBS Curves in the Plane” on page 365) NURBS are defined by the following elements, discussed in this chapter: 007-1680-100 • Nonuniform knot points (see “Knot Points” on page 362) • A control hull consisting of control points (see “Control Hull” on page 362) • Weighting parameters for control points (see “Weights for Control Points” on page 362) 361 9: Higher-Order Geometric Primitives Knot Points The knot points determine how and where the pieces of the NURBS object are joined. The knots are nondecreasing— but not necessarily uniformly spaced or distinct—values of the parameter t for the curve. The sequence of knots need not have uniform spacing in the parameter interval. In fact, the mathematics of NURBS make it possible and, perhaps, necessary to repeat knot values; that is, knots can appear with a certain multiplicity. The number of knot points is determined by counting all the knot points, including all multiplicities. For example, although the sequence (0,0,0,0,1,1,1,1) has only two distinct knot points, the number of knot points is eight. (This example it is the set of knot points for a cubic Bezier curve defined on the interval 0 to 1). How to determine the order of a NURBS curve is discussed in “Features of NURBS and Bezier Curves” on page 363. Control Hull The control hull is the set of all points that determine the basic shape of NURBS object. The effect of the control hull is determined by a “B-spline.” A B-spline is a basis spline; a set of special curves associated with a given knot sequence from which you can generate all other spline curves having the same knot sequence and control hull. For each interval described by the knot sequence, the corresponding piece of a B-spline curve is a Bezier curve. B-spline curves are like Bezier curves in that they are defined by an algorithm that acts on a sequence of control points, the control hull, which lie in the plane or in three-dimensional space. Weights for Control Points The third set of control parameters for a NURBS curve is the set of weights associated with the control points. A rational B-spline consists of curves that have a weight associated with each control point. The individual pieces of a NURBS curve usually are not Bezier curves but rational Bezier curves. The values of the weights have no absolute meaning; they control how “hard” an individual control point pulls on the curve relative to other control points. If the weights for all the control points of a rational Bezier curve are equal, then the curve 362 007-1680-100 Planar Curves becomes a simple Bezier curve. Weights allow construction of exact conic sections, which cannot be made with simple Bezier curves. Features of NURBS and Bezier Curves Bezier curves have the following properties: • They are “nice” polynomial curves whose degree is one less than the number of control points. For a polynomial curve, each of the components is a polynomial function of the parameter t. The number of coefficients in the polynomial, the order of the polynomial, is equal to the number of control points. • The control points determine the shape of the Bezier curve, but they do not lie on the curve, except the first and last control points. NURBS curves differ in the following ways: • The order of the polynomial pieces that make up the NURBS curve depends on the number of control points and the number of knot points. The order of a NURBS curve is the difference between the number of knots, accounting for multiplicity, and the number of control points. That is, order = number of knot points - number of control points • 007-1680-100 The relationship between the curves and the control points is looser than for a Bezier curve. It also depends on the knot sequence and the sequence of weights. 363 9: Higher-Order Geometric Primitives Equation Used to Calculate a NURBS Curve The equation that defines the NURBS curve is ∑ B (t)C p(t) = ---------------------------∑ B (t)W n i i n i i i i • p(t) is a point on the surface p(t) • B in(t) is the ith B-spline basis function of degree n • C i is a control point • W i is the weight for the control point Alternative Equation for a NURBS Curve If you have a surface developed from the alternative expression for a NURBS surface: ∑ B (u)W C p(u, v) = ----------------------------------∑ B (u)W n i i i i n i i i you must change the coordinates of the control points to get the same surface from OpenGL Performer; you convert the coordinates of the control points from (x,y,w) to (wx,wy,w). 364 007-1680-100 Planar Curves NURBS Curves in the Plane The class pfNurbCurve2d defines a nonuniform rational B-spline curve in the plane, the simplest NURBS object provided by OpenGL Performer. Class Declaration for pfNurbCurve2d The class has the following main methods: class pfNurbCurve2d : public pfCurve2d { public: // Creating and destroying pfNurbCurve2d( ); pfNurbCurve2d( pfReal tBegin, pfReal tEnd ); virtual ~pfNurbCurve2d( ); // Accessor functions void setControlHull( void setControlHull( void setWeight( void setKnot( void setControlHullSize( int int int int int i, i, i, i, s const pfRVec2 &p ); const pfRVec3 &p ); pfReal w ); pfReal t ); ); pfRVec2* getControlHull( int i ); pfReal getWeight( int i ); int getControlHullSize( ); int getKnotCount( ); pfReal getKnot( int i ); int getOrder( ); void removeControlHullPnt(int i); void removeKnot(int i); // Evaluator virtual void evalPt( pfReal t, pfRVec2 &pnt ); }; Main Features of the Methods in pfNurbCurve2d pfNurbCurve2d(tBegin, tEnd) Creates a NURBS curve in the plane with the specified parameter domain. The default parameter domain is 0.0 to 1.0. 007-1680-100 365 9: Higher-Order Geometric Primitives evalPt() Is a pure virtual function inherited from pfCurve2d, and produces unpredictable results until you set the control parameters. setControlHull(i, p) and getControlHull(i) Set and get the two-dimensional control point with index i to the value p. If you supply pfRVec3 arguments, the location of the control points is set by the first two components; the last component is their weight. setControlHullSize() Gives a hint about how big the control hull array is. This is not mandatory but uses time and space most efficiently. setKnot(i, t) and getKnot(i) Set and get the knot point with index i and the value t. setWeight(i, w) and getWeight(i) Set and get the weight of the control point with index i and weight w. 366 007-1680-100 Planar Curves Piecewise Polynomial Curves: pfPieceWisePolyCurve2d A piecewise polynomial curve consists of an array of polynomial curves. Each polynomial curve is a polynomial mapping from t to UV plane, where the domain is a subinterval of [0,1]. The polynomial coefficients are set by setControlHull(). Notice that an pfPieceWisePolyCurve2d is a subclass of pfCurve2d. The domain of a pfPieceWisePolyCurve2d is defined to be [0, n] where n is the number of pieces. If reverse is 0, then for any given t in [0, n], its corresponding uv is evaluated in the following way: The index of the piece that corresponds to t is floor(t), and the polynomial of that piece is evaluated at w1 + (t-floor(t)) * (w2-w1) to get the (u,v), where [w1, w2] is the domain interval (set by setLimitParas()) of this piece. If reverse is 1, then for any given t in [0,n], we first transform t into n-t, then perform the normal evaluation (at n-t) as described in the preceding paragraph. Class Declaration for pfPieceWisePolyCurve The class has the following main methods: class pfPieceWisePolyCurve2d : public pfCurve3d { public: // Creating and destroying pfPieceWisePolyCurve2d ( ); virtual ~pfPieceWisePolyCurve2d ( ); //Accessor functions void setControlHull ( int piece, int i, const pfRVec2& p); pfRVec2& getControlHull ( int piece, int i); void setLimitParas ( int piece, pfReal w1, pfReal w2); void setReverse ( int _reverse); pfRVec2& getLimitParas ( int piece); int getReverse ( ) const; int getPatchCount ( ) const; int getOrder ( int piece); virtual void evalPt ( pfReal t, pfRVec2& pnt); virtual void evalBreakPoints ( pfParaSurface* sur); }; 007-1680-100 367 9: Higher-Order Geometric Primitives setControlHull(piece, i, p) defines the ith polynomial coefficient of the pieceth polynomial curve to p. p[0] is for the u coefficient and p[1] is for the v coefficient. setLimitParas() sets the domain interval. The class pfPieceWisePolyCurve3d has parallel functionality and declaration. Discrete Curves in the Plane The class pfDisCurve2d is the base class for making a discrete curve from line segments connecting a sequence of points in the plane. Because pfDisCurve2d is not derived from pfCurve2d, it does not inherit that class’s finite difference functions for calculating derivatives, therefore, pfDisCurve2d includes member functions that calculate arc length, tangents, principal normals, and curvatures using finite central differences. Figure 9-7 illustrates the definition of the curve by a set of points. y pi=(points[2i], points[2i+1]) p3 p1 p4 p2 p0 x Figure 9-7 368 Discrete Curve Definition 007-1680-100 Planar Curves Class Declaration for pfDisCurve2d The class has the following main methods: class pfDisCurve2d : public pfRep { public: // Creating and destroying pfDisCurve2d( void ); pfDisCurve2d( int nPoints, pfReal *points ); virtual ~pfDisCurve2d( void ); // Accessor functions void set (int nPoints, pfReal* points); pfRVec2 getBeginPt() const; pfRVec2 getEndPt() const; pfLoop getClosed(); void setClosed( pfLoop c ); void setPoint( int i, const pfRVec2& pnt ); pfRVec2 getPoint( int i) const; int getPointCount(); const; pfRVec2 getTangent(int i) const; pfRVec2 getNormal(int i) const; pfReal getCurvature(int i) const; // Evaluators void computeTangents( ); void computeNormals( ); void computeCurvatures( ); void computeDerivatives( ); }; 007-1680-100 369 9: Higher-Order Geometric Primitives Main Features of the Methods in pfDisCurve2d pfDisCurve2d(nPoints, points) Creates a discrete curve from an array of point coordinates. The constructor assumes that the coordinates of the points are stored in pairs sequentially; thus the points array is nPoint*2 in length. computeCurvatures() Computes the curvature, which is the magnitude of the normal vector. computeDerivatives() Is a convenience function that calls (in order) the tangent, normal, and curvature functions. computeNormals() Computes the principal normal at a point using finite central differences and stores the result in the class member pfDvector n. For the point p[i], the normal vector is computed to be the difference vector between the tangents at the two neighboring points, t[i+1] - t[i-1], divided by the sum of the distances from p[i] to the two neighboring points. computeTangents() Computes the arc lengths of segments and then uses finite central differences to compute the tangents. For the point p[i], the tangent vector is computed to be the vector between its two neighboring points, p[i+1] - p[i-1], divided by the sum of the distances from p[i] to the two neighboring points. The tangents are stored in the pfDvector t, the arc lengths in the pfDvector ds, and the total arc length in arcLength. getCurvature() Returns the value of the curvature at the ith point. getNormal() Returns the value of the normal at the ith point. getPoint() Returns the value of the ith point. getPointCount() Returns the value of the ith point. getTangent() 370 Returns the value of the tangent at the ith point. 007-1680-100 Spatial Curves Spatial Curves The class pfCurve3d is the base for parametric curves that lie in three-dimensional space. Among other uses, a curve in space could locate a moving viewpoint in a CAD walk-through. The nature of these curves is essentially the same as those of pfCurve2d curves, except pfCurve3d curves are made of points described by pfRVec3s. The components of the points are assumed to be x, y, and z coordinates. Refer to the section “Planar Curves” on page 348 for a discussion of the basic features of parametric curves. This section parallels the discussion in “Planar Curves” on page 348, and emphasizes the (not very great) differences that distinguish spatial curves: • “Lines in Space” on page 371 • “Circles in Space” on page 372 • “Superquadrics in Space” on page 372 • “Hermite Spline Curves in Space” on page 373 • “NURBS Curves in Space” on page 373 • “Curves on Surfaces: pfCompositeCurve3d” on page 374 • “Discrete Curves in Space” on page 375 The class declaration for pfCurve3d is in the file /usr/include/Performer/pf/pfCurve3d.h on IRIX and Linux and %PFROOT%\Include\pf\pfCurve3d.h on Microsoft Windows. Its declaration is essentially identical to the declaration for pfCurve2d. The difference is that all pfRVec2 variables are replaced by pfRVec3 variables. Lines in Space The base class for lines in space, pfLine3d, is essentially the same as pfLine2d, discussed in “Lines in the Plane” on page 353. The main differences are due to the need to manage three-dimensional vectors. Thus all vector variables are pfRVec3 and the constructor takes six variables to define the endpoints of the line. 007-1680-100 371 9: Higher-Order Geometric Primitives The default orientation of the curve is identical to that for the planar curve pfLine2d; you can translate and rotate the line in three-dimensional space with the methods setOrigin() and setOrient() inherited from pfRep. pfOrientedLine3d The class pfOrientedLine3d is derived from pfLine3d, and adds vectors to define a moving three-dimensional reference frame for the line. This object is useful if you want a straight-line path for an pfFrenetSweptSurface (see “Swept Surfaces” on page 395 and, in particular, “Class Declaration for pfFrenetSweptSurface” on page 399). The methods of pfOrientedLine3d add to the description of the line an “up” vector, which you specify. The normal to the line is calculated from the direction of the line and the up vector. Circles in Space The class pfCircle3d defines a parametric circle with an arbitrary location and orientation in space. The parameterization of the circle, before you change its location or orientation, is such that t is the angular displacement, in radians, in a counterclockwise direction from the x axis. The class declaration for pfCircle3d is identical to that for pfCircle2d, discussed in “Circles in the Plane” on page 354, except for the changes from pfRVec2 to pfRVec3. The member functions perform the same operations. For more information, see the discussion in the section “Circles in the Plane” on page 354. If the matrix you use to orient an pfCircle3d does not correspond to a rotation about an axis—that is, the matrix is not orthonormal— you not only change the tilt of the plane in which the circle lies but also change the radius, and may distort the circle into an ellipse. . Superquadrics in Space The class pfSuperQuadCurve3d provides methods to define a superquadric in space (see “Superquadric Curves: pfSuperQuadCurve2d” on page 356). The class declaration is identical to that for pfSuperQuad2d except that position on the curve is defined by an pfRVec3. 372 007-1680-100 Spatial Curves The default orientation of the curve is identical to that for the planar curve pfSuperQuad2d; you can translate and rotate the curve in three-dimensional space with the methods setOrigin() and setOrient() inherited from pfRep. Hermite Spline Curves in Space The class pfHsplineCurve3d provides methods to define a Hermite spline curve in space. The definition of the curve is the same as that for a Hermite spline curve in the plane, discussed in “Hermite-Spline Curves in the Plane” on page 359. The class declaration is the same as that for pfHsplineCurve2d, but the position and tangent vectors are pfRVec3s. NURBS Curves in Space The basic properties of NURBS are discussed in the section “NURBS Overview” on page 360. In an effort to keep things as simple as possible, the discussion in that section has a bias toward curves in the plane. But the principles and control parameters are, with one difference, the same for NURBS curves in space. The difference is that control points for NURBS curves in space can be anywhere in space instead of being restricted to a plane. The section “Examples of NURBS Curves” in Chapter 8 of The Inventor Mentor presents illustrations of NURBS curves in space, along with their control parameters. The class pfNurbCurve3d is the base class for NURBS curves in space. Its class declaration is practically identical to that for pfNurbCurve2d but all occurrences of pfRVec2 are changed to pfRVec3. In addition, the vector argument of setControlHull() can be an pfRVec3, if you just want to specify control point locations, or an pfRVec4, if you want to append weighting information as a fourth component. See the discussion in the section “NURBS Curves in the Plane” on page 365. 007-1680-100 373 9: Higher-Order Geometric Primitives Curves on Surfaces: pfCompositeCurve3d A planar curve in the u-v plane describes a curve on the surface, given a parameterized surface (see the section “Parametric Surfaces” on page 375). Each point on the curve in the parameter plane is “lifted up” to the surface. Such curves are known as composite curves because they are described mathematically as the composition of the function describing the curve and the function describing the surface. The edge of a surface defined by a trim curve is a composite curve. pfCompositeCurve3d is the base class for composite curves. This class is useful for defining trim curves and surface silhouettes in the parametric surface’s coordinate system. Class Declaration for pfCompositeCurve3d The class has the following main methods: class pfCompositeCurve3d : public pfCurve3d { public: // Creating and destroying pfCompositeCurve3d( ); pfCompositeCurve3d( pfParaSurface *sur, pfCurve2d *cur ); virtual ~pfCompositeCurve3d( ); // Accessor functions void set( pfParaSurface *sur, pfCurve2d *cur ); pfParaSurface* getParaSurface() const; pfCurve2d* getCurve2d() const; // Evaluator void evalPt( pfReal u, pfRVec3 &pnt ); }; Main Features of the Methods in pfCompositeCurve3d The constructor takes two arguments: the first is the surface on which the curve lies, the second is the curve in the coordinate system of the surface. The returned object is a curve in space. 374 007-1680-100 Parametric Surfaces Discrete Curves in Space The class pfDisCurve3d is the base class for making a discrete curve of line segments connecting a sequence of points in space. The class declaration for pfDisCurve3d is identical to that for pfDisCurve2d, discussed in “Discrete Curves in the Plane” on page 368, but pfRVec2 changes to pfRVec3. The member functions perform the same operations. Example of Using pfDisCurve3d and pfHsplineCurve3d One application of an pfDisCurve3d and pfHsplineCurve3d is to use them to interactively specify routing for tubing. These are the operations to perform: 1. Create a pfDisCurve3d from a set of points. See “Discrete Curves in Space” on page 375. 2. Use the points and tangents to the discrete curve to create a continuous path with an pfHsplineCurve3d. See “Hermite Spline Curves in Space” on page 373 3. Use the continuous path in an pfFrenetSweptSurface with a circular cross section. See “pfFrenetSweptSurface” on page 399. Parametric Surfaces A parametric surface can be thought of as the result of taking a piece of a plane, twisting and stretching it, maybe gluing edges of the piece together, and placing it in space. The introductory discussion of parametric surfaces occurs in the following sections: 007-1680-100 • “Mathematical Description of a Parametric Surface” on page 376 • “Defining Edges of a Parametric Surface: Trim Loops and Curves” on page 377 • “Adjacency Information: pfEdge” on page 379 • “Base Class for Parametric Surfaces: pfParaSurface” on page 380 375 9: Higher-Order Geometric Primitives The subclasses of pfParaSurface are discussed in the subsequent sections: • “pfPlaneSurface” on page 384 • “pfSphereSurface” on page 386 • “pfCylinderSurface” on page 389 • “pfTorusSurface” on page 391 • “pfConeSurface” on page 393 • “Swept Surfaces” on page 395 • “Ruled Surfaces” on page 400 • “Coons Patches” on page 402 • “NURBS Surfaces” on page 404 • “Hermite-Spline Surfaces” on page 411 Instances of most of the pfParaSurface subclasses are used in the sample application /usr/share/Performer/src/pguide/libpf/C++/repTest.C on IRIX and Linux and %PFROOT\Src\pguide\libpf\C++\repTest.cxx on Microsoft Windows. Mathematical Description of a Parametric Surface To locate a point on a parametric surface, you need two parameters, referred to as u and v in OpenGL Performer. The set of u and v values that describe the surface are known as the parameter space, or coordinate system, of the surface (see Figure 9-8). More precisely, the coordinates of the points in space that define a parametric surface are described by a set of three functions of two parameters: (x(u,v), y(u,v), z(u,v)). Well-known examples of a parametric surface are a sphere or a globe. On a globe you can locate points with two parameters: latitude and longitude. The rectangular grid of latitude and longitudes is the coordinate system that describes points on the globe. 376 007-1680-100 Parametric Surfaces z y x v 1.0 0.0 Figure 9-8 u 1.0 Parametric Surface: Unit-Square Coordinate System Defining Edges of a Parametric Surface: Trim Loops and Curves To define the extent of a parametric curve, pick an interval. For accurate trimming of a parametric surface, you need more complex tools. You are likely to need: • Edges for the surface other than those defined by the limits of the coordinate system. For example, to define a pipe elbow, you might join two cylinders by a piece cut from a torus. • Holes in a surface, for example, to define a T-joint intersection of pipes. OpenGL Performer keeps the trim loop side on the left as you look down on the u-v plane while a point moves along the curve in the direction of increasing t; you can hold on to 007-1680-100 377 9: Higher-Order Geometric Primitives the surface with your left hand as you go along the trim loop. Thus a clockwise loop removes a hole; a counterclockwise loop keeps the enclosed region and eliminates everything outside. Do not create a trim loop that crosses itself like a figure eight. OpenGL Performer allows you to maintain curves to define the edges of a surface. These curves are pfCurve2d objects defined in the u-v plane that are “lifted” to the surface by the parameterization. The main use of these curves is to eliminate a portion of the surface on one side of the curve. The name of a curve in the coordinate system that is used to define (possibly a piece of) such a surface edge is a trim curve. One or more joined trim curves form a sequence called a trim loop. To be of use, trim curves should form a closed loop or reach the edges of the coordinate system for the surface. Figure 9-9 illustrates trim loops and their effect on a surface. z y x Trim1 v Trim2 Trim3 u Figure 9-9 378 Trim Loops and Trimmed Surface: Both Trim Loops Made of Four Trim Curves 007-1680-100 Parametric Surfaces Adjacency Information: pfEdge An pfEdge defines a trim curve in u, v space. pfEdge holds information about a surface’s adjacency. Each pfEdge identifies an pfBoundary, which the class pfTopo uses to keep track of surface connectivity, and continuous and discrete versions of the trim curve associated with the boundary. The members of an pfEdge are set by the toplogy building tools; the methods of pfEdge access the members. Topology building and the classes pfTopo and pfBoundary are discussed further in Chapter 10, “Creating and Maintaining Surface Topology”. The information held in pfEdge allows tessellators to determine whether a set of vertices has already been developed for points shared with other surfaces. You can also find other surfaces that have the same edge or trim-curve endpoint as that defined by a given trim curve. The set*() methods are mainly used when reading surface data from a file and creating OpenGL Performer data structures. Class Declaration for pfEdge The class has the following main methods: class pfEdge : public pfObject { public: // Creating and destroying pfEdge(); ~pfEdge(); pfCurve2d *getContCurve(); void setContCurve(pfCurve2d *c); pfDisCurve2d *getDisCurve(); void setDisCurve( pfDisCurve2d *d); int getBoundary(); void setBoundaryDir( int dir ); int getBoundaryDir(); }; 007-1680-100 379 9: Higher-Order Geometric Primitives Base Class for Parametric Surfaces: pfParaSurface pfParaSurface is the base class for parametric surfaces in OpenGL Performer. As for the base classes pfCurve2d and pfCurve3d, pfParaSurface includes a pure virtual function to evaluate points on the surface and default evaluator functions that calculate derivatives using finite central differences. The surface normal at a point is the cross product of the partial derivatives. For parametric curves whose extent is defined by the interval of values for t, the extent of an pfParaSurface is, initially, defined by all the points in its parameter space. Class Declaration for pfParaSurface The class has the following main methods: class pfParaSurface : public pfRep { public: // Creating and destroying pfParaSurface( pfReal _beginU = 0, pfReal _endU = 1, pfReal _beginV = 0, pfReal _endV = 1, int _topoId = 0, int _solid_id = -1 ); virtual ~pfParaSurface(); // Accessor functions void setBeginU( pfReal u ); void setEndU( pfReal u ); void setBeginV( pfReal v ); void setEndV( pfReal v ); void setSolidId( int solidId); void SetTopoId( int topoId); void setSurfaceId (int surfaceId); pfReal getBeginU() const; pfReal getEndU() const; pfReal getBeginV() const; pfReal getEndV() const; 380 int pfLoop int pfEdge* getTrimLoopCount(); getTrimLoopClosed( int loopNum ); getTrimCurveCount( int loopNum ); getTrimCurve( int loopNum, int curveNum ); int getTopoId(); 007-1680-100 Parametric Surfaces int int getSolidId(); getSurfaceId(); void setHandednessHint( pfbool _clockWise ); pfbool getHandednessHint() const; pfGeoState* getGState( ) const; int setGState( pfGeoState *gState ); void insertTrimCurve( int loopNum, pfCurve2d *c, pfDisCurve2d *d ); // Explicit add a trim curve to a trim loop void addTrimCurve(int loopNum, pfCurve2d *c, pfDisCurve2d *d ); void setTrimLoopClosed( int loopNum, pfLoop closed ); // Surface evaluators virtual void evalPt( virtual void evalDu( virtual void evalDv( virtual void evalDuu( virtual void evalDvv( virtual void evalDuv( virtual void evalNorm( pfReal pfReal pfReal pfReal pfReal pfReal pfReal u, u, u, u, u, u, u, pfReal pfReal pfReal pfReal pfReal pfReal pfReal v, v, v, v, v, v, v, pfRVec3 pfRVec3 pfRVec3 pfRVec3 pfRVec3 pfRVec3 pfRVec3 &pnt ) = 0; &Du ); &Dv ); &Duu ); &Dvv ); &Duv ); &norm ); // Directional derivative evaluators virtual void evalD( pfReal u, pfReal v, pfReal theta, pfRVec3 &D ); virtual void evalDD( pfReal u, pfReal v, pfReal theta, pfRVec3 &DD ); virtual void eval( pfReal u, pfReal v, pfRVec3 &p, // The point pfRVec3 &Du, // The derivative in the pfRVec3 &Dv, // The derivative in the pfRVec3 &Duu, // The 2nd derivative in pfRVec3 &Dvv, // The 2nd derivative in pfRVec3 &Duv, // The cross derivative pfReal &s, // Texture coordinates pfReal &t ); u direction v direction the u direction the v direction void clearTessallation(); }; 007-1680-100 381 9: Higher-Order Geometric Primitives Main Features of the Methods in pfParaSurface addTrimCurve(j, curve, discurve) Is a quick function for building a trim loop that assumes you know the order of trim curves. It adds curve to the end of the list of continuous trim curves for the jth trim loop, and adds discurve to the list of discrete trim curves. For example, you could build the trim loops in Figure 9-9 by starting with one segment and successively adding segments. If the beginning of curve does not match the end of the previously added curve, use insertTrimCurve(), which finds the right place for the curve by assuming topological consistency. eval() Returns the evaluator functions. The last two arguments of eval() are the same as the input coordinates u and v. evalDu(), evalDv(), evalDuu(), evalDvv(), and evalDuv() Are evaluator functions that use central differences to calculate the first and second derivatives, identified by the lowercase u and v in the function names, at a point on the surface. evalD() and evalDD() Calculate the first and second directional derivatives in the direction given by an angle theta from the u axis in the parameter space. evalNorm() Calculates the unit normal to the surface. evalPt() Is a pure virtual function that you define to specify a surface. pfParaSurface() Constructs a parametric surface. You can specify the topology and the surface to which the parametric surface belongs. See “Summary of Scene Graph Topology: pfTopo” on page 428. insertTrimCurve(j, curve, discurve) Is a slower function than addTrimCurve() for building a trim loop that attempts to guarantee all curves form a sensible trim loop sequence. It compares the ends of curve with the ends of the trim curves that are already in the jth trim loop and inserts curve at the appropriate point in the list. Similarly, addTrimCurve() inserts the discrete curve discurve. If insertTrimCurve() cannot find an endpoint match, it adds curve to the end of the list of trim curves. If you are building a trim loop by inserting trim curves end to end, then addTrimCurve() gives the same result but more quickly. 382 007-1680-100 Parametric Surfaces setBeginU(), setBeginV(), etc. Set and get the start and end values for the coordinate space of the surface. The coordinate space is a rectangle in the UV plane. The default is the unit square; u and v both lie in the interval (0,1). getTrimLoopCount() Returns the number of trim loops for the pfParaSurface. getTrimLoopClosed() and setTrimLoopClosed() Get and set the flag indicating whether a given trim loop is closed. OpenGL Performer determines this for you, so use setTrimLoopClosed() with caution; you could get a meaningless result. getTrimCurveCount() Returns the number of trim curves in the specified trim loop. getTrimCurve(i,j) Returns the pfEdge for the trim curve with index i in the trim loop with index j. clearTessellation() Removes all data that resulted from previous tessellation. This removal allows the surface to be retessellated with a different tolerance. For each trim curve, the disCurve is deleted if the contCurve is not NULL. The xyzBoundary in its boundary structure is deleted. Also, the tessellated triangles (csGeometry) are removed. getGState() and setGState() Get and set the pfGeoState to be used when tessellating the surface and setting pfGeoStates on generated geometry (pfGeoSets). setGState() returns 1 if successful, –1 otherwise. 007-1680-100 383 9: Higher-Order Geometric Primitives pfPlaneSurface The simplest parametric surface is a plane. The class pfPlaneSurface defines a plane by two parameter intervals and three points that define the two coordinate directions. Figure 9-10 illustrates the parameterization of an pfPlaneSurface. z (x3,y3,z3) u=u1 v=v3 y (x1,y1,z1) u=u1 v=v1 (x2,y2,z2) u=u2 v=v1 x Figure 9-10 Plane Parameterization Class Declaration for pfPlaneSurface The class has the following main methods: class pfPlaneSurface : public pfParaSurface { public: // Creating and destroying pfPlaneSurface(); pfPlaneSurface( pfReal x1, pfReal y1, pfReal z1, pfReal u1, pfReal v1, pfReal x2, pfReal y2, pfReal z2, pfReal u2, pfReal x3, pfReal y3, pfReal z3, pfReal v3 ); virtual ~pfPlaneSurface(); // Accessor functions void setPoint1( pfReal x1, pfReal y1, pfReal z1, pfReal u1, pfReal v1); void setPoint2( pfReal x2, pfReal y2, pfReal z2, pfReal u2 ); void setPoint3( pfReal x3, pfReal y3, pfReal z3, pfReal v3 ); 384 007-1680-100 Parametric Surfaces void getPoint1( pfReal pfReal void getPoint2( pfReal void getPoint3( pfReal *x1, *u1, *x2, *x3, pfReal pfReal pfReal pfReal *y1, pfReal *z1, *v1 ); *y2, pfReal *z2, pfReal *u2 ); *y3, pfReal *z3, pfReal *v3 ); // Evaluators void evalPt( pfReal u, pfReal v, pfRVec3 &pnt ); void evalDu( pfReal u, pfReal v, pfRVec3 &Du ); void evalDv( pfReal u, pfReal v, pfRVec3 &Dv ); void evalNorm( pfReal u, pfReal v, pfRVec3 &norm ); } Main Features of the Methods in pfPlaneSurface pfPlaneSurface() When you construct the class, you can specify the plane with three points and two parameter intervals or you can use the setPoint*() methods. Those parameters have the following meanings: • the point (x1,y1,z1) and its parameter values, (u1,v1) • the point (x2,y2,z2), which defines the u direction, (x2-x1,y2-y1,z2-z1), and its parameter values (u2,v1) • the point (x3,y3,z3), which defines the v direction, (x3-x1,y3-y1,z3-z1) and its parameter values (u1,v3). setPoint*() and getPoint*() Set and get each of the points that define the plane and their corresponding parameter values (see pfPlaneSurface()). 007-1680-100 385 9: Higher-Order Geometric Primitives pfSphereSurface The surface of the sphere is parameterized by angles, in radians, for latitude and longitude; v corresponds to longitude, u to latitude. Figure 9-11 illustrates the parameterization of an pfSphereSurface. z pnt v y Radius Origin u x Figure 9-11 386 Sphere Parameterization 007-1680-100 Parametric Surfaces Class Declaration for pfSphereSurface The class has the following main methods: class pfSphereSurface : public pfParaSurface { public: // Creating and destroying pfSphereSurface( ); pfSphereSurface( pfReal radius ); virtual ~pfSphereSurface( ); // Accessor functions void setRadius( pfReal radiusVal ); pfReal getRadius( ) const; // Evaluators void evalPt( pfReal void evalNorm( pfReal } u, pfReal v, pfRVec3 &pnt ); u, pfReal v, pfRVec3 &norm ); Main Features of the Methods in pfSphereSurface The constructor defines a sphere centered on the origin with the specified radius. The default radius is 1. The evaluator functions do not use finite-difference calculations for derivatives. Any point on the sphere is represented as: x = radius * cos(u) * sin(v) y = radius * sin(u) * sin(v) z = radius * cos(v) 007-1680-100 387 9: Higher-Order Geometric Primitives pfSphereSurface Example The following code from the sample application repTest illustrates how an instance of an pfSphereSurface of radius three would be created: pfSphereSurface *sphere = new pfSphereSurface( 3 ); // under certain conditions, a trim curve is added that keeps only the // portion of the surface above a circle if ( nVersions <= 0 ) { pfCircle2d *trimCircle2d = new pfCircle2d( 1.0, new pfRVec2(M_PI/2.0,M_PI) ); sphere->addTrimCurve( 0, trimCircle2d ); } setUpShape( sphere, PF_XDIST*numObject++, Y, PF_VIEWDIST ); setUpShape() locates the sphere in the scene, tessellates it, and places it in the scene graph (see /usr/share/Performer/src/pguide/libpf/C++/repTest.C on IRIX and Linux and %PFROOT%\Src\pguide\libpf\C++\repTest.cxx on Microsoft Windows). Creating an instance of any pfRep is basically the same, as subsequent examples in the discussions of other pfReps will show. 388 007-1680-100 Parametric Surfaces pfCylinderSurface The pfCylinderSurface class provides methods for describing a cylinder. A cylinder can be defined geometrically as the surface in space that is swept by moving a circle along an axis that is perpendicular to the plane of the circle and passes through the center of the circle. The parameterization of an pfCylinderSurface is as follows: u represents the position on the circle and that v represents the position along the axis. z v pnt y Origin Height Radiu s u x Figure 9-12 007-1680-100 Cylinder Parameterization 389 9: Higher-Order Geometric Primitives Class Declaration for pfCylinderSurface The class has the following main methods: class pfCylinderSurface : public pfParaSurface { public: // Creating and destroying pfCylinderSurface( void ); pfCylinderSurface( pfReal radius, pfReal height ); virtual ~pfCylinderSurface(); // Accessor functions void setRadius( pfReal radiusVal ) ; void setHeight( pfReal heightVal ); pfReal getRadius( ) const; pfReal getHeight( ) const; // Evaluators void evalPt( pfReal void evalNorm( pfReal }; u, pfReal v, pfRVec3 &pnt ); u, pfReal v, pfRVec3 &norm ); Main Features of the Methods in pfCylinderSurface pfCylinderSurface( radius, height ) constructs a cylinder with the specified height and radius. By default, the z axis is the cylinder’s axis and the cylinder is centered on the origin, extending in the positive and negative z directions for one-half the height. For the default orientation, u measures the angle from the x-z plane in a counterclockwise direction as you look down on the x-y plane and v measures the distance along the z-axis. The default radius is 1 and the default height is 2. 390 007-1680-100 Parametric Surfaces pfTorusSurface The pfTorusSurface class provides methods to describe a torus. Figure 9-13 illustrates a torus, and how it is parameterized in pfTorusSurface. A torus can be defined geometrically as the surface in space that is swept by moving a circle, the minor circle, through space such that its center lies on a second circle, the major circle, and the planes of the two circles are always perpendicular to each other, with the plane of the minor circle aligned along radii of the major circle. The parametrization of the surface is that u represents a position on the major circle and v represents a position on the minor circle. z Origin pnt u y v x Major radius Figure 9-13 Minor radius Torus Parameterization Class Declaration for pfTorusSurface The class has the following main methods: class pfTorusSurface : public pfParaSurface { public: // Creating and destroying 007-1680-100 391 9: Higher-Order Geometric Primitives pfTorusSurface( ); pfTorusSurface( pfReal majorRadius, pfReal minorRadius ); virtual ~pfTorusSurface(); // Accessor functions void setMajorRadius( pfReal majorRadiusVal ); void setMinorRadius( pfReal minorRadiusVal ); pfReal getMajorRadius( ) const; pfReal getMinorRadius( ) const; // Evaluators virtual void evalPt( pfReal virtual void evalNorm( pfReal } u, pfReal v, pfRVec3 &pnt ); u, pfReal v, pfRVec3 &norm ); Main Features of the Methods in pfTorusSurface The constructor pfTorusSurface( majorRadius, minorRadius ) defines a torus with the specified radii such that the major circle is in the x-y plane and the minor circle is initially in the x-z plane. The default value for the major radius is 1; the default for the minor radius is 0.1. 392 007-1680-100 Parametric Surfaces pfConeSurface You can define a cone geometrically by sweeping a circle along an axis in a way similar to the way a cylinder is defined; however, as the circle is swept along the axis, the radius changes linearly with distance. The parameterization of a point on an pfConeSurface is that u measures the angle, in radians, of the point on the circle, and that v measures the distance along the axis from the origin. To truncate a cone, yielding a frustum, adjust the value for v. z y pnt Origin v Height u Half-height Radius Figure 9-14 007-1680-100 x Cone Parameterization 393 9: Higher-Order Geometric Primitives Class Declaration for pfConeSurface The class has the following main methods: class pfConeSurface : public pfParaSurface { public: // Creating and destroying pfConeSurface( void ); pfConeSurface( pfReal radius, pfReal height ); virtual ~pfConeSurface(); // Accessor functions void setRadius( pfReal radius ) ; void setHeight( pfReal height ); pfReal getRadius( ) const; pfReal getHeight( ) const; // Evaluators void evalPt( pfReal void evalNorm( pfReal } u, pfReal v, pfRVec3 &pnt ); u, pfReal v, pfRVec3 &norm ); Main Features of the Methods in pfConeSurface The constructor pfConeSurface( radius, height ) creates a parametric cone with the specified height and a circular base with the specified radius. By default, the base of the cone is parallel to the x-y plane and centered on the z axis and the apex of the cone is on the positive z-axis. The cone extends from the origin in the positive and negative z directions for one half the height. The default for the radius of the base is 1 and the default height is 2. 394 007-1680-100 Parametric Surfaces Swept Surfaces The class pfSweptSurface provides methods to describe a general swept surface. Three examples of swept surfaces have been presented: a cylinder, a torus, and a cone. In the first two cases a simple cross-section, a circle of constant radius, was swept along a path. For a cone, the radius of the circle varied according to a simple profile. To describe a swept surface, you specify a path, a cross section, and a coordinate frame in which the graph of the cross section is drawn at each point on the path. The parameterization of the surface is that u denotes the position along the path and v denotes the position on the cross-section curve. You can also specify a profile, which adjusts the size of the cross-section curve. Thus, for example, with a simple profile method you could generate a sphere from a straight-line path and a circular cross section. Figure 9-15 illustrates the feature of a swept surface. 007-1680-100 395 9: Higher-Order Geometric Primitives Cross section y x b P at h t Figure 9-15 396 Swept Surface: Moving Reference Frame and Effect of Profile Function 007-1680-100 Parametric Surfaces Orientation of the Cross Section Unlike the examples of the cylinder, torus, and cone, the cross-section in an pfSweptSurface generally is not necessarily perpendicular to the path. You set the orientation of the cross-section with two additional instances of pfCurve3d. For a point on the path corresponding the parameter value t0, the vectors on these two additional curves that have the same parameter value define the local coordinate system used to draw the profile: one vector defines the normal to the plane of the graph, the second the x axis for the graph, and their cross product determines the direction of the y axis for the graph. For more details, see the discussion of the constructor below. Class Declaration for pfSweptSurface The class has the following main methods: class pfSweptSurface : public pfParaSurface { public: // Creating and destroying pfSweptSurface( void ); pfSweptSurface( pfCurve3d *crossSection, pfCurve3d *_path, pfCurve3d *_t, pfCurve3d *_b, pfScalar *_profile ); virtual ~pfSweptSurface( ); // Accessor functions void setCrossSection( pfCurve3d *_crossSection ); void setPath( pfCurve3d *_path ); void setT( pfCurve3d *_tng ); void setB( pfCurve3d *_b ); void setProf( pfScalar *_profile ); pfCurve3d *getCrossSection() const; pfCurve3d *getPath() const; pfCurve3d *getT() const; pfCurve3d *getB() const; pfScalar *getProf()const; virtual void evalPt( pfReal u, pfReal v, pfRVec3 &pnt ); }; 007-1680-100 397 9: Higher-Order Geometric Primitives Main Features of the Methods in pfSweptSurface pfSweptSurface( crossSection, path, t, b, profile ) Defines a swept surface with the given path, cross section, and profile. The arguments t and b are vector-valued functions of the path’s parameter. They define the orientation of the profile at each point on the path. The orientation at a particular point on the curve is determined by rendering the graph of crossSection in the coordinate plane perpendicular to t, which locally defines the z axis of an x-y-z coordinate system. The x axis is defined by the projection of b onto the plane, and the y axis forms a right-hand coordinate system with the other two axes. The cross section is plotted in the x-y plane. If you specify a NULL value for profile, crossSection does not vary along path. evalPt( u, v, pnt ) Calculates the point on the surface, pnt, as the vector sum of (a) the point on the path corresponding to the value u and (b) the point on the cross section corresponding to the value v. The vector locating the point on the cross section is scaled by the value at u of the profile function, if profile is not NULL. 398 007-1680-100 Parametric Surfaces pfFrenetSweptSurface As a convenience, the class pfFrenetSweptSurface allows you to use the Frenet frame of the path to define the orientation vectors in a swept surface. The Frenet frame is defined by the three unit vectors derived from the tangent, the principal normal, and their cross product. This set of vectors facilitates orienting the cross section perpendicularly to the path at every point. Note: The path for an pfFrenetSweptSurface must be at least a cubic to allow for the principal normal calculation, which requires a second derivative. Class Declaration for pfFrenetSweptSurface The class has the following main methods: class pfFrenetSweptSurface : public pfSweptSurface { public: // Accessor functions pfFrenetSweptSurface( void ); pfFrenetSweptSurface( pfCurve3d *crossSection, pfCurve3d *path, pfScalar *profile ); virtual ~pfFrenetSweptSurface( ); // Accessor functions void set( pfCurve3d *crossSection, pfCurve3d *path, pfScalar *profile ); }; Main Features of the Methods in pfFrenetSweptSurface The arguments of the constructor for pfFrenetSweptSurface are the same as for pfSweptSurface and have the same effects, except for the orientation vectors, which are set to be the tangent and principal normal to path, and so do not appear as arguments. Use the inherited method evalPt() to locate points on the surface. 007-1680-100 399 9: Higher-Order Geometric Primitives Making a Modulated Torus With pfFrenetSweptSurface The following code uses an pfFrenetSweptSurface to define a torus whose minor radius varies with position on the ring. Other instances of pfFrenetSweptSurface appear in repTest. // Scalar curve used by the swept surface primitive static pfReal profile( pfReal t ) { return 0.5*cos(t*5.0) + 1.25; }; pfCircle3d *cross = new pfCircle3d( 0.75, new pfRVec3( 0.0, 0.0, 0.0) ); pfCircle3d *path = new pfCircle3d( 1.75, new pfRVec3( 0.0, 0.0, 0.0) ); pfFrenetSweptSurface *fswept = new pfFrenetSweptSurface( cross, path, profile ); fswept->setHandednessHint( TRUE ); Ruled Surfaces A ruled surface is generated from two curves in space, both parameterized by the same variable, u. A particular value of u specifies a point on both curves. A ruled surface is defined by connecting the two points with a straight line parameterized by v. The parameterization of the resulting surface is always the unit square in the UV plane, regardless of the parameterizations of the original curves. 400 007-1680-100 Parametric Surfaces c2(u) c1(u) (1-v)c1(u) + v c2(u) Figure 9-16 Ruled Surface Parameterization A bilinear interpolation of four points is perhaps the simplest example of a ruled surface, one for which the “curves” that define the surface are in fact straight lines. Thus, you connect two pairs of points in space with lines and then develop the ruled surface. For a bilinear interpolation, the parameterization by u and v is such that, if one of them is held constant, a point “moves” along the connecting straight line at a uniform speed as the other parameter is varied. Class Declaration for pfRuledSurface The class has the following main methods: class pfRuledSurface : public pfParaSurface { public: // Creating and destroying pfRuledSurface(); pfRuledSurface( pfCurve3d *c1, pfCurve3d *c2 ); virtual ~pfRuledSurface(); // Accessor functions void setCurve1( pfCurve3d *_c1 ); void setCurve2( pfCurve3d *_c2 ); pfCurve3d *getCurve1( ) const; pfCurve3d *getCurve2( ) const; // Evaluators void evalPt( }; 007-1680-100 pfReal u, pfReal v, pfRVec3 &pnt ); 401 9: Higher-Order Geometric Primitives The constructor pfRuledSurface( c1, c2 ) creates an instance of a ruled surface defined by the two curves c1 and c2. Coons Patches A Coons patch is arguably the simplest surface you can define from four curves whose endpoints match and form a closed loop. Think of the four curves as defining the four sides of the patch, with one pair on opposite sides of the patch defining the top and bottom curves and the other pair defining the left and right curves (see Figure 9-17). The top and bottom curves are parameterized by u, and the left and right curves by v. Thus, u is the “horizontal” coordinate and v the “vertical” coordinate. The patch is made by 1. Adding the points on the ruled surface defined by the top and bottom curves to the points on the ruled surface defined by the left and right curves. 2. Subtracting the bilinear interpolation of the four corner points. Figure 9-17 illustrates the construction. To understand the result, notice that, after you add the two ruled surfaces, each side of the boundary of the resulting surface is the sum of the original bounding curve and the straight line connecting the bounding curve’s endpoints. The straight line was introduced by the construction of the ruled surface that did not include the boundary curve. Subtracting the bilinear interpolation eliminates the straight-line components of the sum, leaving just the original four curves as the boundary of the resulting surface. 402 007-1680-100 Parametric Surfaces Top & bottom curves Left & right curves z z z4 z4 z3 z1 z3 z1 y z2 y z2 x x Ruled surfaces z z z4 z4 z3 z1 z3 z1 y z2 y x z2 x 2z4 Sum 2z3 z 2z1 z4 Subtract a bilinear interpolation from the sum of the ruled surfaces 2z2 z3 z1 y z2 Bilinear interpolation z z4 x z3 z1 y z2 Coons patch,bounded by left, right, top & bottom curves x Figure 9-17 007-1680-100 Coons Patch Construction 403 9: Higher-Order Geometric Primitives Class Declaration for pfCoonsSurface The class has the following main methods: class pfCoonsSurface : public pfParaSurface { public: pfCoonsSurface( ); pfCoonsSurface( pfCurve3d *right, pfCurve3d *left, pfCurve3d *bottom, pfCurve3d *top ); virtual ~pfCoonsSurface( ); // Accessor functions void setRight( pfCurve3d *right ); void setLeft( pfCurve3d *left ); void setBottom( pfCurve3d *bottom ); void setTop( pfCurve3d *top ); pfCurve3d* pfCurve3d* pfCurve3d* pfCurve3d* getTop() const; getBottom() const; getLeft() const; getRight() const; // Surface point evaluator void evalPt( pfReal u, pfReal v, pfRVec3 &pnt ); }; The constructor pfCoonsSurface( right, left, bottom, top ) creates an instance of a Coons patch defined by the four curves right, left, bottom, and top. The top and bottom curves are parameterized by u and the left and right curves are parameterized by v. NURBS Surfaces Just as a NURBS curve consists of Bezier curves, a NURBS surface consists of Bezier surfaces. The set of control parameters is essentially the same for the curves and surfaces: a set of knots, a control hull, and a set of weights. However, for a NURBS surface, the knots form a grid in the coordinate system of the surface; that is, in the u-v plane, and the control hull is a grid of points in space that loosely defines the surface. Understanding a Bezier surface helps you understand and use a NURBS surface. A Bezier surface is defined essentially as the surface formed by sweeping a Bezier cross 404 007-1680-100 Parametric Surfaces section curve through space, along a path defined by a Bezier curve. But, unlike an pfSweptSurface, the shape of the cross-section can be changed. You define a Bezier surface as follows: 1. Start with a Bezier curve in space: the cross section parameterized by u. 2. Define a family of Bezier curves, a set of paths all of which are parameterized by v, that start at the control points of the initial cross section. For each value of v, the set of control points defines a Bezier curve. As v changes, the cross-sectional curve “moves” through space, changing shape and defining a Bezier surface. A NURBS surface joins Bezier surfaces in a smooth way, similar to NURBS curves joining Bezier curves. The class pfNurbSurface provides methods to describe a NURBS surface. Class Declaration for pfNurbSurface The class has the following main methods: class pfNurbSurface : public pfParaSurface { public: // Creating and destroying pfNurbSurface( void ); virtual ~pfNurbSurface( ); // Accessor functions void setControlHull( int iu, int iv, const pfRVec3 &p ); void setControlHull( int iu, int iv, const pfRVec4 &p ); void setWeight( int iu, int iv, pfReal w ); void setUknot( int iu, pfReal u ); void setVknot( int iv, pfReal v ); void setControlHullUSize( int s ); void setControlHullVSize( int s ); // Get the same parameters pfRVec3& getControlHull( int iu, int iv) ; int getControlHullUSize( void ); int getControlHullVSize( void ); pfReal getWeight( int iu, int iv) pfReal getUknot( int iu); pfReal getVknot( int iv); int getUknotCount( void ); 007-1680-100 405 9: Higher-Order Geometric Primitives int int int void void void void getVknotCount( void ); getUorder( void ) ; getVorder( void ) ; removeControlHullElm(int ui, int iv); removeUknot(int iu); removeVknow(int iv); flipUV(); // Evaluator virtual void virtual void virtual void virtual void }; evalPt( evalDu( evalDv( evalNorm( pfReal pfReal pfReal pfReal u, u, u, u, pfReal pfReal pfReal pfReal v, v, v, v, pfRVec3 pfRVec3 pfRVec3 pfRVec3 &pnt ); &Du ); &Du ); &norm ); Main Features of the Methods in pfNurbSurface The member functions are essentially the same as those for pfNurbCurve3d (see “NURBS Curves in Space” on page 373), however: • The hull is a grid of pfRVec3s indexed by i and j. • The set of knots is defined by points on the u and v axes. • There are B-spline basis functions (of possibly differing orders) associated with each coordinate direction. Note: pfNurbSurface redefines the virtual evaluators inherited from pfParaSurface for tangent and normal vectors; the methods use the NURBS equation rather than finite, central differences. Indexing Knot Points and the Control Hull Indexing of knot points in coordinate space and control hull points in three-dimensional space is illustrated in Figure 9-18. The indexing works as for gluNurbsSurface, that is, as follows: 406 • iu indexes knots on the u axis. The correspondence is established by setUknot(). • iv indexes knots on the v axis.The correspondence is established by setVknot(). • Each (iu,iv) thus indexes a knot point in the u-v plane. 007-1680-100 Parametric Surfaces setControlHull (iu, iv, p) z (3,2) (0,0) y x v setUknot (iu, u) setVknot ( iv, v) v4 v3 iv = 0,1,2,3... (3,2) v2 v1 v0 (0,0) u u0 u 1 u2 u3 u4 u5 u6 iu = 0,1,2,3... • Each (iu,iv) also indexes a point on the control hull in three-dimensional space. The correspondence is established by setControlHull(). • Thus, setUknot(), setVknot(), and setControlHull() establish a correspondence between an index pair (iu,iv) a knot point (uiu viv), and a point on the control hull in three-dimensional space. Figure 9-18 007-1680-100 NURBS Surface Control Hull Parameterization 407 9: Higher-Order Geometric Primitives Equation Used to Calculate a NURBS Surface Indexing is determined by the following equation that OpenGL Performer uses to calculate a NURBS surface (the index i corresponds to iu in the API, and j corresponds to iv): ∑ B (u)B (v)C p(u, v) = ---------------------------------------------∑ B (u)B (v)W m i n j ij m i n j ij i, j i, j where • p(u, v) is a point on the surface • B im(u) is the ith B-spline basis polynomial of degree m • C ij is a control point • W ij is the weight for the control point Alternative Equation for a NURBS Surface A NURBS surface can also be developed from the following alternative expression: ∑ B (u)B (v)W C p(u, v) = ----------------------------------------------------∑ B (u)B (v)W m i n j ij ij i, j m i n j ij i, j For this case, you must change the coordinates of the control points to get the same surface from OpenGL Performer. You convert the coordinates of the control points from (x,y,z,w) to (wx,wy,wz,w). 408 007-1680-100 Parametric Surfaces Sample of a Trimmed pfNurbSurface From repTest The following code fragment form the repTest sample application illustrates an instance of an pfNurbSurface. Toward the end of the example, an optional pfNurbCurve2d trim curve is created. int i, j; pfNurbSurface *nurb = new pfNurbSurface; // Control hull dimensions #define USIZE 4 #define VSIZE 5 // Set up the control hull size because we know a priori how big // the nurb is. The next two lines are used for space // efficiency but are functionally unnecessary. nurb->setControlHullUSize(USIZE); nurb->setControlHullVSize(VSIZE); // Make the control hull be an oscillating grid for ( i = 0; i < VSIZE; i++ ) { pfReal y = i/(float)(VSIZE - 1) * 2*M_PI - M_PI; for ( j = 0; j < USIZE; j++ ) { pfReal x = j/(float)(USIZE - 1) * 2*M_PI - M_PI; pfReal val = 6*pow( cos(sqrt(x*x + y*y)), 2.0); // Make the control hull a box, j maps to u and i maps to v nurb->setControlHull( i, j, pfRVec3( x, y, val)); // Add the weights nurb->setWeight( i, j, 1.0 ); } } // Add the knot nurb->setUknot( nurb->setUknot( nurb->setUknot( nurb->setUknot( nurb->setUknot( nurb->setUknot( 007-1680-100 points 0, 0.0 1, 0.0 2, 0.0 3, 0.0 4, 1.0 5, 1.0 ); ); ); ); ); ); 409 9: Higher-Order Geometric Primitives nurb->setUknot( 6, nurb->setUknot( 7, 1.0 ); 1.0 ); nurb->setVknot( nurb->setVknot( nurb->setVknot( nurb->setVknot( nurb->setVknot( nurb->setVknot( nurb->setVknot( nurb->setVknot( 0.0 0.0 0.0 0.0 1.0 1.0 1.0 1.0 0, 1, 2, 3, 4, 5, 6, 7, ); ); ); ); ); ); ); ); // Only trim reps in the first row if ( nVersions <= 0 ) { // Add a super quadric trim curve pfSuperQuadCurve2d *trimCircle0 = new pfSuperQuadCurve2d( 0.25, new pfRVec2(0.25, 0.50), 2.0 ); nurb->addTrimCurve( 0, trimCircle0, NULL ); // make a 4-th order nurb trim curve pfNurbCurve2d *l = new pfNurbCurve2d; l->setKnot(0,0.0); l->setKnot(1,0.0); l->setKnot(2,0.0); l->setKnot(3,0.0); l->setKnot(4,1.0); l->setKnot(5,1.0); l->setKnot(6,1.0); l->setKnot(7,1.0); l->setControlHull(0,pfRVec2(0.50,0.50)); l->setControlHull(1,pfRVec2(0.90,0.10)); l->setControlHull(2,pfRVec2(0.90,0.90)); l->setControlHull(3,pfRVec2(0.50,0.50)); nurb->addTrimCurve( 1, l, NULL } 410 ); 007-1680-100 Parametric Surfaces Hermite-Spline Surfaces Hermite-spline surfaces interpolate a grid of points; that is, they pass through the set of specified points under the constraint that you supply the tangents at each point in the u and v directions and the mixed partial derivative at each point. This surface definition is the natural generalization of Hermite-spline curves, discussed in “Hermite-Spline Curves in the Plane” on page 359. tuv tv tuv tv tu tuv tuv tv tv tu tuv tu tv tv tuv tu tv tuv tv tu tu tuv tu tuv tv tu tu Figure 9-19 Hermite Spline Surface With Derivatives Specified at Knot Points Hermite-spline surfaces are made of Hermite patches (see Figure 9-19). A bicubic Hermite patch expands the definition of a bilinear interpolation to include specification of first derivatives and mixed partial derivatives of the surface at each of the four corners. The adjective “bicubic” in the name of the patches refers to the mathematical definition, which includes products of the cubic Hermite polynomials that define a Hermite-spline curve. An advantage of including the derivatives to constrain the surface is that it is simple to combine the patches into a smooth composite surface, that is, into a Hermite-spline surface. 007-1680-100 411 9: Higher-Order Geometric Primitives Class Declaration for pfHsplineSurface The class has the following main methods: class pfHsplineSurface : public pfParaSurface { public: // Creating and destroying pfHsplineSurface(); pfHsplineSurface( pfReal *_p, pfReal *_tu, pfReal *_tv, pfReal *_tuv, pfReal *_uu, pfReal *_vv, int uKnotCount, int vKnotCount ); virtual ~pfHsplineSurface(); // Accessor functions pfRVec3& getP( int i, int j ); pfRVec3& getTu( int i, int j ); pfRVec3& getTv( int i, int j ); pfRVec3& getTuv( int i, int j ); pfReal getUknot( int i ); pfReal getVknot( int j ); int getUknotCount(); int getVknotCount(); pfBool getCylindrical(); void setAll( pfReal *p, pfReal *tu, pfReal *tv, pfReal *tuv, pfReal *uu, pfReal *vv, int uKnotCount, int vKnotCount ); void setCylindrical( pfBool cylinderical ); // Surface point evaluator void evalPt( pfReal u, pfReal v, pfRVec3 &pnt ); }; Main Features of the Methods in pfHsplineSurface The pfHsplineSurface class has two important methods, the constructor and set/getCylinderical(). 412 007-1680-100 Meshes The pfHsplineSurface constructor has the following arguments: _p Specifies the grid of points on the surface. _tu, _tv, and _tuv Specify, respectively, the corresponding tangents in the u and v directions and the mixed partials. The indexing of each of the arrays _p, _tu, _tv, and _tuv is as follows: the x, y, and z components of each vector are grouped in that order, and the sequence of points is defined so that the vKnotCount index changes more rapidly. uKnotCount and vKnotCount Specify the number of points in the grid. The surface is made of (uKnotCount-1) × (vKnotCount-1) Hermite patches. _uu and _vv Define the knot points, the parameter values corresponding to the patch corners; thus, they have uKnotCount and vKnotCount elements, respectively. setCylindrical() and getCylindrical() Control the flag for whether the coordinates and derivatives are assumed to be in cylindrical coordinates. Meshes A mesh, encapsulated in OpenGL Performer by the high-level class pfMesh, is used to store information about the topology of an object. A pfMesh is derived from a pfObject for memory allocation purposes. A pfMesh stores the object as a collection of vertices and flat faces (a vertex being encapsulated as a pfMeshVertex and a face as a pfMeshFace). It is possible to query the mesh to determine the information about neighbors of a given vertex or a face. This information is required by various algorithms—for example, for evaluating subdivision surfaces or for simplification of objects. You can build a pfMesh for a given object in several ways: 007-1680-100 • You can use the function pfdAddNodeToMesh() that parses a given node and adds all its geometry to the mesh. • You can write your own traverser and for each pfGeoSet you can call the function pfMeshAddGeoSet(). 413 9: Higher-Order Geometric Primitives • You can call the function pfMeshAddTriangle() or pfMeshAddFace() and add each face separately. • Where you would like to have full control over the topology stored in the pfMesh, you can directly set the array of faces and vertices. To do that, you can create the corresponding arrays by calling pfMeshNumFaces() and pfMeshNumVertices() and then set each face and vertex separately. You can get the pointer to the array item by calling the function pfGetMeshVertex() and pfGetMeshFace(), respectively. When using the function pfMeshAddGeoSet(), you can specify the pfGeoSet, the number of a mesh part, and the current transformation. The information about parts can be used to mark edges. Each edge is automatically marked with the following flags: Flag Description PFM_EDGE_NORMAL An edge with two adjacent faces +PFM_EDGE_BOUNDARY An edge with one adjacent face to the left of the edge –PFM_EDGE_BOUNDARY An edge with one adjacent face to the right of the edge PFM_EDGE_SPLIT An edge with more than one adjacent face (in the case of non-manifold surfaces). You can mark edges between different parts as PFM_EDGE_CREASE if the flag PFM_FLAG_CREASES_BETWEEN_PARTS, described later in this section, is set. Note that edges between faces with different pfGeoStates are also usually marked as creases. The part index and the transformation is also provided to functions pfMeshAddTriangle() and pfMeshAddFace(). In addition, you also need to set the pfGeoState of the triangle or face. When setting vertices and faces directly and accessing them through a pointer, you need to set all vertex and face parameters. See the later sections “Mesh Faces” on page 416 and “Mesh Vertices” on page 417. You must call the function pfMeshSplitVertices() after all vertices are set, regardless of the method used to build the mesh. This function performs extra post-processing that cleans the mesh in cases where the object is not a manifold (see section “Mesh Vertices” on page 417 for more details). 414 007-1680-100 Meshes You can set the following flags to 0 or 1: 007-1680-100 Flag Description PFM_FLAG_TRIANGULATE If this flag is set, quads and polygons processed by pfMeshAddGeoSet() are triangulated. This flag can be used for Loop subdivision. It is off by default. PFM_FLAG_TEST_ORIENTATION If this flag is set, the normal at the first vertex of a face is compared with the face normal. If they point in the opposite directions, the order of vertices is is changed. This flag is off by default. PFM_FLAG_CREASES_BETWEEN_PARTS If this flag is set, it automatically marks edges between different parts as creases. It is on by default. PFM_FLAG_CREASES_BETWEEN_GEOSTATES If this flag is set, it automatically marks edges between pfGeoSets with different pfGeoStates. It is on by default. PFM_FLAG_QUAD_TSTRIPS If this flag is set, triangle strips processed by pfMeshAddGeoSet() are converted to quads even if the quads are not planar. It is off by default. PFM_FLAG_USE_VERTEX_GRID If this flag is set, a grid is used when adding faces and checking for existing vertices. You must set the bounding sphere of the mesh to use the grid established by pfMeshGridBsphere(). It is off by default. 415 9: Higher-Order Geometric Primitives You can set the following values: Value Description PFM_EPSILON Sets the epsilon to be used when comparing vertices. The default is 1e-6 or if the grid is defined, 1e-6 multiplied by a ratio of the grid diameter to the grid resolution along X axis. PFM_VERTEX_GRID_SIZE_X See the description for flag PFM_VERTEX_GRID_SIZE. PFM_VERTEX_GRID_SIZE_Y See the description for flag PFM_VERTEX_GRID_SIZE. PFM_VERTEX_GRID_SIZE_Z See the description for flag PFM_VERTEX_GRID_SIZE. PFM_VERTEX_GRID_SIZE Sets the resolution of the grid used for comparing vertices. You can set the grid size one value at the time or all three coordinates at once. The default grid size is 32x32x32. You can also query the bounding box of the mesh using the function pfGetMeshBbox(). This bounding box is computed automatically around the mesh vertices. You can use the function pfMeshUpdateMesh() to automatically detect which vertices have been changed and to mark them and the corresponding faces. This can be used in case of dynamically controlled meshes (see the section “Subdivision Surfaces” on page 420 for an example). Since each vertex stores both the pointer to the original vertex location and the vertex coordinates, any changes can be easily detected by comparing these values. Mesh Faces OpenGL Performer uses mesh faces and its vertices to describe a mesh, which in turn describes the topology of an object. The high-level class pfMeshFace encapulsates a face. Each face contains the following: • 416 An array of face vertices The class pfMeshVertex represents a vertex. You can use the functions pfMeshFaceNumVerts() and pfMeshFaceVertex() to set face vertices. The parameters of the function pfMeshFaceVertex() are the vertex index in the array 007-1680-100 Meshes stored with the face (which vertex you want to set) and the vertex index in the array of vertices associated with a pfMesh. The latter index identifies the vertex. You can use the functions pfGetMeshFaceNumVerts() and pfGetMeshFaceVertex() to query face vertices. • A set of texture coordinates You can use the function pfMeshFaceTexCoord() to specify texture coordinates as vertices. • A pfGeoSet • A part index The part index is used by the pfMesh to determine which edges are marked as smooth and which, as creases (see the preceding section “Meshes” on page 413) • Flags There is only one flag used by a pfMeshFace: PFMF_FLAG_FACE_CHANGED. You set this flag with the function pfMeshUpdateMesh() to indicate that a vertex of the face has changed position. Mesh Vertices A pfMeshVertex is a high-level OpenGL Performer class used in a pfMesh to store information about vertices. A pfMeshVertex stores the following: • Vertex coordinates • The pointer to the original vertex coordinates (to be able to detect local changes in the mesh) • An array of vertex neighbors • A set of binary flags • An index of another vertex at the same location (used for non-manifolds,as described later in the subsection “Vertex Neighbors” on page 418) This section describes these items in the following subsections: 007-1680-100 • “Vertex Coordinates” • “Vertex Neighbors” • “Binary Flags” 417 9: Higher-Order Geometric Primitives Vertex Coordinates You can use the functions pfMeshVertexCoord() and pfMeshVertexCoordPtr() to set the vertex coordinates and the pointer to the original coordinates. The value set by pfMeshVertexCoord() should correspond to the value at the location set by pfMeshVertexCoordPtr(). If the specified pointer is NULL (for example, where a pfGeoSet was under a pfSCS node), it is not possible to automatically detect changes in position of vertices and you must modify all vertices manually by calling the function pfMeshVertexCoord(). If you do not plan to animate the mesh, you can set the pointer to NULL. Vertex Neighbors Each vertex stores an array of its neighbors. You can query the neighbors using the functions pfMeshVertexNumNeighbors() and pfMeshVertexNeighbor(). The structure of pfMeshVertexNeighbor() is defined in pf.h as follows: typedef struct { int vertex; int face; short int edgeType; short int next, prev; } pfMeshVertexNeighbor; This structure consists of the vertex index (in the array of vertices in pfMesh), face index (in the array of faces in pfMesh), edge type, and index of the next and previous neighbor in the array of neighbors. 418 007-1680-100 Meshes The edge can be one of the following types: Edge Type Description PFM_EDGE_NORMAL Specifies a smooth edge with two adjacent faces. PFM_EDGE_CREASE Specifies a sharp edge with two adjacent faces. +PFM_EDGE_BOUNDARY Specifies an edge with one adjacent face to the left of the edge. –PFM_EDGE_BOUNDARY Specifies an edge with one adjacent face to the right of the edge. The pertinent face is the face to the left of the edge (unless the edge is marked as –PFM_EDGE_BOUNDARY). The next neighbor is part of this face and the previous neighbor is part of the face to the right. If the edge is of type –PFM_EDGE_BOUNDARY, the next neighbor points to the corresponding edge of type +PFM_EDGE_BOUNDARY and the previous neighbors of the edge of type +PFM_EDGE_BOUNDARY point to the the corresponding edge of type +PFM_EDGE_BOUNDARY. Note that in the case of manifolds each vertex has exactly zero or two boundary edges. In the case of arbitrary surfaces, there may be more than one loop of neighbors (if you follow the next links). That is why the class pfMesh provides the function pfMeshSplitVertices(), which splits each vertex where the surface behaves as non-manifold. The vertex is split into several vertices, each having a single loop of ordered neighbors and the same position. You can access the next and previous neighbor for a given neighbor with vertex indexed v1 by calling the functions pfPreviousNeighborMeshVertex() and pfNextNeighborMeshVertex(). You can also determine the index of such a neighbor in the array of neighbors by calling pfPreviousNeighborIndexMeshVertex() and pfNextNeighborIndexMeshVertex(). 007-1680-100 419 9: Higher-Order Geometric Primitives Binary Flags The following flags can be set for each pfMeshVertex: Flag Description PFMV_FLAG_VERTEX_CHANGED Set by the function pfMeshUpdateMesh() when the coordinate stored at the vertex does not match the value at the coordinate pointer. PFMV_FLAG_VERTEX_NEIGHBOR_CHANGED Set by the function pfMeshUpdateMesh() when the position of any neighbor changes. PFMV_FLAG_VERTEX_FACE_CHANGED Set by the function pfMeshUpdateMesh() when the position of any vertex on any of the faces associated with this vertex changes. PFMV_FLAG_VERTEX_SPLIT Set by the function pfMeshSplitVertices() when the surface around the vertex is not manifold and the vertex is split into several vertices (see the preceding section “Vertex Neighbors” on page 418). Subdivision Surfaces A subdivision surface is specified by a control mesh consisting of a set of connected faces (usually triangles or quads). The control mesh is stored as a pfMesh. The OpenGL Performer class pfSubdivSurface is used to define a subdivision surface. Before rendering a subdivision surface, each face is recursively subdivided into a finer mesh. For example, in the case of Loop subdivision, each triangle is subdivided into four new triangles at each subdivision step. The positions of both original vertices and newly introduced vertices are determined by the subdivision rules. Usually the position is a linear combination of the positions of the original vertices of the face and the vertices of neighboring faces. The parameters of the linear combination are set in a such a way that, after a few subdivision steps, the resulting mesh smooths sharp edges. For example, if the control mesh is a tetrahedron after a few subdivision steps, you get an oval shape. Optionally, it is possible to mark selected edges not to be smooth. An excellent source of 420 007-1680-100 Subdivision Surfaces information on subdivision surfaces is the SIGGRAPH 2000 course "Subdivision for Modeling and Animation" (http://mrl.nyu.edu/publications/subdiv-course2000/). The remainder of this section describes the following topics: • “Creating a Subdivision Surface” • “Loop and Catmull-Clark Subdivisions” • “Dynamic Modification of Vertices” • “The libpfsubdiv Pseudo Loader” • “Special Notes” Creating a Subdivision Surface The function pfSubdivSurface() creates and returns a handle to a pfSubdivSurface. Like other pfNodes, pfSubdivSurfaces are always allocated from shared memory and cannot be created statically on the stack or in arrays. Use the function pfDelete() rather than the delete operator to delete pfSubdivSurfaces. To define a subdivision surface, you must specify its control mesh using the function pfSubdivSurfaceMesh(). The mesh is stored as a pfMesh. A pfMesh is a data structure that stores the connectivity between individual faces of the mesh. For each vertex and face, you can access neighboring vertices and faces, respectively. See the preceding section “Meshes” on page 413 for more details. To create a pfMesh, you can either specify its faces and the connectivity one by one or use the function pfdAddNodeToMesh(). This function takes a pfNode, parses all its pfGeoSets, splits all primitives into planar faces, and adds those faces into the specified pfMesh (see the pfdAddNodeToMesh man page). Before you set the mesh using pfSubdivSurfaceMesh(), you need to set various parameters of the subdivision because different internal data structures may be needed for different types of subdivision. You use the function pfSubdivSurfaceVal() to set the values and the function pfSubdivSurfaceFlags() to set the flags. 007-1680-100 421 9: Higher-Order Geometric Primitives You can set the following values: Parameter Value Description PFSB_SUBDIVISION_METHOD Can be either PFSB_CATMULL_CLARK (Catmull-Clark subdivision, the default) or PFSB_LOOP (Loop subdivision). PFSB_SUBDIVISION_LEVEL Specifies the number of subdivision steps. Usually two or three steps are quite reasonable. The default is 0. PFSB_MAX_DATA_HEIGHT_SW Specifies the limit of data structures used to evaluate the subdivision surface in software. The default is unlimited. However, if you are running out of memory for large models evaluated in software, try a value like 10,000. This would mean that only 10,000 faces would be evaluated at a time. PFSB_MAX_PBUFFER_HEIGHT Specifies the maximum height of a pbuffer (off-screen memory on a GPU) used for evaluating the surface directly on the graphics hardware (GPU). The default is 512. It should be a power of two. You can set the following flags to 0 or 1: Flag Description PFSB_GPU_SUBDIVISION Allows direct evaluation of the surface on the graphics hardware (GPU) if set to 1. The hardware must support floating point fragment shaders (for example, Onyx4 or Prism systems). The default is 0. PFSB_CONTROL_VERTICES_DYNAMICAllows the modification of the vertices of the control surface (only their position) on the fly. See the later subsection “Dynamic Modification of Vertices” on page 424 for more information. The default is 0. PFSB_USE_GEO_ARRAYS 422 Stores the resulting subdivided mesh (in the case of software evaluation) in pfGeoArrays. Otherwise, regular pfGeoSets are used. The default is 1. 007-1680-100 Subdivision Surfaces The following examples set up a subdivision surface and add it into a scene. Example 1: pfSubdivSurface *subdivSurface; pfMesh *mesh; subdSurf = pfNewSubdivSurface(arena); mesh = pfNewMesh(arena); if(method == PFSB_LOOP) pfMeshFlags(mesh, PFM_FLAG_TRIANGULATE, 1); // important! pfdAddNodeToMesh(root, mesh, 0); // part 0 pfSubdivSurfaceVal(subdSurf, PFSB_SUBDIVISION_LEVEL, subdivLevel); pfSubdivSurfaceVal(subdSurf, PFSB_SUBDIVISION_METHOD, method); pfSubdivSurfaceFlags(subdSurf, PFSB_GPU_SUBDIVISION, GPUsubdivision); Example 2: pfSubdivSurfaceMesh(subdSurf, mesh); pfAddChild(scene, subdSurf); pfSubdivSurface *subdivSurface = new pfSubdivSurface; pfMesh *mesh = new pfMesh; if(method == PFSB_LOOP) mesh->setFlags(PFM_FLAG_TRIANGULATE, 1); // important! pfdAddNodeToMesh(root, mesh, 0); // part 0 subdivSurface->setVal(PFSB_SUBDIVISION_LEVEL, subdivLevel); subdivSurface->setVal(PFSB_SUBDIVISION_METHOD, method); subdivSurface->setFlags(PFSB_GPU_SUBDIVISION, GPUsubdivision); subdivSurface->setMesh(mesh); scene->addChild(subdivSurface); You can find sample code in the file perf/samples/pguide/libpf/C++/subdivSurface.C. or in the file %PFROOT%\Src\pguide\libpf\C++\subdivSurface.cxx on Microsoft Windows. 007-1680-100 423 9: Higher-Order Geometric Primitives Loop and Catmull-Clark Subdivisions The major difference between Loop subdivision and Catmull-Clark subdivision is in the type of faces they process. Loop subdivision operates exclusively on triangles. Therefore, the control mesh has to be triangulated. Since a pfMesh does not know whether it is used in a pfSubdivSurface and what subdivision method is used, you must set the flag PFM_FLAG_TRIANGULATE on the pfMesh before specifying the faces (see the sample code in the preceding section “Creating a Subdivision Surface” on page 421). Catmull-Clark subdivision, on the other hand, operates on quads. If the control mesh contains non-quads, the first subdivision step is performed using special rules so that an arbitrary face is divided into a set of quads. Note that this step cannot be performed on a GPU. This step introduces some discontinuities in the second derivative of the curvature. Consequently, the surface may appear more bumpy around the edges of the original control faces. For this reason, start with a control mesh containing only quads for optimal results. The advantage of Catmull-Clark subdivision is that it is supported by many modeling tools, including Maya. Dynamic Modification of Vertices If the flag PFSB_CONTROL_VERTICES_DYNAMIC is set, you can modify the position of vertices of the control mesh to animate the surface. You need to call the function pfSubdivSurfaceUpdateControlMesh() at each frame to confirm the changes. Note that you cannot have in the node any tranforms specifying the control mesh. The libpfsubdiv Pseudo Loader You can subdivide an arbitrary file loaded into OpenGL Performer by using the libpfsubdiv pseudo loader. It works similarly as the libpftrans or libpfscale pseudo loader. Suppose that you want to subdivide truck.pfb in Perfly. You enter the following command: perfly truck.pfb.0,2,0.subdiv The syntax of the libpfsubdiv loader is as follows: <filename>.<method>,<subdivLevel>,<GPUon>.subdiv 424 007-1680-100 Subdivision Surfaces The value for method is 0 for Catmull-Clark and 1 for Loop subdivision. The subdivLevel value is usually around 2 and GPUon is 1 if the evaluation should be done fully on a GPU. Special Notes Note the following limitations and anomalies regarding the use of subdivision surfaces: 007-1680-100 • Subdivision level 0 may result in incorrect normals. • Currently, the GPU subdivision supports only control meshes with vertices that have less than 11 neighbors. • Current drivers on Onyx4 , Prism, and Microsoft Windows platforms do not support super buffers. Therefore, the GPU evaluation of subdivision surfaces on those systems is too slow. 425 Chapter 10 10. Creating and Maintaining Surface Topology Most objects in a large model are made of many parametric surfaces. The OpenGL Performer classes that describe the connectivity of parametric surfaces—that is, their topology—allow you to “stitch” surfaces together by defining shared boundary curves, and to propagate surface contact information. The main purpose for shared-boundary information is to generate tessellations of adjacent surfaces that are consistent, that is, no cracks develop between any pair of rendered surfaces. Tessellations are discrete approximations of surfaces in terms of renderable geometric primitives, typically triangles (see Chapter 11, “Rendering Higher-Order Primitives: Tessellators”). These topics are covered in this chapter: • “Overview of Topology Tasks” on page 427 • “Summary of Scene Graph Topology: pfTopo” on page 428 • “Collecting Connected Surfaces: pfSolid” on page 437 Overview of Topology Tasks The topology classes provide definitions of boundary curves shared by adjacent parametric surfaces. Discrete versions of these curves are used by tessellators to prevent cracks. A rendered image can have artificial cracks due to the following: • Difficulty sampling enough points on the boundary between two surfaces so that mismatches of the tessellations are imperceptible • Finite-precision mismatches between coordinates of ideally identical points, for example at triple junctions where the edges of three surfaces meet at a point Propagating surface contact information is useful for other tasks, such as • 007-1680-100 Maintaining consistent normal vectors for adjacent surfaces 427 10: Creating and Maintaining Surface Topology • Deforming a surface and consistently deform an adjacent surface • Determining whether an edge of a surface is in fact a shared boundary • Creating a mirror image of a compound surface (you can use topological information to reorient the surface) Summary of Scene Graph Topology: pfTopo The class pfTopo holds data that indicates whether, and how, two pfParaSurfaces are in contact. You can create several pfTopos for a particular scene: for example, one each for subassemblies. A static member of pfTopo lists all the pfTopos that you create. pfTopo maintains lists of surfaces and boundaries (pfBoundarys) that are shared by an arbitrary number of surfaces. Figure 10-1 illustrates how these data structures define relations between pfParaSurfaces. When an edge has been tessellated, the associated pfBoundary holds a discrete version of the curve. This discrete version is needed for consistent tessellations because it specifies one set of boundary vertices for tessellating all the surfaces that share the boundary. The role of pfBoundary in determining a consistent tessellation is illustrated in Figure 10-2. The classes pfTopo and pfBoundary are examples of b-reps, which identify objects in terms of their bounding objects. pfBoundary is also winged data structures, a particular form of b-rep. 428 007-1680-100 Summary of Scene Graph Topology: pfTopo ar pfP p un fBo dary pfP a pfP p raS ge fEd urfa ce pfE ara dg pfE pfB e fac Sur pfE e pf pfB aSu e Edg rfac pfE e dge ta Da ture c u r st oun dary dge 2 trim curves specified by 2 edges make up each of the trim loops in the figure oun dary dge u-v coo te na e rdi spac -z x-y odel e m pac s s ace f sur Figure 10-1 007-1680-100 Topological Relations Maintained by Topology Classes 429 10: Creating and Maintaining Surface Topology p ra fPa Sur pfE urf ace raS a pfP o pfB und fac e dge ary e a r at tu D ruc st dge pfE Trim curves te na di or co v e u- ac sp z y- el x- od e s m ac ce sp rfa su n nt e tio te et cr e ta is is c n s D rfa ese con n) su pr e tio re dg el a (E ss te Figure 10-2 430 Consistently Tessellated Adjacent Surfaces and Related Objects 007-1680-100 Summary of Scene Graph Topology: pfTopo Building Topology: Computing and Using Connectivity Information Given a set of pfParaSurfaces in a scene graph, there are several ways to develop a set of shared vertices to be held in pfBoundarys. The following sections describe the topology construction strategies (beyond the low-fidelity alternative of ignoring topology): • “Building Topology Incrementally: A Single-Traversal Build” on page 431 • “Building Topology From All Scene Graph Surfaces: A Two-Traversal Build” on page 432 • “Building Topology From a List of Surfaces” on page 432 • “Building Topology “by Hand”: Imported Surfaces” on page 432 • “Summary of Topology Building Strategies” on page 433 Building Topology Incrementally: A Single-Traversal Build As each surface is tessellated during a traversal, the tessellator checks for previously tessellated adjacent surfaces, uses existing vertices when it can, and adds necessary data to topology data structures. Although OpenGL Performer’s incremental topology building tools attempt to avoid cracks, they can, in principle, appear: When a surface is added, a new junction on the boundary of an existing, tessellated surface may occur and the junction point may not be in the existing tessellation. The tessellation of the added surface introduces the junction point, necessarily at a finite distance from the existing tessellation, and a crack appears between the newly and previously tessellated surfaces. 007-1680-100 431 10: Creating and Maintaining Surface Topology Building Topology From All Scene Graph Surfaces: A Two-Traversal Build Topology built with two passes is very clean; unlike a single-pass build, in principle no cracks due to unforeseen junctions can occur. The added cost of performing a two-traversal build is slight; it is the recommended way to build topology and perform tessellations if you want high-quality images. When building topology in two traversals, the following steps occur: 1. Connectivity of all surfaces is calculated during a topology building traversal of the scene graph, before a tessellation traversal. 2. The surfaces in the scene are tessellated during a second traversal. Building Topology From a List of Surfaces You can explicitly accumulate a list of surfaces for which to build topology and then tessellate the surfaces. The result is clean tessellations of the surfaces on the list. Cracks may appear if an adjacent surface was not included in the list. Building Topology “by Hand”: Imported Surfaces If you have a set of surfaces for which you know connectivity, you can explicitly develop the appropriate topological data structures and develop consistent tessellations. The presence of cracks will depend on how good your input trim curves are. If three surfaces meet at a junction point that is not the shared endpoint of trim curves, a crack may appear. 432 007-1680-100 Summary of Scene Graph Topology: pfTopo Summary of Topology Building Strategies Table 10-1 lists the methods required for each of the topology building strategies. See “Base Class pfTessellateAction” on page 443 for more information about the tessellation methods listed. Table 10-1 Topology Building Methods Topology Building Strategy Methods Ignore topology information and let cracks appear as they will. 1. Do not create an pfTopo or build topology. 2. pfTessellateAction::setBuildTopoWhileTess(FALSE). 3. pfdTessellateGeometry(root, tessAction) Build topology incrementally. 1. Create an pfTopo. 2. pfTessellateAction::setBuildTopoWhileTess(TRUE). 3. pfTessellateAction::setTopo(topo). 4. pfdTessellateAction(root, tessAction). Two-traversal build. 1. Create an pfTopo. 2. pfTopo::buildTopologyTraverse(root). 3. pfTessellateAction::setBuildTopoWhileTess(FALSE). 4. pfdTessellateAction(root, tessAction). Assemble a list of surfaces, build the topology, and then tessellate. 1. Create an pfTopo. 2. Assemble list of surfaces: pfTopo::addSurface(surf). 3. pfTopo::buildTopology(). 4. pfTessellateAction::setBuildTopoWhileTess(FALSE). 5. pfdTessellateGeometry(shape, tessAction). 1. Create an pfTopo. 2. Assemble list of surfaces: pfTopo::addSurface(). 3. Create pfBoundarys. 4. Add to list of boundaries: pfTopo::addBoundary(). 5. Add edges to boundaries: pfBoundary::addEdge(). 6. Set boundary orientation: pfEdge::setBoundaryDir(). %PFROOT\Src\pguide\libpf\C++ 7. pfTessellateAction::setBuildTopoWhileTess(FALSE). \topoTest.cxx 8. pfdTesselateGeometry(shape, tessAction). (Microsoft Windows) Build the topology “by hand.” See the file /usr/share/Performer/src/pgu ide/libpf/C++/topoTest.C (IRIX and Linux) Step 7 does not appear in the code because FALSE is the default. 007-1680-100 433 10: Creating and Maintaining Surface Topology Reading and Writing Topology Information: Using Pseudo Loaders You can add topological information to an existing set of connected, higher-order surfaces in a file—for example, NURBS in an .iv or .csb file—and save the information for future, crack-free surface rendering. As a result, you do not have to repeat the topology build. The function pfdLoadFile() reads the topological information in a .pfb file. Before you save the scene graph data, you can also add tessellations that use the topology to give crack-free images (see Chapter 11, “Rendering Higher-Order Primitives: Tessellators”). Table 10-2 shows three possible file conversions that you can apply to .iv or .csb files that contain reps but no topology or tessellation; they are listed with example pfconv command lines, which demonstrate how to use both the pfctol and pfttol pseudo loaders. Table 10-2 Adding Topology and Tessellations to .iv and .csb Files Conversion Example Command Line Format change only. pfconv sur.csb sur.pfb Add topology information to scene graph: save reps and topology information but not tessellations. pfconv sur.iv.topoTol.ttol surTopo.pfb Add topology information and tessellations to scene graph: save reps, topology, and tessellations. pfconv sur.iv.topoTol.ttol.tessTol.ctol surTopoTess.pfb Add topology, tesselate, but do not save reps. pfconv sur.csb.topoTol.ttol.tessTol.ctol geodes.pfb.~.ctol 434 or pfconv sur.csb.topoTol.ttol surTopo.pfb or pfconv sur.csb.topoTol.ttol.tessTol.ctol surTopoTess.pfb 007-1680-100 Summary of Scene Graph Topology: pfTopo If you perform conversion, you may have files with or without tessellations. Depending on the type of file you read, use one of the command lines in Table 10-3. Table 10-3 Reading and Writing .pfb Files: with and without Tessellations To read a .pfb file and perform tessellation (without having to build topology): perfly surTopo.pfb.tessTol.ctol To read a .pfb file that already has tessellations perfly surTopoTess.pfb To read a .pfb file that already has tessellations and force retesselation (thus, removing any existing geometry associated with the higher order primitives) perfly surTopoTess.pfb.+tessTol.ctol To read a .pfb file that already has tessellations and store it without reps perfly surTopoTess.pfb geodes.pfb.~.ctol To delete the tessellation date, use the method clearTessellation(). Class Declaration for pfTopo The class has the following main methods: class pfTopo : public pfObject { public: // Creating and destroying pfTopo( ); virtual ~pfTopo(); // Accessor functions void setDistanceTol( pfReal tol, pfLengthUnits u ) pfReal getDistanceTol( ) const; pfLengthUnits getLengthUnits() const; static pfTopo* getGlobalTopo(int n); 007-1680-100 435 10: Creating and Maintaining Surface Topology static int getNumTopos(); pfParaSurface* getSurface( int i ); int getSurfaceCount( ) const; pfBoundary* getBoundary( int i ); int getBoundaryCount( ) const; int getSolidCount() const; pfSolid* getSolid( int i ) //Adding topological elements int addSurface( pfParaSurface *sur ); int addBoundary( pfBoundary *bnd ); //Topology construction void buildTopology(); int buildSolids(); }; Main Features of the Methods in pfTopo buildSolids() Collects connected surfaces in the pfTopo into pfSolids (see “Collecting Connected Surfaces: pfSolid” on page 437). buildTopology() Builds consistent set of boundaries from the list of surfaces accumulated by calls to addSurface(). Previously developed boundaries are deleted. pfTopo(tol,u,sizeEstimate) Construct a topological data structure. tol specifies a tolerance for calculating when points are close enough together to be considered the same. Default is 1 millimeter. u specifies the system of units for tol. Default is meters. getLengthUnits() and setLengthUnits() Gets and sets the measurement units in object space for this pfTopo. getNumTopos() Returns the number of pfTopo structures in the global array of pfTopos. getGlobalTopo() Returns the specified pfTopo ID from the global array of pfTopos. 436 007-1680-100 Collecting Connected Surfaces: pfSolid Note: In addition, it is possible to traverse the scene graph and build a consistent set of boundaries for all the surfaces under a specified node by using the pfdBuildTopologyTraverse(pfTopo *topo,pfNode *root) function. The static member topology is an array of all topologies that have been created. Collecting Connected Surfaces: pfSolid To maintain consistent normals or propagate deformation information, organize connected pfParaSurfaces in an pfSolid. With an pfSolid, you can collect connected surface patches in one object for convenient access and manipulation. Despite the name of the class, the set of surfaces need not form a closed surface, that is the boundary of a volume. They can be a set of patches joined to form a surface, for example, you might generate a hood of a car from two pfParaSurafaces that are mirror images of each other. To create solids, collect them in an pfTopo and then call pfTopo::buildSolid() (see “Summary of Scene Graph Topology: pfTopo” on page 428). Class Declaration for pfSolid The class has the following main methods: class pfSolid : public pfObject { public: // Creating and destroying pfSolid() virtual ~pfSolid() // Accessor functions int addSurface( pfParaSurface *sur ); pfParaSurface* getSurface( int i); int getSurfaceCount( ) const; int getSolidId() const; }; 007-1680-100 437 10: Creating and Maintaining Surface Topology Main Features of the Methods in pfSolid Use the methods only after you have created an pfSolid with pfTopo::buildSolid(). Treat the method setSolidId() that appears in pfSolid.h as private: it is used by pfTopo::buildSolid() when building the solid. 438 007-1680-100 Chapter 11 11. Rendering Higher-Order Primitives: Tessellators To render a curve or surface, you must develop an approximation of it with pfGeoSets. The tool that translates these reps into a mesh of contiguous triangles is called a tessellator. Tessellation is interpretive; there is necessarily a difference between the original surface and the tessellated mesh. You can control how closely you want the mesh to resemble the surface. • Close resemblance, requiring many triangles, produces a realistic shape but incurs slow graphic processing. • A gross approximation of the original surface results in fast processing. Applications often create a series of tessellated representations of a shape, each one called a level of detail (LOD). High resolution LODs are used when shapes are close to the viewer and low resolution LODs are used when shapes are far from the viewer. Because distance obscures detail, high resolution LODs are not necessary to represent distant shapes. This chapter describes how to control the tessellation of shapes in the following sections: 007-1680-100 • “Features of Tessellators” on page 440 • “Base Class pfTessellateAction” on page 443 • “Tessellating Parametric Surfaces” on page 445 439 11: Rendering Higher-Order Primitives: Tessellators pfObject pfTessellateAction pfTessParaSurfaceAction Figure 11-1 Tessellators for continuous surfaces Class Hierarchy for Tessellators Features of Tessellators Tessellators generate a sequence of straight-line segments to approximate an edge curve of a surface, then cover the surface with triangular tiles. With each triangle vertex it creates, a tessellator also stores the normal vector at the point from original surface. The normal vectors are necessary for lighting and shading calculations. Tessellations necessarily burden the entire graphics pipeline; they provide a first definition of the rendering task by specifying a maximal set of vertices to be sent to the graphics hardware. 440 007-1680-100 Features of Tessellators Tessellators for Varying Levels of Detail Ideally, you would quickly generate the simplest tessellation that adequately represents surfaces of interest. What is adequate depends on your particular rendering task. You may want to generate several tessellations with varying degrees of complexity and accuracy for one pfRep and place them in level-of-detail nodes, as discussed in “Level-of-Detail Management” in Chapter 5. The tessellators include accessor functions to help you assess the load they create for the graphics hardware. The control parameter for tessellations specifies the maximum deviation from the exact surface. Figure 11-2 illustrates the effects of varying the deviation. The upper left image is appropriate for accurate representation of the surface, the lower right image would be appropriate if the object were in the distant background of a scene. Figure 11-2 007-1680-100 Tessellations Varying With Changes in Control Parameter 441 11: Rendering Higher-Order Primitives: Tessellators Details of Figure 11-2 The surface shown in Figure 11-2 was made with the repTest application using an pfFrenetSweptSurface as follows (see “pfFrenetSweptSurface” on page 399): pfReal profile( pfReal t ) { return 0.5*cos(t*6.0) + 1.25; }; pfSuperQuadCurve3d *cross = new pfSuperQuadCurve3d( 0.75, new pfRVec3(0.0, 0.0, 0.0), 3.0 ); pfCircle3d *path = new pfCircle3d( 1.75, new pfRVec3(0.0, 0.0, 0.0) ); pfFrenetSweptSurface *fswept = new pfFrenetSweptSurface( cross, path, profile ); fswept->setHandednessHint( true ); The number of triangles in Figure 11-2 decreases as the maximum-deviation parameter chordalDevTol varies from .001 to .01 to .1 to .5 (see “Tessellating Parametric Surfaces” on page 445). These numbers should be compared to the scale of the object, which has a maximum diameter of 6.125 = 2(1.75 + 1.75 × .75), a minimum diameter of .875 = 2(1.75 − 1.75 × .75), a maximum height of 2.625 = 2(1.75 × .75), and a minimum height of 1.125 = 2(.75 × .75). Tessellators Act on a Whole Graph or Single Node You can apply a tessellator either to a scene graph or to just one node. The tessellators produce a pfGeoSet from an pfRep and place that pfGeoSet in the pfGeode that holds the pfRep. Tessellators and Topology: Managing Cracks A tessellation begins with a discrete set of vertices at surface edges. To prevent cracks from appearing between adjacent surfaces, the same set of vertices should be used to tessellate both surfaces. To address the crack problem, you have several options, which are discussed in “Building Topology: Computing and Using Connectivity Information” on page 431. Table 10-1 on page 433 lists the different approaches to topology building, and the methods to use for each. 442 007-1680-100 Base Class pfTessellateAction Base Class pfTessellateAction The pfTessellateAction class itself primarily stores statistics concerning the current tessellation. The pfTessParaSurfaceAction class performs the tessellation of surfaces. You can invoke the tessellation on a single surface by calling the following method: pfTessParaSurfaceAction::tessellator(pfNode *n) You can invoke the tessalltion on the entire scene graph by using the following function: pfdTessellateGeometry(pfNode *root,pfTessParaSurfaceAction *tessAction) Nodes which do not derive from pfParaSurface will be left untouched by the pfdTessellateGeometry() function call. Retessellating a Scene Graph A tessellator will not tessellate a pfRep if the geoset count (pfGeode::getNumGSets()) is not zero. If you want to retessellate a pfParaSurface, you must call pfParaSurface::clearTessellation(). Note that you can also load a file and force retessellation to occur by using the libpfctol pseudo loader and specifying the + symbol in front of the ctol value you wish to use during tessellation, as shown in the following: perfly surfaces.pfb.+0.01.ctol Class Declaration for pfTessellateAction The class has the following main methods: class pfTessellateAction : public pfObject { public: // Creating and destroying pfTessellateAction( void ); virtual ~pfTessellateAction( void ); // Accessor functions void setExtSize( int s ); int getExtSize( ); int getTriangleCount() const; int getTriStripCount() const; 007-1680-100 443 11: Rendering Higher-Order Primitives: Tessellators int getTriFanCount() const; void setReverseTrimLoop(const pfBool enable ); pfBool getReverseTrimLoop() const; void setBuildTopoWhileTess(pfBool _buildTopoWhileTess); pfBool getBuildTopoWhileTess() const; void setTopo(pfTopo * _topo); pfTopo *getTopo( void )const; }; Main Features of the Methods in pfTessellateAction getTriangleCount() Returns the number of all triangles generated by this instance of the tessellator. getTriStripCount() and getTriFanCount() Return the number of tristrips or trifans in the tessellation. setBuildTopoWhileTess() and getBuildTopoWhileTess() Sets a flag whether surface connectivity is computed during the tessellation traversal. Set the topology data structure to use with setTopo(). If TRUE, before tessellating each surface, the connectivity of all previously tessellated surfaces is used to avoid cracks when tessellating. Notice that the final tessellations of the surfaces in the scene graph may still have cracks because of unforeseen junctions between surfaces. If FALSE, no topology is constructed while tessellating. This leads to two very different possible results: • 444 If topology information for the surfaces to be tessellated was developed before the tessellation, by calling pfdBuildTopologyBuildTraverse() or pfTopo::buildTopology() or by constructing topology by hand, the tessellator uses the information and avoids cracks between surfaces. This option provides the most crack-free tessellations possible. 007-1680-100 Tessellating Parametric Surfaces • If topology information was not developed before the tessellation traversal, then surfaces are tessellated without regard to connectivity and cracks appear between all adjacent surfaces. This option provides the least crack-free tessellations possible. setReverseTrimLoop() and getReverseTrimLoop() Set and recover the orientation of trim loops. Recall that the side of the surface to the left of the trim loop is rendered (see the section “Parametric Surfaces” on page 375). setTopo() and getTopo() Set and get the pfTopo that holds the topology information used by the tessellator (see “Summary of Scene Graph Topology: pfTopo” on page 428). Tessellating Parametric Surfaces This section discusses the two classes OpenGL Performer provides for tessellating parametric surfaces. The class pfTessParaSurfaceAction has methods for any parametric surface. pfTessParaSurfaceAction The pfTessParaSurfaceAction class develops tessellations of any pfParaSurface. If a surface has boundary curves, the tessellator starts there and specifies vertices at the edges of the surface. The tessellator then covers the surface with pfGeoSets using the boundary vertices to “pin” the edges of the tessellation. If necessary, the tessellator creates edge vertices by constructing a discrete version of the boundary curve associated with each of the surface’s pfEdges. An advantage of starting all tessellations at boundaries is easy coordination of tessellations by several processors. As part of the tessellation process, you can generate the UV coordinates for each vertex created by the tessellator. To control the accuracy of a tessellation, you specify a chordal deviation parameter which constrains the distance of edges in the tessellation from the original surface. 007-1680-100 445 11: Rendering Higher-Order Primitives: Tessellators Class Declaration for pfTessParaSurfaceAction The class has the following main methods: class pfTessParaSurfaceAction : public pfTessellateAction { public: pfTessParaSurfaceAction(); pfTessParaSurfaceAction( pfReal chordalDevTol, pfBool scaleTolByCurvature, int samples); virtual ~pfTessParaSurfaceAction(); // Accessor functions void setChordalDevTol( const pfReal chordalDevTol ); pfReal getChordalDevTol( ) const; void setScaleTolByCurvature( const pfReal scaleTolByCurvature ) pfBool getScaleTolByCurvature() const; void setSampling( const int samples ); int getSampling( ); void setNonUniformSampling(const pfBool samples); pfBool getNonUniformSampling() const; void setGenUVCoordinates( const pfBool genUVCoordinates ); pfBool getGenUVCoordinates( ) const; void setGenGeoArrays(pfBool enable); pfBool getGenGeoArrays() const; void tessellator(pfParaSurface *sur); }; 446 007-1680-100 Tessellating Parametric Surfaces Main Features of the Methods in pfTessParaSurface pfTessParaSurface() Creates the class and provides a hint for the maximum deviation of the tessellation from the original surface, indicates whether the tolerance should be scaled by curvature, and provides a hint for how many vertices to include in the tessellation. setChordalDevTol() and getChordalDevTol() Set and get the maximum distance from the original surface to the edges produced by the tessellation. setGenUVCoordinates() and getGenUVCoordinates() Set and get a flag that indicates whether to generate UV coordinates for the vertices produced in the tessellation. The coordinates for each vertex are stored as the vertex’s texture coordinates. setSampling() and getSampling() Set and get the hint for the number of triangle vertices in the tessellation along each boundary of the surface. If the surface has no trim curves defining its “outer” edges, then the sampling is along the edges of the UV rectangle that parameterizes the surface. setScaleTolByCurvature() and getScaleTolByCurvature() Set and get a flag to control whether the chordal deviation parameter should be scaled by curvature. If nonzero, the tessellation of highly curved areas improves. setGenGeoArrays() and getGenGeoArrays() Set and get a flag that determines whether or not the tessellator should generate pfGeoArrays instead of pfGeoSets. By default, this is set to true. tessellator() Tessellates an individual surface. Although pfTessParaSurface::tessellator() may be used to tessellate an individual surface, it is more common to tessellate a set of surfaces using the pfdTessellateGeometry() function. 007-1680-100 447 11: Rendering Higher-Order Primitives: Tessellators Sample From repTest: Tessellating and Rendering a Sphere The sample code in this section demonstrates how to create higher-level reps such as pfParaSurfaces and pfCurve2ds and to save the output to a file. The lines of code perform the following procedures: • Initializing OpenGL Performer • Creating and tessellating a pfSphereSurface • Saving the scene graph to a .pfb file The code that follows can be found in the file /usr/share/Performer/src/pguide/libpf/C++/repTest.C on IRIX and Linux and %PFROOT%\Src\pguide\libpf\C++\repTest.cxx on Microsoft Windows. From main() Initialize OpenGL Performer and prepare for the creation of higher-level reps. pfInit(); pfdInitConverter(argv[1]?argv[1]:”pfb”); shared = (SharedData *)pfCalloc(1, sizeof(SharedData), pfGetSharedArena()); shared->count = 10; pfConfig(); pfGroup *root = new pfGroup(); Create Geometry Create the geometry by calling makeObjects(), which in turn calls setupShape() to position the reps and specify their geostate. 448 makeObjects(root); 007-1680-100 Tessellating Parametric Surfaces Define setUpShape() The function setupShape() creates a new pfGeode, applies a pfGeoState, and positions the pfRep using pfRep::setOrigin(). static void setupShape(pfGroup *p, pfParaSurface *rep,pfReal x, pfReal y,pfReal z) { // Get the current origin of the object pfRVec3 org; rep->getOrigin(org); // Add the incoming offset to it org[0] += x; org[1] += y; org[2] += z; // Now reset the origin to include the // incoming offset rep->setOrigin(org); // Set the appearance of this shape to // be a random color pfGeoState *gState makeColor((float)rand()/((2<<15) - 1.0f), (float)rand()/((2<<15) - 1.0f), (float)rand()/((2<<15) - 1.0f)); // Set the geostate for the surface to // be used during tessellation rep->setGState(gState); // Attach the rep to the scene graph p->addChild(rep); } 007-1680-100 449 11: Rendering Higher-Order Primitives: Tessellators Define makeObjects() The function makeObjects() sets up the scene graph, defines the grid of reps, and places the surfaces in the scene graph by calling setupShape(). The code here shows the initial lines of makeObjects() (omitting code that controls the grid definition) and the example of defining a trimmed and untrimmed pfSphereSurface. pfGroup *makeObjects(pfGroup *root) { .... pfSphereSurface *sphere = new pfSphereSurface(3.0); if(nVersions <= 0) { pfCircle2d *trimCircle2d = new pfCircle2d(1.0, pfRVec2(M_PI/2.0, M_PI)); sphere->addTrimCurve(0, trimCircle2d, NULL); } setupShape(sphere, PF_XDIST*numObject++, Y, PF_VIEWDIST); .... } 450 007-1680-100 Chapter 12 12. Graphics State The graphics state is a class of fields that defines everything about the shape and texture of an object in an OpenGL Performer scene. Fields include such things as transparency, shading, reflectance, and texture. The graphics state is set globally for all objects in the scene graph. Individual objects, however, can override graphics state settings. The cost, however, is efficiency. For performance reasons, therefore, it is important to set the fields in the graphics state to satisfy the greatest number of objects in the scene. This chapter describes in detail all of the fields in the graphics state. Immediate Mode The graphics libraries are immediate-mode state machines; if you set a mode, all subsequent geometry is drawn in that mode. For the best performance, mode changes need to be minimized and managed carefully. libpr manages a subset of graphics library state and identifies bits of state as graphics state elements. Each state element is identified with a PFSTATE token; for example., PFSTATE_TRANSPARENCY corresponds to the transparency state element. State elements are loosely partitioned into three categories: modes, values, and attributes. Modes are the graphics state variables, such as transparency and texture enable, that have simple values like ON and OFF. An example of a mode command is pfTransparency(mode). Values are not modal, rather they are real numbers which signify a threshold or quantity. An example of a value is the reference alpha value specified with the pfAlphaFunc() function. Attributes are references to encapsulations (structures) of graphics state. They logically group the more complicated elements of state, such as textures and lighting models. Attributes are structures that are modified through a procedural interface and must be 007-1680-100 451 12: Graphics State applied to have an effect. For example, pfApplyTex(tex) applies the texture map, tex, to subsequently drawn geometry. In libpr, there are three methods of setting a state: • Immediate mode • Display list mode • pfGeoState mode Like the graphics libraries, libpr supports the notion of both immediate and display-list modes. In immediate mode, graphics mode changes are sent directly to the Geometry Pipeline; that is, they have an immediate effect. In display-list mode, graphics mode changes are captured by the currently active pfDispList, which can be drawn later. libpr display lists differ from graphics library objects because they capture only libpr commands and are reusable. libpr display lists are useful for multiprocessing applications in which one process builds up the list of visible geometry and another process draws it. “Display Lists” on page 479 describes libpr display lists. A pfGeoState is a structure that encapsulates all the graphics modes and attributes that libpr manages. You can individually set the state elements of a pfGeoState to define a graphics context. The act of applying a pfGeoState with pfApplyGState() configures the state of the Geometry Pipeline according to the modes, values, and attributes set in the pfGeoState. For example, the following code fragment shows equivalent ways (except for some inheritance properties of pfGeoStates described later) of setting up some lighting parameters suitable for a glass surface: /* Immediate mode state specification */ pfMaterial *shinyMtl; pfTransparency(PFTR_ON); pfApplyMtl(shinyMtl); pfEnable(PFEN_LIGHTING); /* is equivalent to: */ /* GeoState state specification */ pfGeoState *gstate; pfGStateMode(gstate, PFSTATE_TRANSPARENCY, PFTR_ON); pfGStateAttr(gstate, PFSTATE_FRONTMTL, shinyMtl); pfGStateMode(gstate, PFSTATE_ENLIGHTING, PF_ON); pfApplyGState(gstate); 452 007-1680-100 Immediate Mode In addition, pfGeoStates have unique state inheritance capabilities that make them very convenient and efficient; they provide independence from ordered drawing. pfGeoStates are described in the section “pfGeoState” on page 482 of this chapter. The libpr routines have been designed to produce an efficient structure for managing graphics state. You can also set a graphics state directly through the GL. However, libpr will have no record of these settings and will not be able to optimize them and may make incorrect assumptions about current graphics state if the resulting state does not match the libpr record when libpr routines are called. Therefore, it is best to use the libpr routines whenever possible to change a graphics state and to restore the libpr state if you go directly through the GL. The following sections will describe the rendering geometry and state elements in detail. There are three types of state elements: modes, values, and attributes. Modes are simple settings that take a set of integer values that include values for enabling and disabling the mode. Modes may also have associated values that allow a setting from a defined range. Attributes are complex state structures that encapsulate a related collection of modes and values. Attribute structures will not include in their definition an enable or disable as the enabling or disabling of a mode is orthogonal to the particular related attribute in use. Rendering Modes The libpr library manages a subset of the rendering modes found in OpenGL. In addition, libpr abstracts certain concepts like transparency, providing a higher-level interface that hides the underlying implementation mechanism. The libpr library provides tokens that identify the modes that it manages. These tokens are used by pfGeoStates and other state-related functions like pfOverride(). The following table enumerates the PFSTATE_* tokens of supported modes, each with a brief description and default value. Table 12-1 lists and describes the mode tokens. Table 12-1 007-1680-100 pfGeoState Mode Tokens Token Name Description Default Value PFSTATE_TRANSPARENCY Transparency modes PFTR_OFF PFSTATE_ALPHAFUNC Alpha function PFAF_ALWAYS 453 12: Graphics State Table 12-1 pfGeoState Mode Tokens (continued) Token Name Description Default Value PFSTATE_ANTIALIAS Antialiasing mode PFAA_OFF PFSTATE_CULLFACE Face culling mode PFCF_OFF PFSTATE_DECAL Decaling mode for coplanar geometry PFDECAL_OFF PFSTATE_SHADEMODEL Shading model PFSM_GOURAUD PFSTATE_ENLIGHTING Lighting enable flag PF_OFF PFSTATE_ENTEXTURE Texturing enable flag PF_OFF PFSTATE_ENFOG Fogging enable flag PF_OFF PFSTATE_ENWIREFRAME pfGeoSet wireframe mode enable flag PF_OFF PFSTATE_ENCOLORTABLE pfGeoSet colortable enable flag PF_OFF PFSTATE_ENHIGHLIGHTING pfGeoSet highlighting enable flag PF_OFF PFSTATE_ENLPOINTSTATE pfGeoSet light point state enable flag PF_OFF PFSTATE_ENTEXGEN Texture coordinate generation enable flag PF_OFF PFSTATE_ENFRAGPROG Fragment program enable flag PF_OFF PFSTATE_ENVTXPROG Vertex program enable flag PF_OFF PFSTATE_ENSHADPROG Shader program enable flag PF_OFF The mode control functions described in the following sections should be used in place of their graphics library counterparts so that OpenGL Performer can correctly track the graphics state. Use pfGStateMode() with the appropriate PFSTATE token to set the mode of a pfGeoState. 454 007-1680-100 Immediate Mode Transparency You can control transparency using pfTransparency(). Possible transparency modes are listed in the following table. Table 12-2 pfTransparency Tokens Transparency mode Description PFTR_OFF Transparency disabled. PFTR_ON PFTR_FAST Use the fastest, but not necessarily the best, transparency provided by the hardware. PFTR_HIGH_QUALITY Use the best, but not necessarily the fastest, transparency provided by the hardware. PFTR_MS_ALPHA Use screen-door transparency when multisampling. Fast but limited number of transparency levels. PFTR_BLEND_ALPHA Use alpha-based blend with background color. Slower but high number of transparency levels. In addition, the flag PFTR_NO_OCCLUDE may be logically ORed into the transparency mode in which case geometry will not write depth values into the frame buffer. This will prevent it from occluding subsequently rendered geometry. Enabling this flag improves the appearance of unordered, blended transparent surfaces. There are two basic transparency mechanisms: screen-door transparency, which requires hardware multisampling, and blending. Blending offers very high-quality transparency but for proper results requires that transparent surfaces be rendered in back-to-front order after all opaque geometry has been drawn. When using transparent texture maps to “etch” geometry or if the surface has constant transparency, screen-door transparency is usually good enough. Blended transparency is usually required to avoid “banding” on surfaces with low transparency gradients like clouds and smoke. Shading Model You can select either flat shading or Gouraud (smooth) shading. pfShadeModel() takes one of two tokens: PFSM_FLAT or PFSM_GOURAUD. One some graphics hardware flat shading can offer a significant performance advantage. 007-1680-100 455 12: Graphics State Alpha Function The pfAlphaFunc() function is an extension of the glAlphaFunc() function; it allows OpenGL Performer to keep track of the hardware mode. The alpha function is a pixel test that compares the incoming alpha to a reference value and uses the result to determine whether or not the pixel is rendered. The reference value must be specified in the range [0, 1]. For example, a pixel whose alpha value is 0 is not rendered if the alpha function is PFAF_GREATER and the alpha reference value is also 0. Note that rejecting pixels-based alpha can be faster than using transparency alone. A common technique for improving the performance of filling polygons is to set an alpha function that will reject pixels of low (possibly nonzero) contribution. The alpha function is typically used for see-through textures like trees. Decals On Z-buffer-based graphics hardware, coplanar geometry can cause unwanted artifacts due to the finite numerical precision of the hardware which cannot accurately resolve which surface has visual priority. This can result in flimmering, a visual “tearing” or “twinkling” of the surfaces. pfDecal() is used to accurately draw coplanar geometry on SGI graphics platforms and it supports two basic implementation methods : stencil decaling and displace decaling. The stencil decaling method uses a hardware resource known as a stencil buffer and requires that a single stencil plane (see the man page for glStencilOp()) be available for OpenGL Performer. This method offers the highest image quality but requires that geometry be coplanar and rendered in a specific order which reduces opportunities for the performance advantage of sorting by graphics mode. A potentially faster method is the displace decaling method. In this case, each layer is displaced towards the eye so it hovers slightly above the preceding layer. Displaced decals need not be coplanar, and can be drawn in any orde, but the displacement may cause geometry to incorrectly poke through other geometry. The specificaton of a decal plane can improve the displace decaling method. The object geometry will be projected onto the specified plane and if the same plane is specified for base and layer geometry, the base and layer polygons will be generated with identical depth values. If the objects are drawn in priority order, no further operation is necessary. Otherwise, displace can be applied to the planed geometry for a superior result. Decal planes can be specified on pfGeoSets with pfGSetDecalPlane(), on a pfGeoState with the PFSTATE_DECALPLANE attribute to pfGStateAttr(), or globally with pfApplyDecalPlane(). 456 007-1680-100 Immediate Mode Decals consist of base geometry and layer geometry. The base defines the depth values of the decal while layer geometry is simply inlaid on top of the base. Multiple layers are supported but limited to eight when using displaced decals. Realize that these layers imply superposition; there is no limit to the number of polygons in a layer, only to the number of distinct layers. The decal mode indicates whether the subsequent geometry is base or layer and the decal method to use. For example, a mode of PFDECAL_BASE_STENCIL means that subsequent geometry is to be considered as base geometry and drawn using the stencil method. All combinations of base/layer and displace/stencil modes are supported but you should make sure to use the same method for a given base-layer pair. Example 12-1 illustrates the use of pfDecal(). Example 12-1 Using pfDecal() to a Draw Road with Stripes pfDecal(PFDECAL_BASE_STENCIL); /* ... draw underlying geometry (roadway) here ...*/ pfDecal(PFDECAL_LAYER_STENCIL); /* ... draw coplanar layer geometry (stripes) here ... */ pfDecal(PFDECAL_OFF); Note: libpf applications can use the pfLayer node to include decals within a scene graph. Frontface / Backface The pfCullFace() function controls which side of a polygon (if any) is discarded in the Geometry Pipeline. Polygons are either front-facing or back-facing. A front-facing polygon is described by a counterclockwise order of vertices in screen coordinates, and a back-facing one has a clockwise order. pfCullFace() has four possible arguments: 007-1680-100 PFCF_OFF Disable face-orientation culling. PFCF_BACK Cull back-facing polygons. PFCF_FRONT Cull front-facing polygons. 457 12: Graphics State PFCF_BOTH Cull both front- and back-facing polygons. In particular, back-face culling is highly recommended since it offers a significant performance advantage for databases where polygons are never be seen from both sides (databases of “solid” objects or with constrained eyepoints). Antialiasing The pfAntialias() function is used to turn the antialiasing mode of the hardware on or off. Currently, antialiasing is implemented differently by each different graphics system. Antialiasing can produce artifacts as a result of the way OpenGL Performer and the active hardware platform implement the feature. See the man page for pfAntialias() for implementation details. Rendering Values Some modes may also have associated values. These values are set through pfGStateVal(). Table 12-3 lists and describes the value tokens. Table 12-3 pfGeoState Value Tokens Token Name Description Range PFSTATE_ALPHAREF Set the alpha function reference value. 0.0 - 1.0 Default Value 0.0 Enable / Disable The pfEnable() and pfDisable() functions control certain rendering modes. Certain modes do not have effect when enabled but require that other attribute(s) be applied. 458 007-1680-100 Immediate Mode Table 12-4 lists and describes the tokens and also lists the attributes required for the mode to become truly active. Table 12-4 Enable and Disable Tokens Token Action Attribute(s) Required PFEN_LIGHTING Enable or disable lighting. pfMaterial pfLight pfLightModel PFEN_TEXTURE Enable or disable texture. pfTexEnv pfTexture PFEN_FOG Enable or disable fog. pfFog PFEN_WIREFRAME Enable or disable pfGeoSet wireframe rendering. none PFEN_COLORTABLE Enable or disable pfGeoSet colortable mode. pfColortable PFEN_HIGHLIGHTING Enable or disable pfGeoSet highlighting. pfHighlight PFEN_TEXGEN Enable or disable automatic texture coordinate generation. pfTexGen PFEN_LPOINTSTATE Enable or disable pfGeoSet light points. pfLPointState PFEN_FRAGPROG Enable or disable fragment program. pfFragmentProgram PFEN_VTXPROG Enable or disable vertex program. pfVertexProgram PFEN_SHADPROG Enable or disable shader programs. pfShaderProgram By default all modes are disabled. Rendering Attributes Rendering attributes are state structures that are manipulated through a procedural interface. Examples include pfTexture, pfMaterial, and pfFog. libpr provides tokens that enumerate the graphics attributes it manages. These tokens are used by pfGeoStates 007-1680-100 459 12: Graphics State and other state-related functions like pfOverride(). Table 12-5 lists and describes the tokens. Table 12-5 Rendering Attribute Tokens Attribute Token Description Apply Routine PFSTATE_LIGHTMODEL Lighting model pfApplyLModel() PFSTATE_LIGHTS Light source definitions pfLightOn() PFSTATE_FRONTMTL Front-face material pfApplyMtl() PFSTATE_BACKMTL Back-face material pfApplyMtl() PFSTATE_TEXTURE Texture pfApplyTex() PFSTATE_TEXENV Texture environment pfApplyTEnv() PFSTATE_FOG Fog model pfApplyFog() PFSTATE_COLORTABLE Color table for pfGeoSets pfApplyCtab() PFSTATE_HIGHLIGHT pfGeoSet highlighting style pfApplyHlight() PFSTATE_LPOINTSTATE pfGeoSet light point definition pfApplyLPState() PFSTATE_TEXGEN Texture coordinate generation pfApplyTGen() PFSTATE_FRAGPROG Fragment program definition pfGProgramApply() PFSTATE_VTXPROG Vertex program definition pfGProgramApply() PFSTATE_GPROGPARMS GPU program parameters definition pfGPParamsApply() PFSTATE_SHADPROG Shader program definition pfShaderProgramApply() Rendering attributes control which attributes are applied to geometric primitives when they are processed by the hardware. All OpenGL Performer attributes consist of a control structure, definition routines, and an apply function, pfApply* (except for lights which are “turned on”). Each attribute has an associated pfNew*() routine that allocates storage for the control structure. When sharing attributes across processors in a multiprocessor application, you should pass the pfNew*() routine a shared memory arena from which to allocate the structure. If you pass NULL as the arena, the attribute is allocated from the heap and is not sharable in a nonshared address space (fork()) multiprocessing application. 460 007-1680-100 Immediate Mode All attributes can be applied directly, referenced by a pfGeoState or captured by a display list. When changing an attribute, that change is not visible until the attribute is reapplied. Detailed coverage of attribute implementation is available in the man pages. Texture OpenGL Performer supports texturing through pfTextures and pfTexEnvs, which provide encapsulated suppport for graphics library textures (see glTexImage2D() ) and texture environments (see glTexEnv()). A pfTexture defines a texture image, format, and filtering. A pfTexEnv specifies how the texture should interact with the colors of the geometry where it is applied. You need both to display textured data, but you do not need to specify them both at the same time. For example, you could have pfGeoStates each of which had a different texture specified as an attribute and still use an overall texture environment specified with pfApplyTEnv(). A pfTexture is created by calling pfNewTex(). If the desired texture image exists as a disk file in IRIS RGB image format (the file often has a “.rgb” suffix ) or in the OpenGL Performer fast-loading image format (a “.pfi” suffix), you can call pfLoadTexFile() to load the image into CPU memory and completely configure the pfTexture, as shown in the following: pfTexture *tex = pfLoadTexFile(“brick.rgba”); Otherwise, pfTexImage() lets you directly provide a GL-ready image array in the same external format as specified on the pfTexture and as expected by glTexImage2D(), as shown in the following: void pfTexImage(pfTexture* tex, uint* image, int comp, int sx, int sy, int sz); OpenGL expects packed texture data with each row beginning on a long word boundary. However, OpenGL expects the individual components of a texel to be packed in opposite order. For example, OpenGL expects the texels to be packed as RGBA. If you provide your own image array in a multiprocessing environment, it should be allocated from shared memory (along with your pfTexture) to allow different processes to access it. A basic example demonstrating loading a image file and placing the resulting pfTexture on scene graph geometry is at /usr/share/Performer/src/pguide/libpf/C/texture.c on IRIX and Linux and %PFROOT%\Src\pguide\libpf\C\texture.c on Microsoft Windows. 007-1680-100 461 12: Graphics State Note: The size of your texture must be an integral power of two on each side. OpenGL refuses to accept badly sized textures. You can rescale your texture images with the izoom or imgworks programs (shipped with IRIX 5.3 in the eoe2.sw.imagetools and imgtools.sw.tools subsystems; and with IRIX 6.2 in the eoe.sw.imagetools and imgworks.sw.tools subsystems). Your texture source does not have to be a static image. pfTexLoadMode() can be used to set one of the sources listed in Table 12-6 with PFTEX_LOAD_SOURCE. Note that sources other than CPU memory may not be supported on all graphics platforms, or may have some special restrictions. There are several sample programs that demonstrate paging sequences of texture from different texture sources. For paging from host memory there are the following: • • On IRIX and Linux: – /usr/share/Performer/src/pguide/libpr/C/texlist.c – /usr/share/Performer/src/pguide/libpr/C/mipmap.c – /usr/share/Performer/src/pguide/libpfutil/movietex.c On Microsoft Windows: – %PFROOT%\Src\pguide\libpr\C\texlist.c – %PFROOT%\Src\pguide\libpr\C\mipmap.c – %PFROOT%\Src\pguide\libpfutil\movietex.c These examples demonstrate the use of different texture sources for paging textures, and libpufitl utilties for managing texture resources. One thing these examples do is use pfTexLoadImage() to update the pointer to the image data to avoid the expensive reformating the texture. This requires that the provided image data be the same size as the original image data, as well as same number of components and same formats. Table 12-6 Texture Image Sources PFTEX_SOURCE_ Token Source of the Texture Image IMAGE CPU memory location specified by pfTexLoadImage() or pfTexImage() 462 007-1680-100 Immediate Mode Table 12-6 Texture Image Sources (continued) PFTEX_SOURCE_ Token Source of the Texture Image FRAMEBUFFER Framebuffer location offset from window origin as specified by pfTexLoadOrigin() VIDEO Default video source on the system Video Texturing The source of texture image data can be live video input. OpenGL Performer supports the video input mechanisms of Sirius Video on RealityEngine and InfiniteReality, DIVO on InfiniteReality, and Silicon Graphics O2 and Octane video input. OpenGL Performer includes a sample program that features video texturing: /usr/share/Performer/src/pguide/libpf/movietex.c (IRIX and Linux) %PFROOT%\Src\pguide\libpf\movietex.c (Microsoft Windows) This example demonstrates that all video initialization, including the creation of video library resources, is done in the draw process, as required by the video library. Part of that initialization includes setting the proper number of components on the pfTexture and choosing a texture filter and potentially internal format. Those basic operations are discussed further in this section. OpenGL Performer will automatically download the frame of video when the texture object is applied through the referencing pfGeoState. Alternatively, you may want to schedule this download to happen at the beginning or end of the rendering frame; you can force it with pfLoadTex(). Textures must be created with sizes that are powers of 2; the input video frame is usually not in powers of 2. The [0,1] texture coordinate range can be scaled into the valid part of the pfTexture with a texture matrix. This matrix can be applied directly to the global state with pfApplyTMat() to affect all pfGeoSets, or can be set on a pfGeoState with the PFSTATE_TEXMAT attribute to pfGStateAttr(). Texture Management Texture storage is limited only by virtual memory, but for real-time applications you must consider the amount of texture storage the graphics hardware supports. Textures that do not fit in the graphics subsystem are paged as needed when pfApplyTex() is called. The libpr library provides routines for managing hardware texture memory so 007-1680-100 463 12: Graphics State that a real-time application does not have to get a surprise texture load. pfIsTexLoaded(), called from the drawing process, tells you if the pfTexture is currently properly loaded in texture memory. The initially required textures of an application, or all of the textures if they fit, can be preloaded into texture memory as part of application initialization. pfuMakeSceneTexList() makes a list of all textures referenced by a scene graph and pfuDownloadTexList() loads a list of textures into hardware texture memory (and must be called in the draw process). The Perfly sample program does this as part of its initialization and displays the textures as it preloads them. There are additional routines to assist with the progressive loading and unloading of pfTextures. pfIdleTex() can be used to free the hardware texture memory owned by a pfTexture. GL host and hardware texture memory resources can be freed with pfDeleteGLHandle() from the drawing process. OpenGL Performer will automatically re-allocate those resources if the pfTexture is used again. For an example of management of texture resources, see the example program /usr/share/Performer/src/pguide/libpfutil/texmem.c for IRIX and Linux or %PFROOT%\Src\pguide\libpfutil\texmem.c for Microsoft Windows. The program uses the pfuTextureManager from libpfutil for basic texture paging support. The pfLoadTex() function, called from the drawing process, can be used to explicitly load a texture into graphics hardware texture memory (which includes doing any necessary formatting of the texture image). By default, pfLoadTex() loads the entire texture image, including any required minification or magnification levels, into texture memory. pfSubloadTex() and pfSubloadTexLevel() can also be used in the drawing process to do an immediate load of texture memory managed by the given pfTexture and these routines allow you to specify all loading parameters (source, origin, size, etc.). This is useful for loading different images for the same pfTexture in different graphics pipelines. pfSubloadTex() allows you to load a subsection of the texture tile by tile. A special pfTexFormat() formatting mode, PFTEX_SUBLOAD_FORMAT, allows part or all of the image in texture memory owned by the pfTexture to be replaced using pfApplyTex(), pfLoadTex(), or pfSubloadTex(), without having to go through the expensive reformatting phase. This allows you to quickly update the image of a pfTexture in texture memory. The PFTEX_SUBLOAD_FORMAT used with an appropriate pfTexLoadSize() and pfTexLoadOrigin() allows you to control what part of the texture will be loaded by subsequent calls to pfLoadTex() or pfApplyTex(). There are also different loading modes that cause pfApplyTex() to automatically reload or subload a texture from a specified source. If you want the image of a pfTexture to be updated upon every call to pfApplyTex(), you can set the loading mode of the pfTexture with pfTexLoadMode() to be PFTEX_BASE_AUTO_REPLACE. pfTexLoadImage() allows you 464 007-1680-100 Immediate Mode to continuously update the memory location of an IMAGE source texture without triggering any reformatting of the texture. Hint: There are texture formatting modes that can improve texture performance and these are the modes that are used by default by OpenGL Performer. Of most importance is the 16-bit texel internal formats. These formats cause the resulting texels to have 16 bits of resolution instead of the standard 32. These formats can have dramatically faster texture-fill performance and cause the texture to take up half the hardware texture memory. Therefore, they are strongly recommended and are used by default. There are different formats for each possible number of components to give a choice of how the compression is to be done. These formats are described in the pfTexFormat(3pf) man page. There may also be formatting modes for internal or external image formats for which OpenGL Performer does not have a token. However, the GL value can be specified. Specifying GL values will make your application GL-specific and may also cause future porting problems; so, it should only be done if absolutely necessary. The pfTexture class also allows you to define a set of textures that are mutually exclusive; they should always be applied to the same set of geometry; and, thus, they can share the same location in hardware texture memory. With pfTexList(tex, list) you can specify a list of textures to be in a texture set managed by the base texture, tex. The base texture is what gets applied with pfApplyTex(), or assigned to geometry through pfGeoStates. With pfTexFrame(), you can select a given texture from the list (–1 selects the base texture and is the default). This allows you to define a texture movie where each image is the frame of the movie. You can have an image on the base texture to display when the movie is not playing. There are additional loading modes for pfTexLoadMode() described in 007-1680-100 465 12: Graphics State Table 12-7 to control how the textures in the texture list share memory with the base texture. Table 12-7 Texture Load Modes PFTEX_LOAD_ modeToken Load Mode Values Description SOURCE SOURCE_IMAGE, SOURCE_FRAMEBUFFER PFTEX_SOURCE_VIDEO PFTEX_SOURCE_DMBUF PFTEX_SOURCE_DMVIDEO Source of image data is host memory image, framebuffer, default video source, digital media buffer, or a video library digital media buffer. BASE BASE_APPLY BASE_AUTO_SUBLOAD Loading of image for pfTexture is done as required for pfApply, or automatically subloaded upon every pfApply(). LIST LIST_APPLY LIST_AUTO_IDLE LIST_AUTO_SUBLOAD Loading of list texture image is done as separate apply, causes freeing of previous list texture in hardware texture memory, or is subloaded into memory managed by the base texture. VIDEO_ INTERLACED OFF, INTERLACED_ODD, INTERLACED_EVEN, Video input is interlaced or not and if so, which field (even or odd) is spatially higher. Texture list textures can share the exact graphics texture memory as the base texture but this has the restriction that the textures must all be the exact same size and format as the base texture. Texture list textures can also indicate that they are mutually exclusive, which will cause the texture memory of previous textures to be freed before applying the new texture. This method has no restrictions on the texture list, but is less efficient than the previous method. Finally, texture list textures can be treated as completely independent textures that should all be kept resident in memory for rapid access upon their application. The pfTexFilter() function sets a desired filter to a specified filtering method on a pfTexture. The minification and magnification texture filters are described with bitmask tokens. If filters are partially specified, OpenGL Performer fills the rest with machine-dependent fast defaults. The PFTEX_FAST token can be included in the bitmask to allow OpenGL Performer to make machine dependent substitutions where there are large performance differences. 466 007-1680-100 Immediate Mode There are a variety of texture filter functions that can improve the look of textures when they are minified and magnified. By default, textures use MIPmapping when minified (though this costs an extra 1/3 in storage space to store the minification levels). Each level of minification or magnification of a texture is twice the size of the previous level. Minification levels are indicated with positive numbers and magnification levels are indicated with nonpositive numbers. The default magnification filter for textures is bilinear interpolation. The use of detail textures and sharpening filters can improve the look of magnified textures. Detailing actually uses an extra detail texture that you provide that is based on a specified level of magnification from the corresponding base texture. The detail texture can be specified with the pfTexDetail() function. By default, MIPmap levels are generated for the texture automatically. OpenGL operation allows for the specification of custom MIPmap levels. Both MIPmap levels and detail levels can be specified with pfTexLevel(). The level number should be a positive number for a minification level and a nonpositive number for a magnification (detail) level. If you are providing your own minification levels, you must provide all log2(MAX(texSizeX, texSizeY)) minification levels. There is only one detail texture for a pfTexture. Standard MIPmap filtering can induce blurring on a texture if the texture is applied to a polygon which is angled away from the viewer. To reduce this blurring, an anisotropic filter can be used to improve visual quality. pfTexAnisotropy() sets the degree of anisotropy to be used by the specified pfTexture. The default degree of anisotropy is 1, which is the same as the standard isotropic filter. A value of 2 will apply a 2:1 anisotropic filter. The maximum degree of anisotropy can be queried with pfQuerySys(). Anisotropic filtering is supported on OpenGL implementations that support the GL_EXT_texture_filter_anisotropic extension. pfQueryFeature() can be used to determine if anisotropic filtering is supported on the current platform. If the environment variable PF_MAX_ANISOTROPY is set, then an anisotropic filter of the value specified by PF_MAX_ANISOTROPY will be applied to pfTextures that do not set the degree of anisotropy. The magnification filters use spline functions to control their rate of application as a function of magnification and specified level of magnification for detail textures. These splines can be specified with pfTexSpline(). The specification of the spline is a set of control points that are pairs of nondecreasing magnification levels (specified with nonpositive numbers) and corresponding scaling factors. Magnification filters can be applied to all components of a texture, only the RGB components of a texture, or to just the alpha components. OpenGL does not allow different magnification filters (between detail and sharpen) for RGB and alpha channels. 007-1680-100 467 12: Graphics State Note: The specification of detail textures may have GL dependencies and magnifications filters may not be available on all hardware configurations. The pfTexture man page describes these details. Texture Formats The format in which an image is stored in texture memory is defined with pfTexFormat(): void pfTexFormat(pfTexture *tex, int format, int type) The format variable specifies which format to set. Valid formats and their basic types include the following: • PFTEX_INTERNAL_FORMAT— Specifies how many bits per component are to be used in internal hardware texture memory storage. The default is 16 bits per full texel and is based on the number of components and external format. See the pfTexture man page for the list of supported formats. Floating point formats are supported only on selected platforms (for example, Onyx4 and Prism systems). • PFTEX_IMAGE_FORMAT—Describes the type of image data and must match the number of components, such as PFTEX_LUMINANCE, PFTEX_LUMINANCE_ALPHA, PFTEX_RGB, and PFTEX_RGBA. The default is the token in this list that matches the number of components. Other OpenGL selections can be specified with the GL token. • PFTEX_EXTERNAL_FORMAT—Specifies the format of the data in the pfTexImage array. The default is packed 8 bits per component. There are special, fast-loading hardware-ready formats, such as PFTEX_UNSIGNED_SHORT_5_5_5_1. • PFTEX_SUBLOAD_FORMAT—Specifies if the texture will be a subloadable paging texture. The default is FALSE. • PFTEX_CUBE_MAP—Specifies that the texture is a cube map texture that contains six images. The default is FALSE. Cube maps are supported only on selected platforms (for example, Onyx4 or Prism systems). In the case of cube maps, where there are six images, the images are specified using the function pfTexMultiImage(). The parameter imageIndex with value 0–5 specifies the cube face in the following order: min_x, max_x, min_y, max_y, min_z, max_z 468 007-1680-100 Immediate Mode For the order of faces and how the vector from the cube center to each texel is computed, see the sample program in following file: /usr/share/Performer/src/pguide/libpf/C++/cubeMap.C (IRIX and Linux) %PFROOT%\Src\pguide\libpf\C++\cubeMap.C (Microsoft Windows) All six images have to be specified, they must have the same size, and the value ns has to be equal to nt. The following are other variants of functions that are used by the cube maps: • pfTexMultiName() • pfGetTexMultiName() • pfLoadMultiTexFile() • pfSaveMultiTexFile() • pfSubloadMultiTex() • pfSubloadTexMultiLevel() Other than having six images, the cube maps are used as any other pfTexture. The exception is that subloads from other sources than user specified memory are not supported. In general, you will just need to specify the number of components in pfTexImage(). You may want to specify a fast-loading hardware-ready external format, such as PFTEX_UNSIGNED_SHORT_5_5_5_1, in which case OpenGL Performer automatically chooses a matching internal format. See the pfTexFormat(3pf) man page for more informaton on texture configuration details. Controlling Texture LOD with pfTexLOD You can control the levels of detail (LODs) of a texture that are accessed and used with pfTexLOD to force higher or lower MIPmap levels to be used when minifying. You can use this to give the graphics hardware a hint about what levels can be accessed (Impact hardware takes great advantage of such a hint) and you can use this to have multiple textures sharing a single MIPmap pyramid in texture memory. For example, a distant object and a close one may use different LODs of the same pfTexture texture. The pfGeoStates of those pfGeoSets would have different pfTexLOD objects that referenced 007-1680-100 469 12: Graphics State the proper texture LODs. pfTexLevel() would be used to specify and update the proper image for each LOD in the pfTexture. You can use LODs to specify to yourself and the GL which LODs of texture should be loaded from disk into main memory. For example, if the viewer is in one LOD, most of the texture in that LOD can often be viewed and, consequently, should be paged into texture memory. You can set LOD parameters on a pfTexture directly or use pfTexLOD. To use a pfTexLOD object, you do the following: 1. Set the ranges of the LOD using pfTLODRange() and their corresponding minimum and maximum resolution MIPmap. Because the minimum and maximum limits can be floating-point values, new levels can be smoothly blended in when they become available to avoid popping from one LOD to another. 2. Optionally, set the bias levels using pfTLODBias() to force blurring of a texture to simulate motion blur and depth of field, to force a texture to be sharper, or to compensate for asymmetric minification of a MIPmapped texture. Note: Any LOD settings on pfTexture take priority over current pfTexLOD settings. 3. Enable LOD control over texture by using the following methods: pfEnable(PFEN_TEXLOD); pfGeoState::pfGStateMode(myTxLOD, PFSTATE_ENTEXLOD, ON); where myTxLOD is an instance of pfTexLOD, and ON is a nonzero integer. 4. Apply the LOD settings to the texture using pfApplyTLOD(). See the following sample program for an example of using a pfTexLOD: /usr/share/Performer/src/pguide/libpr/C/texlod.c (IRIX and Linux) %PFROOT%\Src\pguide\libpr\C\texlod.con (Microsoft Windows) Setting the Texture Environment with pfTexEnv The environment specifies how the colors of the geometry, potentially lit, and the texture image interact. This is described with a pfTexEnv object. The mode of interaction is set with pfTEnvMode() and valid modes include the following: • 470 PFTE_MODULATE—The gray scale of the geometry is mixed with the color of the texture (the default). 007-1680-100 Immediate Mode This option multiplies the shaded color of the geometry by the texture color. If the texture has an alpha component, the alpha value modulates the geometry’s transparency; for example, if a black and white texture, such as text, is applied to a green polygon, the polygon remains green and the writing appears as dark green lettering. • PFTE_DECAL—The texture alpha component acts as a selector between 1.0 for the texture color and 0.0 for the base color to decal an image onto geometry. • PFTE_BLEND—The alpha acts as a selector between 0.0 for the base color and 1.0 for the texture color modulated by a constant texture blend color specified with pfTEnvBlendColor(). The alpha/intensity components are multiplied. • PFTE_ADD—The RGB components of the base color are added to the product of the texture color modulated by the current texture environment blend color. The alpha/intensity components are multiplied. Automatic Texture Coordinate Generation Automatic texture coordinate generation is provided with the pfTexGen state attribute. pfTexGen closely corresponds to OpenGL’s glTexGen() function. When texture coordinate generation is enabled, a pfTexGen applied with pfApplyTGen() automatically generates texture coordinates for all rendered geometry. Texture coordinates are generated from geometry vertices according to the texture generation mode set with pfTGenMode(). Available modes and their function are listed in Table 12-8. Table 12-8 007-1680-100 Texture Generation Modes Mode Token Description PFTG_OFF Disables texture coordinate generation. PFTG_OBJECT_LINEAR Generates the texture coordinate as the distance from plane in object space. PFTG_EYE_LINEAR Generates the texture coordinate as the distance from plane in eye space. The plane is transformed by the inverse of the ModelView matrix when tgen, the pfTexGen, is applied. 471 12: Graphics State Table 12-8 Texture Generation Modes (continued) Mode Token Description PFTG_EYE_LINEAR_IDENT Generates the texture coordinate as the distance from plane in eye space. The plane is not transformed by the inverse of the ModelView matrix. PFTG_SPHERE_MAP Generates the texture coordinate based on the view vector reflected about the vertex normal in eye space. PFTG_OBJECT_DISTANCE_TO_LINE Sets the texture coordinate as the distance in object space from the vertex to a line specified with a point and direction vector through pfTGenPoint. PFTG_EYE_DISTANCE_TO_LINE Sets the texture coordinate as the distance in eye space from the eye to a line specified with pfTGenPoint through the vertex. PFTG_REFLECTION_MAP Sets the texture coordinate to the view vector reflected about the vertex normal in eye space. PFTG_NORMAL_MAP Sets the texture coordinate to the vertex normal in eye space. Some modes refer to a plane which is set with pfTGenPlane() and to a line that is specified as a point and direction with pfTGenPoint(). The default texture generation mode for all texture coordinates is PFTG_OFF. The function pfGetTGenMode() returns the mode of the pfTexGen. See the man page for the OpenGL function glTexGen() for the specific mathematics of the generation modes for texture coordinates. Lighting OpenGL Performer lighting is an extension of graphics library lighting (see glLight() and related functions in OpenGL). The light embodies the color, position, and type (for example, infinite or spot) of the light. The light model specifies the environment for infinite (the default) or local viewing, and two-sided illumination. The lighting model describes the type of lighting operations to be considered, including local lighting, two-sided lighting, and light attenuation. The fastest light model is infinite, single-sided lighting. A pfLightModel state attribute object is created with 472 007-1680-100 Immediate Mode pfNewLModel(). A light model also allows you to specify ambient light for the scene, such as might come from the sun with pfLModelAmbient(). The pfLights are created by calling pfNewLight(). A light has color and position. The light colors are specified with pfLightColor() as follows: void pfLightColor(pfLightSource* lsource, int which, float r, float g, float b); which specifies one of three light colors as follows: • PFLT_AMBIENT • PFLT_DIFFUSE • PFLT_SPECULAR You to position the light source using pfLightPos(): void pfLightPos(pfLight* light, float x, float y, float z, float w); The variable w is the distance between the location in the scene defined by (x, y, z) and the light source lsource. If w equals 0, lsource is infinitely far away and (x, y, z) defines a vector pointing from the origin in the direction of lsource; if w equals 1, lsource is located at the position (x, y, z). The default position is (0, 0, 1, 0), directly overhead and infinitely far away. The pfLights are attached to a pfGeoState through the PFSTATE_LIGHTS attribute. The transformation matrix that is on the matrix stack at the time the light is applied controls the interpretation of the light source direction: • To attach a light to the viewer (like a miner’s head-mounted light), call pfLightOn() only once with an identity matrix on the stack. • To attach a light to the world (like the sun or moon), call pfLightOn() every frame with only the viewing transformation on the stack. • To attach a light to an object (like the headlights of a car), call pfLightOn() every frame with the combined viewing and modeling transformation on the stack. The number of lights you can have turned on at any one time is limited by PF_MAX_LIGHTS, just as is true with the graphics libraries. 007-1680-100 473 12: Graphics State Note: In previous versions, attenuation was also part of the light model definition. In OpenGL, attenuation is defined per light . The libpr API for setting it is pfLightAtten(). Note: libpf applications can include light sources in a scene graph with the pfLightSource node. Materials OpenGL Performer materials are an extension of graphics library material (see glMaterial()). pfMaterials encapsulate the ambient, diffuse, specular, and emissive colors of an object as well as its shininess and transparency. A pfMaterial is created by calling pfNewMtl(). As with any of the other attributes, a pfMaterial can be referenced in a pfGeoState, captured by a display list, or invoked as an immediate mode command. The pfMaterials, by default, allow object colors to set the ambient and diffuse colors. This allows the same pfMaterial to be used for objects of different colors, removing the need for material changes and thus improving performance. This mode can be changed with pfMtlColorMode(mtl, side, PFMTL_CMODE_*). OpenGL allows front or back materials to track the current color. If the same material is used for both front and back materials, there is no difference in functionality. With the function pfMtlSide() you can specify whether to apply the the material on the side facing the viewer (PFMTL_FRONT), the side not facing the viewer (PFMTL_BACK), or both (PFMTL_BOTH). Back-sided lighting will only take affect if there is a two-sided lighting model active. Two-sided lighting typically has some significant performance cost. Object materials only have an effect when lighting is active. Color Tables A pfColortable substitutes its own color array for the normal color attribute array (PFGS_COLOR4) of a pfGeoSet. This allows the same geometry to appear differently in different views simply by applying a different pfColortable for each view. By leaving the selection of color tables to the global state, you can use a single call to switch color tables for an entire scene. In this way, color tables can simulate time-of-day changes, infrared imaging, psychedelia, and other effects. 474 007-1680-100 Immediate Mode The pfNewCtab() function creates and returns a handle to a pfColortable. As with other attributes, you can specify which color table to use in a pfGeoState or you can use pfApplyCtab() to set the global color table, either in immediate mode or in a display list. For an applied color table to have effect, color table mode must also be enabled. Fog A pfFog is created by calling pfNewFog(). As with any of the other attributes, a pfFog can be referenced in a pfGeoState, captured by a display list, or invoked as an immediate mode command. Fog is the atmospheric effect of aerosol water particles that occlude vision over distance. SGI graphics hardware can simulate this phenomenon in several different fashions. A fog color is blended with the resultant pixel color based on the range from the viewpoint and the fog function. pfFog supports several different fogging methods. Table 12-9 lists the pfFog tokens and their corresponding actions. Table 12-9 pfFog Tokens pfFog Token Action PFFOG_VTX_LIN Compute fog linearly at vertices. PFFOG_VTX_EXP Compute fog exponentially at vertices (ex). PFFOG_VTX_EXP2 Compute fog exponentially at vertices (ex squared). PFFOG_PIX_LIN Compute fog linearly at pixels. PFFOG_PIX_EXP Compute fog exponentially at pixels (ex). PFFOG_PIX_EXP2 Compute fog exponentially at pixels (ex squared). PFFOG_PIX_SPLINE Compute fog using a spline function at pixels. The pfFogType() function uses these tokens to set the type of fog. A detailed explanation of fog types is given in the man pages pfFog(3pf) and glFog(3g). You can set the near and far edges of the fog with pfFogRange(). For exponential fog functions, the near edge of fog is always zero in eye coordinates. The near edge is where the onset of fog blending occurs, and the far edge is where all pixels are 100% fog color. 007-1680-100 475 12: Graphics State The token PFFOG_PIX_SPLINE selects a spline function to be applied when generating the hardware fog tables. This is further described in the pfFog(3pf) man page. Spline fog allows you to define an arbitrary fog ramp that can more closely simulate real-world phenomena like horizon haze. For best fogging effects, the ratio of the far to the near clipping planes should be minimized. In general, it is more effective to add a small amount to the near plane than to reduce the far plane. Highlights OpenGL Performer provides a mechanism for highlighting geometry with alternative rendering styles, useful for debugging and interactivity. A pfHighlight, created with pfNewHlight(), encapsulates the state elements and modes for these rendering styles. A pfHighlight can be applied to an individual pfGeoSet with pfGSetHlight() or can be applied to multiple pfGeoStates through a pfGeoState or pfApplyHlight(). The highlighting effects are added to the normal rendering phase of the geometry. pfHighlights make use of special outlining and fill modes and have a concept of a foreground color and a background color that can both be set with pfHlightColor(). The available rendering styles can be combined by ORing together tokens for pfHlightMode() and are described in Table 12-10. Table 12-10 PFHL_ Mode Bitmask Token LINES pfHlightMode() Tokens Description Outlines the triangles in the highlight foreground color according to pfHlightLineWidth(). 476 LINESPAT LINESPAT2 Outlines triangles with patterned lines in the highlight foreground color or in two colors using the background color. FILL Draws geometry with the highlight foreground color. Combined with SKIP_BASE, this is a fast highlighting mode. FILLPAT FILLPAT2 Draws the highlighted geometry as patterned with one or two colors. FILLTEX Draws a highlighting fill pass with a special highlight texture. LINES_R FILL_R Reverses the highlighting foreground and background colors for lines and fill, respectively. 007-1680-100 Immediate Mode Table 12-10 pfHlightMode() Tokens (continued) PFHL_ Mode Bitmask Token Description POINTS Renders the vertices of the geometry as points according to pfHlightPntSize(). NORMALS Displays the normals of the geometry with lines according to pfHlightNormalLength(). BBOX_LINES BBOX_FILL Displays the bounding box of the pfGeoSet as outlines and/or a filled box. Combined with PFHL_SKIP_BASE, this is a fast highlighting mode. SKIP_BASE Causes the normal drawing phase of the pfGeoSet to be skipped. This is recommended when using PFHL_FILL or PFHL_BBOX_FILL. For a demonstration of the highlighting styles, see the sample program /usr/share/Performer/pguide/src/libpr/C/hlcube.c on IRIX and Linux and %PFROOT%\Src\pguide\libpr\C\hlcube.c on Microsoft Windows. Graphics Library Matrix Routines OpenGL Performer provides extensions to the standard graphics library matrix-manipulation functions. These functions are similar to their graphics library counterparts, with the exception that they can be placed in OpenGL Performer display lists. Table 12-11 lists and describes the matrix manipulation routines. Table 12-11 007-1680-100 Matrix Manipulation Routines Routines Action pfScale() Concatenate a scaling matrix. pfTranslate() Concatenate a translation matrix. pfRotate() Concatenate a rotation matrix. pfPushMatrix() Push down the matrix stack. pfPushIdentMatrix() Push the matrix stack and load an identity matrix on top. pfPopMatrix() Pop the matrix stack. 477 12: Graphics State Table 12-11 Matrix Manipulation Routines (continued) Routines Action pfLoadMatrix() Add a matrix to the top of the stack. pfMultMatrix() Concatenate a matrix. Sprite Transformations A sprite is a special transformation used to efficiently render complex geometry with axial or point symmetry. A classic sprite example is a tree which is rendered as a single, texture-mapped quadrilateral. The texture image is of a tree and has an alpha component whose values “etch” the tree shape into the quad. In this case, the sprite transformation rotates the quad around the tree trunk axis so that it always faces the viewer. Another example is a puff of smoke which again is a texture-mapped quad but is rotated about a point to face the viewer so it appears the same from any viewing angle. The pfSprite transformation mechanism supports both these simple examples as well as more complicated ones involving arbitrary 3D geometry. A pfSprite is a structure that is manipulated through a procedural interface. It is different from attributes like pfTexture and pfMaterial since it affects transformation, rather than state related to appearance. A pfSprite is activated with pfBeginSprite(). This enables sprite mode and any pfGeoSet that is drawn before sprite mode is ended with pfEndSprite() will be transformed by the pfSprite. First, the pfGeoSet is translated to the location specified with pfPositionSprite(). Then, it is rotated, either about the sprite position or axis depending on the pfSprite’s configuration. Note that pfBeginSprite(), pfPositionSprite(), and pfEndSprite() are display listable and this will be captured by any active pfDispList. 478 007-1680-100 Immediate Mode A pfSprite’s rotation mode is set by specifying the PFSPRITE_ROT token to pfSpriteMode(). In all modes, the Y axis of the geometry is rotated to point to the eye position. Rotation modes are listed below. Table 12-12 pfSprite Rotation Modes PFSPRITE_ Rotation Token Rotation Characteristics AXIAL_ROT Geometry’s Z axis is rotated about the axis specified with pfSpriteAxis(). POINT_ROT_EYE Geometry is rotated about the sprite position with the object coordinate Z axis constrained to the window coordinate Y axis; that is, the geometry’s Z axis stays “upright.” POINT_ROT_WORLD Geometry is rotated about the sprite position with the object coordinate Z axis constrained to the sprite axis. Rather than using the graphics hardware’s matrix stack, pfSprites transform small pfGeoSets on the CPU for improved performance. However, when a pfGeoSet contains a certain number of primitives, it becomes more efficient to use the hardware matrix stack. While this threshold is dependent on the CPU and graphics hardware used, you may specify it with the PFSPRITE_MATRIX_THRESHOLD token to pfSpriteMode(). The corresponding value is the minimum vertex requirement for hardware matrix transformation. Any pfGeoSet with fewer vertices will be transformed on the CPU. If you want a pfSprite to affect non-pfGeoSet geometry, you should set the matrix threshold to zero so that the pfSprite will always use the matrix stack. When using the matrix stack, pfBeginSprite() pushes the stack and pfEndSprite() pops the matrix stack so the sprite transformation is limited in scope. The pfSprites are dependent on the viewing location and orientation and the current modeling transformation. You can specify these with calls to pfViewMat() and pfModelMat(), respectively. Note that libpf-based applications need not call these routines since libpf does it automatically. Display Lists The libpr library supports display lists, which can capture and later execute libpr graphics commands. pfNewDList() creates and returns a handle to a new pfDispList. A pfDispList can be selected as the current display list with pfOpenDList(), which puts the system in display list mode. Any subsequent libpr graphics commands, such as 007-1680-100 479 12: Graphics State pfTransparency(), pfApplyTex(), or pfDrawGSet() are added to the current display list. Commands are added until pfCloseDList() returns the system to immediate mode. It is not valid to have multiple pfDispLists open at a given time but a pfDispList may be reopened, in which case, commands are appended to the end of the list. Once a display list is constructed, it can be executed by calling pfDrawDList(), which traverses the list and sends commands down the Geometry Pipeline. The pfDispLists are designed for multiprocessing, where one process builds a display list of the visible scene and another process draws it. The function pfResetDList() facilitates this by making pfDispLists reusable. Commands added to a reset display list overwrite any previously entered commands. A display list is typically reset at the beginning of a frame and then filled with the visible scene. The pfDispLists support concurrent multiprocessing, where the producer and consumer processes simultaneously write and read the display list. The PFDL_RING argument to pfNewDList() creates a ring buffer or FIFO-type display list. pfDispLists automatically ensure ring buffer consistency by providing synchronization and mutual exclusion to processes on ring buffer full or empty conditions. For more information and the application of display lists, see Chapter 15, “ClipTextures.” Combining Display Lists The contents of one pfDispList may be appended to a second pfDispList by using the function, pfAppendDList(). All pfDispList elements in src are appended to the pfDispList dlist. Alternately, you can append the contents of one pfDispList to a second pfDispList by using the function pfDispList::append(). All pfDispList elements in src are appended to the pfDispList on which the append method is invoked. State Management A pfState is a structure that represents the entire libpr graphics state. A pfState maintains a stack of graphics states that can be pushed and popped to save and restore the state. The top of the stack describes the current graphics state of a window as it is known to OpenGL Performer. 480 007-1680-100 Immediate Mode The pfInitState() function initializes internal libpr state structures and should be called at the beginning of an application before any pfStates are created. Multiprocessing applications should pass a usinit() semaphore arena pointer to pfInitState(), such as pfGetSemaArena(), so OpenGL Performer can safely manage state between processes. pfNewState() creates and returns a handle to a new pfState, which is typically used to define the state of a single window. If using pfWindows, discussed in Chapter 16, “Windows,” a pfState is automatically created for the pfWindow when the window is opened and the current pfState is switched when the current pfWindow changes. pfSelectState() can be used to efficiently switch a different, complete pfState. pfLoadState() forces the full application of a pfState. Pushing and Popping State The pfPushState() function pushes the state stack of the currently active pfState, duplicating the top state. Subsequent modifications of the state through libpr routines are recorded in the top of the stack. Consequently, a call to pfPopState() restores the state elements that were modified after pfPushState(). The code fragment in Example 12-2 illustrates how to push and pop state. Example 12-2 Pushing and Popping Graphics State /* set state to transparency=off and texture=brickTex */ pfTransparency(PFTR_OFF); pfApplyTex(brickTex); /* ... draw geometry here using original state ... */ /* save old state. establish new state */ pfPushState(); pfTransparency(PFTR_ON); pfApplyTex(woodTex); /* ... draw geometry here using new state ...*/ /* restore state to transparency=off and texture=brickTex */ pfPopState(); 007-1680-100 481 12: Graphics State State Override The pfOverride() function implements a global override feature for libpr graphics state and attributes. pfOverride() takes a mask that indicates which state elements to affect and a value specifying whether the elements should be overridden. The mask is a bitwise OR of the state tokens listed previously. The values of the state elements at the time of overriding become fixed and cannot be changed until pfOverride() is called again with a value of zero to release the state elements. The code fragment in Example 12-3 illustrates the use of pfOverride(). Example 12-3 Using pfOverride() pfTransparency(PFTR_OFF); pfApplyTex(brickTex); /* * Transparency will be disabled and only the brick texture * will be applied to subsequent geometry. */ pfOverride(PFSTATE_TRANSPARENCY | PFSTATE_TEXTURE, 1); /* Draw geometry */ /* Transparency and texture can now be changed */ pfOverride(PFSTATE_TRANSPARENCY | PFSTATE_TEXTURE, 0); pfGeoState A pfGeoState encapsulates all the rendering modes, values, and attributes managed by libpr. See “Rendering Modes” on page 453, “Rendering Values” on page 458, and “Rendering Attributes” on page 459 for more information. pfGeoStates provide a mechanism for combining state into logical units and define the appearance of geometry. For example, you can set a brick-like texture and a reddish-orange material on a pfGeoSet and use it when drawing brick buildings. You can specify texture matricies on pfGeoSets. 482 007-1680-100 Immediate Mode Local and Global State There are two levels of rendering state: local and global. A record of both is kept in the current pfState. The local state is that defined by the settings of the current pfGeoState. The rendering state and attributes of a pfGeoState can be either locally set or globally inherited. If all state elements are set locally, a pfGeoState becomes a full graphics context—that is, all state is then defined at the pfGeoState level. Global state elements are set with libpr immediate-mode routines like pfEnable(), pfApplyTex(), pfDecal(), or pfTransparency() or by drawing a pfDispList containing these commands with pfDrawDList(). Local state elements for subsequent pfGeoSets are set by applying a pfGeoState with pfApplyGState() (note that pfDrawGSet() automatically calls pfApplyGState() if the pfGeoSet has an attached pfGeoState). The state elements applied by a pfGeoState are those modes, enables, and attributes that are explicitly set on the pfGeoState. Those settings revert back to the pfState settings for the next call to pfApplyGState(). A pfGeoState can be explicitly loaded into a pfState to affect future pfGeoStates with pfLoadGState(). Note: By default, all state elements are inherited from the global state. Inherited state elements are evaluated faster than values that have been explicitly set. While it can be useful to have all state defined at the pfGeoState level, it usually makes sense to inherit most state from global default values and then explicitly set only those state elements that are expected to change often. Examples of useful global defaults are lighting model, lights, texture environment, and fog. Highly variable state is likely to be limited to a small set such as textures, materials, and transparency. For example, if the majority of your database is lighted, simply configure and enable lighting at the beginning of your application. All pfGeoStates will be lighted, except the ones for which you explicitly disable lighting. Then, attach different pfMaterials and pfTextures to pfGeoStates to define specific state combinations. Note: Use caution when enabling modes in the global state. These modes may have cost even when they have no visible effect. Therefore, geometry that cannot use these modes should have a pfGeoState that explicitly disables the mode. Modes that require special care include the texturing enable and transparency. You specify that a pfGeoState should inherit state elements from the global default with pfGStateInherit(gstate, mask). mask is a bitmask of tokens that indicates which state 007-1680-100 483 12: Graphics State elements to inherit. These tokens are listed in the “Rendering Modes” on page 453, “Rendering Values” on page 458, and “Rendering Attributes” on page 459 sections of this chapter. For example, PFSTATE_ENLIGHTING | PFSTATE_ENTEXTURE makes gstate inherit the enable modes for lighting and texturing. A state element ceases to be inherited when it is set in a pfGeoState. Rendering modes, values, and attributes are set with pfGStateMode(), pfGStateVal(), and pfGStateAttr(), respectively. For example, to specify that gstate is transparent and textured with treeTex, use the following: pfGStateMode(gstate, PFSTATE_TRANSPARENCY, PFTR_ON); pfGStateAttr(gstate, PFSTATE_TEXTURE, treeTex); Applying pfGeoStates Use pfApplyGState() to apply the state encapsulated by a pfGeoState to the Geometry Pipeline. The effect of applying a pfGeoState is similar to applying each state element individually. For example, if you set a pfTexture and enable a decal mode on a pfGeoState, applying it essentially calls pfApplyTex() and pfDecal(). If in display-list mode, pfApplyGState() is captured by the current display list. State is (logically) pushed before and popped after pfGeoStates are applied so that pfGeoStates do not inherit state from each other. This is a very powerful and convenient characteristic since, as a result, pfGeoStates are order-independent, and you do not have to worry about one pfGeoState corrupting another. The code fragment in Example 12-4 illustrates how pfGeoStates inherit state. Example 12-4 Inheriting State /* gstateA should be textured */ pfGStateMode(gstateA, PFSTATE_ENTEXTURE, PF_ON); /* gstateB inherits the global texture enable mode */ pfGStateInherit(gstateB, PFSTATE_ENTEXTURE); /* Texturing is disabled as the global default */ pfDisable(PFEN_TEXTURE); /* Texturing is enabled when gstateA is applied */ pfApplyGState(gstateA); /* Draw geometry that will be textured */ /* The global texture enable mode of OFF is restored 484 007-1680-100 Immediate Mode so that gstateB is NOT textured. */ pfApplyGState(gstateB); /* Draw geometry that will not be textured */ The actual pfGeoState pop is a "lazy" pop that does not happen unless a subsequent pfGeoState requires the global state to be restored. This means that the actual state between pfGeoStates is not necessarily the global state. If a return to global state is required, call pfFlushState() to restore the global state. Any modification to the global state made using libpr functions—pfTransparency(), pfDecal(), and so on—becomes the default global state. For best performance, set as little local pfGeoState state as possible. You can accomplish this by setting global defaults that satisfy the majority of the requirements of the pfGeoStates being drawn. By default, all pfGeoState state is inherited from the global default. pfGeoSets and pfGeoStates There is a special relationship between pfGeoSets and pfGeoStates. Together they completely define both geometry and graphics state. You can attach a pfGeoState to a pfGeoSet with pfGSetGState() to specify the appearance of geometry. Whenever the pfGeoSet is drawn with pfDrawGSet(), the attached pfGeoState is first applied using pfApplyGState(). If a pfGeoSet does not have a pfGeoState, its state description is considered undefined. To inherit all values from the global pfState, a pfGeoSet should have a pfGeoState with all values set to inherit, which is the default. This combination of routines allows the application to combine geometry and state in high-performance units that are unaffected by rendering order. To further increase performance, sharing pfGeoStates among pfGeoSets is encouraged. Table 12-13 lists and describes the pfGeoState routines. Table 12-13 007-1680-100 pfGeoState Routines Routiine Description pfNewGState() Create a new pfGeoState. pfCopy() Make a copy of the pfGeoState. pfDelete() Delete the pfGeoState. pfGStateMode() Set a specific state mode. 485 12: Graphics State Table 12-13 pfGeoState Routines (continued) Routiine Description pfGStateVal() Set a specific state value. pfGStateAttr() Set a specific state attribute. pfGStateInherit() Specify which state elements are inherited from the global state. pfApplyGState() Apply pfGeoState’s non-inherited state elements to graphics. pfLoadGState() Load pfGeoState’s settings into the pfState, inherited by future pfGeoStates. pfGetCurGState() Return the current pfGeoState in effect. pfGStateFuncs() Assign pre/post callbacks to pfGeoState. pfApplyGStateTable() Specify a able of pfGeoStates used for indexing. Figure 12-1 diagrams the conceptual structure of a pfGeoState. 486 007-1680-100 Immediate Mode t igh pfL del Mo a pfM two loc -side al/i fla nfin g am ite fla b atte ient g nua tion on/ pf of am f tag bie col nt pos or dire ition c spr tion e exp ad one nt p mo des tat Lig e ues ligh tm ode l ligh ts [ fro 8] nt m ate rial tex tur e ee nvi rom fog ial ent or t tex ter alp am ha bi diff ent em use is shi sion nin spe ess cul ar rial km col si iz iz i com ze com e com ze com e po p p p couonen couonent couonen cou nent n t nt n t nt ima t ima ima t ima ge ge ge ge mo mo mo mo d d d d det esdeta esdet esdeta es a ail tex il text il tex il text ure tur tur ure e e a pfM ate bac tur e ure ure ure tur ext fText fText pfTex ps ps ial alp am ha bi diff ent em use is shi sion ni spe ness cul ar val ht tex psfT oS fGe ter abl e mo tur del eg pfL ene v Env Env EnvxEn rati Poi x tex tex texpfTteextfTexpfTexpfTe tur tu tur p ure e in re i e in in d ble bdleex nbdleex dbelexn ex nd nd nd d m mo mo mo od des de des es s on ntS tate og col pfF or sta rt end le e ure ure ure tur ext fText fText pfTex T f p ps ps s si iz iz i com ze com e com ze com e po p p p couonen couonent couonen cou nent n t nt n t nt ima t ima ima t ima ge ge ge ge mo mo mo mo d d d d det esdeta esdet esdeta es a ail tex il text il tex il text ure tur tur ure e e Figure 12-1 007-1680-100 tab en eGxeGn T f p ex n pfeTxGen T f p exGe pfT rtab lo fCo p le a d tab dress le s ize pfGeoState Structure 487 12: Graphics State Multitexture Support in pfGeoState Some graphic hardware supports the use of multiple texture maps on a single polygon. These multiple texture maps are blended together according to a collection of texture environments. Figure 12-2 demonstrates the OpenGL definition for generating the color of a multitextured pixel. The figure assumes that the hardware has four texture units and so each pixel can receive contribution from four texture maps. : Shaded color Texture 0 Texture 1 Texture 2 Texture 3 Figure 12-2 TexEnv 0 TexEnv 1 TexEnv 2 TexEnv 3 Final Pixel Color Generating the Color of a Multitextured Pixel In the figure, the shaded and un-textured color of a pixel enters the first texture blending unit together with the texture color computed by the first texture unit. The texture environment marked TexEnv 0 determines the math operation between the two. The output color of this operation feeds the second texture blending unit together with the texture color computed by the second texture unit. This process continues four times until the final color of the pixel is generated. The pfGeoState class allows specifying multiple texture maps on a single pfGeoSet. All these texture maps will be applied when the pfGeoSet is applied (providing that the graphic hardware has enough texture mapping units). pfGeoState also allows specifying multiple pfTexEnv, pfTexGen, pfTexMat, and pfTexLOD objects—one for each pfTexture. 488 007-1680-100 Immediate Mode The following code fragment shows how to add multiple textures to a pfGeoState: { pfGeoState *gstate; pfTexture *tex; pfTexEnv *tev; gstate = pfNewGState (pfGetSharedArena()); for (i = 0 ; i < PF_MAX_TEXTURES ; i ++) { /* Load texture # i from a file */ tex = pfNewTex (pfGetSharedArena()); pfLoadTexFile (tex, texture_file_name[i]); tev = pfNewTEnv (pfGetSharedArena()); /* Enable texture unit # i on the pfGeoState. */ pfGStateMultiMode (gstate, PFSTATE_ENTEXTURE, i, 1); /* Attach texture for texture unit # i */ pfGStateMultiAttr (gstate, PFSTATE_TEXTURE, i, tex); /* Attach texture environment for texture unit # i */ pfGStateMultiAttr (gstate, PFSTATE_TEXENV, i, tev); } } Notes: 007-1680-100 • pfGeoState recognizes texture units starting at the first array (index of 0) and ending immediately before the first disabled texture unit. For example, enabling texture units 0, 1, and 3 is equivalent to enabling only texture units 0 and 1. • pfGeoState can inherit all or none of the texture units. It is enough to specify one texture unit in order to avoid inheriting any other texture unit. In order to inherit all texture units, one must specify no texture units on the pfGeoState. • For every texture unit enabled, the application must provide texture coordinates. Neither OpenGL Performer nor OpenGL will share texture coordinates between texture units. There are two ways to set texture coordinates: • Specifying a pfTexGen for a texture unit • Specifying a texture-coordinate attribute array for a texture unit on a pfGeoSet See section “Attributes” in Chapter 8. 489 Chapter 13 13. Shaders Recent graphics hardware allows you to write vertex and fragment programs to replace the corresponding fixed functionality of earlier systems. An independently compiliable unit of such programs is referred to as a shader. OpenGL Performer supports the OpenGL Shading Language (GLSL), which provides a platform-independent interface for such programs. This chapter describes the OpenGL Performer interface to GLSL. This implementation is based on two new classes, which are described in the following sections: • “The pfShaderProgram Class” on page 491 • “The pfShaderObject Class” on page 497 • “Example Code” on page 500 Note: In Perfly, you can enable/disable shader programs by using a keyboard entry of a. The pfShaderProgram Class The pfShaderProgram class encapsulates the functionality associated with OpenGL shader programs. A pfShaderProgram is a comprised of a collection of pfShaderObjects and an collection of uniform variables that are an opaque type and can only be accessed through an index from the pfShaderProgram interface. This section covers the following topics: 007-1680-100 • “Allocating Memory for a Shader Program” • “Creating a Shader Program” • “Applying Shader Programs” 491 13: Shaders Allocating Memory for a Shader Program The function pfNewShaderObject() creates and returns a handle to a pfShaderProgram. The value arena specifies a malloc() arena out of which the pfShaderProgram is allocated or NULL for allocation off the process heap. You can delete pfShaderPrograms with pfDelete(). The function new(arena) allocates a pfShaderProgram from the specified memory arena, or from the heap if arena is NULL. The function allocates a pfShaderProgram from the default memory arena (see the pfGetSharedArena man page). Like other pfObjects, pfShaderPrograms cannot be automatically created statically on the stack or in arrays. Delete pfShaderPrograms with pfDelete() rather than with the delete operator. The function pfGetShaderObjectClassType() returns the pfType* for the class pfShaderProgram. The pfType* returned by pfGetShaderObjectClassType() is the same as the pfType* returned by invoking pfGetType(), the virtual function getType() on any instance of class pfShaderProgram. Because OpenGL Performer allows subclassing of built-in types when decisions are made based on the type of an object, use pfIsOfType() the member function isOfType() to test if an object is of a type derived from an OpenGL Performer type rather than to test for strict equality of the pfType*s. In order to use a pfShaderProgram as a piece of state, you must specify it as an attribute for a pfGeoState and enable that mode, as shown in the following code: pfGeoState *gState = pfNewGState(arena); pfShaderProgram *sProg = pfNewSProg(arena); pfGStateMode(gState, PFSTATE_ENSHADPROG, PF_ON); pfGStateAttr(gState, PFSTATE_SHADPROG, sProg); 492 007-1680-100 The pfShaderProgram Class Creating a Shader Program Creating a valid pfShaderProgram involves the following steps: • Adding a set of pfShaderObjects to the shader program • Specifying uniform variables (optional) • Clamping uniform variables (optional) • Normalizing uniform variables (optional) Adding pfShaderObjects to a Shader program For shader programs, you can add, replace, and delete pfShaderObjects with the following functions, respectively: pfSProgAddShader() pfSProgReplaceShader() pfSProgRemoveShader() In addition to the methods for adding/deleting/replacing shader objects from a shader program, you can query an existing pfShaderProgram to determine the number of associated shader objects with pfGetSProgNumShaders(). To retreive a pointer to the ith shader object, use pfGetSProgShader(). Specifying Uniform Variables (optional) In addition to specifying a set of pfShaderObjects for a pfShaderProgram, you can specify a set of uniform variables for the program. These uniforms, which are not necessarily the same as those set internally by OpenGL, can then be referenced by index. A uniform variable is comprised of the following: 007-1680-100 • A name (stored as a GLcharARB*) • A type • A value indicating the number of variables of that type being stored in the uniform • A flag indicating if the value is to be clamped to a minimum and/or maximum value (or not at all) 493 13: Shaders • A flag indicating if the value should be normalized (for those types which can be normalized) • Pointers to the current value (as well as the minimum and maximum values for this uniform variable if, indeed, it is clamped) Table 13-1 shows the uniform variable types. Table 13-1 494 Uniform Variable Types Type Description PFUNI_FLOAT1 Single UGLfloat PFUNI_FLOAT2 Array of two GLfloats PFUNI_FLOAT3 Array of three GLfloats PFUNI_FLOAT4 Array of four GLfloats PFUNI_INT1 Single GLint PFUNI_INT2 Array of two GLints PFUNI_INT3 Array of three GLints PFUNI_INT4 Array of four GLints PFUNI_BOOL1 Single GLint specifying boolean value PFUNI_BOOL2 Array of two GLints specifying boolean values PFUNI_BOOL3 Array of three GLints specifying boolean values PFUNI_BOOL4 Array of four GLints specifying boolean values PFUNI_MAT2 Four GLfloats specifying 2x2 matrix PFUNI_MAT3 Nine GLfloats specifying 3x3 matrix PFUNI_MAT4 Sixteen GLfloats specifying 4x4 matrix PFUNI_SAMP1D Single GLint specifying which texture unit to query for this sampler PFUNI_SAMP2D Single GLint specifying which texture unit to query for this sampler 007-1680-100 The pfShaderProgram Class Table 13-1 Uniform Variable Types (continued) Type Description PFUNI_SAMP3D Single GLint specifying which texture unit to query for this sampler PFUNI_SAMPCUBE Single GLint specifying which texture unit to query for this sampler PFUNI_SAMP1DSHADOW Single GLint specifying which texture unit to query for this sampler PFUNI_SAMP2DSHADOW Single GLint specifying which texture unit to query for this sampler Adding Uniform Variables to Shader Programs In order to add a uniform variable to a pfShaderProgram, use pfSprogAddUniform(), whose parameters follow: name Specifies the name for the uniform variable. uniType Specifies one of the types listed in Table 13-1. size Indicates how many variables of this type will be found in the fourth and final parameter data. Internally, OpenGL Performer will make a copy of this data if it is not a pointer to a pfMemory; if it is, then the reference count for this piece of memory will get incremented and no copy will be performed. For example, the following code adds a uniform variable called scaleFactor, which is a 4-byte float (size of a GLfloat) set to 0.5: int uniformIndex; GLfloat scale = 0.5f; pfShaderProgram *sProg = pfNewSprog(arena); uniformIndex = pfSProgAddUniform(sProg, "scaleFactor", PFUNI_FLOAT1, 1, &scale); 007-1680-100 495 13: Shaders Clamping Uniform Variables (optional) You can set a flag to indicate whether or not a uniform variable should be clamped and if it should be normalized. A uniform variable can be clamped by using pfSProgClampMode() with any one of the following parameters: • PFUNI_CLAMP_NONE • PFUNI_CLAMP_MIN • PFUNI_CLAMP_MAX • PFUNI_CLAMP_ALL The default value is PFUNI_CLAMP_NONE. In order to set the clamp value, call pfSProgUniformMin() or pfSProgUniformMax(). The following code shows an example: GLfloat minValue = 0.0f; GLfloat maxValue = 1.0f; pfSProgUniformMin(sProg, uniformIndex, &minValue); pfSProgUniformMax(sProg, uniformIndex, &maxValue); In order to query the minimum and maximum values, use pfGetSProgUniformMin() and pfGetSProgUniformMax(). In order to determine if the value is being clamped used, you can use pfGetSProgClampMode() and check the return value. Normalizing Uniform Variables (optional) For uniform variables of type PFUNI_FLOAT2, PFUNI_FLOAT3, or PFUNI_FLOAT4, it is also possible to normalize the values such that they correspond to vectors of size 1.0. In order to do this, you must use pfSProgNormalizeFlag() and this flag may also be queried for a given uniform variable with pfGetSProgNormalizeFlag(). By default, uniform variables are not normalized. If this flag is set on a uniform variable of a type other than one that supports this feature, the flag will be ignored and a warning will be issued at run time. Applying Shader Programs You can apply a pfShaderProgram using pfShaderProgramApply(), but only in the draw process. When this operation is performed, OpenGL Performer will determine if the shader program needs to be recompiled and perform that operation if required. It is also 496 007-1680-100 The pfShaderObject Class possible to force compilation to occur by calling pfSProgLink(). This method will return the following: Return Value Meaning 0 The complilation was successful. –1 The associated OpenGL handle is NULL or some other event occurred. n The link process failed. The positive integer n indicates how many pfShaderObjects did not compile. In the case where one would like to force OpenGL Performer to relink a pfShaderProgram once, call pfSProgForceRelink(). One can verify if a given pfShaderProgram is in a state where it is ready to be applied by calling pfSProgValidate(), which will return 1 if the program can be applied given the current state or 0, otherwise. The OpenGL handle associated with a given pfShaderProgram can be retrieved using pfGetSProgHandle(). The pfShaderObject Class The pfShaderObject is a class that encapsulates the functionality associated with either vertex or fragment programs used by the OpenGL Shading Language. A pfShaderObject is represented by a string containing the source code and a shader type. A collection of pfShaderObjects can be assembled to form a valid pfShaderProgram, which can then be used as a piece of state used by pfGeoState with the PFSTATE_SHADPROG attribute. This section describes the following topics: 007-1680-100 • “Creating New Shader Objects” • “Specifying Shader Objects” • “Specifying the Object Type” • “Compiling Shader Objects” 497 13: Shaders Creating New Shader Objects The function pfNewShaderObject() creates a new shader object and returns a handle to a pfShaderObject. The value arena specifies a malloc() arena out of which the pfShaderObject is allocated or NULL for allocation off the process heap. You can delete pfShaderObjects with pfDelete(). The function call new(arena) allocates a pfShaderObject from the specified memory arena or from the heap if arena is NULL. The function allocates a pfShaderObject from the default memory arena (see the pfGetSharedArena man page). Like other pfObjects, pfShaderObjects cannot be automatically created statically on the stack or in arrays. Delete pfShaderObjects with pfDelete() rather than the delete operator. The function pfGetShaderObjectClassType() returns the pfType* for the class pfShaderObject. The pfType* returned by pfGetShaderObjectClassType() is the same as the pfType* returned by invoking pfGetType(), the virtual function getType() on any instance of class pfShaderObject. Because OpenGL Performer allows subclassing of built-in types when decisions are made based on the type of an object, use pfIsOfType(), the member function isOfType(), to test if an object is of a type derived from an OpenGL Performer type rather than to test for strict equality of the pfType*s. Specifying Shader Objects A pfShaderObject can be specified either by loading the source explicitly or by simply specifying the filename parameter with pfShaderObjectName(). The location for shader objects specified by filename corresponds to the semantics of pfFindFile() and, hence, the PFPATH environment variable can be used to specify the location of source files. In order to retreive the name of the current shader source, you can use pfGetShaderObjectName(). If the return srting is NULL, it means that the source is inlined, not loaded from an external file. If the shader source is loaded from a file, you must also call pfShaderObjectLoad() in order to load the source code into the shader object. The source code for the shader object can also be specified explicitly with pfShaderObjectSource(). The corresponding get method is pfGetShaderObjectSource(). 498 007-1680-100 The pfShaderObject Class Specifying the Object Type In addition to setting the source in the form of either a filename or an ASCII string, you must also specify the shader type for a pfShaderObject. By default, this is set to –1 (invalid) and it must be set to either PFSHAD_FRAGMENT_SHADER or PFSHD_VERTEX_SHADER with pfShaderObjectShaderType(). You can also retrieve the shader type for a given pfShaderObject with pfGetShaderObjectShaderType(). If either the source code for the shader object or the type for the shader object have changed, the compilation status for the shader object will change. One can determine the necessity for recompiling a given pfShaderObject by calling pfGetShaderObjectCompileStatus(), which will return 1 if recompilation is required and 0, otherwise. This is used internally to determine if a pfShaderProgram needs to be re-linked and, hence, should not normally be called from a user-specified program. Compiling Shader Objects A pfShaderObject may be compiled with pfShaderObjectCompile(). The log for the compilation process will be stored in the log parameter. If successful, the compilation will return 1 and 0, otherwise. Once a pfShaderObject has been bound to the current graphics context, you can retrieve its GL handle with pfGetShaderObjectHandle(). The handle is created as needed during the compilation process. If the pfShaderObject has not yet been compiled, then the handle will be NULL. 007-1680-100 499 13: Shaders Example Code The following example illustrates the use of the pfShaderProgram and pfShaderObject classes. #include #include #include #include #include #include <Performer/pr/pfGeoState.h> <Performer/pr/pfGeoArray.h> <Performer/pr/pfTexture.h> <Performer/pf/pfGeode.h> <Performer/pr/pfShaderObject.h> <Performer/pr/pfShaderProgram.h> #include <Performer/pfdu.h> int main(int argc,char *argv[]) { int i; pfInit(); pfdInitConverter("pfb"); pfConfig(); char *vertexShaderSource = "varying vec2 tc;\n\n" "void main() {\n" " tc = gl_MultiTexCoord0.xy;\n" " gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;\n" "}\n"; FILE *fp = fopen("multiTex.frag", "w"); if(fp) { for(i=1; i<argc; i++) fprintf(fp,"uniform sampler2D texture_%d;\n",i-1); fprintf(fp,"varying vec2 tc;\n"); fprintf(fp, "void main()\n" "{\n"); float stepSize = 1.0/(argc-1); fprintf(fp," float stepSize = %g;\n",stepSize); for(i=0; i<argc-1; i++) { fprintf(fp, 500 007-1680-100 Example Code " " if(tc.x >= stepSize*%.1f && tc.x <= stepSize*%.1f)\n" gl_FragColor = texture2D(texture_%d, vec2(tc.x*%.1f, tc.y));\n", float(i), float(i+1), i, float(argc-1)); if((i+1) != (argc-1)) fprintf(fp," else "); } fprintf(fp,"}\n"); fclose(fp); } else { pfNotify(PFNFY_FATAL,PFNFY_PRINT, "Unable to open multiTex.frag for writing."); pfExit(); return 1; } pfShaderObject *soV = new pfShaderObject(); soV->setShaderType(PFSHD_VERTEX_SHADER); soV->setSource(vertexShaderSource); pfShaderObject *so = new pfShaderObject(); so->setShaderType(PFSHD_FRAGMENT_SHADER); so->setName("multiTex.frag"); pfShaderProgram *sp = new pfShaderProgram(); sp->addShader(so); sp->addShader(soV); pfGeoState *gs = new pfGeoState(); gs->setMode(PFSTATE_ENSHADPROG, PF_ON); gs->setAttr(PFSTATE_SHADPROG, sp); for(i=1; i<argc; i++) { pfTexture *tex = new pfTexture(); tex->setName(argv[i]); int uniValue = i-1; char name[64]; sprintf(name,"texture_%d",i-1); sp->addUniform(name, PFUNI_SAMP2D, 1, &uniValue); 007-1680-100 501 13: Shaders pfNotify(PFNFY_NOTICE,PFNFY_PRINT,"Setting texture %d to %s, named %s", i-1,argv[i],name); gs->setMultiAttr(PFSTATE_TEXTURE, i-1, tex); } pfVec3 *verts = (pfVec3 *)pfMalloc(sizeof(pfVec3) * 4); PFSET_VEC3(verts[0], PFSET_VEC3(verts[1], PFSET_VEC3(verts[2], PFSET_VEC3(verts[3], 0.f, 0.f, 0.f (argc-1)*1.f, (argc-1)*1.f, 0.f, 0.f, 1.f ); 0.f, 0.f ); 0.f, 1.f ); ); pfVec2 *tCoords = (pfVec2 *)pfMalloc(sizeof(pfVec2) * 4); PFSET_VEC2(tCoords[0], 0.f, 0.f); PFSET_VEC2(tCoords[1], 1.f, 0.f); PFSET_VEC2(tCoords[2], 1.f, 1.f); PFSET_VEC2(tCoords[3], 0.f, 1.f); int lengths[1] = { 4 }; pfGeoArray *ga = new pfGeoArray(); ga->setNumPrims(1); ga->setPrimType(PFGS_TRIFANS); ga->setPrimLengths(lengths); ga->setAttr(PFGA_COORD_ARRAY, 3, GL_FLOAT, 0, verts); // we can use the same set of tex coords for all ... ga->setAttr(PFGA_TEX_ARRAY, 2, GL_FLOAT, 0, tCoords); ga->setGState(gs); pfGeode *geode = new pfGeode(); geode->addGSet(ga); pfdStoreFile(geode, getenv("OUTFILE")?getenv("OUTFILE"):"multiTexShader.pfb"); pfExit(); return 0; } 502 007-1680-100 Chapter 14 14. Using Scalable Graphics Hardware Scalable graphics hardware provides nearly perfect scaling of both geometry rate and fill rate on some applications. This chapter describes how you use OpenGL Performer in conjunction with an SGI Video Digital Multiplexer (DPLEX), an SGI Scalable Graphics Compositor, and graphics processing units (GPUs). The corresponding sections are the following: • “Using OpenGL Performer with a DPLEX” on page 503 • “Using OpenGL Performer with an SGI Scalable Graphics Compositor” on page 517 • “Using OpenGL Performer with GPUs” on page 532 Using OpenGL Performer with a DPLEX A DPLEX is an optional daughtercard that permits multiple graphics hardware pipelines to work simultaneiously on a single visual application. DPLEX hardware is available on Silicon Graphics Onyx2, SGI Onyx 3000, and SGI Onyx 300 systems. For an overview of the DPLEX hardware, see the document Onyx2 DPLEX Option Hardware User’s Guide. OpenGL Performer taps the power of a DPLEX by using hyperpipes. The following sections describe how to use hyperpipes: 007-1680-100 • “Hyperpipe Concepts” on page 504 • “Configuring Hyperpipes” on page 504 • “Configuring pfPipeWindows and pfChannels” on page 511 • “Programming with Hyperpipes” on page 515 503 14: Using Scalable Graphics Hardware Hyperpipe Concepts A pfHyperpipe is a combination of pfPipes or pfMultipipes; there is one pfPipe for each graphics pipe in a DPLEX ring or chain. A DPLEX ring or chain is a collection of interconnected graphic boards. A key concept with hyperpipes is that of temporal decomposition. Think of a rendered sequence as a 3D data set with time being the third axis. With temporal decomposition, the dataset is subdivided along the time axis and distributed across, in this case, each of the graphic pipes in the hyperpipe group. Temporal decomposition is different from spatial decomposition, in which the dataset is subdivided along the X axis, Y axis, or both X and Y axes. Configuring Hyperpipes It is the responsibility of the application to establish the hyperpipe group configuration for OpenGL Performer. There are two steps in the configuration process: 1. Establish the number of graphic pipes (or pfPipes because there is a one-to-one correspondence) in each hyperpipe group. 2. Map the pfPipes to specific graphic pipes. Establishing the Number of Graphic Pipes Use the argument in the pfHyperpipe() function to establish the number of graphic pipes in the hyperpipe group, for example: pfHyperpipe(2); pfConfig(); In this example, two pfPipes combine to create the pfHyperpipe, as shown in Figure 14-1. 504 007-1680-100 Using OpenGL Performer with a DPLEX h pfC an ne l p p fHy erp pfP pfP ipe ipe ipe pfPipeWindow pfPipeWindow Figure 14-1 pfPipes Creating pfHyperpipes Like the pfMultipipe() function, pfHyperpipe() must be invoked prior to configuring the pfPipes using pfConfig() and after the call to pfInit(). The number of pipes is used by pfConfig() to associate the configured pfPipes. The pfHyperpipe() function can be invoked multiple times to construct multiple hyperpipe groups, as shown in Figure 14-2. 007-1680-100 505 14: Using Scalable Graphics Hardware pfC pfC h an l ne p p fHy erp pfP pfP ha e nn ipe l p pfP p fHy erp pfP ipe ipe ipe ipe ipe pfPipeWindow pfPipeWindow pfPipeWindow pfPipeWindow pfH e yp pfP pfP rpi pe pfP ipe ipe ipe pfPipeWindow pfPipeWindow pfPipeWindow Figure 14-2 Multiple Hyperpipes Additionally, the pfHyperpipe() function can be combined with the pfMultipipe() call to configure pfPipes that are not associated with a hyperpipe group. The num argument to the pfMultipipe() function defines the total number of pfPipes to configure (including those in hyperpipe groups). Example 14-1, diagrammed in Figure 14-2, shows the configuration of a system with three hyperpipe groups. The first hyperpipe group consists of three graphic pipes. The 506 007-1680-100 Using OpenGL Performer with a DPLEX remaining two hyperpipe groups have two graphic pipes each. This example also configures one non-hyperpipe group graphic pipe. Example 14-1 Configuring a System with Three Hyperpipe Groups pfInit(); pfMultipipe(8); pfHyperpipe(3); pfHyperpipe(2); pfHyperpipe(2); pfConfig(); /* /* /* /* /* need eight pfPipes 3-2-2-1 */ pfPipes 0, 1, 2 are the first group */ pfPipes 3, 4 are the second group */ pfPipes 5, 6 are the third group */ construct the pfPipes */ If the target configuration includes only hyperpipe groups, it is not necessary to invoke pfMultipipe(). OpenGL Performer correctly determines the number of pfPipes from the pfHyperpipe() calls. Using the Default Hyperpipe Mapping to Graphic Pipes The pfPipes constructed by pfConfig() are ordered into a linear array and are selected with the pfGetPipe() function. The pfPipes that are part of a hyperpipe group always appear in this array before any non-hyperpipe group pfPipes. The pfHyperpipe() function groups pfPipes together starting, by default, with pfPipe number 0. In the following example, there are four pfPipes; the first two are combined into a hyperpipe group: pfMultipipe(4); pfHyperpipe(2); pfConfig(); OpenGL Performer maps each pfPipe to a graphic pipe, which is associated with a specific X display, as shown in Figure 14-3: Hyperpipe pfPipe 0 1 2 3 Graphic pipe 0 1 2 3 Figure 14-3 007-1680-100 Single pipes Default Hyperpipe Mapping to Graphic Pipes 507 14: Using Scalable Graphics Hardware Using Nondefault Hyperpipe Mappings to Graphics Pipes Each graphics pipe is associated with only one X screen. By default, OpenGL Performer assigns each pfPipe to the screen of the default X display that matches the pfPipe index in the pfPipe array; in other words, pfPipe(0) in the hyperpipe is mapped to X screen 0. In most configurations, this default mapping is not sufficient. The second phase, therefore, involves associating the configured pfPipes with the graphic pipes. This is achieved through the pfPipeScreen() or pfPipeWSConnectionName() function on the pfPipes of the hyperpipe group. Example 14-2 shows, given the configuration in Example 14-1, how to map the pfPipes to the appropriate screens. In this example, all of the graphic pipes are managed under the same X display, that is, a different screen on the same display. Example 14-2 Mapping Hyperpipes to Graphic Pipes /* assign the single pfPipe to screen 0 */ pfPipeScreen(pfGetPipe(7), 0); /* assign the pfPipes of hyperpipe group 0 to screens 1,2,3 */ for (i=0; i < 3; i++) pfPipeScreen(pfGetPipe(i), i+1); /* assign the pfPipes of hyperpipe group 1 to screens 4,5 */ for (i=3; i<5; i++) pfPipeScreen(pfGetPipe(i), i+1); /* assign the pfPipes of hyperpipe group 2 to screens 6,7 */ for (i=5; i<7; i++) pfPipeScreen(pfGetPipe(i), i+1); The following is a more complex example that uses GLXHyperpipeNetworkSGIX returned from glXQueryHyperpipeNetworkSGIX() to configure the pfPipes. This example is much more complete and is referred to in the following sections. Example 14-3 More Complete Example: Mapping Hyperpipes to Graphic Pipes int hasHyperpipe; GLXHyperpipeNetworkSGIX* hyperNet; int numHyperNet; int i; Display* dsp; int numNet; 508 007-1680-100 Using OpenGL Performer with a DPLEX int pipeIdx; pfChannel* masterChan; /* initialize Performer */ pfInit(); /* does this configuration support hyperpipe */ pfQueryFeature(PFQFTR_HYPERPIPE, &hasHyperpipe); if (!hasHyperpipe) { pfNotify(PFNFY_FATAL, PFNFY_RESOURCE, "no hyperpipe support"); exit(1); } /* query the network */ dsp = pfGetCurWSConnection(); hyperNet = glXQueryHyperpipeNetworkSGIX(dsp, &numHyperNet); if (numHyperNet == 0) { pfNotify(PFNFY_FATAL, PFNFY_RESOURCE, "no hyperpipes"); exit(1); } /* * determine the number of distinct hyperpipe networks. network * ids are monotonically increasing from zero. a value < 0 * is used to indicate pipes that are not members of any hyperpipe. */ for (i=0, numNet=-1; i<numHyperNet; i++) if (numNet < hyperNet[i].networkId) numNet = hyperNet[i].networkId; numNet += 1; /* * configure all of the hyperpipes in the net * * NOTE * while it is possible to be selective about which hyperpipe(s) * to configure, that is left as an exercise. */ for (i=0; i<numNet; i++) { int count = 0; int j; for (j=0; j<numHyperNet; j++) if (hyperNet[i].networkId == i) count++; pfHyperpipe(count); } 007-1680-100 509 14: Using Scalable Graphics Hardware pfConfig(); /* associate pfPipes with screens */ for (i=0, pipeIdx=0; i<numNet; i++) { int j; for (j=0; j<numHyperNet; j++) if (hyperNet[i].networkId == i) pfPipeWSConnectionName(pfGetPipe(pipeIdx++), hyperNet[i].pipeName); } /* construct the pfPipeWindows for each hyperpipe */ masterChan = NULL; for (i=0, pipeIdx=0; i<numNet; i++) { pfPipe* pipe; pfPipeWindow* pwin; pfChannel* chan; PFVEC3 xyz, hpr; pipe = pfGetPipe(pipeIdx); pwin = pfNewPWin(pipe); pfPWinName(pwin, "Hyperpipe Window"); /* * void * openPipeWindow(pfPipeWindow* pwin) * { * pfPWinOpen(pwin); * } */ pfPWinConfigFunc(pwin, openPipeWindow); pfPWinFullScreen(pwin); pfPWinMode(pwin, PFWIN_NOBORDER, 1); pfPWinConfig(pwin); chan = pfNewChan(pipe); pfPWinAddChan(pwin, chan); /* * layout channels left to right in hyperpipe order. this * ordering is arbitrary and should be redefined for the * specific application. */ pfChanShare(chan, 510 007-1680-100 Using OpenGL Performer with a DPLEX pfGetChanShare() | PFCHAN_VIEWPORT | PFCHAN_SWAPBUFFERS | PFCHAN_SWAPBUFFERS_HW); pfMakeSimpleChan(chan, 45); pfChanAutoAspect(chan, PFFRUST_CALC_VERT); xyz[0] = xyz[1] = xyz[2] = 0; hpr[0] = (((numNet-1)*.5f)-i)*45.f; hpr[1] = hpr[2] = 0; pfChanViewOffsets(chan, xyz, hpr); pfChanNearFar(.000001, 100000); /* * void * drawFunc(pfChannel* chan, void* notUsed) * { * pfClearChan(chan); * pfDraw(); * } */ pfChanTravFunc(PFTRAV_DRAW, drawFunc); if (i == 0) masterChan = chan; else pfAttachChan(masterChan, chan); /* bump to the first pipe of the next hyperpipe */ pipeIdx += pfGetHyperpipe(pipe); } /* * the next step is to construct the scene, attach it to * masterChan and start the main loop. this bit of code * is not included here since it follows other demonstration * applications included elsewhere in the Programmer’s Guide. */ Configuring pfPipeWindows and pfChannels The pfPipes grouped into a pfHyperpipe are indexed; the first pfPipe is pfPipe(0) and it is referred to as the master pfPipe. Most actions taken on the hyperpipe group are effected through this pfPipe; for example, all objects, such as pfPipeWindows and pfChannels, are attached to the master pfPipe. OpenGL Performer automatically clones 007-1680-100 511 14: Using Scalable Graphics Hardware all objects, except pfChannels, across all of the pfPipes in the pfHyperpipe, as shown in Figure 14-4. h pfC an ne l p p fHy erp pfP Master pipe pfP ipe ipe ipe Cloned from master pipe pfPipeWindow pfPipeWindow Figure 14-4 Attaching Objects to the Master pfPipe When constructing pfPipeWindows or pfChannels, the pfPipe argument should be the master pfPipe. OpenGL Performer ensures that the constructed objects are cloned (pfPipeWindows) or attached (pfChannels) as needed to the other pfPipes in the hyperpipe group. 512 007-1680-100 Using OpenGL Performer with a DPLEX With the exception of certain attributes, detailed in Table 14-1, OpenGL Performer propagates attribute updates to the cloned pfPipeWindows when they occur. The following is a list of pfPipeWindow functions for which the attributes do not propagate. Table 14-1 007-1680-100 pfPipeWindow Functions That Do Not Propagate C Function C++ Member Function pfPWinSwapBarrier() setSwapBarrier() pfPWinWSConnectionName() setWSConnectionName() pfPWinOverlayWin() setOverlayWin() pfPWinStatsWin() setStatsWin() pfPWinScreen() setScreen() pfPWinWSWindow() setWSWindow() pfPWinWSDrawable() setWSDrawable() pfPWinFBConfigData() setFBConfigData() pfPWinFBConfigAttrs() setFBConfigAttrs() pfPWinFBConfig() setFBConfig() pfPWinFBConfigId() setFBConfigId() pfPWinGLCxt() setGLCxt() pfPWinList() setWinList() pfPWinPVChan() setPVChan() pfPWinAddPVChan() addPVChan() pfPWinRemovePVChan() removePVChan() pfPWinRemovePVChanIndex() removePVChanIndex() pfBindPWinPVChans() bindPVChans() pfUnbindPWinPVChans() unbindPVChans() pfSelectPWin() select() pfAttachPWinWin() attachWin() 513 14: Using Scalable Graphics Hardware Table 14-1 pfPipeWindow Functions That Do Not Propagate (continued) C Function C++ Member Function pfDetachPWinWin() detachWin() pfAttachPWin() attach() pfAttachPWinSwapGroup() attachSwapGroup() pfAttachPWinWinSwapGroup() attachWinSwapGroup() pfDetachPWinSwapGroup() detachSwapGroup() pfChoosePWinFBConfig() chooseFBConfig() When using any of the preceding interfaces within an application, set the appropriate attribute in the cloned pfPipeWindow. Clones Clones are identified by an index value. The index of a clone matches that of the master pfPipeWindow. This index is used to retrieve the clone pfPipeWindow from the other pfPipes in the hyperpipe group. Example 14-4 sets the FBConfigAttrs for each of the pfPipeWindows in the first hyperpipe group. Example 14-4 Set FBConfigAttrs for Each pfPipeWindow static int attr[] = { GLX_RGBA, GLX_DOUBLEBUFFER, GLX_LEVEL, 0, GLX_RED_SIZE, 8, GLX_GREEN_SIZE, 8, GLX_BLUE_SIZE, 8, GLX_ALPHA_SIZE, 8, GLX_DEPTH_SIZE, 16, GLX_STENCIL_SIZE, 0, GLX_ACCUM_RED_SIZE, 0, GLX_SAMPLE_BUFFERS_SGIS, 1, GLX_SAMPLES_SGIS, 4, None }; int numHyper = pfGetHyperpipe(pfGetPipe(0)); 514 007-1680-100 Using OpenGL Performer with a DPLEX for (i=0; i<numHyper; i++) { /* get the first pfPipeWindow on pfPipe */ pfPipeWindow* pwin = pfGetPipePWin(pfGetPipe(i), 0); pfPipeFBConfigAttrs(pwin, attr); } The current API has no support for directly querying the pfPipeWindow index within the pfPipe. The only mechanism to determine an index value is to track it in the application or search the pfPipeWindow list of the pfPipe. Example 14-5 performs such a search. Example 14-5 Search the pfPipeWindow List of the pfPipe /* search the master pfPipe pipe for the pfPipeWindow in pwin */ int pwinIdx; int numPWins = pfGetPipeNumPWins(pipe); for (i=0; i<numPWins; i++) if (pfGetPipePWin(pipe) == pwin) break; if (i == numPWins) pfNotify(PFNFY_FATAL, PFNFY_PRINT, "oops!"); pwinIdx = i; Synchronization When working with pfPipeWindows, it is possible for some updates to occur within the DRAW process. For this release (and possibly future releases) of OpenGL Performer, these updates are not automatically propagated to the clone pfPipeWindows. It is the responsibility of the application to ensure that the appropriate attributes are propagated or that similar actions occur on the clones. The CULL and DRAW stages of different pfPipes within a hyperpipe group can run in parallel. For this reason, applications that assume a fixed pfChannel to pfPipe relationship or maintain global configuration data associated with a pfChannel that is updated in either the CULL or DRAW stages may fail. It is currently impossible (or at least very difficult) to transmit information from the CULL or DRAW stages of one pfPipe to another CULL or DRAW stage of another pfPipe within a hyperpipe group. All changes should be affected by the APP stage. Programming with Hyperpipes Programming with hyperpipes, as described in the preceding sections, generally involves the following steps: 007-1680-100 515 14: Using Scalable Graphics Hardware 1. Configure the hyperpipe either on the fly or using a configuration file. 2. Map screens to hyperpipes, if necessary. 3. Allocate pfPipeWindow and pfChannels: • Create one pfPipeWindow for each pfHyperpipe. • Attach a pfPipeWindow to the master pfPipe. • Create a pfChannel for each pfHyperpipe. 4. Start the main loop (pfFrame()...pfSync()). There are two additional requirements for DPLEX: • You cannot use single buffer visuals. The DPLEX option uses the glXSwapBuffers() call as an indication to switch the multiplexer. This logic is bypassed for single buffered visuals. • glXSwapBuffers() and pfSwapWinBuffers() functions must not be invoked outside of the internal draw synchronization logic. Because the pfuDownloadTexList() function with the style parameter set to PFUTEX_SHOW calls glXSwapBuffers(), this feature must be disabled. (Simply set the style parameter to PFUTEX_APPLY). Also, the Perfly application displays a message at startup which also swaps the buffers. Again, this function must be disabled when using hyperpipe groups. The version of Perfly that ships with performer_demo correctly disables these features. Each pfPipe software rendering pipeline runs at a fraction of the target frame rate as defined by pfFrameRate(). The fraction is 1/(number of pipes in hyperpipe group). For example, if there are two pfPipes in the pfHyperpipe, each pfPipe runs at one half of the pfFrameRate(). Although the CULL and DRAW stages run at a slower rate, the APP stage must run at the target frame rate. 516 007-1680-100 Using OpenGL Performer with an SGI Scalable Graphics Compositor Using OpenGL Performer with an SGI Scalable Graphics Compositor This section gives a brief overview of the SGI Scalable Graphics Compositor and how to use it with OpenGL Performer. For more information on the compositor, including the details of the hardware setup, see the document SGI InfinitePerformance: Scalable Graphics Compositor User’s Guide. Note: The compositor is currently supported on InfinitePerformance, Onyx4, and Prism graphics systems. This section contains the following subsections: • “How the Compositor Functions” on page 517 • “The pfCompositor Class” on page 519 • “Querying the System for Hardware Compositors” on page 519 • “Creating a pfCompositor” on page 520 • “Querying pfCompositors” on page 522 • “Load Balancing” on page 524 • “Setting Compositor Modes” on page 525 • “Querying Compositor Modes” on page 528 • “Managing Screen Space, Channel Clipping, and Antialiasing” on page 529 How the Compositor Functions The compositor receives two to four input signals and outputs a single signal either in analog or digital format. Hence, it can handle spatial composition of four inputs which enables multiple pipes to contribute to a single output. Four different composition schemes are available: 007-1680-100 • Vertical stripes • Horizontal stripes • 2D tiles • Cut-ins 517 14: Using Scalable Graphics Hardware Figure 14-5 illustrates the various hardware composition schemes. Vertical stripes Horizontal stripes 2D tiles Cut-ins Figure 14-5 Hardware Composition Schemes The following items are noteworthy regarding the compositor’s capabilities: • In addition to the spatial composition modes shown in Figure 14-5, the compositor provides applications with the means to do full-scene antialiasing (FSAA) in hardware. This capability stems from the following feature: for every output pixel, the compositor averages all values from all the pipes. • Stereo support is not provided explicitly through the compositor, but OpenGL Performer does allow you to structure your application to do so. Note: For more information on the current limitations and anomalies associated with the use of the SGI Scalable Graphics Compositor, refer to the hardware documentation. 518 007-1680-100 Using OpenGL Performer with an SGI Scalable Graphics Compositor The pfCompositor Class A compositor is a hardware device that takes a number of video inputs and combines them to produce a single video output. The video inputs can be divided spatially or blended together to form one output. OpenGL Performer uses the pfCompositor class to support compositors. The pfCompositor class transparently distributes rendering across multiple hardware pipes and combines their outputs by either feeding them to a hardware compositor device or through software composition. Several different spatial composition modes are supported, as well as an antialias mode in which channel frustums on composited pipes are slightly jittered and the outputs blended together by the hardware compositor. The pfCompositor class also supports dynamic load balancing. When enabled, the spatial subdivision of the compositor inputs will be updated on each frame based on the load of each contributing pfPipe. Load balancing is disabled by default but can be enabled through setMode(). Querying the System for Hardware Compositors During initialization, the pfCompositor class will perform a system topology query to determine the availability of hardware compositors. The results of this query can be examined by the application through the static methods listed in Table 14-2. Table 14-2 007-1680-100 Methods for Querying the System for Hardware Compositors Methods Description getNumHWCompositors() Returns the number of available hardware compositors found on the system. getHWCompositorNetworkId() Returns the network ID of the cth hardware compositor, or –1 if c is not a valid index. getHWCompositorNumInputs() Returns the number of inputs physically connected to the cth hardware compositor. Each input can either be a single pipe or the output of another hardware compositor. If c is not a valid index, –1 is returned. 519 14: Using Scalable Graphics Hardware Table 14-2 Methods for Querying the System for Hardware Compositors (continued) Methods Description getHWCompositorInputType() Returns PFCOMP_INPUTTYPE_PIPE if the ith input of the cth hardware compositor is a single pipe, or PFCOMP_INPUTTYPE_COMPOSITOR if it is another hardware compositor. If c is not a valid index, 0 is returned. getHWCompositorInputPipeName() Returns the string identifying the display of that pipe (for example, ":0.0") if the ith input of the cth hardware compositor is a single pipe. If c or i is not a valid index or if the ith input of the cth compositor is not a single pipe, NULL is returned. getHWCompositorInputNetworkId() Returns its network id if the ith input of the cth hardware compositor is a compositor. If c or i is not a valid index or if the ith input of the cth compositor is not a compositor, –1 is returned. getHWCompositorPfCompositor() Returns a pointer to the pfCompositor object managing the cth hardware compositor if one exists and c is a valid index. Otherwise, NULL is returned. Creating a pfCompositor A pfCompositor is created through new pfCompositor(netId), where netId is the network ID for the hardware compositor device that will be managed by the new pfCompositor. If netId is PFCOMP_SOFTWARE, no hardware compositor device will be involved, and composition will be carried out through software readbacks. This chapter refers to pfCompositors utilizing software composition as software compositors while pfCompositors associated with a hardware compositor device will be referred to as hardware compositors. 520 007-1680-100 Using OpenGL Performer with an SGI Scalable Graphics Compositor You can use the methods described in Table 14-3 to configure pfCompositors that have been created. Table 14-3 Methods Used in Creating pfCompositors Methods Description getNetworkId() Returns the network id identifying the hardware compositor device managed by a pfCompositor object or PFCOMP_SOFTWARE if the pfCompositor uses software composition. addChild(pipe_name) Adds a pipe child (input) to a pfCompositor. If the pfCompositor is a hardware compositor, pipe_name must match the display string of one of the hardware pipes physically connected to the compositor device. If the compositor is a software compositor, then pipe_name can be any valid display string. The pfPipes configured by the software compositor will be created on the specified displays through calls to pfPipe::setWSConnectionName(). The pipe_name value can also be "" (empty string) for software compositors; in this case, pipes will be created on the default screens (:0.0, :0.1, and so on). addChild(comp) Adds a compositor child to a pfCompositor, creating a compositor hierarchy. Currently only hardware compositor parents and software compositor children are supported. Care must be taken in configuring compositor hierarchies to ensure that the first child of the software compositor child (its master pipe) is physically connected to the compositor device managed by the parent pfCompositor. autoSetup() Configures a pfCompositor with the desired number of inputs. For hardware compositors, num_inputs cannot exceed the number of inputs physically connected to the hardware device. A zero or negative value for num_inputs will cause all physically connected inputs to be configured. For software compositors, num_inputs will be clamped to the number of available hardware pipes. If num_inputs is less than one, all available hardware pipes on the system will be configured. Note that autoSetup() will take no action at all if pfCompositor already has one or more children. The call autoSetup(–1) is made within a pfConfig for all pfCompositor objects in order to automatically configure them if the application has not done so already. 007-1680-100 521 14: Using Scalable Graphics Hardware Querying pfCompositors The methods described in Table 14-4 can be used to query compositors. Table 14-4 522 Methods for Querying pfCompositors Method Description getNumChildren() Returns the number of children (inputs) that have been added to a pfCompositor. Each child can either be a single pipe or a pfCompositor. getChildType() Returns PFCOMP_INPUTTYPE_PIPE if the ith child is a single pipe or PFCOMP_INPUTTYPE_COMPOSITOR if it is a pfCompositor. If i is not a valid index, 0 is returned. getChildCompositor() Returns a pointer to the ith child of a pfCompositor if the child is a pfCompositor. If i is not a valid index, or if the ith child is not a compositor, NULL is returned. getChildPipe() Returns a pointer to the ith child of a pfCompositor if the child is a single pipe. This can only be called after pfConfig(). getChildPipeName() Returns the display string for the ith child of a pfCompositor if the child is a single pipe. Note that for software compositors, getChildPipeName() returns an empty string ("") for all children unless a display string was explicitly assigned by the application through a call to addChild(pipe_name). getChildPipeId() Returns the OpenGL Performer ID of the pipe child and should only be called after pfConfig(). getParent() Returns a pointer to a pfCompositor parent (another pfCompositor) if the first has been added to the latter as a child through a call to addChild(comp). If a pfCompositor has no parent, NULL is returned. setNumActiveChildren() Sets the number of active children. Not all configured children must contribute to the composited image at all times. There must be at least one active child and no more than the total number of configured children. Note that children are activated from first to last; this means that when there are n active children, these will be children 0 to (n-1); thus, child 0 is always active. getNumActiveChildren() Returns the number of currently active children. 007-1680-100 Using OpenGL Performer with an SGI Scalable Graphics Compositor Table 14-4 007-1680-100 Methods for Querying pfCompositors (continued) Method Description getMasterPipe() Returns a pointer to the master pfPipe for a pfCompositor. The master pipe is the pipe that the application should use to create a pipe window and one or more channels. The pfPipeWindows and pfChannels are created automatically on all other composited pipes (slave pipes) by the pfCompositor class. In a single-tier compositor (one with no compositor parent and no compositor children), the master pipe will be its first child. In a compositor hierarchy, all pfCompositors will share a single master pipe. getMasterPipeId() Returns the OpenGL Performer pipe ID of pfCompositor's master pipe. Each pfCompositor object maintains a list of all the pfPipes contributing to its output. This includes all single-pipe children, as well as all single pipes connected to compositor children. getNumPipes() Returns the total number of pfPipes contributing to a pfCompositor. For a single-tier compositor, this value is equal to the number of its children. In a compositor hierarchy, pipes contributing to leaf compositors (bottom of the hierarchy) also contribute to the root compositor; therefore, if called on the root compositor of a compositor hierarchy, getnumPipes() returns the total number of pipes in hierarchy. getPipe() Returns a pointer to the pth pfPipe in a pfCompositor's pipe list. If p is an invalid index, NULL is returned. getPWin() Returns a pointer to the pfPipeWindow on the pth pipe in a pfCompositor's pipe list. Currently only one (full-screen) pipe window is supported on composited pipes. If p is an invalid index, NULL is returned. getChan() Returns a pointer to the cth pfChannel on the pth pipe in a pfCompositor's pipe list. If p or c are invalid indexes, NULL is returned. getRoot() Returns a pointer to the pfCompositor at the root of the compositor hierarchy to which compositor belongs. For a parent-less pfCompositor, getRoot() returns a pointer to the compositor itself. For a compositor child, getRoot() returns parent->getRoot. 523 14: Using Scalable Graphics Hardware In addition to the methods shown in Table 14-4, the static methods shown in Table 14-5 are also available. Table 14-5 Static Methods for Querying pfCompositors Method Description getNumCompositors() Returns the number of pfCompositor objects in this list. getCompositor() Returns a pointer to the ith pfCompositor object from the global list of pfCompositors. The pfCompositors are added to this list in the order of creation. getNumCompositedPipes() Returns the total number of pfPipes that are (or will be) managed by pfCompositor objects. This is known for certain only after a pfConfig() call because, until then, pipes may be added to existing compositors. However, if called before pfConfig(), this method will attempt to make a reasonable guess by assuming that pfCompositors with no explicitly assigned children will end up being (automatically) configured with all the inputs that are physically connected to them. This can be useful when an application creates one or more single pipes in addition to pipes managed by pfCompositors. In such cases, the application is required to make a call to pfMultipipe() to provide the total number of pipes to be created. Method getNumCompositedPipes() returns the total number of composited pipes to which the desired number of single pipes may be added. Load Balancing A pfCompositor requires a pfLoadBalance object for carrying out load balancing computations. The pfLoadBalance class determines the resulting workload for each of the compositor's children. The behavior can be customized by subclassing pfLoadBalance and overriding the appropriate methods. A pfCompositor can use a customized pfLoadBalance object specified through the setLoadBalancer() method. If a load balancer is not specified, one will be automatically created and used. The method getLoadBalancer() returns a pointer to the pfLoadBalancer object used by the compositor. 524 007-1680-100 Using OpenGL Performer with an SGI Scalable Graphics Compositor The two methods in Table 14-6 can be used to control the transition between load balancing states. Table 14-6 Methods to Control the Load Balancing Transitions Method Description setVal() Accepts only PFLOAD_COEFF and passes the value to the pfLoadBalance class. This coefficient determines how quickly the balancer transitions from the current state to the desired balanced state. This load balancing filter coefficient should be in the range [0..1]. The smaller its value, the slower load balancing follows pipe loading, and the less noise-sensitive it is. getVal() Accepts only PFLOAD_COEFF and returns the current value of the filter coefficient used by the pfLoadBalance object associated with the pfCompositor. For more information, see the pfLoadBalance man page. Setting Compositor Modes The method setMode() accepts the following tokens as its first argument: PFLOAD_BALANCE Enables or disables dynamic load balancing. The second argument must be PF_ON or PF_OFF. PFCOMP_CLIPPING Enables or disables channel clipping for all channels on all pipes managed by this compositor. By default, channel clipping is enabled, and the viewports of pfChannels in composited pipes are clipped to the screen region assigned to each pipe by the compositor. If channel clipping is disabled, all pipes will render all channels in their full (original) size. Note that clipping is not carried out when in antialias mode. PFCOMP_SWAPBARRIER 007-1680-100 Specifies the swap barrier to which pipes contributing to pfCompositor should bind. The second argument should be a valid swap barrier ID (see the glXQueryMaxSwapBarriersSGIX man page). By default, all pfCompositors will bind to swap barrier 1 if the swap barrier extension is 525 14: Using Scalable Graphics Hardware supported. Binding to a swap barrier can be disabled by passing a value smaller than 1. If the specified barrier_id is out of range, the call to setMode() has no effect. PFCOMP_COMPOSITION_MODE Specifies the composition mode used by the pfCompositor. The second argument can be one of the following: PFCOMP_COMPMODE_HORIZ_STRIPES PFCOMP_COMPMODE_VERT_STRIPES PFCOMP_COMPMODE_LEFT_TILES PFCOMP_COMPMODE_RIGHT_TILES PFCOMP_COMPMODE_BOTT_TILES PFCOMP_COMPMODE_TOP_TILES PFCOMP_COMPMODE_ANTIALIAS All composition modes are valid for any number of active children. Figure 14-6 illustrates how one to four inputs are laid out for PFCOMP_COMPMODE_HORIZ_STRIPES mode. PFCOMP_COMPMODE_HORIZ_STRIPES 1 0 1 0 Figure 14-6 2 0 3 2 1 0 Horizontal Stripes (pfCompositor Mode) Figure 14-7 illustrates how one to four inputs are laid out for PFCOMP_COMPMODE_VERT_STRIPES mode. 526 007-1680-100 Using OpenGL Performer with an SGI Scalable Graphics Compositor PFCOMP_COMPMODE_VERT_STRIPES 0 Figure 14-7 0 1 0 1 2 0 1 2 3 Vertical Stripes (pfCompositor Mode) Figure 14-8 illustrates how one to four inputs are laid out for PFCOMP_COMPMODE_LEFT_TILES mode. PFCOMP_COMPMODE_LEFT_TILES 0 Figure 14-8 0 1 2 1 1 0 3 0 2 Left Tiles (pfCompositor Mode) Figure 14-9 illustrates how one to four inputs are laid out for PFCOMP_COMPMODE_RIGHT_TILES mode. PFCOMP_COMPMODE_RIGHT_TILES 2 0 0 1 3 0 1 Figure 14-9 1 0 2 Right Tiles (pfCompositor Mode) Figure 14-10 illustrates how one to four inputs are laid out for PFCOMP_COMPMODE_BOTT_TILES mode. 007-1680-100 527 14: Using Scalable Graphics Hardware PFCOMP_COMPMODE_BOTT_TILES 1 1 2 2 3 0 0 Figure 14-10 0 0 1 Bottom Tiles (pfCompositor Mode) Figure 14-11 illustrates how one to four inputs are laid out for PFCOMP_COMPMODE_TOP_TILES mode. PFCOMP_COMPMODE_TOP_TILES 0 1 3 2 0 0 Figure 14-11 1 2 0 1 Top Tiles (pfCompositor Mode) Querying Compositor Modes The method getMode() can be called to query the compositor mode. You can use the following tokens: 528 PFLOAD_BALANCE The returned value is 1 if dynamic load balancing is enabled or 0 if it is disabled. PFCOMP_CLIPPING The returned value is 1 if channel clipping is enabled or 0 if it is disabled. 007-1680-100 Using OpenGL Performer with an SGI Scalable Graphics Compositor PFCOMP_SOFTWARE The returned value is 1 if pfCompositor uses software composition or 0 if pfCompositor controls a hardware compositor device. PFCOMP_SWAPBARRIER The returned value is the index of the swap barrier to which pipes will bind (or have bound). If binding to swap barriers has been (or will be) skipped, return value is 0. PFCOMP_COMPOSITION_MODE The returned value is the current composition mode used by the pfCompositor and can be one of the following: PFCOMP_COMPMODE_HORIZ_STRIPES PFCOMP_COMPMODE_VERT_STRIPES PFCOMP_COMPMODE_LEFT_TILES PFCOMP_COMPMODE_RIGHT_TILES PFCOMP_COMPMODE_BOTT_TILES PFCOMP_COMPMODE_TOP_TILES PFCOMP_COMPMODE_ANTIALIAS Managing Screen Space, Channel Clipping, and Antialiasing You can use the methods described in Table 14-7 to manage screen space, channel clipping, and antialiasing. Table 14-7 007-1680-100 Methods for Managing Screen Space, Channel Clipping, and Antialiasing Method Description setViewport() Specifies the screen-space bounds of the region managed by a pfCompositor. The viewports assigned to all pipes managed by this compositor will be clipped to this region. The default viewport for a pfCompositor is 0.0, 1.0, 0.0, 1.0 (the whole screen). Do not call setViewport() for compositor children in compositor hierarchies; use setChildViewport() on the parent compositor instead. getViewport() Returns the screen-space bounds of the region managed by a pfCompositor. 529 14: Using Scalable Graphics Hardware Table 14-7 Methods for Managing Screen Space, Channel Clipping, and Antialiasing Method Description setChildViewport() Specifies the screen-space bounds of the 2D region assigned to the ith child of the pfCompositor. The specified viewport is automatically clipped to the viewport of the compositor and aligned horizontally to a four-pixel boundary (required by hardware compositor devices). The default viewports of a pfCompositor's children are determined based on the number of active children and on the current composition mode; see the preceding description for setMode(). Note that when dynamic load balancing is active, setting children viewports through setChildViewport() will have no affect. getChildViewport() Returns the screen-space bounds of the 2D region managed by the ith child of the pfCompositor. This region will always be contained by the viewport of the pfCompositor itself. setChannelClipped() Specifies whether channel clipping should be enabled for the ith channel. Channel clipping is enabled on all channels by default. Channel clipping is not performed if it is globally disabled through a call to setMode(). Disabling clipping on a pfChannel can be useful in certain situations; for example, the GUI channel for Perfly has clipping disabled and is rendered entirely on the master pipe. getChannelClipped() Returns 1 if channel clipping is enabled for the ith channel if i is a valid index or 0, otherwise. setAntialiasJitter() Specifies the jitter pattern to be used for antialias composition when there are n active children. The parameter jitter must point to an array of floats containing 2*n values, specifying subpixel offsets (horizontal and vertical) for each of the n contributing inputs. A pfCompositor maintains a list of jitter patterns to be used for antialias mode, depending on the number of active children. A jitter pattern is encoded as an array of subpixel offsets with two floats (horizontal and vertical offset) for each contributing child. getAntialiasJitter() Returns the jitter pattern to be used for antialias composition when there are n active children. The parameter jitter must point to an array of floats (with at least 2*n elements), which contains the queried jitter values. Note: All viewports are specified in normalized screen coordinates with 0.0,0.0 as the bottom-left corner of the screen and 1.0,1.0 as the top-right corner. 530 007-1680-100 Using OpenGL Performer with GPUs Using OpenGL Performer with GPUs GPUs are used widely in commodity graphics hardware and also in graphics platforms like Onyx4 or Prism systems. OpenGL Performer supports GPU programming through the use vertex programs and fragment programs. Vertex programs are used by the GPU to modify various parameters of each vertex. Similarly, fragment programs are used to modify the color and depth value of each fragment (pixel) as it is being rendered. A description of vertex and fragment program instruction sets is beyond the scope of this guide. You can find a description of these instruction sets in the OpenGL extension registry at http://oss.sgi.com/projects/ogl-sample/registry/ under GL_ARB_vertex_program and GL_ARB_fragment_program. This chapter describes how you can use GPU programs in OpenGL Performer in the following sections: • “The pfGProgram Class” on page 532 • “The pfGProgramParms Class” on page 534 • “The pfVertexProgram and pfFragmentProgram Classes” on page 535 The pfGProgram Class OpenGL Performer implements GPU programming through the general class pfGProgram. This class allows you to set GPU programs, vertex programs and fragment programs. The function pfNewGProgram() creates and returns a handle to a pfGProgram. The parameter arena specifies a malloc() arena out of which the pfGProgram is allocated or the value NULL specifies allocation off the process heap. pfGPrograms can be deleted with pfDelete(). The call new(arena) allocates a pfGProgram from the specified memory arena or from the heap if arena is NULL. The new() call allocates a pfGProgram from the default memory arena (see function pfGetSharedArena() in the pfSharedMem(3pf) man page). Like other pfObjects, pfGPrograms cannot be automatically created statically on the stack or in arrays. pfGPrograms should be deleted with pfDelete() rather than the delete operator. 007-1680-100 531 14: Using Scalable Graphics Hardware The function pfGetGProgramClassType() returns the pfType* for the class pfGProgram. The pfType* returned by pfGetGProgramClassType() is the same as the pfType* returned by invoking pfGetType(), the virtual function getType() on any instance of class pfGProgram. Because OpenGL Performer allows subclassing of built-in types, when decisions are made based on the type of an object, it is usually better to use pfIsOfType(), the member function isOfType(), to test if an object is of a type derived from an OpenGL Performer type rather than to test for strict equality of the types. A pfGProgram is a sequence of assembly-like instructions. You can specify the instructions in two ways: • In a string with new line characters separating instructions • In a text file If the program is specified in a string, you use the function pfGProgramProgram() or pfGProgramProgramLen(). The first parameter of each is the string defining the program. In the second function, you can specify the length when you want to load only part of the string. If the program is loaded from a text file, you use the function pfGProgramLoadProgram(). Using the function pfGProgramApplypfGProgram(), you can apply the pfGProgram but only in the draw process. Once the pfGProgram has been applied, you can query its state using the following functions: pfGetGProgramProgramLength() Returns the number of instructions of the program. pfGetGProgramNativeProgramLength() Returns the number of instructions used by the specific GPU. pfGProgramIsValidpfGProgram() Returns 1 if the program has been successfully loaded into the GPU. You should not use a pfGProgram directly but one of its subclasses. There are two classes of specific GPU programs subclassed from pfGProgram: pfVertexProgram and pfFragmentProgram. A pfVertexProgram or a pfFragmentProgram is set in a pfGeoState and is enabled in a pfGeoState. The user parameters for the vertex and fragment programs can be defined using the class pfGProgramParams, which is described in the following section. 532 007-1680-100 Using OpenGL Performer with GPUs For sample code, see the following file: /usr/share/Performer/src/pguide/libpf/C++/gprogram.C (IRIX and Linux) %PFROOT\Src\pguide\libpf\C++\gprogram.cxx (Microsoft Windows) The pfGProgramParms Class The pfGProgramParms is a class that is used to store parameters of GPU programs, specifically of pfVertexPrograms and pfFragmentPrograms. The function pfNewGProgramParms() creates and returns a handle to a pfGProgramParms. The parameter arena specifies a malloc() arena out of which the pfGProgram is allocated or NULL for allocation off the process heap. You can delete pfGPrograms with pfDelete(). A pfGProgramParms is a set of indexed quadruples of floating point values that are used as parameters for vertex and fragment programs. You can specify the values using the function pfGPParamsParameters() which has the following syntax: pfGPParamsParameters(pfGProgramParms* gpparams, int index, int type, int count, void* ptr); The parameter index specifies the first index of the specified parameters (the index by which the parameters are accessed in the GPU program) and the the parameter count specifies how many indices will be set. The parameter type may be one of the following: PFGP_FRAGMENT_LOCAL Local parameters of a single fragment program. PFGP_FRAGMENT_ENV Environment parameters. Shared between all fragment programs. PFGP_VERTEX_LOCAL Environment parameters of a single vertex program. PFGP_VERTEX_ENV Environment parameters. Shared between all vertex programs. The pointer ptr points to the parameter data. 007-1680-100 533 14: Using Scalable Graphics Hardware Using the following functions, you can query the existing parameters in a pfGProgramParms: pfGetGPParamsNumParameters() Returns the number of parameters. pfGetGPParamsParameters() Returns the parameters in the order of their specification. pfGetGPParamsParametersByIndex() Returns the parameters by the index by which the parameters are accessed. You can apply the pfGProgramParms using pfGProgramParamsApply() but only in the draw process. If you modify the pfGProgramParms after they have been applied, you must call pfGProgramParamsUpdate() for the change to take effect. A pfGProgramParms is set in a pfGeoState. Each pfGeoState can have one pfGProgramParms of each of the four types, two for the pfVertexProgram associated with the pfGeoState and two for the pfFragmentProgram. The pfVertexProgram and pfFragmentProgram Classes The pfVertexProgram and pfFragmentProgram classes are derived from the class pfGProgram. These subclasses do not add any new methods. A vertex program or a fragment program is used by the GPU to modify various parameters of each vertex or fragment (pixel), respectively. The GPU allows you to specify a sequence of floating-point 4-component operations that are executed for each vertex or fragment. These operations transform an input set of per-vertex or per-fragment parameters to another set of per-vertex or per-fragment parameters. A vertex program replaces the standard OpenGL set of lighting and texture coordinate generation modes. Consequently, the vertex program must take care of the basic transformation of vertex coordinates to the screen coordinates, the generation of texture coordinates, and the application of the lighting equation. This programming model allows you to modify the position of each vertex, producing, for example, a displacement mapping. Similar to a vertex program, a fragment program replaces the standard OpenGL set of texture and fog application modes. The fragment program has to access the textures and to modulate the resulting color according to the fog equation, if necessary. This programming model allows you to modify the resulting color and depth of each pixel, making it possible, for example, to apply a complex per-pixel shading. 534 007-1680-100 Using OpenGL Performer with GPUs You can find the instruction sets for vertex and fragment programs in the OpenGL extension registry at http://oss.sgi.com/projects/ogl-sample/registry/ under the GL_ARB_vertex_program. As subclasses of pfGProgram, pfVertexPrograms and pfFragmentPrograms can use the management methods of a pfGProgram to set, load, and apply programs. Section “The pfGProgram Class” on page 532 describe these methods. You set and enable pfVertexPrograms and pfFragmentPrograms in a pfGeoState. As described in section “The pfGProgramParms Class” on page 534, the user parameters for GPU programs can be defined using the pfGProgramParms class. For sample code, see the following file: /usr/share/Performer/src/pguide/libpf/C++/gprogram.C (IRIX and Linux) %PFROOT\Src\pguide\libpf\C++\gprogram.cxx (Microsoft Windows) 007-1680-100 535 14: Using Scalable Graphics Hardware 536 007-1680-100 Chapter 15 15. ClipTextures As CPUs get faster and storage gets cheaper, applications are moving away from scenes with small, synthetic textures to large textures, taken from real environments, giving the viewer realistic renderings of actual locations. There has customarily been a trade-off between the complexity of a texture and the area it covers: if a texture covers a large area, its resolution must be limited so that it can fit into texture memory; high-resolution textures are limited to small regions for the same reason. A cliptexture allows you to circumvent many of these system resource restrictions by virtualizing MIPmapped textures. Only those parts of the texture needed to display the textured geometry from a given location are stored in system and texture memory. OpenGL Performer provides support for this technique, called cliptexturing, as a subclass of a pfTexture called a pfClipTexture. This functionality allows you to display textures too large to fit in texture memory or even in system memory; you can put the entire world into a single texture. OpenGL Performer supports texture load management from disk to system memory and from system to texture memory, synchronizing clipped regions with the viewpoint, and with the many other tasks needed to virtualize a texture relative to the viewer location. This chapter describes cliptextures in the following parts: 007-1680-100 • “Overview” on page 538 • “Cliptexture API” on page 553 • “Preprocessing ClipTextures” on page 553 • “Cliptexture Configuration” on page 556 • “Configuration API” on page 557 • “Post-Scene Graph Load Configuration” on page 579 • “Manipulating Cliptextures” on page 587 • “Using Cliptextures” on page 601 537 15: ClipTextures Overview Cliptexturing avoids the size limitations of normal MIPmaps by clipping the size of each level of a MIPmap texture to a fixed area, called the clip region. A MIPmap contains a range of levels, each four times the size of the previous one. If the clip region is larger than a particular level, the entire level is kept in texture memory. Levels larger than the clip region are clipped to the clip region’s size. The clip region is set by the application, trading off texture memory consumption against image quality. The clip region size is set through the clip size, which is the length of the clip regions’s sides (in texels). Clip size Clip region Entire level in texture memory Figure 15-1 Cliptexture Components The clip region positioned so as to be centered about the clip center, or as close as possible to the clipcenter while remaining entirely within the cliptexture. The clipcenter is set by the application, usually to the location on the texture corresponding to the location closest to the viewer on the cliptextured geometry. The clipcenter is specified in texel coordinates, which is the texture coordinates (s and t values, ranging from 0.0 to 1.0, scaled by the dimensions of the finest level of the cliptexture, level 0). 538 007-1680-100 Overview Cliptexture Levels Texture memory contains the MIPmap levels, the larger ones clipped to the clip region size; the rectangle of texture memory corresponding to each clipped level is called a tex region. As the viewer moves relative to the cliptextured geometry, the clipcenter must be updated. When this happens, the clipped MIPmap levels must have their texture data updated, in order to represent the area closest to the center. This updating usually must happen every frame, and is done by OpenGL Performer image caches. To facilitate loading only portions of the texture at a time, the texture data must first be subdivided into a contiguous set of rectangular areas, called tiles. These tiles can then loaded individually from disk into texture memory. Texture memory must be loaded from system memory; it can’t be loaded directly from disk. In order to improve the performance of texel downloading, the region in system memory is made larger than the destination texture memory and organized into a lookahead cache, called the mem region. Clip region Mem region Tex region Entire level in texture memory Figure 15-2 007-1680-100 Image Cache Components 539 15: ClipTextures Image caches must know three things in order to update clipped texture levels: • Where and how the data is stored on disk, so they can retrieve it, • Location and size of system memory cache, called the mem region, • The texture memory they are responsible to update when the cilpcenter moves (the tex region). Cliptexture Assumptions For the cliptexture algorithm to work seamlessly, applications must abide by the following assumptions: • An application can only view a clip region’s worth of high resolution texel data on its textured geometry from any viewpoint. • The application views the texture from one location at a time. Multiple views require multiple cliptextures. • The viewer must move smoothly relative to the cliptextured geometry; no “teleporting” (abrupt changes in position). Given these assumptions, your application can maintain a high-resolution texture by keeping only those parts of the texture closest to the viewer in texture memory; the remainder of the texture is on disk and cached in system memory. Why Do These Assumptions Work? Only the textured geometry closest to the viewer needs a high-resolution texture. Far away objects are smaller on the screen, so the texels used on that object also appear smaller (cover a smaller screen area). In normal MIPmapping, coarser MIPmap levels are chosen as the texel size gets smaller relative to the pixel size. These coarser levels contain less texels, since each texel covers a larger area on the textured geometry. Cliptextures take advantage of this fact by storing only part of each large MIPmap level in texture memory, just enough so that when you look over the geometry, the MIPmap algorithm starts choosing texels from a lower level (because the texels are getting small on the screen) before you run out of texels on the clipped level. Because coarser levels have texels that cover a larger area, at a great enough distance, MIPmapping is choosing texels from the unclipped, smaller levels. 540 007-1680-100 Overview When a clip size is chosen, cliptexture levels can be thought of as belonging to one of two categories: • Clipped levels, which are texture levels that are larger than the clip size. • Non-clipped levels, which are small enough to fit entirely within the clip region. The non-clipped levels are viewpoint independent; each non-clipped texture level is complete. Clipped levels, however, must be updated as the viewer moves relative to the textured geometry. Image Cache The image cache organizes its system memory as a grid of fixed size texture tiles. This grid of texture data forms a lookahead cache, called the mem region. The cache automatically anticipates texture download requirements, updating itself with texture tiles it expects to use soon. Image caches update texture memory by transferring image data from disk files. The data is transferred in two steps. Data is moved from disk files a tile at a time into the mem region in system memory. The mem region is updated so that it always contains the image data corresponding to the tex region and its immediate surroundings. The border of extra surrounding data allows the image cache to update the tex region as necessary without having to wait for tiles to be loaded into the mem region from disk. The image cache also contains a tex region, the rectangle of texel data in a given level’s texture memory. This rectangle of data is in texture memory, and is being updated from a corresponding rectangle of data in the memregion. As the center moves, the tex region being loaded into texture memory can get close to the edge of the mem region. When this happens, tiles in the mem region are updated with new data from disk so that the tex region is moved closer to the center of the image data. 007-1680-100 541 Tex region Mem region Disk System memory Texture memory 15: ClipTextures Disk files Figure 15-3 Mem Region Update As the center moves, the clipped region on each clipped level of the image cache shifts position. The clipped regions on each level move at different rates; each coarser level only moves at one half the speed of the level above it. The image cache reflects the change on its level by tracking the position of the clipped region with its tex region. Data in texture memory must be updated to match the texel data in the translated tex region. This updating is done by copying rectangles of texel data from the shifted tex region area in the mem region to the appropriate locations in texture memory. The amount of updating is minimized by only updating the portions of the texture memory that actually need new data. The majority of the tex region data only has to shift position in texture memory; this is done by translating texture coordinates, and taking advantage of the wrap mode when accessing texels from texture memory. 542 007-1680-100 Tex region Tex region update Mem region Disk System memory Texture memory Overview Disk files Figure 15-4 Tex Region Update By loading textures to system memory before they are needed in texture memory, the latency caused by waiting for tiles downloading from a disk is reduced. 1. Texture data on disk is cached into system memory in an image cache’s mem region. 2. Texture data in the tex region part of the mem region is used to update texture memory. 007-1680-100 543 15: ClipTextures Image cache Texture data on disk Tex region Image cache grid Texture memory Figure 15-5 Cliptexture Cache Hierarchy Toroidal Loading In order to minimize the bandwidth required to download texels from system to texture memory, the image cache’s tex regions are updated using toroidal loading. A toroidal load assumes that changes in the contents of the clip region are incremental, such that the update consists of: • New texels that need to be loaded. • Texels that are no longer valid. • Texels that are still in the clip region, but have shifted position. Toroidal loading minimizes texture downloading by only updating the part of the texture region that needs new texels. Shifting texels that remain visible is not necessary, since the coordinates of the clip region wrap around to the opposite side. Invalid Borders Being able to impose alignment requirements to the regions being downloaded to texture memory improves performance. Cliptextures support the concept of an invalid border to provide this feature. It is the area around the perimeter of a clip region that can’t be used. The invalid border shrinks the usable area of the clip region, and can be used to dynamically change the effective size of the clip region. Shrinking the effective clip size can be a useful load control technique. 544 007-1680-100 Overview When texturing requires texels from a portion of an invalid border at a given MIPmap level, the texturing system moves down a level, and tries again. It keeps going down to coarser levels until it finds texels at the proper coordinates that are not in the invalid region. This is always guaranteed to happen, since each level covers the same area with less texels (coarser level texels cover more area on textured geometry). Even if the required texel is clipped out of every clipped level, the unclipped pyramid levels will contain it. You can use an invalid border to force the use of lower levels of the MIPmap to do the following: • Reduce the abrupt discontinuity between MIPmap levels if the clip region is small: using coarser LODs blends MIPmap levels over a larger textured region. • Improve performance when a texture must be roamed very quickly. Since the invalid border can be adjusted dynamically, it can reduce the texture and system memory loading requirements at the expense of a blurrier image. Required texel Clip center Clip region Fine Invalid border Required texel Clip center Coarser Figure 15-6 007-1680-100 Invalid Border 545 15: ClipTextures Updating the Clipcenter To figure out what part of the texture must be loaded in each of the clipped levels, you must know where the viewer is relative to the geometry being textured. Often this position is computed by finding the location of the cliptextured geometry that is closest to the viewer, and converting that to a location on the texture. This position is called the cliptexture center and it must be updated every frame as the viewer moves relative to the cliptextured geometry. Centered Center moves Toroidal loads Figure 15-7 Texture coordinates wrap Same as centered Clipcenter Moving The clipcenter is set by the application for level 0, The cliptexture code then derives the clipcenter location on all MIPmap levels. As the viewer roams over a cliptexture, the centers of each MIPmap level move at a different rate. For example, moving the clipcenter one unit corresponds to the center moving one half that distance in each dimension in the next-coarser MIPmap level. Most of the work of cliptexturing is updating the center properly and updating the texture data in the clipped levels reliably and efficiently each frame. Virtual Cliptextures on InfiniteReality Systems Cliptextures save texture memory by limiting the extent of texture levels. Every level in the mipmap is represented in texture memory, and can be accessed as the geometry is 546 007-1680-100 Overview textured. On InfiniteReality systems, there are limits to the number of levels the cliptexturing hardware can access while rendering, which restricts the cliptextures maximum size. This limit can be exceeded by only accessing a subset of all the MIPmap’s levels in texture memory on each piece of geometry, “virtualizing” the cliptexture. The virtual offset is sets a virtual “level 0” in the MIPmap, while the number of effective levels indicates how many levels starting from the new level 0 can be accessed. The minlod and maxlod parameters are used to ensure that only valid levels are displayed. The application typically divides the cliptextured terrain into pieces, using the relative position of the viewer and the terrain to update the parameter values as each piece is traversed. Callback Effective levels Callback Effective levels Figure 15-8 Virtual Cliptexture Concepts For more information about virtual cliptextures, see “Virtual ClipTextures” on page 593. 007-1680-100 547 15: ClipTextures Cliptexture Support Requirements Ideally, pfClipTextures would be interchangeable with pfTextures in OpenGL Performer. Unfortunately, this is only partially true. The following sections describe some of the differences between OpenGL Performer textures and cliptextures. Centering Every level is complete in a regular texture. Cliptextures have clipped levels, where only the portion of the level near the cliptexture center is complete. In order to look correct, a cliptextures center must be updated as the channel’s viewport moves relative to the cliptextured geometry. Cliptextures require functionality that recalculates the center position whenever the viewer moves (essentially each frame). This means that a relationship has to exist between the cliptexture and a channel. Applying Textures only need to be applied once. Cliptextures must be applied every time the center moves (essentially each frame). In order to apply at the right time, cliptextures need to be connected to a pfPipe. Texel Data A texture does not know where its data comes from. The application just supplies it as a pointer to a region of system memory when the texture is applied. Cliptextures need to update their contents as the center moves and they are reapplied each frame. As a result, they need to know where their image data resides on the disk. In order to maximize performance, cliptextures also cache their texel data in system memory. As a result, cliptextures are a lot more work to configure, since you have to tell them how to find their data on disk, and how you want the data cached in system memory. Special Features Since cliptexture levels are so large, OpenGL Performer offers additional features not available to regular textures. 548 007-1680-100 Overview Insets With certain restrictions, cliptexture levels can be partially populated, containing “islands” of high resolution data. This can be useful if the application only needs high-resolution texel data in relatively small, widely scattered areas of a large cliptexture. An example of this might be an airline flight simulator, where high resolution data is only needed in the vicinity of the airports used by the simulator. For more information about insets, see “Cliptexture Insets” on page 601. Virtualization on InfiniteReality Systems To further increase the size of cliptextures that OpenGL Performer can use, the levels themselves can be virtualized; It then selects a subset of all the available texture levels to be loaded into memory. This requires additional support by the application. Virtual cliptextures are described in detail in “Virtual ClipTextures” on page 593. Multiple Pipe Support Since cliptextures require both system and texture memory resources, OpenGL Performer has provided functionality to share the system memory resources when a cliptexture is used in a multipipe application. “Slave” cliptextures and a “master” cliptexture share system memory resources, but have their own classes and texture memory. How Cliptextures Interact with the Rest of the System As a result of their special requirements, cliptextures are used differently than pfTextures with many different OpenGL Performer classes. The following sections describe these differences. Geostates When everything is configured properly, a pfClipTexture is interchangeable with a pfTexture when used in a geostate. Note that when using emulated cliptextures, the pfClipTexture must be assigned to the pfGeoState before the pfGeoState is associated with any pfGeoSet. 007-1680-100 549 15: ClipTextures Pipes A pfClipTexture can be connected to a pfMPClipTexture, a multiprocessing component, which is connected to a pfPipe. From the pipe’s point of view, a pfMPClipTexture is something it can apply to. Channels Some functionality must be supplied to update a cliptexture’s center as the channel moves with respect to the cliptextured geometry. This functionality can be supplied by the application, or OpenGL Performer can do it automatically if the application uses clipcenter nodes. A clipcenter node is added to the scenegraph and is traversed by the APP process just like every other node in the scenegraph. When the clipcenter node is traversed by a channel, the clipcenter node computes the relationship between the cliptextured geometry and the channel’s eyepoint, and updates the cliptexture’s center appropriately. Cliptexture Support in OpenGL Performer Cliptexture is a large and diverse piece of functionality. As a result, cliptexture support is found in nearly every major library in OpenGL Performer. libpr Support The pfImageCache class defines image caches which manage the updating of clipped levels, pfImageTile classes are used to define non-clipped cliptexture levels and define pieces of clipped levels downloaded from disk to system memory. The pfQueue class supports read queues, which manage the read requests from disk to system memory in image caches, while the pfClipTexture class itself defines cliptextures themselves, virtual mipmaps composed of image caches and image tile levels. The pfTexLoad class defines download requests when image caches download texels from system to texture memory. libpf Support The libpf library adds multiprocessing support for using cliptextures in scene graphs. the pfMPClipTexture class ties together pfClipTextures, pfPipes, cliptexture centering functionality (often pfuClipCenterNode nodes) and the application itself in a 550 007-1680-100 Overview multiprocessing environment. additional functionality in the pfPipe class ensures that cliptextures are applied properly. libpfutil Support The libpfutil library provides easy to use clipcentering functionality through the pfuClipCenterNode class, a subclass of the pfGroup class. This library also provides traversals to simplify the work of finding cliptextures in a scene graph using pfuFindClipTextures(), code for post loader configuration, where pfMPClipTextures are created, and attached to pipes and clipcenter nodes using pfuProcessClipCenters() and pfuProcessClipCentersWithChannel(). The pfuAddMPClipTextureToPipes() and pfuAddMPClipTexturesToPipes() routines connect pfMPClipTextures to the proper pipes, handling multipipe issues in a clean way. Load time configuration is simplified using the pfuInitClipTexConfig(), pfuMakeClipTexture(), and pfuFreeClipTexConfig() along with the appropriate callbacks for image caches and image tiles. Image cache configuration is supported with pfuInitImgCacheConfig(), pfuMakeImageCache(), and pfuFreeImgCacheConfig() routines. libpfdu Support The cliptexture configuration file parsers are supported here; pfdLoadClipTexture() and pfdLoadClipTextureState() work with cliptexture configuration files to simplify the creation and configuration of cliptextures. The companion programs that create and configure pfdLoadImageCache() and pfdLoadImageCacheState(). All of these parsers use the pfuMakeClipTexture() and pfuMakeImageCache() configuration routines. libpfdb Support Example cliptexture loaders, including the libpfim example cliptexture loader, the libpfct demo loader, the libpfspherepatch loader, and the libpfvct virtual pseudo loader are all included here. Sample Applications Performer also ships with examples of applications that support cliptexturing. The main example is clipfly (a modified version of perfly). Another program that uses cliptextures is the clipdemo sample. 007-1680-100 551 15: ClipTextures Cliptexture Manipulation While the scene graph is being viewed, the application may want to dynamically alter the appearance or performance characteristics of the cliptexture. The mpcliptexture provides functionality to support parameter changes in the APP process, providing frame-accurate updating. Here are some of the parameters that might be changed. Load Control The DTR functionality (described in detail elsewhere in this chapter) is largely automatic. Some high performance applications may need to adjust DTR parameters to improve appearance performance trade-offs. Invalid Border The invalid border can be adjusted at runtime to shrink the effective size of the clip region. This might be done to provide additional load control beyond the per-level control that DTR provides. Share Masks When operating master and slave cliptextures in a multipipe application, the application may want to change the sharemask, which controls the synchronization of parameters between master and slave cliptextures. Read Function The image cache creates requests to read image tiles from disk to the image cache’s system memory cache. The read function processes these requests and actually does the data transfer. OpenGL Performer provides set of read functions that attempts to do direct-IO reads for speed, but falls back to normal reads if direct IO is not possible. The application can replace the OpenGL Performer default function with its own custom read function. This could be useful for implementing special functionality, such as dynamic decompression pfClipTexture data. 552 007-1680-100 Cliptexture API Read Queue Sorting The read queue provides dynamic sorting of the read requests to improve performance and minimize latency. The application can provide custom sorting routines. Cliptexture API Cliptexturing has a large API. Not only is there are lot of cliptexture functionality scattered throughout the library, but there is often more than one way to use a particular piece of functionality. In order to make things clearer, and make it easier to use the API described here, the cliptexture API is grouped and ordered in the same way an application writer would use it. The API is grouped into four sections: • Preprocessing the cliptexture data. • Configuring cliptextures and image caches. • Post-load-time configuration. • Run-time manipulation. Preprocessing ClipTextures Before using cliptextures, large textures must be preprocessed, as follows: 1. Start with the highest-resolution version of the image (texture) and build a MIPmap of the image. 2. Choose a clip size. 3. Tile each MIPmap level. Every image that is larger than the clip size must be cut into tiles. All of the tiles in one MIPmap level must be equal in size. You generally choose a tile size that is about 1/4 of the clip size or less. 4. Divide the levels into separate files to maximize download performance. The files should be named properly so that the image caches can access them. 007-1680-100 553 15: ClipTextures 5. If the configuration parsers are used, cliptexture configuration files are also created at this time. The following sections describe the steps in this procedure in greater detail. Building a MIPmap Building a MIPmap of an image requires an algorithm that performs the following tasks: 1. Start with the highest-resolution version of the image (texture). The image dimensions in pixels must be in powers of 2, for example, 8192 X 8192. 2. Average every four adjacent texels of a high resolution image into a single texture (essentially blurring it and shrinking it by a factor of two in both dimensions). 3. Save the result as a new, blurrier, smaller image. 4. Convert the MIPmaps into a compatible format. 5. Repeat the first two steps with each blurrier image until you have a single texel whose color is the average of all the texel colors in the original image. Each successive reduction is called a level of detail (LOD). The more the reduction, the higher the level of detail, the coarser the image. There are a variety of tools that tile textures. OpenGL Performer provides some simple ones available in the /usr/share/Performer/src/tools directory for IRIX and Linux and in %PFROOT%\Src\tools for Microsoft Windows. They are listed in Table 15-1. Table 15-1 554 Tiling Algorithms Program Description rsets Shrinks and tiles one or more .rgb image files recursively. rsets stops tiling when it reaches the clip size you give it. rsets assumes that the original image is square. rgb2raw Converts .rgb images into a raw format that can be downloaded directly into texture memory. Files should be in a raw format to avoid conversions at download time. shrink Is a subset of rsets functionality; makes a tree-like structure of LOD images from an .rgb image. to5551 Converts from .rgb to the 5551 raw format. 007-1680-100 Preprocessing ClipTextures Table 15-1 Tiling Algorithms (continued) Program Description to888 Converts from .rgb to the 888 raw format. to8888 Converts from .rgb to the 8888 raw format. viewtile Enables you to view a raw format image tile. For more information about MIPmaps, see the OpenGL Programming Guide. Formatting Image Data The texel data must be in a format that can be used in OpenGL Performer textures. This means the texels must have contiguous color components, whose size and type match a supported format. Keep in mind that these texels will be loaded dynamically, on an as-needed basis, so the smaller the size of each texel, the better the performance of the cliptexture. You should choose the smallest texel format that provides acceptable color quality. A good choice might be RGBA 5551, which takes up 16 bits per texel. OpenGL Performer provides some tools for converting from rgb format to 5551 or 888 RGBA. They are named to5551 and to888 and are found in /usr/share/Performer/src/tools for IRIX and Linux and in %PFROOT%\Src\tools for Microsoft Windows. For more information about file formats, see “Building a MIPmap” on page 554. Tiling an Image Dividing a texture into tiles allows you to look at a subset of all texels in the texture. In this way, you can selectively download from disk into the texture memory only those texels that the user is viewing and those they might soon look at. Since downloading texture tile files from disk to texture memory takes a long time, the image cashes decide which tiles a viewer might need next and download them in advance. Note: In the highest resolution LOD, one texel corresponds to one pixel. 007-1680-100 555 15: ClipTextures Texel tiles in each level are loaded into memory separately, from coarsest to finest. The high-resolution tiles take longer to download than the coarser tiles. If a viewer advances through a scene so quickly that the high-resolution tiles cannot download from disk into texture memory in time, lower-resolution tiles are displayed instead. The effect is that if the viewer goes too fast, the tiles become blurry. When the viewer slows down, the tiles displayed are less coarse. Using lower instead of higher-resolution levels is controlled by cliptexture’s load control mechanism, DTR. Without DTR, OpenGL Performer waits for all of the levels to download before displaying any one of them. DTR removes this restriction, displaying the levels that have been downloaded. If you want to break up a .rgb image into tiles, OpenGL Performer provides the subimg program in /usr/share/Performer/src/tools for IRIX and Linux and in %PFROOT%\Src\tools for Microsoft Windows. Tile Size Small tiles, while less efficient, are better at load leveling, since the time it takes to load a new tile into system memory is smaller. It also means that the total size of an image cache in system memory can be smaller. We’ve found that tile sizes of 512 x 512 and 1024 x 1024 provide a good trade-off between download efficiency and low latency, but download performance is very sensitive to system configuration. Experimenting is the best way to find a good tile size. Cliptexture Configuration After preprocessing the texture data, you need to configure cliptextures. Configuration is actually a two step process; the configuration that can be done by the scenegraph loader, and the configuration that requires pfPipes and pfChannels to be present. This section describes the first stage of configuration. Configuration Considerations An application must configure the cliptexture in two steps: 556 • Loader—when the scene graph is constructed. • Post-loading—when the channel and pipes are known to the application. 007-1680-100 Configuration API This process is complex. OpenGL Performer supplies a number of utilities to make the job easier. To manipulate cliptexture parameters, the application makes calls to the pfMPClipTexture in the APP process. The pfMPClipTexture updates the cliptexture in a frame-accurate manner. Load-Time Configuration This is the time the scene graph is being constructed. Geostates are pointed to cliptextures; the cliptextures themselves are created and configured using the cliptexture configuration files and the libpfdu parsers. If the application does its own configuration, it should use the libpfutil routines to simplify the process and to ensure adequate error checking. If the application opts to use OpenGL Performer clipcentering support, clipcenter nodes are inserted into the scene graph at the root of the cliptextured geometry and connected to the corresponding cliptexture. Note: When using emulated cliptextures, assign the cliptexture to the pfGeoState first, then assign the pfGeoState to pfGeoSets. Post-Load-Time Configuration At this stage the scene graph has been created and the channels and pipes have been defined. The libpfutil traversers (pfuProcessClipCenters() or pfuProcessClipCentersWithChannel()) are used to create pfMPClipTextures, connecting them with the appropriate cliptextures and clipcenter nodes. These routines return a list of pfMPClipTextures, which can be used with pfuAddMPClipTextureToPipes() and pfuAddMPClipTexturesToPipes() to attach the pfMPClipTextures to the appropriate pfPipes. These routines can be used for single pipe and multipipe applications with little or no change to the calling sequence. Configuration API Since cliptexture configuration is complex, we provide three different cliptexture configuration API layers, allowing different trade-offs between flexibility and simplicity. 007-1680-100 557 15: ClipTextures libpr Functionality The lowest layer, using the libpr calls, is the most complex and difficult. A cliptexture has the same configuration requirements as a MIPmapped pfTexture, where texel format, type and texture dimensions must be configured. In addition, cliptextures have to know about system memory caching, the file configuration of the texture data, load control, read queue, and other cliptexture specific configurations. Using the libpr layer directly is not recommended, since it is error prone and does not buy much flexibility compared to the libpfutil configuration routines. In the following subsections are the libpr calls you must consider when configuring a cliptexture directly. These are the functions needed to configure the cliptexture itself. The cliptexture contains two types of levels: image cache levels and image tile levels. Image caches support clipped levels in a cliptexture. They know where their texture data resides on disk, they understand clip regions, and set up system memory caching and updating. Every properly-configured image cache points to an image tile, called a proto tile, which contains global information about the texel format, size, and file information about the image tiles the image cache uses to update clipped texture levels. Configuring an Image Cache Level Image tiles can be used by themselves to represent unclipped levels. Essentially the unclipped level is represented by a single tile covering the entire level. Because image tiles do not understand clip regions and cannot do dynamic updating, image tiles cannot be used to represent clipped levels. To configure an image cache level, use the following calls: 558 • pfNewClipTexture() • pfTexName() • pfClipTextureVirtualSize() • pfClipTextureClipSize() • pfTexImage() • pfTexFormat() • pfClipTextureInvalidBorder() • pfClipTextureEffectiveLevels() 007-1680-100 Configuration API • pfClipTextureAllocatedLevels() • pfClipTextureLevel() Configuring an Image Cache Proto Tile There are also image tile calls in this sequence. They are used to configure the image cache’s proto tile, which is used as a template for the tiles the image cache will use to load texel data from disk to system memory cache. To configure an image cache proto tile, use the following calls: • pfNewImageTile() • pfImageTileReadFunc() • pfGetImageTileMemInfo (page size) • pfImageTileMemInfo() • pfImageTileReadQueue() • pfImageTileHeaderOffset() • pfImageTileNumFileTiles() • pfImageTileSize() • pfImageTileFileName() • pfImageTileFileImageType() • pfImageTileMemImageType() Configuring an Image Cache To configure an image cache, use the following calls: 007-1680-100 • pfImageCacheName() • pfImageCacheTexRegionOrigin() • pfImageCacheMemRegionOrigin() • pfImageCacheImageSize() • pfImageCacheMemRegionSize() • pfImageCacheTileFileNameFormat() 559 15: ClipTextures • pfImageCacheTexRegionSize() • pfImageCacheMemRegionSize() • pfImageCacheTex() • pfImageCacheTexSize() • pfImageCacheFileStreamServer() • pfImageCacheProtoTile()—Copies the information into the image cache’s proto tile. • pfDelete (tmp_proto_tile)—Now that it is copied into the image cache, you can delete it. Configuring a pfTexture Image caches can be used independently of cliptextures, if they are, they need to be associated with a pfTexture, and that texture needed to be configured. To configure a pfTexture, use the following calls: • pfTexImage() • pfTexFormat() Configuring the Default Tile Image caches can have a default tile defined, which is the tile to use if a tile on disk can’t be found. Default tiles can be useful for “filling in” border regions of a cliptexture level. Default tiles are covered in more detail in section “default_tile” on page 572. To configure the default tile, use the following calls: 560 • pfNewImageTile() • pfCopy() (proto to default) • pfImageTileFileName() • pfImageTileReadQueue() • pfImageTileDefaultTile() 007-1680-100 Configuration API Configuring Image Tiles Image tiles need their own configuration, since they need to know about the file they should load from texel formats, etc. To configure an image tile, use the following calls: • pfNewImageTile() • pfImageTileMemImageFormat() • pfImageTileFileImageFormat() • pfImageTileMemImageType() • pfImageTileSize() • pfImageTileHeaderOffset() • pfClipTextureLevel() • pfLoadImageTile() Configuration Utilities Using the libpr calls to configure a cliptexture is difficult and error prone. OpenGL Performer provides utilities to make cliptexture configuration easier and more robust. The configuration utility API is broken into two groups. One group is used to configure cliptextures, the other configures image caches. Each group contains three functions, an init function, a config function, and a free function. These functions work with a structure that the application fills in. The initialize function initializes the optional fields in the structure with default values, and the mandatory fields with invalid values. Configuring the structure allows the configuration function to do more error checking, and to allow the application to avoid the tedium of filling in optional field. The application then sets fields in the structure to parameterize how the cliptexture or image cache should be configured. The application then calls the configuration function on the filled in structure. The free function is then called with the structure to ensure that all allocated values are freed. Cliptexture Configuration Methods to configure the cliptexture include the following: 007-1680-100 561 15: ClipTextures pfuInitClipTexConfig(pfuClipTexConfig *config) Initialize the values of the pfuClipTexConfig structure that has been allocated by the application. pfuMakeClipTexture(pfuClipTexConfig *config) Return a cliptexture configured as directed by the values in the pfuClipTexConfig structure. pfuFreeClipTexConfig(pfuClipTexConfig *config) Free any malloc’d structures that the application or the initialize function may have created. Image Cache Configuration Methods to configure the image cache include the following: pfuInitImgCacheConfig(pfuImgCacheConfig *config) Initialize the values of the pfuClipTexConfig structure that has been allocated by the application. pfuMakeImageCache(pfuImgCacheConfig *config) Return a cliptexture configured as directed by the values in the pfuClipTexConfig structure. pfuFreeImgCacheConfig(pfuImgCacheConfig *config) Free any malloc’d structures that the application or the init() function may have created. All of these functions are defined in libpfutil/cliptexture.c. The structures themselves are defined in pfutil.h. Filling in the Structures Filling the pfuImgCacheConfig structure to create and configure the image cache is considerably simpler than setting fields in the pfuClipTexConfig structure. This is because the cliptexture configuration must also create and configure image cache and image tiles to populate its levels. The configuration code does this supplying a function pointer to configure the image cache levels and a function pointer for configuring image tile levels. Each function pointer also has a void data pointer so you can pass data to the functions. The function pointers expect functions with the following forms: pfImageCache *exampleICacheConfigFunction(pfClipTexture *ct, int level, struct _pfuCilpTexConfig *icInfo) 562 007-1680-100 Configuration API pfImageTile *exampleITileConfigFunction(pfClipTexture *ct, int level, struct _pfuClipTexConfig *icInfo) The cliptexture and image cache configuration parsers, described in the next section, use the configuration utilities. You can look at the parsers as example code. For example, you may want to look at pfdLoadImageTileFormat() and pfdLoadImageCache() formats for example functions for the function pointers. The parsers are in the /usr/share/Performer/src/lib/libpfdu/pfdLoadImage.c file for IRIX and Linux and in file %PFROOT%\Src\lib\libpfdu\pfdLoadImage.c for Microsoft Windows. Configuration Files The easiest and most commonly used method to configure cliptextures is to create cliptexture and image cache configuration files, then use the configuration parsers to create and configure cliptextures. The configuration files can be created and stored along with the texture data files. Configuration files allow an application or loader to simply call a single function to create and configure cliptextures. Configuration files are ascii text files containing a token parameter format. Values are separated by white space and the token parameter sequences can be placed in the file in arbitrary order. Comments can also be added to the configuration files, making them self-documenting. Using Configuration Files Four parser functions are available to create and configure cliptextures and image caches using configuration files: pfClipTexture *pfdLoadClipTexture(const char *fileName) pfImageCache *pfdLoadImageCache(const char *fileName) These parser functions take a configuration file name, and use it to configure and create a cliptexture or an image cache respectively. The cliptexture configuration file may refer to image cache configuration files, which will be searched for and used automatically. Two other versions of these parsers also take a pointer to a configuration utility structure. This allows you to preconfigure using the configuration structure and then finish with the parser and configuration files. 007-1680-100 563 15: ClipTextures pfClipTexture *pfdLoadClipTextureState(const char *fileName, pfuClipTexConfig *state) pfImageCache *pfdLoadImageCacheState(const char *fileName, pfuImgCacheConfig *state) The parsers use OpenGL Performer’s pfFindFile() functionality to search for the configuration files. The parsers support environment variable expansion and relative pathnames to make it simpler to create configuration files that refer to other configuration or data files. Creating Configuration Files To successfully use cliptextures, you must first prepare the texture data and create the configuration files: 1. Create an image cache configuration file for each level using an image cache in the cliptexture. The configuration file should describe the following: • Format and tiling of the texture data. • Location and names of the files containing the texture data. • Size of the tex region in texture memory. • Size and layout of the mem region in system memory. 2. Create a cliptexture configuration file. It contains the following: 564 • Name and location of each image cache configuration file. • Names and locations of the texture data for each image tile level in the cliptexture. Remember, image tile levels cannot be clipped levels; so, they can only be used in the pyramid levels. Image cache levels can be used anywhere. • General properties of the cliptexture. • Look at the example cliptexture configuration files in the /usr/share/Performer/data/clipdata directory for IRIX and Linux and in %PFROOT%\Data\clipdata for Microsoft Windows. The cliptexture configuration files use the .ct suffix. The image cache configuration files use .ic for their suffixes. 007-1680-100 Configuration API 3. Test the image cache configuration files individually, using the pguide/libpr/C/icache program. 4. Test the cliptexture configuration file using the /sample/C/clipfly or /sample/C++/clipdemo sample appllications or the /pguide/libpr/C/cliptex or the /pguide/libpf/C/cliptex programs. 5. When the configuration and data files are complete and tested, your application can create and configure a cliptexture by calling pfdLoadClipTexture(fname) using the name of the cliptexture configuration file. If more control is needed, you can use pfdLoadClipTextureState(fname, state) initializing and configuring the configuration utility cliptexture structure, pfuClipTexConfig. Configuration File Tips Unfortunately, cliptexture configuration is not trivial, even with documentation and example programs. Success in creating working configuration files requires a two-prong approach: • Keep them simple: set the minimum number of fields possible. Take advantage of the default values. Try to find a similar example configuration file to copy from. • Work bottom-up: create and test image cache configuration files first, gradually building up to the cliptexture configuration file. We have found that parameterized naming of the image caches and tile files works the best. If you have named your files consistently, this can be easy. If things do not work, you can fall back and name your file explicitly as a sanity check. Read the error messages carefully; they try to point out where in the configuration file the parser found problems. If you need more information, try rerunning the program with PFNFYLEVEL set to 5 or 9. A number of example configuration files and cliptextures are available on the OpenGL Performer release. Working from one of them can save a lot of time. Some places to look are the following: • data/clipdata/hunter • data/clipdata/moffett • data/asddata Note that emulated cliptextures are currently incompatible with pfASD. 007-1680-100 565 15: ClipTextures Cliptexture Loaders Finally, your application might be able to take advantage of some of the cliptexture loaders. The libpfim loader supports loading a cliptexture, and updating its center as a function of viewposition. The libpfct loader creates a cliptexture with simple terrain. Virtual cliptextures, mentioned in “Virtual ClipTextures” on page 593, can also be created using the libpfspherepatch or libpfvct loaders. These loaders can be used as examples if you need to write your own loader that supports cliptextures. The libpfct and the libpfspherepatch loaders are the most up-to-date cliptexture loaders and include support for emulated cliptextures as well as support for virtual cliptextures on InfiniteReality systems. Image Cache Configuration File Details Image cache configuration files supply the following information to OpenGL Performer: • Format of the texel data. • Size of the entire texture at a particular MIPmap level. • How to find the files containing the texel data for this image cache. • Size and layout of image cache tiles in memory. • Size of the image cache that should be kept in texture memory. • A default image tile to use if one is missing. • The size each level should be clipped to. • The amount of border that should be invalidated at each level. Configuration Fields Configuration fields are either tokens or parameter values, as listed in Table 15-2. All fields are character strings and all parameters must be separated by white space. The token names marked with an asterisk (*) are optional and default to reasonable values. Table 15-2 566 Image Cache Configuration File Fields Token Name Parameters Description ic_version2.0 no data field Start of image cache config files: type and version *tex_size 3 integers Area of tex memory for level if not tex_region_size *header_offset integer Beginning of file to skip over in bytes 007-1680-100 Configuration API Table 15-2 Image Cache Configuration File Fields (continued) Token Name Parameters Description *tiles_in_file 3 integers Dimensions of grid of tiles stored in each file *s_streams filepath list List of streams used to access files in S dimension *t_streams filepath list List of streams used to access files in T dimension *r_streams filepath list List of streams used to access files in R dimensions *default_tile filepath string Tile to use if expected tile is not available *page_size integer System page size; memory allocation alignment *read_func 1 or 2 strings Custom read function; library, func or func in app *lookahead integer Extra tiles in mem region for lookahead caching ext_format string External format of stored texels int_format string Internal format used by graphics hw img_format string Image format of stored texels icache_size 3 integers Size of complete image level in texels tex_region_size 3 integers Area to load in texture memory; matches clip size mem_region_size 3 integers Dimensions of system memory cache in tiles tile_size 3 integers Dimensions of each file in texels tile_format scanf-style string Parameterized path to tile files tile_params list of symbols Parameter types in order in tile_format string Image Cache Configuration File Description The ic_version2.0 token must be first in an image cache configuration file. This token identifies the file as an image cache configuration file and the format (version) of the configuration file. Next the parser looks for tokens and any associated data values. In general, the order of the tokens in the file must follow the sequence specified in the table above. The tokens marked with an asterisk are optional. Optional tokens have default values, which are used if the token and value are omitted.Tokens can have the following: 007-1680-100 567 15: ClipTextures • No arguments • A fixed number of arguments • A variable number of arguments If a token has a fixed number of arguments, the token must be followed by a white space-separated list containing the specified number of arguments. If the token has a variable number of arguments, one of its arguments specifies the number of arguments used. Any time a token is expected by the parser, a comment can be substituted. A comment cannot be put anywhere in the file, however. For example, if a token expects arguments, you cannot place a comment between any of them; you have to place it after all of the previous tokens arguments. There are a variety of supported comment tokens; they are interchangeable. The comment tokens are #, //, ;, comment, or rem. ext_format, int_format, and img_format One of the first things that must be specified in an image cache is the format of the texel data. This includes the external format (ext_format), internal format (int_format) and image format (img_format). The arguments expected by these format parameters are the ASCII string names of the format’s enumerates. For example, a valid external format would be ext_format PFTEX_FLOAT. Consult the pfTexture man pages for a list of the valid formats of each type. icache_size, mem_region_size, and tex_region_size The next set of parameters that must be specified in the image cache configuration file is its size on disk, in system memory, and in texture memory. The icache_size token requires the size of the image cache. This means the dimensions, in texels, in the s, t, and r dimensions of the complete texture at this level. Since three dimensional textures are not currently supported, the r parameter will always be 1. An image cache’s texels are organized into a set of fixed sized pieces, called tiles. Both in system memory and on disk, the texels are broken up this way. At any given time, an array of these texel tiles are cached in system memory. They are arranged as an array in system memory. If the center of the image cache nears the edge of this array, the most distant tiles are dropped out, and new tiles are read in from disk. The larger the array of tiles in system memory, the more of the complete texture is cached there, and the less 568 007-1680-100 Configuration API likely new tiles may need to be swapped in. The benefit is offset by the cost of tying up more system memory to hold the texel tiles. The arrangement and dimensions of tiles in system is defined for each image cache, and is set with the mem_region_size token. This token expects three arguments which determine the number of tiles in the s, t, and r dimensions of the grid. Since three dimensional textures aren’t currently supported, the r dimension is always 1. A subset of the texels in system memory are cached in the texture memory itself. These texels are arranged in a rectangular region. The dimensions of this region are defined by the tex_region_size token. It expects three arguments, the number of texels in the s, t, and r dimensions. Again, since three dimensional textures are not supported, the r value is always 1. The image cache configuration file allows some leeway in the arrangement of texel tiles on disk. There can be one or more tiles on each disk file, and the file itself could contain non-texel information at the beginning of the file. The tiles themselves can have user-specified dimensions. While there is some flexibility in how tiles are stored in files on disk, there are restrictions also. Any header must be the same size for every file in an image cache. The same is true for the tile size, and the number and layout of tiles in each file. If there is more than one tile in a file, the tiles must be arranged in row-major order. In other words, as you pass from the first tile to the last, the s dimension must be incrementing fastest. tile_format and tile_params The image cache texel data is stored in one or more files. The configuration file provides a way for OpenGL Performer to find these files. The files usually have similar names, varying in a predictable way, such as by tile position in the image cache array and size of the image cache. The files themselves are grouped in on or more directories. The file name and file path information is divided into a number of groups within the configuration file. There is a scanf-style string specifying the path to find image cache files. There are a number of parameters in the string that vary as a function of the tile required and the characteristics of the image cache. The next group of tokens describes the location of the configuration files defining the location of the texture data tiles for the image cache. You can define the texture tile configuration filenames with a scanf-style string containing parameter values, as is done with image caches. To create parameterized image cache names, you must define the tile_format and tile_params tokens. 007-1680-100 569 15: ClipTextures The tile_format token is followed by a scanf-style string describing the file path and filename of the image cache configuration files. The argument contains constant parts, interspersed with %d or %s parameters. The number of parameters must match the number of symbols supplied as parameters to the tile_params token. If the tile_format string starts with the pattern $ENVNAME, ${ENVNAME}, or $(ENVNAME), then the value of ENVNAME will be assumed to be an environment variable and expanded into the base name. The possible values of the image tile file name parameters is given in the table below. Table 15-3 Image Tile Filename Tokens Image Tile Filename Tokens Description PFIMAGECACHE_TILE_FILENAMEARG_VSIZE_S Virtual size S width PFIMAGECACHE_TILE_FILENAMEARG_VSIZE_T Virtual size T width PFIMAGECACHE_TILE_FILENAMEARG_VSIZE_R Virtual size R width PFIMAGECACHE_TILE_FILENAMEARG_TILENUM_S Tiles from origin in S PFIMAGECACHE_TILE_FILENAMEARG_TILENUM_T Tiles from origin in T PFIMAGECACHE_TILE_FILENAMEARG_TILENUM_R Tiles from origin in R PFIMAGECACHE_TILE_FILENAMEARG_TILEORG_S Texels from origin in S PFIMAGECACHE_TILE_FILENAMEARG_TILEORG_T Texels from origin in T PFIMAGECACHE_TILE_FILENAMEARG_TILEORG_R Texels from origin in R PFIMAGECACHE_TILE_FILENAMEARG_STREAMSERVERNAME From streams PFIMAGECACHE_TILE_FILENAMEARG_CACHENAME The tile_base value PFIMAGECACHE_TILE_FILENAMEARG_FILENUM_S Files from origin in S PFIMAGECACHE_TILE_FILENAMEARG_FILENUM_T Files from origin in T PFIMAGECACHE_TILE_FILENAMEARG_FILENUM_R Files from origin in R header_offset, tiles_in_file, and tile_size The header_offset argument specifies the size of the file’s header in bytes. This many bytes will be skipped over as a file is read. The tiles_in_file token requires three arguments, specifying the number of tiles in the s, t, and r dimensions. The r dimension 570 007-1680-100 Configuration API must always be 1, since 3D textures are not supported. The tile_size parameter defines the texel dimensions of each tile in s, t, and r. Again, r must be 1. Both the header_offset and the tiles_in_file tokens are optional. They default to the values 0 and 1 1 1, respectively, specifying no header and a single tile in each file. One of the major bottlenecks to sustained cliptexture performance is the speed of copying texels from disk to system memory. Cliptextures can be configured to maximize the bandwidth of this transfer by distributing image tiles over multiple disks and downloading them in parallel. The streams section of the configuration file is used for this purpose. num_streams, s_streams, t_streams, and r_streams A stream, short for stream device, can be thought of as a separate disk that can be accessed in parallel with other disks. Each disk is mounted in a file system and, therefore, has a unique filepath segment. The streams tokens allow you to identify these stream filepath segments and how the image tiles are distributed among them. The stream devices are arranged in a three dimensional grid with s, t, and r dimensions just like the image tiles are in memory. The stream device is accessed by taking the position of the tile, counting tiles from the origin in the s, t, and r directions, and generating a coordinate, modulo the number of stream devices in the corresponding s, t, and r directions. The s, t, and r values generated are used to look up the appropriate stream device. If the stream server name is part of the tile file name format string, it effects which disk is used to find the tile. Stream servers improve bandwidth at the expense of duplicating image tiles over multiple disks. You must insure that the proper image tiles are available for any disk which is addressed by the tile’s s, t, and r coordinates modulo the available number of stream servers for each of those dimensions. The stream server tokens are optional. The s_streams token is followed by a list of filepaths. These are the names that will be indexed from the list by taking the s coordinate of the tile’s position in the image cache grid, modulo the number of s stream devices. The names in the s_stream list do not have to be unique. The t_streams and r_streams tokens work in exactly the same way, in the t and r directions, respectively. Sometimes only a subregion of the entire cliptexture is of interest to the application. This is especially true when you consider that the number of tiles in the s, t, and r directions must all be a power of two. To save space, improve performance, and make creating image caches more convenient, a default tile can be defined, and tiles of no interest can 007-1680-100 571 15: ClipTextures simply be omitted. If a tile cannot be found and a default tile is defined, then the default one is used in place of the missing one. default_tile Unlike normal tiles, which are read from disk as they are needed, the default tile is loaded as part of the configuration process. The tile is named in the configuration file as the argument to the default_tile token. The argument is a filepath to the default tile. If the tile_base token has been defined, it is pre-pended to the file path; otherwise, it is used as is. Cliptexture Configuration File Details Image cache configuration files supply the following information to OpenGL Performer: • format of the texel data • Size of the highest resolution level (level 0) • Size of clipped levels in texture memory • How to find the configuration files for each image cache • Size of the smallest level to be loaded as an image cache • Number of effective levels for virtual cliptextures • Number of allocated levels for virtual cliptextures Additionally, if no image-cache configurations are used, the cliptexture configuration file will include the following: 572 • Size of image tiles on disk • Number of tiles to be stored in RAM for each level • Format of tile filenames • Size of header in tile files 007-1680-100 Configuration API Configuration Fields Configuration fields are either tokens or parameter values, as listed in Table 15-4. All fields are character strings and all parameters must be separated by white space. Table 15-4 Cliptexture Configuration File Fields Token Name Parameters # or // or ; or comment comment 007-1680-100 Description comment symbols; comment to end of line ct_version2.0 no data field the beginning of the file: type and version ext_format string external format of stored texels int_format string internal format used by graphics hw img_format string image format of stored texels virt_size 3 integers size of complete texture at level 0 (finest level) clip_size integer size of clip region square for clipped levels *invalid_border integer width of clip region perimeter to not use *tile_size 3 integers size of tiles (used if no icache config files) *smallest_icache 3 integers smallest icache-level dimensions *lookahead integer extra tiles in mem region *icache_format scanf string icache fnames: no field? list files *effective_levels integer levels used for texturing in virtual cliptexture *icache_params string list format tokens in order *icache_files list of filenames only if icache_format is default *tile_files list of filenames pyramid; only if tile_format default *effective_levels integer levels used for texturing in virtual cliptexture *allocated_levels integer total virtual cliptexture levels in texture memory *header_offset 1 integer byte offset to skip user’s file header *tiles_in_file 3 integers Image tile arrangement in each file *read_func 1 or 2 strings custom read function; lib & func or func in app 573 15: ClipTextures Table 15-4 Cliptexture Configuration File Fields (continued) Token Name Parameters Description *tile_format scanf string Tile filename format *tile_params string list format parameter tokens in order *page_size integer system page size; memory allocation alignment Cliptexture Configuration File Description The ct_version2.0 token must be first in an cliptexture configuration file. This token identifies the file as an cliptexture configuration file and the format (version) of the configuration file. Next the parser looks for tokens and any associated data values. In general, the order of the tokens in the file must follow the sequence specified in the table above. The tokens marked with an asterisk are optional. Optional tokens have default values, which are used if the token and value are omitted. Tokens can have the following: • No arguments • A fixed number of arguments • A variable number of arguments If a token has a fixed number of arguments, the token must be followed by a white space-separated list containing the specified number of arguments. If the token has a variable number of arguments, one of its arguments specifies the number of arguments used. Any time a token is expected by the parser, a comment can be substituted. A comment can’t be put anywhere in the file, however. For example, if a token expects arguments, you can’t place a comment between any of them; you have to place it after all of the previous tokens arguments. There are a variety of supported comment tokens; they are interchangeable. The comment tokens are #, //, ;, comment, or rem. 574 007-1680-100 Configuration API ext_format, int_format, and img_format One of the first things that must be specified in a cliptexture is the format of the texel data. This includes the external format (ext_format), internal format (int_format) and image format (img_format). The arguments expected by these format parameters are the ASCII string names of the format’s enumerates. For example, a valid external format would be ext_format PFTEX_FLOAT. Consult the pfTexture man pages for a list of the valid formats of each type. virt_size and clip_size The next group of tokens characterizes the image cache itself. The virt_size token expects three integer arguments. They define the s, t, and r dimensions of the level 0 layer of the cliptexture in texels. The clip_size token describes the size of each layer that exists in texture memory. It takes one integer, describing the s and t dimensions of the clipped region. This value is the same for all levels of a cliptexture. If the image cache configuration files’ clip_size differs from this value, the cliptexture overrides it. invalid_border The invalid_border defines the region of each clipped level that should not be used. If a texel is needed in that region, the next level down is used instead. If the invalid border is large, the system may have to go down multiple levels, or even down to the pyramidal, unclipped part of the MIPmap. The invalid border argument is a single integer, describing the width of the border in texels. smallest_icache The smallest_icache token describes the s, t, and r dimensions of the lowest level that is described as an image cache. This parameter is needed because the unclipped, pyramidal part of the MIPmap can also be configured as image caches. This is an optional token. If it is not included in the file, the last clipped level is considered the smallest image cache in the cliptexture. icache_files, icache_format and icache_params The next group of tokens describes the location of the configuration files defining the image cache levels of the cliptexture. There are two methods of describing where the image cache configuration files. You can explicitly list the filenames in order with icache_files. 007-1680-100 575 15: ClipTextures The other method is to define the image cache configuration filenames with a scanf-style string containing parameter values, as is done with image caches. This is usually the preferred method. To create parameterized image cache names, you must define the icache_format and icache_params tokens. If the format string starts with the pattern $ENVNAME, ${ENVNAME} or $(ENVNAME), then the value of ENVNAME will be assumed to be an environment variable and expanded into the base name. The icache_format token is followed by a scanf-style string describing the file path and filename of the image cache configuration files. The argument contains constant parts, interspersed with %d or %s parameters. The number of parameters must match the integer given with the num_icache_params token. The tile parameters themselves follow the icache_params token. icache_files The number of parameters must match the number of parameters in icache_format. All of these parameters are optional. The list of available parameter tokens is given in Table 15-5. Table 15-5 Parameter Tokens Parameter Token Name Description PFCLIPTEX_FNAMEARG_LEVEL Cliptexture level (top is 0) PFCLIPTEX_FNAMEARG_LEVEL_SIZE Largest value of level’s virtual size PFCLIPTEX_FNAMEARG_IMAGE_CACHE_BASE Value of icache_base PFCLIPTEX_FNAMEARG_TILE_BASE Value of tile_base Uniquely naming that file for each level of the cliptexture, the parameter values are used to construct the name of the image cache configuration file. Near the bottom of the cliptexture, the size of lower levels are too small to warrant image caches. These levels are specified directly, referring to a single filename containing a single image tile for each level. The filenames for these tile files are specified in exactly the same way as the image cache configuration files are. Instead of icache_base, icache_format, num_icache_parameters, and icache_parameters, tile_base, tile_format, num_tile_parameters, and tile_parameters are used. The parameters available for use in the tile_format string are identical to the ones used for icache_format. 576 007-1680-100 Configuration API tile_files If image cache configuration files and/or image tiles are to be explicitly named, they are listed in order, from the top (largest) level to the bottom, using the icache_files and tile_files tokens. These tokens can only be used if the corresponding format, num_parameters, and parameter tokens are not. The number of filenames listed after icache_files and tile_files must exactly match the number of cached and uncached levels, respectively, in the cliptexture. header_offset, tiles_in_file, and tile_size The header_offset argument specifies the size of the file’s header in bytes. This many bytes will be skipped over as a file is read. The tiles_in_file token requires three arguments, specifying the number of tiles in the s, t, and r dimensions. The r dimension must always be 1, since 3D cliptextures are not supported. The tile_size parameter defines the texel dimensions of each tile in s, t, and r. Again, r must be 1. Both the header_offset and the tiles_in_file tokens are optional. They default to the values 0 and 1 1 1, respectively, specifying no header and a single tile in each file. The image cache texel data is stored in one or more files. The configuration file provides a way for OpenGL Performer to find these files. The files usually have similar names, varying in a predictable way, such as by tile position in the image cache array and size of the image cache. The files themselves are grouped in on or more directories. The file name and file path information is divided into a number of groups within the configuration file. There is a scanf-style string specifying the path to find image cache files. There are a number of parameters in the string that vary as a function of the tile required, and characteristics of the image cache. tile_base, tile_format and tile_params The tile_format token expects a scanf-style argument. If the string starts with the pattern $ENVNAME, ${ENVNAME} or $(ENVNAME), then the value of ENVNAME will be assumed to be an environment variable and expanded into the base name. The argument contains constant parts, interpersed with %d or %s parameters.The tile parameters themselves follow the tile_params token. The number of parameters must match the number of parameters in tile_format. 007-1680-100 577 15: ClipTextures The possible values of the image tile file name parameters are given in the table below. Table 15-6 Image Tile Filename Tokens Image Tile Filename Tokens Description PFIMAGECACHE_TILE_FILENAMEARG_VSIZE_S Virtual size S width PFIMAGECACHE_TILE_FILENAMEARG_VSIZE_T Virtual size T width PFIMAGECACHE_TILE_FILENAMEARG_VSIZE_R Virtual size R width PFIMAGECACHE_TILE_FILENAMEARG_TILENUM_S Tiles from origin in S PFIMAGECACHE_TILE_FILENAMEARG_TILENUM_T Tiles from origin in T PFIMAGECACHE_TILE_FILENAMEARG_TILENUM_R Tiles from origin in R PFIMAGECACHE_TILE_FILENAMEARG_TILEORG_S Texels from origin in S PFIMAGECACHE_TILE_FILENAMEARG_TILEORG_T Texels from origin in T PFIMAGECACHE_TILE_FILENAMEARG_TILEORG_R Texels from origin in R PFIMAGECACHE_TILE_FILENAMEARG_STREAMSERVERNAME From streams PFIMAGECACHE_TILE_FILENAMEARG_CACHENAME The tile_base value PFIMAGECACHE_TILE_FILENAMEARG_FILENUM_S Files from origin in S PFIMAGECACHE_TILE_FILENAMEARG_FILENUM_T Files from origin in T PFIMAGECACHE_TILE_FILENAMEARG_FILENUM_R Files from origin in R Optional Image Cache Configuration Files If the cliptexture has a very regular structure from level to level, the cliptexture configuration file can be augmented with some extra fields, and the image cache configuration files dispensed with. We recommend you start with the image cache configuration files, however, because it makes it easier to gradually create and test your configuration files using the icache and cliptex utilities in the /usr/share/Performer/src/pguide/libpr/C directory for IRIX and Linux and in %PFROOT%\Src\pguide\libpr\C for Microsoft Windows. Image cache configuration files can be removed if the image caches of the cliptexture are essentially the same, and configuration of each image cache is simple. The image caches should only differ in size between levels; the tile size, formats, tile filename format, etc. 578 007-1680-100 Post-Scene Graph Load Configuration should be the same. Also image cache configuration files are not optional when features like streams are configured. To stop using image cache configuration files, you should add a tile_size token to the cliptexture configuration file, and be sure to have tile_format and tile_params specified.The tile specification in the cliptexture configuration file will be used for all tile files: the ones used by the image caches and the ones representing pyramid levels. In order to make the parser stop using the image cache configuration files, remove the entries referring to them such as icache_format, icache_params, or icache_tiles. An example of a cliptexture configuration file that does not use image cache configuration files is /usr/share/Performer/data/clipdata/hunter/hl.noic.ct for IRIX and Linux and %PFROOT%\Data\clipdata\hunter\hl.noic.ct for Microsoft Windows. Post-Scene Graph Load Configuration There are a number of cliptexture configuration steps that cannot be completed until the OpenGL Performer application’s pipes and channels have been created. This configuration stage centers around configuring cliptextures to be properly applied and centered each frame. Two jobs must be accomplished. Each cliptexture must be attached to a pipe through its own pfMPClipTexture so it can be applied each frame, and a centering callback must be established to update the cliptexture as the channel’s viewpoint moves with respect to the cliptextured geometry. MPClipTextures pfMPClipTexture is a multiprocess wrapper for a pfClipTexture. A pfMPClipTexture allows you to do the following: 007-1680-100 • Change the center of the pfClipTexture in the APP process. • Automatically schedule the necessary texture downloading (applying) in the CULL process. Downloads are then performed in the DRAW process. 579 15: ClipTextures • Control the cliptexture parameters in the APP process. APP process Clip Center node MPClipTexture Pipe Clip Texture Figure 15-9 pfMPClipTexture Connections Connecting MPcliptextures to pfPipes To automatically apply of the pfClipTexture at the correct times and in the correct processes, you must do the following: 1. Create a pfMPClipTexture object. 2. Attach the pfMPClipTexture to the cliptexture you want to control. 3. Attach the pfMPClipTexture object to a pfPipe using the pfPipe. Note: If you use pfMPClipTexture, you should never call either pfUpdateMPClipTexture() or pfApplyMPClipTexture(); the pfPipe should do the applying. When you attach a pfMPClipTexture to a pfPipe using pfAddMPClipTextureToPipes() or pfAddMPClipTexturesToPipes(), pfPipe automatically updates and applies 580 007-1680-100 Post-Scene Graph Load Configuration pfClipTexture at the correct time. The functions take three arguments: a pfMPClipTexture or list of pfMPClipTextures, a pipe to which to attach (called the master pipe), and a list of other pipes the application wants to use with the pfMPClipTextures. • pfAddMPClipTextureToPipes(pfMPClipTexture, masterpipe, pipe_list) • pfAddMPClipTexturesToPipes(pfMPClipTexture_list, masterpipe, pipe_list) The pipe_list is used for multipipe applications. It is the list of pipes that slave pfMPClipTextures should be attached to. Setting pipe_list to NULL is equivalent to adding slave pfMPClipTextures to every other pipe in the application. There are additional libpf routines that can be useful: • pfRemoveMPClipTexture() detaches a pfMPClipTexture from a pfPipe. If a pfMPClipTexture is removed that is the master of other pfMPClipTextures, the slaves will be removed from their pipes as well. • pfGetNumMPClipTextures() returns the number of pfMPClipTextures attached to a pfPipe. • pfGetMPClipTexture() returns a pointer to the pfMPClipTexture that is attached to a pfPipe. libpf Functionality You can do this directly with the libpf API using the following calls: • pfNewMPClipTexture()—Create a new pfMPClipTexture. • pfMPClipTextureClipTexture()—Attach the pfMPClipTexture to the cliptexture. • pfAddMPClipTexture() - (a pfPipeCall)—Attach the pfMPClipTexture to a pipe. • pfMPClipTexturePipe()—Specifies to the pfMPClipTexture the pipe to which it is attached. pfMPClipTexture Utilities OpenGL Performer provides utilities to make it easy to attach pfMPClipTextures to pipes, and to automatically do pfMPClipTexture centering as well. As a bonus, the utility code requires little or no changes to convert a single pipe application to a multipipe one. 007-1680-100 581 15: ClipTextures To use the pfMPClipTexture utilities, you need to use OpenGL Performer’s clipcenter nodes to center the pfMPClipTexture. clipcenter nodes are a subclass of pfGroup nodes. They have additional functionality that allows them to connect to a pfMPClipTexture, the cliptextured geometry (through their child nodes), and properly update the pfMPClipTexture’s center each frame. At load time, clipcenter nodes are placed at the root of the subtree containing the cliptextured geometry. All the cliptextures in the scene are created configured and attached to the clipcenter node at this time as well. Once you have a scenegraph with geometry, cliptextures, and clipcenter nodes, it is easy to make pfMPClipTextures, attach them to pipes and to centering callbacks. The function pfuProcessClipCenters() traverses the scene graph, looking for clipcenter nodes. As each node is encountered, the function creates an MP cliptexture, attaches it to the associated cliptexture and the clipcenter node, and saves a pointer to the MP cliptexture in a pfList. When the function returns, it provides the list of MP cliptextures that were created. The pfuProcessClipCentersWithChannel() routine performs the same operations but also sets a channel pointer in the clipcenter node. When the channel pointer is set, the clipcenter node only will update a pfMPClipTexture center when that channel traverses it. This is useful for multichannel applications. Clipcenter Node In order for cliptextures to be rendered correctly, the clipcenter must move along with the viewer. OpenGL Performer has made this task simpler by providing a special node for the scene graph that does this calculation and applies it to the cliptexture each frame. This node, called the clipcenter node, is a subclass of a pfGroup node. In addition to pfGroup functionality, pfuClipCenterNode’s can do the following: 582 • Points to the cliptexture. This allows cliptextures to be attached to clipcenter nodes at load time. • Points to the geometry textured by the clipcenter node’s cliptexture. The clipcenter node is assumed rooted in the subtree containing the cliptextured geometry. • Points to an optional simplified version of the cliptextured geometry to make centering calculations go faster. • Points to the pfMPClipTexture attached to the cliptexture. The node also has API to automatically create an pfMPClipTexture and attach it to the cliptexture. • Contains a replaceable post-APP callback function for updating a pfMPClipTexture’s center. • Can point to a pfChannel and only update the pfMPClipTexture center when that pfChannel traverses the clipcenter node. 007-1680-100 Post-Scene Graph Load Configuration Channel Clip Center node Scene graph Geometry MPClipTexture Figure 15-10 pfuClipCenterNode Connections The clipcenter node uses a simple algorithm, setting the cliptexture center to be the point on the textured geometry closest to the viewer. Other algorithms can be used by replacing the callback function. Clipcenter nodes can be created by calling the utility routine pfuNewClipCenterNode(). There are set and get functions to attach cliptextures, channels, custom centering callbacks, simplified cliptextured geometry, as well as a get to return the pfMPClipTexture. See the pfuClipCenterNode man page for details on the API. The clipcenter node source code is available in pfuClipCenterNode.C and pfuClipCenterNode.h in the /usr/share/Performer/src/lib/libputil directory for IRIX and Linux and in %PFROOT%\Src\lib\libpfutil for Microsoft Windows. It is implemented as a C++ class with C++ and C API. It also has example code illustrating how to subclass the clipcenternode further to customize it. If the configuration has been done properly, and if pfuClipCenterNodes have been used for centering, most of the per-frame operations for cliptextures is automatic. Centering is computed and applied by the clipcenter nodes during the APP traversal, and cliptexture application is automatically handled by the pfPipes attached to the pfMPClipTextures. 007-1680-100 583 15: ClipTextures Using Cliptextures with Multiple Pipes Cliptextures use a lot of texture memory, system memory (for their caches) and disk I/O bandwidth. Many multipipe applications produce multiple views from the same location, looking in different directions. It would be very inefficient to create a completely separate cliptexture for each pipe; although there is separate texture memory and graphics hardware from each pipe, the system memory and disk resources are shared by the entire system. Cliptextures have been designed to support multipipe rendering without excessive drain on system memory and disk I/O bandwidth. Cliptextures that are to be used in multiple pipes can be split into master and slave cliptextures. The master cliptexture is complete; it contains an image cache and a region of texture memory to control. A slave cliptexture points to its master and shares its image cache, using it to download into its own texture memory. All the slave cliptextures share their master’s system memory cache and disk I/O resources, reducing the load on the system. Tex region Mem region Tex region Tex region Ma ste r Sla ve Sla Figure 15-11 ve Master and Slave Cliptexture Resource Sharing Making Masters and Slaves Master and slave relationships can be established between image caches, cliptextures, and pfMPClipTextures. The process starts with an object already configured the way you want. Then another object of the same type is created and is set to be a slave of the configured object. This is done with the setMaster() function. When an object is made the slave of another object, it automatically configures itself to match it’s master. It also makes all the connections necessary to share its master’s resources. 584 007-1680-100 Post-Scene Graph Load Configuration If two cliptextures are made into a master and slave, all of their image caches must have the same master-slave relationship. This is done automatically. This is also true for pfMPClipTextures. The pfMPClipTextures that will be the master and slave must be connected to cliptextures. Only the masters have to be configured, however. When the other pfMPClipTexture becomes a slave, it configures its cliptexture and makes it and its image caches slaves as well. Multipipe Cliptexture API OpenGL Performer tries to make multipipe cliptexturing as transparent as possible. Simply call setMaster() on a cliptexture and pass it a pointer to the cliptexture that should be its master: • pfMPClipTexture *slave_mct = pfNewMPClipTexture() • pfClipTexture *slave_ct = pfNewClipTexture() • pfMPClipTextureClipTexture(slave_mct, slave_ct) • pfMPClipTextureMaster(slave_mct, master_mct) master_mct is a pfMPClipTexture that is already configured. At this point, slave_mct and master_mct are connected; slave_mct is configured to match master_mct and shares its image cache resources. The cliptextures and image caches are also configured and linked. To make pfClipTextures or pfImageCaches masters and slaves, use the same procedure. Attaching a pfMPClipTexture to a pfPipe with pfAddMPClipTexture() provides automatic multipipe support. If a pfMPClipTexture is added to a pipe that is already connected to another pipe, the function silently creates a new pfMPClipTexture, makes it a slave of the pfMPClipTexture that is already connected to another pipe, and adds the slave to the pipe in place of the one passed as an argument to the function. Multipipe Utilities Although it is not difficult to set up master and slave cliptextures directly, it is usually not necessary.The previously described utility routines, pfuAddMPClipTextureToPipes() and pfuAddMPClipTexturesToPipes() can take multiple pipe arguments. A master pipe and a list of slave pipes is specified. The routine makes the pfMPClipTexture a master and attaches it to the master pipe. It then creates slave pfMPClipTextures, attaches them to the master cliptexture, and attaches a slave cliptexture to each pipe in the slave pipes 007-1680-100 585 15: ClipTextures list. This routine does extra checking of pipe and cliptexture state, and is guaranteed not to generate errors, even if the function is applied more than once. Master/Slave Share Masks A group of cliptextures grouped by master-slave relationships can do more than share mem region resources. By default, slave cliptextures also track a number of their master’s attribute values. This means changing a master’s center, for example, automatically causes the slaves to change their center locations to match their master’s. The attributes that a slave can track are divided into groups called share groups. The application can control which groups are shared by setting a slave’s share mask. Changing the sharing of a slave only affects that slave’s sharing with its master. Changing the master share mask has no effect. The share mask is set with the following call: pfMPClipTextureShareMask(uint mask) The mask can be set using one or more of the following values: • PFMPCLIPTEXTURE_SHARE_CENTER—Slaves track the master’s center. • PFMPCLIPTEXTURE_SHARE_DTR—Slaves track DTR: DTR mode, tex load time (actual or calculated), fade count, and blur margin. • PFMPCLIPTEXTURE_SHARE_EDGE—Slaves track texture level parameters, LODbias invalid border. • PFMPCLIPTEXTURE_SHARE_LOD—Slaves track minLOD and maxLOD. • PFMPCLIPTEXTURE_SHARE_VIRTUAL—Slaves track lodOffset and num effective levels. • PFMPCLIPTEXTURE_SHARE_DEFAULT—A bit-wise OR of all the masks listed above. PFMPCLIPTEXTURE_SHARE_DEFAULT is the default share-mask value, which provides maximum sharing between master and slave cliptextures. If an application would like to control one or more slaves independently, it needs to change the slave’s share mask; then start setting the slaves parameters directly as needed. Texture Memory and Hardware Support Checking At the first application or formatting of a cliptexture, OpenGL Performer compares the expected size of the cliptexture texel data in texture memory against the systems texture 586 007-1680-100 Manipulating Cliptextures memory size. If it looks like the cliptexture will not fit into texture memory, it shrinks the clip size by two and tries again. It will keep shrinking the clip size until either the cliptexture will fit or the clip size is zero. The system takes into account texture memory banking and paging to come up with a more accurate estimate. Note that the resizing mechanism does not take into account other textures or cliptextures in use by the application. You should adjust your application so that OpenGL Performer does not have to auto-shrink the cliptexture. See “Estimating Cliptexture Memory Usage” on page 605 for calculating cliptexture system memory and texture memory usage. During the checking phase, OpenGL Performer also checks to see if cliptextures are supported in hardware. If cliptexturing is not supported, one of two emulation modes will be selected: PFCTEMODE_FRAGPROG or PFCTEMODE_BASIC. PFCTEMODE_FRAGPROG uses ARB fragment programs to blend imagery stored in multiple texture units and is automatically selected on systems that support the ARB fragment program extension. The BASIC cliptexture emulation mode is selected on all other platforms and only requires OpenGL 1.0 functionality. Both emulation modes work by transparently assigning cliptextured geometry (pfGeoSets) to dedicated pfDrawBins that are managed internally within the library. Pre- and post-draw callbacks for such pfDrawBins are used to override and restore graphics state and to render cliptextured geometry using the cliptexture emulation state. Manipulating Cliptextures Once cliptextures have been configured and connected into the application, they can be manipulated by the application in the APP process. Applying and centering cliptextures happens each frame, and is usually an automatic process, set up during post-load configuration. Other parameters that can be adjusted include load control parameters, min and max LOD levels, and virtual cliptexture control. Some of these parameters may only need to be set once in the application, others, like the parameter setting for virtual cliptextures, need to happen multiple times per frame. Cliptexture Load Control The virtualization of pfTextures into pfClipTextures, allowing very large texture maps, comes at a price. As the clipcenter moves, cliptextures have to download data from disk to system memory, and from system memory to texture memory. Because of these 007-1680-100 587 15: ClipTextures download requirements, cliptextures are sensitive to available system bandwidth. Without some sort of download load control, a fast moving center would cause a cliptextures to “freeze”, waiting for the system to catch up with its updates. While mem region updates happen asynchronously, tex region updates must happen in the DRAW process, competing with geometry rendering and individual texture loading. Real time applications require that cliptextures, like other OpenGL Performer features, must be controlled in a way such that an upper bound can be set on their use of resources. OpenGL Performer’s cliptexture load control, called Dynamic Texture Resolution (DTR), provides this functionality. Dynamic Texture Resolution Dynamic Texture Resolution (DTR) is similar to Dynamic Visual Resolution (DVR): the bandwidth requirements are adjusted to meet system limitations by lowering the resolution of the texture data displayed by the cliptexture. DTR controls bandwidth by analyzing the cliptexture in the CULL process. It checks each cliptexture level, ensuring that the mem region contains updated tiles corresponding to the tex region, and that there is enough time to update the tex region within the download time limit. This checking goes from level to level, from coarser levels to finer ones. When a level is found that cannot be displayed, DTR adjusts the cliptexture parameters so that no levels above the finest complete level are displayed. At that point, DTR stops checking levels until the next frame. In order not to waste CULL processing time on levels that are not visible, DTR will not try to sharpen more than one level beyond the current minLOD and virtualLODoffset values. It will go one level beyond these values so that it can react quickly if the values change. In this way the cliptexture updating will always keep up with the movement of the clipcenter, and will never display invalid data. When the center moves too quickly, DTR will “blur down” to coarser complete levels, then “sharpen up” to finer levels when the center slows down and the system can catch up. In this way DTR can trade visual quality against updating bandwidth. The visual result is that the faster a viewer goes, the less time there is to download texture and the blurrier the texture data gets. The nature of cliptexturing makes load control work. When the clipcenter moves, this change is reflected at every clipped level of the cliptexture. But because each texel in a level covers four times the geometry of the texel in the next finer level, the clipcenter only 588 007-1680-100 Manipulating Cliptextures moves half the distance each time you go down a level. This translates into less demanding texture download requirement. DTR has other features, such as read queue sorting, which prioritize the order in which read requests are done to improve mem region update performance. The rate at which levels are blurred and sharpened can also be controlled to minimize visual artifacts. Load Control API DTR controls three aspects of load control, which can be turned on and off independently: tex region updating (from the mem region in system memory), mem region updating (loading from disks), and read queue sorting (reducing the latency of read requests for downloads from disk to the mem region). pfMPClipTextureDTRMode(DTRMode) DTRMode is a bitmask; if a bit is set, that DTR feature is enabled. It has the following bits defined: • PF_DTR_MEMLOAD - Enable mem region load control from disk. • PF_DTR_TEXLOAD - Enable tex region load control; DRAW download time. • PF_DTR_READSORT - Enable priority sorting of the read queue. All three bits are enabled by default, which means that DTR has all modes enabled. Besides the bitmask to control what parts of DTR are enabled, there are parameters to available to adjust load control performance. The DTR parameters and how they affect DTR functionality are discussed the following subsections. Download Time The memload component of DTR is relatively simple; it computes whether all the tiles in a level’s mem region that cover the tex region are valid. If any are not, the tex region cannot be updated and DTR invalidates that level. If the texload component of DTR is enabled, DTR must also compute the time it takes to download from the mem region to the tex region. The application provides the load control with a total download time in milliseconds: pfMPClipTextureTexLoadTime(float _msec) This is the total time DTR has available to update the cliptexture’s texture memory each frame. As DTR analyzes each cliptexture level that needs updating, it computes all the 007-1680-100 589 15: ClipTextures regions in the level’s texture memory that need updating.If a level can be updated, DTR determines whether there is enough download time left to update the level. If there is, DTR marks that level valid, subtracts the time needed to download that level from the total, and starts analyzing the next finest level in the cliptexture. Cost Tables OpenGL Performer contains texture download cost tables, which DTR uses to estimate the time it will take to carry out those texture subloads. These tables are a 2D array of floating point values, indexed by width and height of the texture rectangle being subloaded. The cost tables themselves are indexed by machine type and can be read by the application. The application can also define its own cost tables and configure the system to use it. The cost table API is shown below: pfPerf(int type, void *table) pfQueryPerf(int type, void **table) The text field indicates whether the cost table should be the one chosen by the system: • PFQPERF_DEFAULT_TEXLOAD_TABLE - The one supplied by the application • PFQPERF_USER_TEXLOAD_TABLE - The one currently in use. • PFQPERF_CUR_TEXLOAD_TABLE - The default table is the current one unless a application supplies a cost table, in which case, the application’s cost table takes precedence. For more details on cost tables, see the man pages for pfPerf() and pfQueryPerf(). The cost table structure is named pfTexSubloadCostTable, defined in /usr/include/Performer/pr.h for IRIX and Linux and in %PFROOT%\Include\pr.h for Microsoft Windows. Changing Levels The DTR load control system is designed to minimize visual artifacts as it adjusts for different download demands. Instead of abruptly sharpening the texture as new levels with valid texture data become available, DTR blurs in new levels over a number of frames, making the process of load control less noticeable. The application can control the rate at which newly valid levels are displayed. The application sets a fade count, which controls the number of frames it takes to fade in a new level . Each frame, the cliptexture will sharpen 1/fadecount of the way from its current (possible fractional) level to the next level. This process is repeated each frame, resulting in an exponential fade-in function. If the fade count is 0, then fading is disabled, and DTR will show new levels immediately. 590 007-1680-100 Manipulating Cliptextures pfMPClipTextureFadeCount(int _frames) If the clipcenter roaming speed leaves barely enough bandwidth to bring in a new cliptexture level, a distracting “LOD flicker” between two cliptexture LOD levels can result. Since DTR must blur immediately if a level becomes invalid, the only way to prevent flicker is to be conservative when sharpening, building in a hysteresis factor. The parameter called blur margin helps determine when DTR should sharpen. The blurmargin parameter also helps cliptextures blur smoothly when DTR cannot keep up. It is a floating point value, which can be interpreted as a fraction of the cliptexture’s tex load time. When blurmargin is not zero, DTR will load all the levels it can within the texload time, but not display all of them. Instead it will only sharpen to the level that would have been reached if the texload time was scaled by blurmargin. This leaves a cushion of extra time that can be used up before DTR will be forced to blur to a coarser level. The default blurmargin value of .5 usually causes the finest level displayed to be one level coarser then the finest level loaded.The application can adjust blurmargin with this call: pfMPClipTextureBlurMargin(float margin) DTR needs this cushion in order to fade smoothly. A cliptexture can only fade between two valid levels; if it waits until its current level is invalid, the cliptexture must immediately jump to the next coarser level or it will show invalid data. This abrupt blurring is very noticeable. The blur margin allows the DTR system to anticipate when it will lose a level, and smoothly fade to the next coarser level over a number of frames. Total Texload Time and Texload Time Fraction Using the texload time, blur margin, and fade count parameters is sufficient to control a single cliptexture from a pipe, but the interface is awkward if multiple cliptextures are applying from the same pipe. Since each pipe has the same amount ofDRAW process time available per frame, no matter how many cliptextures are applied from it, it would be more convenient to provide a total amount of download time, then divide it among the cliptextures using the pipe. OpenGL Performer provides this interface using the total texload time and texload time fraction parameters. The application can set the total texture download time available on a pipe, then assign fractional values for each cliptexture, indicating how the download time should be divided. The total texload time is a pfPipe call, while the fractional values are set on pfMPClipTextures: 007-1680-100 591 15: ClipTextures pfPipeTotalTexLoadTime(float msecs) pfMPClipTextureTexLoadTimeFrac(float frac) The fractional values should indicate the relative priority of each pfMPClipTexture on the pipe. The fractional values do not have to add up to 1; the DTR code will normalize them against the sum of all the fractional values set on the pipe’s pfMPClipTextures. The total tex load time on the pipe is scaled by the normalized fractional value on each cliptexture. The scaled tex load time is then used as the cliptexture’s texture download time. Explicitly setting the tex load time on a pfMPClipTexture will override the computed fractional time. Read Queue Sorting When the clipcenter moves quickly, the number of read requests for texture data tiles that move into the clipped levels mem regions can grow much faster than the read function can service them. If there is not enough bandwidth to display a particular level, its read requests may become “stale”, becoming obsolete as the location of the requested tile moves into, then passes out of a level’s mem region.For DTR to be robust, the read queue must be culled and sorted to remove stale read requests, and move the requests for tiles closest to the clipcenter to the front of the queue. The cliptexture’s read queue is a sorting queue, which means that a function can process the elements of the queue asynchronously. DTR uses the read queue to cull read requests for tiles that are no longer in their mem region, and to prioritize the other requested tiles as a function of level and distance from the clipcenter. Sophisticated applications can provide their own sorting function. Invalidating Cliptextures Sometimes an application may want to force a cliptexture to completely reload itself. For example, The pfuGridifyClipTexture() function modifies the cliptexture’s texel data in system memory with a system of grid marks to make debugging and analysis easier. It modifies the read function to add a grid to every tile as it’s loaded into system memory, then invalidates the cliptexture. For more information on gridify, look at the source code in the /usr/share/Performer/src/lib/libpfutil/gridify.C file for IRIX and Linux and in %PFROOT%\Src\lib\libpfutil\gridify.C for Microsoft Windows. Invalidating a cliptexture forces it to completely reload its texture memory. Invalidating is only supported for cliptextures, not MPcliptextures. This means that an application 592 007-1680-100 Manipulating Cliptextures cannot call invalidate from the APP process. Instead, it must call invalidate from the CULL process, usually in a pre-cull callback. The invalidate call itself is simple: pfInvalidateClipTexture(pfClipTexture *cliptex) Invalidation is not needed for normal operation, but it is useful as a way to immediately update a cliptexture’s texture memory. Virtual ClipTextures Note: Emulated cliptextures on systems other than InfiniteReality systems are never virtual. Regular cliptextures limit the size of each level but do not restrict the number of levels you can access. Virtual cliptextures take the virtualization a step further by allowing you to use only a subset of all the levels for which you have data. Although InfiniteReality supports cliptextures of virtual size up to 8Mx8M = 2^23x2^23 texels (that is, 24 levels), the hardware is only capable of addressing a region of at most 32Kx32K = 2^15x2^15 texels (that is, 16 levels). By limiting the set of texture MIPMap levels, the cliptextures can be enlarged. A larger, virtual, cliptexture is defined just like a normal cliptexture, except that the size of the cliptexture can exceed the 32K X 32K maximum level size dictated by the hardware. Virtual cliptextures do use more texture memory and require more callbacks in the CULL process, but they allow enormous cliptextures that are limited only by the precision of the texture coordinates. Cliptextures over one million texels on a side have been demonstrated. Although virtual cliptextures require dividing the cliptextured geometry into sections for a given MIPmap levelrange, the division is much coarser and less restrictive than texture tiling. Cliptextured geometry usually does not need to be clipped to sectional boundaries, for example, since there is a lot of leeway when there are more MIPmap levels available than are needed for a given section of geometry. For a sample application implementing virtual cliptextures, see /usr/share/Performer/src/pguide/libpf/C/virtcliptex.c for IRIX and Linux. 007-1680-100 593 15: ClipTextures Selecting the Levels The application is responsible for choosing which 16 (or less) levels can be accessed at any given time by setting two parameters: virtualLODOffset and numEffectiveLevels. Most applications make numEffectiveLevels the maximum number allowed by the hardware, 16 on InfiniteReality. Smaller values may be chosen in some cases to improve stability. VirtualLODOffset sets the initial level in the cliptexture where 0 is the finest level. For example, if numEffectiveLevels = 16 and virtualLODOffset = 0 then the texels the hardware can access are limited to the 32Kx32K region surrounding the current clipcenter, measured in finest-level texels (actually somewhat less than this. On IRIX and Linux, see the file /usr/share/Performer/doc/clipmap/IRClipmapBugs.html or /usr/share/Performer/doc/clipmap/IRClipmapBugs.txt for details on cliptexture limitations on InfiniteReality graphics). On Microsoft Windows, see the file %PFROOT%\Doc\clipmap\IRClipmapBugs.html or %PFROOT%\Doc\clipmap\IRClipmapBugs.txt. Attempting to access outside this range results in the value of the nearest texel in the good region; that is, the texels forming the border of the 32Kx32K area will appear to be “smeared” out to fill the virtual cliptexture. Increasing virtualLODOffset from 0 to 1 doubles the size of the accessible region in both S and T (so that it is 32Kx32K level 1 texels, which are twice as big as level 0 texels) but makes the finest level inaccessible. The maximum virtualLODOffset allowable is numVirtualLevels-numEffectiveLevels; when set to that value, the entire S,T range of the virtual cliptexture is accessible, and the finest level from which texels are available is the 32Kx32K level. In general, it is appropriate to choose a large value of virtualLODOffset when the viewpoint is far away from the scene and more S,T area is visible; smaller values of virtualLODOffset are appropriate as the eye moves closer to the scene, gaining needed higher resolution at the expense of range in S,T. Changing virtualLODOffset and numEffectiveLevels has no effect on the contents of texture memory nor any effect on the texture coordinates stored in the geosets and passed to the graphics: the texture coordinates, as well as the clipcenter, are always expressed in the space of the entire virtual cliptexture rather than the smaller “effective” cliptexture of up to 16 levels within it. (In contrast, changing the clipcenter requires texture downloading; thus it is a much more expensive operation and therefore it is not practical to change the clipcenter more than once per frame, whereas virtualLODOffset and numEffectiveLevels can be changed multiple times per frame, as we will see in the following subsections.) 594 007-1680-100 Manipulating Cliptextures How to Set Virtual Cliptexture Parameters OpenGL Performer supports two different methods for managing virtualLODOffset and numEffectiveLevels of a cliptexture. The simpler of the two methods allows the parameters to be set and changed at most once per frame; the more sophisticated method allows them to be changed multiple times per frame (different values for different parts of the scene). In addition to virtualLODOffset and numEffectiveLevels described earlier, the parameters minLOD, maxLOD, LODBiasS and LODBiasT often need to be set in the same way; so, we will show how to set those as well. Per-Frame Setting of Virtual Cliptexture Parameters The easy way to manage the virtual cliptexture parameters is to set the values of the parameters on the pfMPClipTexture controlling the pfClipTexture: int LODOffset, numEffectiveLevels; float minLOD, maxLOD; float LODBiasS, LODBiasT, LODBiasR; ... mpcliptex->setVirtualLODOffset(LODOffset); mpcliptex->setNumEffectiveLevels(numEffectiveLevels); mpcliptex->setLODRange(minLOD, maxLOD); mpcliptex->setLODBias(LODBiasS, LODBiasT, LODBiasR); You make these calls in the APP process, either in the main program loop, a channel APP func, or a pre- or post-node APP func. The last value you give during the APP in a particular frame will be used for rendering that frame and all subsequent frames until you change the value again. This simple technique is the one that is used by the clipfly program when you manipulate the LODOffset and EffectiveLevels sliders (when using a naive scene loader such as the .im loader that does not do its own management of virtualLODOffset and numEffectiveLevels): clipfly makes these calls in its channel pre-APP function. This technique is also used by the .spherepatch loader; in this case, the calls are made in a post-APP function of a node in the scene graph, using parameters that are intelligently chosen based on the current distance from the eye to the closest point on the textured geometry and are updated every frame. Notice that even though the .spherepatch loader manages the virtualLODOffset and numEffectiveLevels, you can still modify or override its behavior with the clipfly GUI controls. This is accomplished using a special set of “limit” parameters that are provided 007-1680-100 595 15: ClipTextures as a convenience and stored on the pfMPClipTexture. The intended use is for applications such as clipfly to set the limits based on GUI input or other criteria: mpcliptex->setLODOffsetLimit(lo, hi); mpcliptex->setEffectiveLevelsLimit(lo, hi); mpcliptex->setMinLODLimit(lo, hi); mpcliptex->setMinLODLimit(lo, hi); mpcliptex->setLODBiasLimit(Slo, Shi, Tlo, Thi, Rlo, Rhi); Then the callback functions of intelligent loaders such as .spherepatch query the limits: mpcliptex->getLODOffsetLimit(&lo, &hi); mpcliptex->getEffectiveLevelsLimit(&lo, &hi); mpcliptex->getMinLODLimit(&lo, &hi); mpcliptex->getMinLODLimit(&lo, &hi); mpcliptex->setLODBiasLimit(&Slo, &Shi, &Tlo, &Thi, &Rlo, &Rhi); The loaders use the limits to modify the selection of the final parameters sent to pfMPClipTexture. The limits are not enforced by pfMPClipTexture; they are provided merely to facilitate communication from the application to the function controlling the parameters. That function is free to ignore or only partially honor the limits if it wishes. The limits may also be queried frame-accurately from the pfMPClipTexture in the CULL process, so they can also be used by scene loaders such as the .ct loader that use the per-tile method described in the next section. Per-Tile Setting of Virtual Cliptexture Parameters Many applications require accessing a wider range of the cliptexture’s data than can be obtained by a single setting of virtualLODOffset and numEffectiveLevels. This can be accomplished by partitioning the database into “tiles” roughly according to distance from the eye or from the texture’s clipcenter and setting the parameters for each tile every frame in the pre-CULL func of the pfGroup or pfGeode representing that tile by calling pfClipTexture::applyVirtual(), pfTexture::applyMinLOD(), pfTexture::applyMaxLOD(), and pfTexture::applyLODBias(). 596 007-1680-100 Manipulating Cliptextures Tiling Strategies Choosing a database tiling strategy requires careful thought and tuning. The most conceptually straightforward method is to use a static 2D grid-like spatial partitioning. This method requires tuning the granularity of the partitioning for the particular database and capabilities of the machine: if a tile is too big and sufficiently close to the eye, there may be no possible combination of virtualLODOffset and numEffectiveLevels that allows access to both the necessary spatial range and texture LOD range without garbage in the distance or excess bluriness in the foreground; but if there are too many tiles, the overhead of changing the parameters for each tile can become excessive. In general, assuming the maximum active area is 32Kx32K (as it is on InfiniteReality), each tile should be small enough so that it covers at most approximately 16K texels at the finest texture LOD that will be used when rendering it; this is so that when the clipcenter is close enough to the tile to require accessing that finest texture LOD, the 32Kx32K good area centered at approximately the clipcenter will be able to cover it with some slop left over to account for the inexact placement of the good area (see the IR cliptexture bugs doc). (Finer tiles such as 8Kx8K or even 4Kx4K can be used for improved stability under extreme magnification; see the IR cliptexture bugs doc). This rule has two important consequences: • If your cliptexture has insets (that is, localized regions in which higher-resolution data is available) you can make the tiling coarser in the regions where only low-resolution data is available and finer at the insets. • If you use pfLODs to optimize your database, the coarse LODs of the pfLOD can (and should) be tiled more coarsely than the fine ones. This is because the coarser LODs are used at far distances, and at those far distances the Mipmapping hardware will only want to access correspondingly coarse texture levels anyway, so the 16Kx16K can be measured in terms of the texels of those coarse texture levels. A more general tiling strategy that requires less empirical database tuning than the static tiling method is to make the tiles be concentric rings around the texture’s clipcenter (in 2D) or around the eye point (in 3D), with sizes increasing in approximately powers of 2. However, since the clipcenter and view position changes, this means the tiles must move as well, which requires dynamically changing the topology of the scene graph and/or morphing the geometry so that the tiles always form those concentric rings around the current focus. 007-1680-100 597 15: ClipTextures The .ct loader and pfASD’s ClipRings both use this dynamic strategy. The .ct loader is interesting in that the morphing is done for the sole purpose of forming these concentric tiles for virtual-cliptexturing an otherwise trivial scene. It looks like simply a square textured by the cliptexture, but if you turn on scribed mode in perfly or clipfly, you can see the morphing rings that make up the square. Doing Per-tile Updates To do per-tile updates, use the following procedure: 1. On each tile (typically a pfGroup or pfGeode) put a pre-node CULL func: tile->setTravFuncs(PFTRAV_CULL, tilePreCull, NULL); 2. Make sure the effect of the tile’s pre-CULL func happens in the DRAW before the contents of the tile are rendered, and that the tile’s contents do not co-mingle with other tiles (this is not guaranteed by default, for the benefit of CULL whose sole purpose is to return a CULL result without losing the advantages of uncontained CULL sorting): tile->setTravMode(PFTRAV_CULL, PFTRAV_CULL_SORT, PFN_CULL_SORT_CONTAINED); 3. In the pre-node CULL func for the tile, set the parameters: static int tilePreCull(pfTraverser *trav, void *) { int virtualLODOffset, numEffectiveLevels; float minLOD, maxLOD; float biasS, biasT, biasR; //Choose intelligent values for parameters. cliptex->applyVirtualParams(virtualLODOffset, numEffectiveLevels); cliptex->applyMinLOD(minLOD); cliptex->applyMaxLOD(maxLOD); cliptex->applyLODBias(biasS,biasT,biasR); } The values given to the apply functions are not stored in the pfClipTexture or retained from frame to frame; when you call these functions, they override the corresponding values stored in the cliptexture. 598 007-1680-100 Manipulating Cliptextures It is not necessary to call all four of the apply...() functions; only use the ones you care about (for example, most applications would not care about LODBias). However, if you ever call a given one of these functions, applyMinLOD(), for example, on a particular cliptexture for any tile, then you must call applyMinLOD() for every tile on that cliptexture during that frame and forever after; if you omit it, the tile will not necessarily get the value stored on the pfMPClipTexture or pfClipTexture; rather, it will get whatever value happened to be most recently set when rendering that tile in the DRAW (which may be nondeterministic due to CULL sorting of the scene graph). How to Choose Virtual Cliptexture Parameters The libpfutil library provides the function pfuCalcVirtualClipTexParams(), which can be very useful in selecting the virtual cliptexture parameters, regardless of whether you are updating per-frame or per-tile. Essentially, you give to pfuCalcVirtualClipTexParams() every piece of information you know about the cliptexture: • the tile in question • the limits specified elsewhere, for example, by the clipfly GUI pfuCalcSizeFinestMipLOD() returns the lower bounds on minLODPixTex, which is one of the input parameters to pfuCalcVirtualClipTexParams(). The function returns optimal values for virtualLODOffset, numEffectiveLevels, minLOD, maxLOD. You can do the following with them: • Set on the pfMPClipTexture in the APP process if your application is using the per-frame method. • Apply to the pfClipTexture per-tile in the CULL process if using the per-tile method. For more details, you may also want to read the commented source code to understand its constraints and heuristics, and how to modify pfuCalcVirtualClipTexParams to implement your own algorithm if it does not exactly suit your needs. Custom Read Functions Sometimes the read function supplied by OpenGL Performer to download texture data from disk to mem region is not good enough. The application may need to do additional operations at read time, such as uncompression, or may need a more sophisticated read 007-1680-100 599 15: ClipTextures function, such as an interruptible one for reading large tiles from slow storage devices. A read function may need to signal an applications secondary caching system; for example, reading from tape storage to disk. OpenGL Performer provides support for application supplied custom read functions. The read function is supplied at configuration time, and there is API in both the configuration utilities and the cliptexture and image cache configuration files for supplying a read function. A read function is called by the image caches read queue. The read queue expects a read function with the following function signature: int ExampleReadFunction(pfImageTile *it, int ntexels) The image tile pointer provides information about the read request, such as the disk to read from, the dimensions and format of the texel data, and the destination tile in system memory to write to. The ntexels argument is an integer indicating the number of texels to read from disk. The read function returns another integer indicating the number of texels actually read. Two example read functions, ReadNormal() and ReadDirect(), are supplied in /usr/share/Performer/src/lib/libpfdu/pfdLoadImage.c for IRIX and Linux and in %PFROOT%\Src\lib\libpfdu\pfdLoadImage.c for Microsoft Windows. These functions are C versions of the C++ functions that OpenGL Performer uses to read texture data. In OpenGL Performer, the ReadDirect() function is called by the read queue; it tries to use direct I/O to get the highest possible disk read performance. If the read direct call fails, it calls ReadNormal(), which uses normal fopen()-style read. When providing a read function at configuration time, You supply the function name, and optionally the name of a DSO library containing the function. If no dynamic shared library is supplied, the read function is searched for in the application’s executable. To set custom read functions using the configuration utilities, simply fill in the readFunc field in the pfuImgCacheConfig or pfuClipTexConfig structure (the first structure has priority over the second if both are set). The field should contain a pointer to the custom read function. Be sure the function has the proper signature. When supplying custom read functions in the configuration files, you simply provide an entry in one of two formats: read_func ReadFunctionName read_func DSOlibraryName ReadFunctionName 600 007-1680-100 Using Cliptextures For hints on when and how to use custom read functions, see the customizing read functions in “Custom Read Functions” on page 612. Using Cliptextures This section provides guidelines for using cliptextures, describing common cliptexture application techniques, ways to solve problems, and some hints and tips to make using cliptextures easier. Cliptexture Insets Cliptexture load control makes it possible to create cliptextures with incompletely filled levels. A cliptexture, being much larger than an ordinary texture, may not be used in a homogeneous way. Some areas of the cliptexture may be viewed in detail, others only at a distance. A good example of this usage pattern is flight simulation. The terrain around an airport will be seen from low altitude, terrain far from population centers may never be seen below 40,000 feet. It is also possible that high resolution data is simply not available for the entire cliptexture. Both of these cases make it valuable to create cliptextures with incompletely populated levels. Regions of filled in data are called insets. Insets can be any shape, and do not need to match tile boundaries (although this requires filling the rest of the tile with super sampled data). For an inset to work properly, all of the levels from the pyramid up to the finest level desired, must be available within the inset boundaries. 007-1680-100 601 15: ClipTextures Insets Figure 15-12 Cliptexture Insets Insets are supported in cliptextures as a natural consequence of load control. As the clipped region moves from a region that has texel data to one that does not, DTR will blur the texture down to the highest level that can completely fill the clipped region. Adding Insets to Cliptextured Data In large cliptextures, it may not be practical or even desirable to completely fill each level with texel data. Cliptexture’s load control, DTR, automatically adjusts the finest visible level based on what texels are available. If finer levels are not available, DTR automatically “blurs down” to the highest complete level in the clip region. Applications may use insets if there are only limited areas where the viewer is close to the terrain. An example application would be a commercial flight simulator, where the inset high-resolution data would be around the airports where the aircraft takes off and lands. The terrain over which the aircraft cruises can be lower resolution. Insets and DTR To create insets properly, you have to understand how DTR load control works. At the beginning of each frame, DTR examines a level’s mem region to see if the tiles covering the tex region are all loaded. If the tiles are all available, DTR will make that level visible. 602 007-1680-100 Using Cliptextures DTR