Chapter 25. Building a Visual Simulation Application Using libpfv

This chapter describes how to use the library libpfv to build an application using a graphical viewer.

The following sections appear:

Overview

OpenGL Performer includes libpfv, a C++ library for easy construction of modular, interactive OpenGL Performer applications.

The library libpfv supports the following features:

  • Reading and writing XML files

  • Specifying complex display configuration (pipes, windows, and channels) from a file or through API calls

  • Tracking mouse and keyboard input

  • Setting up user interaction with 3D scene elements

  • Managing multiple scene graphs (worlds)

  • Managing multiple camera positions (views)

  • Extending program functionality using program modules

The principal class in libpfv is pfvViewer. It allows complex multiworld and multiview applications to be implemented in a modular fashion, allowing individual features to be encapsulated into configurable and re-usable modules.

In addition to libpfv, OpenGL Performer includes ready-to-use modules that provide the following features:

  • Loading geometry into a pfvViewer world

  • Picking geometry under the mouse pointer

  • Manipulating geometry (rotating, translating, scaling, deleting)

  • Navigating through a world using mouse and keyboard controls

  • Controlling the render style of models

  • Setting up colorful earth and sky backgrounds

  • Displaying 2D images in overlay

  • Saving snapshots of the rendered images

  • Smoothly transitioning from one world to another

  • Collecting and displaying scene graph statistics

The Simplest pfvViewer Program

The pfvViewer class starts with a very simple programming interface. It maintains a very simple programming interface even when accessing high-level features. As shown in the following program, the simplest program using pfvViewer loads a model and places the camera at a comfortable viewing distance:

#include <Performer/pfdu.h>
#include <Performer/pf/pfLightSource.h>
#include <Performer/pfv/pfvViewer.h>

main (int argc, char *argv[])
{
    // Initialize Performer
    pfInit();

    // Create a new pfvViewer
    pfvViewer* viewer = new pfvViewer;

    // Initialize loading of a model file.
    pfdInitConverter(argv[1]);

    // Configure/Initialize pfvViewer
    viewer->config();

    // Add a light source to the world.
    viewer->addNode(new pfLightSource);

    // Add a model to the world
    viewer->addNode(pfdLoadFile(argv[1]));

    // Start viewing
    viewer->run();
}

Adding Interaction to a pfvViewer Program

To add interaction, load the following two standard modules into the viewer:

  •   pfvmNavigator module

    Allows the user to move around the 3D scene through mouse and keyboard input.

  • pfvmPicker module

    Allows the user to select, manipulate, and delete portions of the 3D scene through mouse and keyboard input.

The following program shows the addition of these modules to the simple program in the preceding subsection:

#include <Performer/pfdu.h>
#include <Performer/pf/pfLightSource.h>
#include <Performer/pfv/pfvViewer.h>

main (int argc, char *argv[])
{
    pfvModule* module;

    // Initialize Performer
    pfInit();

    // Create a new pfvViewer
    pfvViewer* viewer = new pfvViewer;

    // Add navigation module
    module = pfvModule::load("pfvmNavigator");
    viewer->addModule(module);

    // Add mouse-picking module
    module = pfvModule::load("pfvmPicker");
    viewer->addModule(module);

    // Initialize loading of model files.
    pfdInitConverter(argv[1]);

    // Configure/Initialize pfvViewer
    viewer->config();

    // Add a light source to the world.
    viewer->addNode(new pfLightSource);

    // Add a model to the world
    viewer->addNode(pfdLoadFile(argv[1]));

    // Start interaction
    viewer->run();
}

Reading XML Configuration Files

A pfvViewer can read most of its parameters from an XML configuration file. A pfvViewer configuration file, denoted by the .pfv extension, can contain any of the following items:

  • Display configuration (pipes, windows, and channels)

  • Specification of multiple worlds

  • Specification of multiple views (camera positions)

  • Extension modules to be loaded into the pfvViewer and their specific configuration parameters

The simplest pfvViewer program using an XML configuration file is pfview, which is provided as a precompiled executable with OpenGL Performer.

The source code for the pfview program looks like the following:

#include <Performer/pfv/pfvViewer.h>

int
main (int argc, char *argv[])
{
    // Initialize Performer
    pfInit();

    // Create a new pfvViewer and read XML configuration file argv[1]
    pfvViewer* viewer = new pfvViewer(argv[1]);

    // Configure/Initialize pfvViewer.
    viewer->config();

    // Start interaction
    viewer->run();
}

 A minimal XML configuration file suitable for pfview has the following structure:

<?xml version="1.0" ?>
<viewer>
    <module>
        <!-- Loader module: loads models into world -->
        <class>pfvmLoader</class>
        <data>
            <Model>
                <FileName>esprit.flt</FileName>
            </Model>
        </data>
    </module>

    <!-- Add a trackball navigation module -->
    <module>
        <class>pfvmTrackball</class>
    </module>

    <!-- Add a picking module -->
    <module>
        <class>pfvmPicker</class>
    </module>

</viewer>

 You can use a more complex XML configuration file, one including a display tag, to set up complex display configurations. The following file is an example that sets up a panoramic view over three channels, each rendered in a separate graphics pipe:

<?xml version="1.0" ?>

<viewer>
    <!-- Display specifications -->
    <display>
        <!-- Configure middle pipe -->
        <pipe>
            <!-- direct middle pipe to screen 0 -->
            <screen>0</screen>
            <!-- Configure a single pipe-window on middle pipe -->
            <pwin>
                <!-- set pipe-window to fullscreen, no border -->
                <fullscreen>1</fullscreen>
                <border>0</border>
                <!-- Configure a single channel on pipe-window -->
                <chan>
                    <viewrange>0.32,0.68,0.0,1.0</viewrange>
                    <hprOffset>0.0,0.0,0.0</hprOffset>
                    <fov>59.0,46.0</fov>
                </chan>
            </pwin>
        </pipe>

       <!-- Configure right pipe -->
        <pipe>
            <screen>1</screen>
            <pwin>
                <!-- set pipe-window to fullscreen, no border -->
                <fullscreen>1</fullscreen>
                <border>0</border>
                <!-- Configure a single channel on pipe-window -->
                <chan>
                    <viewrange>0.64,1.0,0.0,1.0</viewrange>
                    <hprOffset>-53.333,0.0,0.0</hprOffset>
                    <fov>59.0,46.0</fov>
                </chan>
            </pwin>
        </pipe>		

       <!-- Configure left pipe -->
        <pipe>
            <screen>2</screen>
            <pwin>
                <!-- set pipe-window to fullscreen, no border -->
                <fullscreen>1</fullscreen>
                <border>0</border>
                <!-- Configure a single channel on pipe-window -->
                <chan>
                    <viewrange>0.0,0.36,0.0,1.0</viewrange>
                    <hprOffset>53.333,0.0,0.0</hprOffset>
                    <fov>59.0,46.0</fov>
                </chan>
            </pwin>
        </pipe>		
    </display>
 
    <module>
        <!-- Loader module: loads models into world -->
        <class>pfvmLoader</class>
        <data>
            <Model>
                <FileName>esprit.flt</FileName>
            </Model>
        </data>
    </module>

    <!-- Add a trackball navigation module -->
    <module>
        <class>pfvmTrackball</class>
    </module>

    <!-- Add a picking module -->
    <module>
        <class>pfvmPicker</class>
    </module>

</viewer>

Module Scoping, Multiple Worlds and Multiple Views

In more complex pfvViewer applications, you can create multiple views and/or multiple worlds. Each view will always render (view) one of the specified worlds. Each world may be viewed by zero, one, or more views at any point during the life of the application.


Note: You can direct views from one world to another during the course of an application.

The following simple XML configuration file defines two worlds, each being rendered into a separate view:

<?xml version="1.0" ?>
<viewer>
    
    <!-- Specify two worlds, and assign each a unique name -->
    <world>
        <name>world0</name>
    </world>

    <world>
        <name>world1</name>
    </world>

    <!-- Specify two views, assign each a unique name,
         and direct to the corresponding world -->
    <view>
        <name>view0</name>
        <world>world0</world>
    </view>

    <view>
        <name>view1</name>
        <world>world1</world>
    </view>

    <!-- Specify two instances of the pfvmLoader module.
         By scoping these modules to different worlds, 
         each loader module will add its geometry to the
         appropriate scene graph -->
           
    <module>
        <class>myLoader</class>
        <scope>world,world0</scope>
        <data>
            <Model>
                <FileName>esprit.flt</FileName>
            </Model>
        </data>
    </module>

    <module>
        <class>myLoader</class>
        <scope>world,world1</scope>
        <data>
            <Model>
                <FileName>truck.pfb</FileName>
            </Model>
        </data>
    </module>

    <!-- Specify two instances of the pfvmNavigator module.
         By scoping these modules to different views, each module
         will take care of navigation within the appropriate view -->
           
    <module>
        <class>myNavigator</class>
        <scope>view,view0</scope>
    </module>

    <module>
        <class>myNavigator</class>
        <scope>view,view1</scope>
    </module>

</viewer>

You can achieve the same result through API calls, as shown in the following program:

#include <Performer/pfdu.h>
#include <Performer/pfv/pfvViewer.h>

main (int argc, char *argv[])
{
    pfvModule* module;

    // Initialize Performer
    pfInit();

    // Initialize loading of model files.
    pfdInitConverter("flt");
    pfdInitConverter("pfb");

    // Create a new pfvViewer.
    pfvViewer* viewer = new pfvViewer;

    //Create first world.
    pfvWorld* w0 = viewer->createWorld();

    //Create second world.
    pfvWorld* w1 = viewer->createWorld();
    
    //Create first view. v0 becomes viewer's current view.
    pfvView* v0 = (pfvView*)(viewer->createView());

    // Direct first view to first world.
    v0->setTargetWorld(w0);

    // Add navigation module to viewer's current view (v0).
    module = pfvModule::load("pfvmNavigator");
    viewer->addModule(module, PFV_SCOPE_VIEW);

    //Create second view. v1 becomes viewer's current view.
    pfvView* v1 = (pfvView*)(viewer->createView());

    // Direct second view to second world
    v1->setTargetWorld(w1);

    // Add navigation module to viewer's current view (v1).
    module = pfvModule::load("pfvmNavigator");
    viewer->addModule(module, PFV_SCOPE_VIEW);

    // Configure/Initialize pfvViewer
    viewer->config();

    // Add car model to first world
    w0->addNode(pfdLoadFile("esprit.flt"));

    // Add truck model to first world
    w1->addNode(pfdLoadFile("truck.pfb"));

    // Start viewing
    viewer->run();
}

Extending a pfvViewer—Writing Custom Modules

A pfvViewer accepts user-written modules and incorporates their functions into its behavior. In order to extend a pfvViewer, you can write a new module. The following very simple module informs the pfvViewer to invoke the handleEvent() method and to print a message when the F1 key is pressed:

class myModule : public pfvModule
{
public: 

    myModule::myModule()
    {
        char keys[64];

        // Declare what keyboard inputs this module is interested in.
        sprintf(keys,"%c",PFVKEY_F1);
        bindKeys(keys);
    }
    
    myModule::~myModule() {;}

    // Keyboard event handler. pfvViewer calls this method every 
    // time the user hits the F1 key.
    int handleEvent(int evType, char key)
    {
        printf("myModule::handleEvent called for key %s\n",
               pfvInputMngr::getKeyName(key) );
        return 0;
    }
};

In order to add this module to a pfvViewer program, add the following line:

viewer->addModule(new myModule);

Note that if this module is scoped to a view, pfvViewer will only inform the module of F1 key presses within channels belonging to that specific view. Similarly, if this module is scoped to a specific world, pfvViewer would inform the module of F1 key presses over any channel belonging to any view currently viewing such world.

You can scope this module to a world by making the following call:

pfvWorld* w;
viewer->addModule(new myModule, PFV_SCOPE_WORLD);

You can scope this module to a view by making the following call:

pfvView* w;
viewer->addModule(new myModule, PFV_SCOPE_VIEW);

