Improving performance by flattening scenes

While the scene graph and renderer of the Panda3D engine generally do a good job managing and drawing scene data, they are easily overloaded with data. Too many different models consisting of lots of geometry nodes while all being transformed to various positions will quickly push the scene graph to its limits, while the renderer has to wait for the graphics card because all these models in the scene need to be sent one by one to the graphics card for drawing to the screen.

The easiest step to decreased scene complexity, for sure, is to remove objects from the scene graph. This works, but it removes details from our game and may make it look empty and cheap.

Fortunately, there's an alternative solution we can try before starting to cut models from our scenes. Panda3D provides an API for simplifying scenes by precalculating transformations as well as combining scene nodes and their geometry to please the graphics card by sending this data in one big batch.

Getting ready

Setup a new game project structure as described in Chapter 1, Setting Up Panda3D and Configuring Development Tools before going on with the following tasks.

How to do it...

For optimizing a scene, you will need to follow these steps:

  1. Edit Application.py and fill in the following code:
    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *
    import random
    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.cam.setPos(0, -100, 10)
    self.setFrameRateMeter(True)
    envRoot = render.attachNewNode("envRoot")
    for i in range(100):
    self.addEnvironment(envRoot)
    envRoot.flattenStrong()
    combiner = RigidBodyCombiner("cmb")
    self.smRoot = render.attachNewNode(combiner)
    for i in range(200):
    self.addSmiley(self.smRoot)
    combiner.collect()
    taskMgr.add(self.updateSmileys, "UpdateSmileys")
    
  2. Below the code you just added, append the following:
    def addSmiley(self, parent):
    sm = loader.loadModel("smiley")
    sm.reparentTo(parent)
    sm.setPos(random.uniform(-20, 20), random.uniform(-30, 30), random.uniform(0, 30))
    sm.setPythonTag("velocity", 0)
    def updateSmileys(self, task):
    for smiley in self.smRoot.findAllMatches("smiley.egg"):
    vel = smiley.getPythonTag("velocity")
    z = smiley.getZ()
    if z <= 0:
    vel = random.uniform(0.1, 0.8)
    smiley.setZ(z + vel)
    vel -= 0.01
    smiley.setPythonTag("velocity", vel)
    return task.cont
    def addEnvironment(self, parent):
    env = loader.loadModel("environment")
    env.reparentTo(parent)
    env.setScale(0.01, 0.01, 0.01)
    env.setPos(render, random.uniform(-20, 20), random.uniform(-30, 30), random.uniform(0, 30))
    

How it works...

The important parts about this code sample are the flattenStrong() method and the RigidBodyCombiner class.

Actually, flattenStrong() is part of a group of methods of the NodePath class used for simplifying the subtree of child nodes under the scene node the method is being called on. Using flattenLight(), the vertices of child nodes are multiplied by their nodes' transformation matrices. This has the effect that these nodes do not need to be transformed to their positions anymore, sparing the CPU a set of matrix multiplications per frame. The flattenMedium() method does a flattenLight() pass and additionally makes the scene tree hierarchy simpler by removing and combining obsolete nodes and their children. We can try to use this method to increase performance of static scenes with a very deep and nested node hierarchy. By calling flattenStrong() on a node in the scene graph, the complete scene node subtree under the affected node is flattened and combined to one single node, making it possible to send the node geometry in one big batch, which can greatly decrease the time needed for rendering the scene. The price for this gain though, is that this is a destructive action because after using this method, the hierarchy of child nodes connected to the modified node is destroyed.

We are using a similar optimization technique for the subtree containing the smileys. But while the nodes we applied flattenStrong() to were static, our smileys are moved every frame, which is why we are using the RigidBodyContainer class in this case. Although used in a slightly different way than the flattening methods, the concept behind RigidBodyContainer and its effect on how the scene is rendered is very similar. Before being sent to the renderer, the child nodes of the combiner node are joined into one, causing only one batch of geometry to be sent to the graphics device.

These optimization methods are no magic bullet, however. In some cases they are able to greatly improve performance, while in others it can even become worse than before. Therefore it is very important for us to keep on experimenting with the various degrees of flattening while profiling our game to observe the results!

Implementing performance critical code in C++

While in direct comparison, compiled C++ code performs better than the same code ported to Python, it would be wrong to generally state that the use of the Python interpreter in Panda3D is detrimental to the performance of your game. This would be wrong and utter nonsense, as Python just acts as a simpler interface to the engine's core libraries. Most of a game's code that uses the Python interface of Panda3D consists of calls to the engine's APIs, which are implemented in C++ and simply forwarded by the Python runtime.

