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.
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.
For optimizing a scene, you will need to follow these steps:
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")
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))
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!
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!
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.
To complete this recipe, work your way through these tasks:
pandasrc
, create a new directory called bounce
. 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
. 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. 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
bounce_composite.cxx
so it contains the following line:#include "bounce_composite1.cxx"
bounce_composite1.cxx
so it contains the following two lines:#include "config_bounce.cxx" #include "bounce.cxx"
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
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; }
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; }
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
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(); }
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
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
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)
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)
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", }
makepanda
tool. 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. 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
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