Chapter 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:

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:

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 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() pfNodeIsect-
Segs(), pfChan-
NodeIsectSegs()

Global Callbacks

pfChanTrav-
Func()

pfChanTrav-
Func()

pfChanTrav-
Func()

pfIsectFunc()

Activation within Callback

pfApp()

pfCull()

pfDraw()

pfFrame() pfNodeIsect-
Segs(), pfChan-
NodeIsectSegs()

Path Activation

N/A

pfCullPath()

N/A

N/A

Modes

pfChanTrav-
Mode()

pfChanTrav-
Mode()

pfChanTrav-
Mode()

pfSegSet (also discriminator callback)

Node Callbacks

pfNodeTrav-
Funcs()

pfNodeTrav-
Funcs()

pfNodeTrav-
Funcs()

pfNodeTrav-
Funcs()

Traverser Masks

pfChanTrav-
Mask()

pfChanTrav-
Mask()

pfChanTrav-
Mask()

pfSegSet mask

Traversee Masks

pfNodeTrav-
Mask()

pfNodeTrav-
Mask()

pfNodeTrav-
Mask()

pfNodeTrav-
Mask()

pfGSetIsect-
Mask()


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 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” 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()).

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;
 
    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 cu ll 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.

  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.

Figure 4-1. Culling to the Frustum

Culling to the Frustum

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.

Figure 4-2. Sample Database Objects and Bounding Volumes

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.

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.

Figure 4-3. How to Partition a Database for Maximum Efficiency

How to Partition a Database for Maximum Efficiency

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”.

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”.

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 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” 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.

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:

  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” 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” 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 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”).

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.

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

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”) 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.

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.

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”. 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:

  • Test instructions

  • Assign instructions

  • Add-bin instructions

  • Jump instructions

  • Return instruction

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. 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.

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. 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.

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.

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. 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.

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.

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)

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.

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.

  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;
 
    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;
}



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)
{
    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);
}


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.

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)
{
    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.

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. 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

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)

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 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.

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.

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”.

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().

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.

Figure 4-4. Intersection Methods

Intersection Methods