While this architecture generally delivers quite acceptable performance, there might be an occasion or two where, after thoroughly profiling and optimizing your Python code, you still might not have reached the performance goals you set for that piece of code. Only if you are sure about there not being any gains possible to achieve anymore should you start thinking about writing a C++ implementation of your code.

This recipe will show you the steps necessary for adding a new C++ class to the Panda3D engine and making it available to be instantiated from Python code. You will add the new module containing the newly created class to Panda3D's API and build it with the rest of the engine's source code. This approach might seem bloated and overly complex, but while it is possible to build custom libraries for Panda3D outside the engine's source tree, it is a lot harder and cumbersome to set up. Furthermore, the documentation on adding C++ classes to the engine is really very sparse. Following the example of the rest of the source code of Panda3D does at least provide you with lots of sample material you can compare your custom efforts to. Finally and very importantly, this way the code is already prepared to be integrated into the official source code in case you wish to contribute the code to the community. Panda3D is an open source project and available for free, so giving back is just fair!

Getting ready

Prior to going on with this recipe you should have read and understood the recipe about building Panda3D from source code found in Chapter 1.

How to do it...

To complete this recipe, work your way through these tasks:

  1. In pandasrc, create a new directory called bounce.
  2. Copy the files config_skel.cxx, config_skel.h, skel_composite.cxx, skel_composite1.cxx, Sources.pp, typedSkel.cxx, typedSkel.h, and typedSkel.I from pandasrcskel to pandasrcounce.
  3. In pandasrcounce, rename config_skel.cxx to config_bounce.cxx, config_skel.h to config_bounce.h, skel_composite.cxx to bounce_composite.cxx, skel_composite1.cxx to bounce_composite1.cxx as well as typedSkel.cxx, typedSkel.h, and typedSkel.I to bounce.cxx, bounce.h and bounce.I, respectively.
  4. Open Sources.pp in a text editor and replace its content with the following lines:
    #define OTHER_LIBS interrogatedb:c dconfig:c dtoolconfig:m 
    dtoolutil:c dtoolbase:c dtool:m prc:c
    #define USE_PACKAGES
    #define BUILDING_DLL BUILDING_PANDABOUNCE
    #begin lib_target
    #define TARGET bounce
    #define LOCAL_LIBS 
    putil
    #define COMBINED_SOURCES $[TARGET]_composite1.cxx
    #define SOURCES 
    config_bounce.h 
    bounce.I bounce.h
    #define INCLUDED_SOURCES 
    config_bounce.cxx 
    bounce.cxx
    #define INSTALL_HEADERS 
    bounce.I bounce.h
    #define IGATESCAN all
    #end lib_target
    
  5. Edit bounce_composite.cxx so it contains the following line:
    #include "bounce_composite1.cxx"
    
  6. Change bounce_composite1.cxx so it contains the following two lines:
    #include "config_bounce.cxx"
    #include "bounce.cxx"
    
  7. Open bounce.h and change its content to reflect the following code:
    #ifndef BOUNCE_H
    #define BOUNCE_H
    #include "pandabase.h"
    #include "typedObject.h"
    #include "randomizer.h"
    class EXPCL_PANDABOUNCE Bounce : public TypedObject {
    PUBLISHED:
    INLINE Bounce();
    INLINE ~Bounce();
    INLINE float get_z();
    INLINE void set_z(float z);
    void update();
    private:
    float _velocity;
    float _z;
    Randomizer _rand;
    public:
    static TypeHandle get_class_type() {
    return _type_handle;
    }
    static void init_type() {
    TypedObject::init_type();
    register_type(_type_handle, "Bounce",
    TypedObject::get_class_type());
    }
    virtual TypeHandle get_type() const {
    return get_class_type();
    }
    virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
    private:
    static TypeHandle _type_handle;
    };
    #include "bounce.I"
    #endif
    
  8. After you are done with bounce.cxx it should look like the following:
    #include "bounce.h"
    TypeHandle Bounce::_type_handle;
    void Bounce::
    update() {
    if (_z <= bounce_floor_level)
    _velocity = _rand.random_real(0.7f) + 0.1f;
    _z = _z + _velocity;
    _velocity -= 0.01f;
    }
    
  9. Now open bounce.I and change the code to the following:
    INLINE Bounce::
    Bounce() {
    _z = 0;
    _velocity = 0;
    }
    INLINE Bounce::
    ~Bounce() {
    }
    INLINE float Bounce::
    get_z() {
    return _z;
    }
    INLINE void Bounce::
    set_z(float z) {
    _z = z;
    }
    
  10. Make sure config_bounce.h contains the following lines of code:
    #ifndef CONFIG_BOUNCE_H
    #define CONFIG_BOUNCE_H
    #include "pandabase.h"
    #include "notifyCategoryProxy.h"
    #include "configVariableDouble.h"
    NotifyCategoryDecl(bounce, EXPCL_PANDABOUNCE, EXPTP_PANDABOUNCE);
    extern ConfigVariableDouble bounce_floor_level;
    extern EXPCL_PANDABOUNCE void init_libbounce();
    #endif
    
  11. Edit config_bounce.cxx. The file's content should look as follows:
    #include "config_bounce.h"
    #include "bounce.h"
    #include "dconfig.h"
    Configure(config_bounce);
    NotifyCategoryDef(bounce, "");
    ConfigureFn(config_bounce) {
    init_libbounce();
    }
    ConfigVariableDouble bounce_floor_level
    ("bounce-floor-level", 0);
    void
    init_libbounce() {
    static bool initialized = false;
    if (initialized) {
    return;
    }
    initialized = true;
    Bounce::init_type();
    }
    
  12. Open the file pandasrcpandabasepandasymbols.h. Look for the following block of code and add the highlighted code:
    #ifdef BUILDING_PANDASKEL
    #define EXPCL_PANDASKEL __declspec(dllexport)
    #define EXPTP_PANDASKEL
    #else
    #define EXPCL_PANDASKEL __declspec(dllimport)
    #define EXPTP_PANDASKEL extern
    #endif
    #ifdef BUILDING_PANDABOUNCE
    #define EXPCL_PANDABOUNCE __declspec(dllexport)
    #define EXPTP_PANDABOUNCE
    #else
    #define EXPCL_PANDABOUNCE __declspec(dllimport)
    #define EXPTP_PANDABOUNCE extern
    #endif
    
  13. Still in pandasymbols.h, look out for the following code and add the marked lines:
    #define EXPCL_PANDASKEL
    #define EXPTP_PANDASKEL
    #define EXPCL_PANDABOUNCE
    #define EXPTP_PANDABOUNCE
    
  14. Search for the following block of code in makepandamakepanda.py:
    if (not RUNTIME):
    OPTS=['BUILDING:PANDASKEL', 'ADVAPI']
    TargetAdd('libpandaskel_module.obj', input='libskel.in')
    TargetAdd('libpandaskel_module.obj', opts=OPTS)
    TargetAdd('libpandaskel_module.obj', opts=['IMOD:pandaskel', 'ILIB:libpandaskel'])
    TargetAdd('libpandaskel.dll', input='skel_composite.obj')
    TargetAdd('libpandaskel.dll', input='libskel_igate.obj')
    TargetAdd('libpandaskel.dll', input='libpandaskel_module.obj')
    TargetAdd('libpandaskel.dll', input=COMMON_PANDA_LIBS)
    TargetAdd('libpandaskel.dll', opts=OPTS)
    
  15. Directly below the aforementioned block of code add the following:
    if (not RUNTIME):
    OPTS=['DIR:panda/src/bounce', 'BUILDING:PANDABOUNCE', 'ADVAPI']
    TargetAdd('bounce_composite.obj', opts=OPTS, input='bounce_composite.cxx')
    IGATEFILES=GetDirectoryContents("panda/src/bounce", ["*.h", "*_composite.cxx"])
    TargetAdd('libbounce.in', opts=OPTS, input=IGATEFILES)
    TargetAdd('libbounce.in', opts=['IMOD:pandabounce', 'ILIB:libbounce', 'SRCDIR:panda/src/bounce'])
    TargetAdd('libbounce_igate.obj', input='libbounce.in', opts=["DEPENDENCYONLY"])
    TargetAdd('libpandabounce_module.obj', input='libbounce.in')
    TargetAdd('libpandabounce_module.obj', opts=OPTS)
    TargetAdd('libpandabounce_module.obj', opts=['IMOD:pandabounce', 'ILIB:libpandabounce'])
    TargetAdd('libpandabounce.dll', input='bounce_composite.obj')
    TargetAdd('libpandabounce.dll', input='libbounce_igate.obj')
    TargetAdd('libpandabounce.dll', input='libpandabounce_module.obj')
    TargetAdd('libpandabounce.dll', input=COMMON_PANDA_LIBS)
    TargetAdd('libpandabounce.dll', opts=OPTS)
    
  16. Add the highlighted line to directsrcffipanda3d.py:
    panda3d_modules = {
    "core" :("libpandaexpress", "libpanda"),
    "dtoolconfig" : "libp3dtoolconfig",
    "physics" : "libpandaphysics",
    "fx" : "libpandafx",
    "direct" : "libp3direct",
    "egg" : "libpandaegg",
    "ode" : "libpandaode",
    "vision" : "libp3vision",
    "physx" : "libpandaphysx",
    "ai" : "libpandaai",
    "bounce" : "libpandabounce",
    }
    
  17. Compile the Panda3D source code using the makepanda tool.
  18. Add your custom built version of Panda3D to NetBeans. Follow steps 13 to 17 of the recipe Downloading and configuring NetBeans to work with Panda3D found in Chapter 1. Instead of the ppython.exe file in the Panda3D installation directory, choose the one found in the builtpython subdirectory of the Panda3D source code. Type CustomPython into the Platform Name field of the Python Platform Manager window.
  19. Create a new project as described in Setting up the game structure. Be sure to choose CustomPython in step 3.
  20. Open Application.py and replace its content with the following:
    from direct.showbase.ShowBase import ShowBase
    from panda3d.core import *
    from panda3d.bounce import *
    import random
    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.smiley = loader.loadModel("smiley")
    self.smileyCount = 0
    self.cam.setPos(0, -100, 10)
    taskMgr.doMethodLater(0.1, self.addSmiley, "AddSmiley")
    taskMgr.add(self.updateSmileys, "UpdateSmileys", uponDeath = self.removeSmileys)
    taskMgr.doMethodLater(60, taskMgr.remove, "RemoveUpdate", extraArgs = ["UpdateSmileys"])
    def addSmiley(self, task):
    sm = render.attachNewNode("smiley-instance")
    sm.setPos(random.uniform(-20, 20), random.uniform(-30, 30), random.uniform(0, 30))
    bounce = Bounce()
    bounce.setZ(sm.getZ())
    sm.setPythonTag("bounce", bounce)
    self.smiley.instanceTo(sm)
    self.smileyCount += 1
    if self.smileyCount == 300:
    return task.done
    return task.again
    def updateSmileys(self, task):
    for smiley in render.findAllMatches("smiley-instance"):
    bounce = smiley.getPythonTag("bounce")
    bounce.update()
    smiley.setZ(bounce.getZ())
    return task.cont
    def removeSmileys(self, task):
    for smiley in render.findAllMatches("smiley-instance"):
    smiley.removeNode()
    return task.done
    
  21. Press F6 to launch the program.

