Panda draw callbacks

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. :slight_smile:

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

Great news, excellent work!
I’ve just picked up these changes for 1.6.0.

Is this also possible for DirectX now?

Should be, though I haven’t tried it. I don’t actually have a handy Windows computer to build it on today.

David

Hmm, I seem to be getting a segfault running your code. First frame renders good, but then it immediately crashes.
Here’s full output from run under gdb:
pastebin.ubuntu.com/131295/

Does PyOpenGL work in general on your machine?

David

Ah, I have a repro on my own Linux box. I think I see what’s happening: this is a driver issue, in a way. Certain OpenGL drivers crash if you call a display list while a vertex buffer is still bound. The solution will be to unbind all vertex buffers before invoking the callback. I’ll get this fix in shortly.

David

Fixed, and tagged.

David

Excellent, it works now. Thanks.

so, to speak to simple minds, does this means that with this code I could, theoretically, show the graphic output of another running application, mplayer for example, into a panda texure card?

No, I don’t think so, this allows you to use OpenGL or DirectX calls in a Panda app.

Which allows you to do anything with OpenGL or DirectX that Panda doesn’t do.

No, only an OpenGL (or maybe DirectX) application, and only one for which you had access to the code and could rip out just the internal render loop (not all the wrapper stuff that opens a window and manages keyboard inputs and so on).

It’s not about integrating with a standalone application, it’s about integrating with a third-party whatsit-drawing library, or about taking control of the rendering and issuing your own exotic graphics calls directly.

David

Did you forget to add a function to clear the callback function? :wink:
Right now, it looks like I have to assign it to an empty function when I want to clear it.

Oops, that I did. Fixed. :slight_smile:

David

This seems like great news. Does it mean that Panda would now work with pyglet and any framework based on pyglet like PyMT for example?

Hmm, I’m not sure about that. I don’t know much about pyglet, but from a quick perusal, it looks like it might want to own the whole window and OpenGL context, just like Panda does. It might be possible still to integrate with Panda, but you’ll have two systems fighting over managing the same resources.

David

Yeah it seems you’re probably right :frowning: So, support for multi-touch systems seems like a feature that would benefit Panda since so many people are becoming increasingly active in this kind of interface. I had a try a few months ago but had no success (despite your valuable help) so I gave up due to limited time :frowning: I guess I should post this at the Feature Requests.

Thanx a lot