Chapter 25. Building a Visual Simulation Application Using libpfv
Prev |  | Next

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

- “The Simplest pfvViewer Program”

- “Adding Interaction to a pfvViewer Program”

- “Reading XML Configuration Files”

- “Module Scoping, Multiple Worlds and Multiple Views”

- “Extending a pfvViewer—Writing Custom Modules”

- “Extending a pfvViewer—Module Entry Points”

- “Picking, Selection, and Interaction”

- “More Sample Programs, Configuration Files, and Source Code”

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

Prev | Table of Contents | Next
Chapter 24. Performance Tuning and Debugging |  | Chapter 26. Programming with C++