How it works...

After copying and renaming the files of the skeleton module provided with the rest of the Panda3D source code, our first real task is preparing the files needed by the build system.

In Sources.pp, we need to list the source and header files that make up our project, as well as the libraries our code depends on and needs to be linked against. Additionally, we set the name of the build target to bounce, which will be the name of the library being built from our code. This file also defines which header files will be distributed with the build, and is needed for automatically generating the Python bindings for our class.

Using #include on C++ source files instead of just using it on headers may seem a bit odd, but this is how the build system works. Panda3D is built using a compilation technique that is very specific to C++ projects called "unity build". In this kind of setup, instead of compiling individual source files to object files and linking them, the preprocessor is used to generate one big file containing all of the source code. This big, unified source file is then compiled as one, which can reduce build times of big C++ projects.

Next, we define the interface of our class. Because we want our new class to be exposed to the Python runtime, we need to derive from TypedObject and add the init_type() and get_type() member functions that will be called by the engine to register and initialize our class with Python's type system. In init_type() we have to call the init_type() function of the base class and fill in the type name we are going to use in Python. So if we wanted to derive a new class called Tumble from Bounce, the derived class' init_type() function would have to look like this:

static void init_type() {
Bounce::init_type();
register_type(_type_handle, "Tumble", Bounce::get_class_type());
}

Besides the type system management code, we mark the member functions we want to be exposed to Python as PUBLISHED. To the C++ compiler, these are just public member functions, but the tools invoked by Panda3D's build system will pick it up for automatically generating Python bindings for the class.

We then go on to implementing the Bounce class and adding functions for configuring and initializing the library. In the files config_bounce.h and config_bounce.cxx, we register a new category for log messages using the NotifyCategoryDecl and NotifyCategoryDef macros. In addition, we add a new configuration variable, so we are able to change the behavior of our class library using the engine's configuration file. ConfigVariableDouble for floating point values is not the only possible type for configuration variables. There are also the types ConfigVariableBool, ConfigVariableInt and ConfigVariableString.

Before building, we need to add a few preprocessor symbols. The EXPCL_PANDABOUNCE and EXPTP_PANDABOUNCE symbols are defined differently, depending on whether they are used for building the engine code or they are included in client code. This avoids having to keep around two versions of the file for these two use cases.

After adding the directory that contains our source to the makepanda script and adding our library to the list of submodules of panda3d, we can go on to build the source code. When this rather lengthy process has finished, all we have to do is register our customized runtime version to the IDE, setup our project and use our Bounce class just like any other type found

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset