Inspired by nik’s efforts to integrate CEGUI, and by queries from other users on-and-off over the years about draw callbacks in Panda, I’ve just added this feature. You can associate a callback function with a DisplayRegion, which will get called when the overall DisplayRegion is drawn (whether or not it includes any geometry), or you can associate a callback function with a particular CallbackNode in the scene graph, which will get called whenever the node is drawn. The node callback even inherits the node’s transform and state, so you can easily integrate third-party drawing libraries into Panda, and still take advantage of the power of Panda’s scene graph.

What’s more, these callback functions can be Python functions, so you no longer need to write even a lick of C++ code to hook deep into Panda’s low-level rendering system. You can integrate with PyOpenGL, or something similar, and off you go. Of course, interrupting the draw thread with an outcall to Python isn’t going to get you the highest frame rate imaginable, but sometimes it’s more about developer time than frame rate, and there are certainly cases when this feature will speed developer time.

Here’s a sample program that works with the current CVS build of Panda. It uses PyOpenGL to draw everyone’s favorite GL gears in a Panda scene.

```
from direct.directbase.DirectStart import *
from pandac.PandaModules import *
from OpenGL.GL import *
import sys
from math import sin,cos,sqrt,pi
def gear(inner_radius, outer_radius, width, teeth, tooth_depth):
""" This function is invoked at startup time to generate the
display list necessary to draw one particular gear. """
r0 = inner_radius
r1 = outer_radius - tooth_depth/2.0
r2 = outer_radius + tooth_depth/2.0
da = 2.0*pi / teeth / 4.0
glShadeModel(GL_FLAT)
glNormal3f(0.0, 0.0, 1.0)
# draw front face
glBegin(GL_QUAD_STRIP)
for i in range(teeth + 1):
angle = i * 2.0 * pi / teeth
glVertex3f(r0*cos(angle), r0*sin(angle), width*0.5)
glVertex3f(r1*cos(angle), r1*sin(angle), width*0.5)
glVertex3f(r0*cos(angle), r0*sin(angle), width*0.5)
glVertex3f(r1*cos(angle+3*da), r1*sin(angle+3*da), width*0.5)
glEnd()
# draw front sides of teeth
glBegin(GL_QUADS)
da = 2.0*pi / teeth / 4.0
for i in range(teeth):
angle = i * 2.0*pi / teeth
glVertex3f(r1*cos(angle), r1*sin(angle), width*0.5)
glVertex3f(r2*cos(angle+da), r2*sin(angle+da), width*0.5)
glVertex3f(r2*cos(angle+2*da), r2*sin(angle+2*da), width*0.5)
glVertex3f(r1*cos(angle+3*da), r1*sin(angle+3*da), width*0.5)
glEnd()
glNormal3f(0.0, 0.0, -1.0)
# draw back face
glBegin(GL_QUAD_STRIP)
for i in range(teeth + 1):
angle = i * 2.0*pi / teeth
glVertex3f(r1*cos(angle), r1*sin(angle), -width*0.5)
glVertex3f(r0*cos(angle), r0*sin(angle), -width*0.5)
glVertex3f(r1*cos(angle+3*da), r1*sin(angle+3*da),-width*0.5)
glVertex3f(r0*cos(angle), r0*sin(angle), -width*0.5)
glEnd()
# draw back sides of teeth
glBegin(GL_QUADS)
da = 2.0*pi / teeth / 4.0
for i in range(teeth):
angle = i * 2.0*pi / teeth
glVertex3f(r1*cos(angle+3*da), r1*sin(angle+3*da),-width*0.5)
glVertex3f(r2*cos(angle+2*da), r2*sin(angle+2*da),-width*0.5)
glVertex3f(r2*cos(angle+da), r2*sin(angle+da), -width*0.5)
glVertex3f(r1*cos(angle), r1*sin(angle), -width*0.5)
glEnd()
# draw outward faces of teeth
glBegin(GL_QUAD_STRIP);
for i in range(teeth):
angle = i * 2.0*pi / teeth
glVertex3f(r1*cos(angle), r1*sin(angle), width*0.5)
glVertex3f(r1*cos(angle), r1*sin(angle), -width*0.5)
u = r2*cos(angle+da) - r1*cos(angle)
v = r2*sin(angle+da) - r1*sin(angle)
len = sqrt(u*u + v*v)
u = u / len
v = v / len
glNormal3f(v, -u, 0.0)
glVertex3f(r2*cos(angle+da), r2*sin(angle+da), width*0.5)
glVertex3f(r2*cos(angle+da), r2*sin(angle+da), -width*0.5)
glNormal3f(cos(angle), sin(angle), 0.0)
glVertex3f(r2*cos(angle+2*da), r2*sin(angle+2*da), width*0.5)
glVertex3f(r2*cos(angle+2*da), r2*sin(angle+2*da),-width*0.5)
u = r1*cos(angle+3*da) - r2*cos(angle+2*da)
v = r1*sin(angle+3*da) - r2*sin(angle+2*da)
glNormal3f(v, -u, 0.0)
glVertex3f(r1*cos(angle+3*da), r1*sin(angle+3*da), width*0.5)
glVertex3f(r1*cos(angle+3*da), r1*sin(angle+3*da),-width*0.5)
glNormal3f(cos(angle), sin(angle), 0.0)
glVertex3f(r1*cos(0), r1*sin(0), width*0.5)
glVertex3f(r1*cos(0), r1*sin(0), -width*0.5)
glEnd()
glShadeModel(GL_SMOOTH)
# draw inside radius cylinder
glBegin(GL_QUAD_STRIP)
for i in range(teeth + 1):
angle = i * 2.0*pi / teeth;
glNormal3f(-cos(angle), -sin(angle), 0.0)
glVertex3f(r0*cos(angle), r0*sin(angle), -width*0.5)
glVertex3f(r0*cos(angle), r0*sin(angle), width*0.5)
glEnd()
(gear1, gear2, gear3) = (0,0,0)
tStart = globalClock.getFrameTime()
rotationRate = 10
def draw(cbdata):
""" This callback is attached to cbnode to be called every frame
that cbnode is in view. It calls the GL display lists built up by
init(), to draw the three gears in their spinning
configuration. """
# This is a CallbackNode draw_callback. At the time this callback
# is received, the render state and modelview matrix are already
# loaded with the net state and transform for this node. By using
# normal OpenGL relative matrix operations, we can render our
# geometry relative to the CallbackNode's position in the scene
# graph.
angle = (2 * pi) * ((globalClock.getFrameTime() - tStart)*rotationRate)
glPushMatrix()
glTranslatef(-3.0, -2.0, 0.0)
glRotatef(angle, 0.0, 0.0, 1.0)
glCallList(gear1)
glPopMatrix()
glPushMatrix()
glTranslatef(3.1, -2.0, 0.0)
glRotatef(-2.0*angle-9.0, 0.0, 0.0, 1.0)
glCallList(gear2)
glPopMatrix()
glPushMatrix()
glTranslatef(-3.1, 4.2, 0.0)
glRotatef(-2.0*angle-25.0, 0.0, 0.0, 1.0)
glCallList(gear3)
glPopMatrix()
# There's not a lot of point in upcalling here, since a
# CallbackNode doesn't have any geometry of its own, and this
# upcall does nothing. However, we do it anyway, just for
# completeness.
cbdata.upcall()
def init(cbdata):
""" This callback is attached to cbnode initially. It compiles
the GL display lists necessary to draw the three gears, then it
yields itself to the draw() function, above. """
global gear1, gear2, gear3
pos = (5.0, 5.0, 10.0, 0.0)
red = (0.8, 0.1, 0.0, 1.0)
green = (0.0, 0.8, 0.2, 1.0)
blue = (0.2, 0.2, 1.0, 1.0)
# make the gears
gear1 = glGenLists(1)
glNewList(gear1, GL_COMPILE)
glDisable(GL_COLOR_MATERIAL)
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, red)
gear(1.0, 4.0, 1.0, 20, 0.7)
glEndList()
gear2 = glGenLists(1)
glNewList(gear2, GL_COMPILE)
glDisable(GL_COLOR_MATERIAL)
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, green)
gear(0.5, 2.0, 2.0, 10, 0.7)
glEndList()
gear3 = glGenLists(1)
glNewList(gear3, GL_COMPILE)
glDisable(GL_COLOR_MATERIAL)
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, blue)
gear(1.3, 2.0, 0.5, 10, 0.7)
glEndList()
# Now draw the gears.
draw(cbdata)
# After the first frame, we just want to draw the gears.
cbnode.setDrawCallback(PythonCallbackObject(draw))
# Make sure we're running OpenGL.
if base.pipe.getInterfaceName() != 'OpenGL':
print "This program requires OpenGL."
sys.exit(1)
def frame(cbdata):
""" This callback is attached to the render2d DisplayRegion. It
draws a frame around the scene for no real good reason. """
# This is a DisplayRegion draw_callback. At the time this
# callback is received, the DisplayRegion has been cleared, but no
# state, modelview, or projection matrix has been set. The
# DisplayRegion is a blank slate; we can do whatever we want to
# draw into it.
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0.0, 1.0, 0.0, 1.0, 0.0, 1.0)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glBegin(GL_LINE_LOOP)
glColor3f(1, 1, 0)
glVertex3f(0.1, 0.9, 0)
glVertex3f(0.9, 0.9, 0)
glVertex3f(0.9, 0.1, 0)
glVertex3f(0.1, 0.1, 0)
glEnd()
# We have to upcall to the normal DisplayRegion handling, or
# anything parented to render2d won't be drawn.
cbdata.upcall()
# Set up a CallbackNode to do all the gear magic.
cbnode = CallbackNode('cbnode')
cbnode.setDrawCallback(PythonCallbackObject(init))
cbnp = render.attachNewNode(cbnode)
# We have to set an explicit bounding volume on cbnode, because Panda
# has no way of knowing how big is the thing we plan to be drawing in
# there, so it can't compute the bounding volume automatically.
cbnode.setBounds(BoundingSphere((-1, -1, 0), 8))
# Set up a callback on the render2d DisplayRegion to draw the yellow
# frame.
base.cam2d.node().getDisplayRegion(0).setDrawCallback(PythonCallbackObject(frame))
# The gears rotate all by themselves, but we set up a LerpInterval to
# tumble the CallbackNode itself--which contains the whole gear
# assembly--using normal Panda interfaces.
i = cbnp.hprInterval(10, hpr = (0, 360, 0))
i.loop()
# Create a Panda light to view the scene.
dlnp = base.camera.attachNewNode(DirectionalLight('dl'))
render.setLight(dlnp)
# Position the camera in a prime spot for viewing.
base.trackball.node().setPos(0, 30, 0)
run()
```

It looks like this in action:

This callback solution might solve nik’s immediate problems with integrating CEGUI and its OpenGL renderer, since the callbacks make more of an effort to preserve OpenGL state across calls. The DisplayRegion callback would be particularly suited for this. Of course, I think for long-term CEGUI integration, a pure Panda3D-based CEGUI renderer is still the way to go.

David