The following example illustrates how to implement a basic custom module that controls the camera position (for the first view in pfvViewer's list) based on the mouse position:

class myModule : public pfvModule
{
public:
    myModule(){
        bindCallback(PFV_CB_FRAME);
    }

    ~myModule(){;}

    void frame() {
        // Only set eye for view0 if mouse is over view0
        if(pfvInputMngr::getFocusViewIndex()!=0)
            return;

        pfVec3 xyz,hpr;
        // Get current eye position (we don't want to change xyz)
        viewer->getView(0)->getEye(xyz,hpr);

        float mx, my;
       // Get current mouse position in view-normalized values
        // (0.0 to 1.0)
        pfvInputMngr::getViewNormXY( &mx, &my );

       // Compute new values for Heading and Pitch based on mouse
        // position
        hpr[0]= (mx-0.5f)*180.0f;
        hpr[1]= (my-0.5f)*-90.0f;

        // Set new eye position for view0
        viewer->getView(0)->setEye(xyz,hpr);
    }
};

Extending a pfvViewer—Module Entry Points

A module can gain program control at the various stages of rendering. The following are some of these stages:

  • Event-Driven methods

    handleEvent()

    Called in response to a key-press event. A pfvViewer invokes this method only if the pressed key was bound by this module and if the event was generated over a view relevant to the module.

  • Configuration methods (called once in the life of the application)

    • preConfig()

      Called before pfvViewer calls pfConfig().

    • postConfig()

      Called after the pfvViewer calls pfConfig().

  • Run-Time methods (called every frame):

    • sync()

      Called each frame immediately after pfvViewer calls pfSync().

    • frame()

      Called each frame after pfvViewer calls pfFrame().

    • preCull()

      Called in all CULL processes before calling pfCull().

    • postCull()

      Called in all CULL processes after calling pfCull().

    • preDraw()

      Called in all DRAW processes before calling pfDraw().

    • postDraw()

      Called in all DRAW processes after calling pfDraw().

    • overlay()

      Called in all DRAW processes after postDraw() callbacks.

  • Enter and exit methods (called for scoped modules only)

    • enterWorld()

      A pfvViewer invokes this method on view-scoped modules to inform them that their view is about to start viewing a new world.

    • exitWorld()

      A pfvViewer invokes this method on view-scoped modules to inform them that their view is about to stop viewing current world.

    • enterView()

      A pfvViewer invokes this method on world-scoped modules to inform them that a new view is about to start viewing their world.

    • exitView()

      A pfvViewer invokes this method on world-scoped modules to inform them that a view is about to stop viewing their world.

Picking, Selection, and Interaction

The library libpfv also provides a framework for specifying custom interaction behavior through the pfvPicker, pfvInteractor, and pfvSelector classes.

A single pfvPicker instance will be able to coordinate multiple interaction classes derived from pfvInteractor and/or pfvSelector.

The following example illustrates how to derive entities from the pfvInteractor class in order to be able to highlight the geometry under the mouse cursor:

#include <Performer/pfv/pfvViewer.h>
#include <Performer/pfv/pfvInputMngrPicker.h>
#include <Performer/pr/pfHighlight.h>
#include <Performer/pf/pfLightSource.h>
#include <Performer/pfutil.h>
#include <Performer/pfdu.h>

class myInteractor : public pfvInteractor
{
public:

    myInteractor(){ 
        // create and configure a pfHighlight instance
        hl = new pfHighlight;
        hl->setMode( PFHL_LINES );
        hl->setColor( PFHL_FGCOLOR, 1.0f, 1.0f, 0.0f );
    }
    ~myInteractor(){ pfDelete(hl); }

    // startHlite will be called by picker once whenever mouse cursor  
    // is moved over some geometry after being pointed away from all 
    // geometry.
    int startHlite( pfvPicker*p, int prmsn ){
        // Obtain a pointer to the pfNode that was picked by picker
        p->getPickResults(&node);
        // Traverse picked node and highlight it
        pfuTravNodeHlight( node, hl );
        // return 1 indicating we accept highlighted state
        return 1;
    }

    // updateHlite will be called by picker on each frame as long as  
    // mouse cursor remains over some geometry.
    int updateHlite( pfvPicker* p,int ev,int prmsn, pfvPickerRequest*r ){
        pfNode* curnode = node;
        // Obtain a pointer to the pfNode that was picked by picker
        p->getPickResults(&node);
        // if node picked by picker is not the node that is
        // currently highlighted
        if(node!=curnode)
        {
            // De-highlight previously highlighted node
            pfuTravNodeHlight( node, hl );
            // Traverse picked node and highlight it
            pfuTravNodeHlight( curnode, NULL );
        }
        return 1;
    }

    // endHlite will be called by picker once whenever mouse cursor is  
    // moved away from all geometry..
    void endHlite( pfvPicker* p ){
        // De-highlight previously highlighted node
        pfuTravNodeHlight( node, NULL );
    }   

private:

    pfHighlight* hl;
    pfNode *node;
};


class myModule : public pfvModule
{
public:

    myModule(){
        // This module will use two callbacks (entry-points):
        bindCallback(PFV_CB_POSTCONFIG);
        bindCallback(PFV_CB_FRAME);
    }

    ~myModule(){;}

    // postConfig is called once by pfvViewer, after calling
    //  pfConfig().
    void postConfig(){
        // Create a pfvInputMngrPicker instance
        picker = new pfvInputMngrPicker;
        // Set up picker so it will automatically isect scene and
        // allow interactors to highlight
        picker->setState(PFPICKER_ALLOW_HLITE, NULL, NULL);
        // Create an instance of our custom interactor
        ia = new myInteractor;
        // set up a pointer to our interactor on scene's root-node
        ia->nodeSetup( viewer->getWorld(0)->getScene(), picker);
    }

    // on every frame, call picker->update() from APP process.
    void frame() {
        picker->update();
    }

private:
    pfvInputMngrPicker* picker;
    myInteractor* ia;
};

int
main (int argc, char *argv[])
{
    pfInit();

    pfvViewer* viewer = new pfvViewer();
 
    pfFilePath(".:/usr/share/Performer/data");

    pfdInitConverter("esprit.flt");

    viewer->addModule(new myModule);
    viewer->addModule(pfvModule::load("pfvmTrackball"));

    viewer->config();

    viewer->addNode(new pfLightSource);
    viewer->addNode(pfdLoadFile("esprit.flt"));

    viewer->run();  
}

More Sample Programs, Configuration Files, and Source Code

OpenGL Performer provides many libpfv  sample programs, configuration files, and source code for modules in the following directories:

  • /usr/share/Performer/src/pguide/libpfv/picker (IRIX and Linux)
    %PFROOT%\Src\pguide\libpfv\picker (Microsoft Windows)

    The samples in this directory demonstrate the use of pfvPicker and derived classes as well as how to extend the pfvInteractor and pfvSelector classes to implement your custom interaction behaviors.

  • /usr/share/Performer/src/pguide/libpfv/viewer (IRIX and Linux)
    %PFROOT%\Src\pguide\libpfv\viewer (Microsoft Windows)

    The samples in this directory demonstrate how to do the following:

    • Load models into pfvViewer applications.

    • Load standard or custom modules.

    • Create pfvViewers with multiple camera positions (views).

    • Create pfvViewers with multiple independent scene graphs (worlds).

    • Create complex multichannel display configurations.

    • Load complex display configurations from an XML file.

    • Write custom modules.

    • Compile modules into re-usable DSOs.

  • /usr/share/Performer/src/pguide/libpfv/viewer/modules
    (IRIX and Linux)
    %PFROOT%\Src\pguide\libpfv\viewer\modules (Microsoft Windows)

    This directory contains source code for the following modules:

    • pfvmDrawStyle

    • pfvmEarthSky

    • pfvmLoader

    • pfvmLogo

    • pfvmNavigator

    • pfvmPicker

    • pfvmSnapshot

    • pfvmStats

    • pfvmTrackball

    • pfvmWorldSwitcher

  • /usr/share/Performer/config (IRIX and Linux)
    %PFROOT%\Config (Microsoft Windows)

    This directory contains examples of pfvViewer configuration files, denoted by the .pfv filename extension.