Controlling refresh rate or Mouse/keys from wxPython.

Hi all,
I am new to Panda3d and I am loving it. This is my first post as the forums have been really helpful in answering my questions.

I am building a network/graph viewer using wxPython and Panda3D and I already have a panda3d window embedded in a wxFrame inside wxPanel.

This is my question…
I see that even though my scene is static and unchanging, the program has relatively high CPU usage (40% or even more). This is because of calling taskMgr.step() periodically. It seems we are redrawing the scene even if nothing is changing.
However, as panda3d processes the mouse and keyboard in its client window area, I need to call taskMgr.step() to be responsive to mouse and key events…

Is there a way to tell panda whether to refresh the scene or not when taskMgr.step() is called? I would usually want taskMgr.step() to handle the mouse and keystrokes and return unless explicitly asked to refresh.

If not, is there a way to handle the mouse / keystrokes in wxPython so that I can decide outside panda3d when to call taskMgr.step(). I have tried to capture the wx events for mouse and keyboard for the wxPanel, but they don’t get called. I am guessing that the events go directly to the panda window which consumes them and they dont bubble up to the wxPython Panel. I see that there is a addPythonEventHandler on base.win… but its not clear to me what to pass it as arguments (probably because of my newbness rather than the lacking on part of the documentation).

Is there a way to make one of the above approaches work?
Much thanks…

I am also inserting the code to run my example…
NOTE: you would also need the square.egg model to be present in the same folder where you run this code… (the source for square.egg follows below the code)

import sys
import wx

#####################################################################
#####################################################################

# Panda module imports

#####################################################################
#####################################################################

from direct.showbase import DirectObject
from direct.gui.OnscreenText import OnscreenText
from direct.showbase.ShowBase import ShowBase
from direct.task.Task import Task

from panda3d.core import TextNode
from panda3d.core import AmbientLight,DirectionalLight,LightAttrib
from panda3d.core import Point3,Vec3,Vec4,BitMask32,Mat4
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue,CollisionRay

from pandac.PandaModules import WindowProperties


#####################################################################
#####################################################################

# Common Definitions

#####################################################################
#####################################################################


BLACK = Vec4(0,0,0,1)
WHITE = Vec4(1,1,1,1)
HIGHLIGHT = Vec4(0,1,1,1)
NxN = 8

#####################################################################
#####################################################################

# Methods and classes

#####################################################################
#####################################################################

def SquarePos(i):
    return Point3((i%NxN) - (NxN/2.0 - 0.5), int(i/NxN) - (NxN/2.0 - 0.5), 0)

#Helper function for determining wheter a square should be white or black
#The modulo operations (%) generate the every-other pattern of a chess-board
def SquareColor(i):
    if (i + ((i/NxN)%2))%2: return BLACK
    else: return WHITE

# Function to put instructions on the screen.
def addInstructions(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
                        pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)

# Function to put title on the screen.
def addTitle(text):
    return OnscreenText(text=text, style=1, fg=(1,1,1,1),
                        pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)


class PandaPanel(wx.Panel, DirectObject.DirectObject):
    _label          = 'Panda3D'
    def __init__(self, parent, network, *args, **kwargs):
        wx.Panel.__init__(self, parent, *args, **kwargs)

        self.Bind(wx.EVT_SIZE, self.OnResize)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_MOTION, self.OnMouseMove)

        self.base = ShowBase()
        self.network = network
        self.initialize()
        self.drawScene()
        wx.CallLater(30, self.timedFunc)
        self.count = 0

    def OnKeyDown(self, event):
        print 'on keypress'

    def OnMouseMove(self, event):
        print "on mouse move"

    def timedFunc(self, *a, **k):
        self.count += 1

        try:
            taskMgr.step()
        except:
            pass
        wx.CallLater(30, self.timedFunc)

    def initialize(self):
        assert self.GetHandle() != 0
        wp = WindowProperties()
        wp.setOrigin(0,0)
        w, h = self.GetParent().GetClientSizeTuple()
        wp.setSize(w, h)
        wp.setParentWindow(self.GetHandle())
        self.base.openDefaultWindow(props = wp, gsg = None)
        self.mouseMoveEventCount = 0

    def OnResize(self, event):
        frame_size = self.GetParent().GetClientSizeTuple()
        wp = WindowProperties()
        wp.setOrigin(0,0)
        w, h = self.GetParent().GetClientSizeTuple()
        wp.setParentWindow(self.GetHandle())
        wp.setSize(w, h)
        try:
            self.base.win.requestProperties(wp)
        except:
            from traceback import print_exc
            print 'error in requesting window properties'
            print_exc()
        self.Parent.Refresh()

    def drawScene(self):
        self.prepareGrid()

    def prepareGrid(self):
        self.title = addTitle("Panda3D Picking test")

        self.accept('escape', sys.exit)              #Escape quits
        self.base.disableMouse()
        camera.setPosHpr(0, -13.75, 6, 0, -25, 0)
        self.setupLights()

        self.picker = CollisionTraverser()            #Make a traverser
        self.pq     = CollisionHandlerQueue()         #Make a handler
        self.pickerNode = CollisionNode('mouseRay')
        self.pickerNP = camera.attachNewNode(self.pickerNode)
        self.pickerNode.setFromCollideMask(BitMask32.bit(1))
        self.pickerRay = CollisionRay()               #Make our ray
        self.pickerNode.addSolid(self.pickerRay)      #Add it to the collision node
        self.picker.addCollider(self.pickerNP, self.pq)

        self.squareRoot = render.attachNewNode("squareRoot")

        #For each square
        self.squares = [None for i in range(NxN * NxN)]
        for i in range(NxN*NxN):
            #Load, parent, color, and position the model (a single square polygon)
            self.squares[i] = self.base.loader.loadModel('square')
            self.squares[i].reparentTo(self.squareRoot)
            self.squares[i].setPos(SquarePos(i))
            self.squares[i].setColor(SquareColor(i))
            #Set the model itself to be collideable with the ray. If this model was
            #any more complex than a single polygon, you should set up a collision
            #sphere around it instead. But for single polygons this works fine.
            self.squares[i].find("**/polygon").node().setIntoCollideMask(
                BitMask32.bit(1))
            #Set a tag on the square's node so we can look up what square this is
            #later during the collision pass
            self.squares[i].find("**/polygon").node().setTag('square', str(i))

        self.mouseTask = taskMgr.add(self.mouseTask, 'mouseTask')
        self.hiSq = False

    def mouseTask(self, task):
        #This task deals with the highlighting and dragging based on the mouse

        #First, clear the current highlight
        if self.hiSq is not False:
            self.squares[self.hiSq].setColor(SquareColor(self.hiSq))
            self.hiSq = False

        #Check to see if we can access the mouse. We need it to do anything else
        if base.mouseWatcherNode.hasMouse():
            #get the mouse position
            mpos = base.mouseWatcherNode.getMouse()

            #Set the position of the ray based on the mouse position
            self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())

            #Do the actual collision pass (Do it only on the squares for
            #efficiency purposes)
            self.picker.traverse(self.squareRoot)
            if self.pq.getNumEntries() > 0:
                #if we have hit something, sort the hits so that the closest
                #is first, and highlight that node
                self.pq.sortEntries()
                i = int(self.pq.getEntry(0).getIntoNode().getTag('square'))
                #Set the highlight on the picked square
                self.squares[i].setColor(HIGHLIGHT)
                self.hiSq = i

        return Task.cont


    def setupLights(self):    #This function sets up some default lighting
        ambientLight = AmbientLight( "ambientLight" )
        ambientLight.setColor( Vec4(.8, .8, .8, 1) )
        directionalLight = DirectionalLight( "directionalLight" )
        directionalLight.setDirection( Vec3( 0, 45, -45 ) )
        directionalLight.setColor( Vec4( 0.2, 0.2, 0.2, 1 ) )
        self.base.render.setLight(render.attachNewNode( directionalLight ) )
        self.base.render.setLight(render.attachNewNode( ambientLight ) )


class PandaFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        title = kwargs.pop('title', '')
        wx.Frame.__init__(self, title=title, *args, **kwargs)
        self.Show(True)
        self.pandapanel = PandaPanel(self, *args, **kwargs)
        #self.pandapanel.initialize() # this is called from inside the panda panel init method

class PandaApp(wx.App):
    def __init__(self, *args, **kw):
        wx.App.__init__(self)
        self.frame = PandaFrame(None, wx.ID_ANY, title='Panda App', *args, **kw)
        self.frame.Bind(wx.EVT_CLOSE, self.quit)

    def onDestroy(self, event=None):
        self.Exit()
        pass

    def quit(self, event=None):
        self.onDestroy(event)
        try:
            base
        except NameError:
            sys.exit()
        base.userExit()

if __name__ == '__main__':
    app = PandaApp(size=(600, 300))
    app.MainLoop()

square.egg contents…

<CoordinateSystem> { Y-Up }
<Comment> {
  "Handmade quad"
}

<Group> polygon {
  <VertexPool> pPlaneShape1.verts {
    <Vertex> 1 {
      -0.5 0 -0.5
      <Normal> { 0 0 -1 }
      <UV> { 0 0 }
      <RGBA> { 1 1 1 1 }
    }
    <Vertex> 2 {
      -0.5 0 0.5
      <Normal> { 0 0 -1 }
      <UV> { 0 1 }
      <RGBA> { 1 1 1 1 }
    }
    <Vertex> 3 {
      0.5 0 -0.5
      <Normal> { 0 0 -1 }
      <UV> { 1 0 }
      <RGBA> { 1 1 1 1 }
    }
    <Vertex> 4 {
      0.5 0 0.5
      <Normal> { 0 0 -1 }
      <UV> { 1 1 }
      <RGBA> { 1 1 1 1 }
    }
  }
  <Polygon> {
    <Normal> { 0 0 -1 }
    <VertexRef> { 1 2 4 3 <Ref> { pPlaneShape1.verts } }
  }
}

After understanding my own question a little better and on doing some more search of the forums I found this thread…

[events without redraw)

A workaround to control rendering is to use

base.win.setActive(True) 
base.graphicsEngine.renderFrame() 
base.win.setActive(False)

Without base.win.setActive(True), taskMgr.step() does not render…

I ran into a new issue after solving the last one. So, after adding this code, I can control the rate of refreshes and the Cpu usage is well below 10% when idle.

base.win.setActive(True)
#base.graphicsEngine.renderFrame()
base.graphicsEngine.renderFrame()
base.win.setActive(False)

However, now, when I highlight an object, the highlight does not take effect immediately. I have checked that the color of my square to be highlighted has the correct highlight color before the call to base.graphicsEngine.renderFrame(). However, it seems that this call is not drawing the most recent scenegraph, but its drawing the last one.

Say, if I move mouse from background to highlight square 1, no highlight shows up. Now, if I move mouse to square 2, square 1 gets highlighted. Move to 3 highlights square 2 and so on.

I debugged the code to see if I was lagging in the update to the highlighted square… but thats not the case. base.graphicsEngine.renderFrame() is drawing the old scenegraph.

I can call the base.graphicsEngine.renderFrame() twice in a row to get the correct object highlighted, but is there a more efficient way to tell base.graphicsEngine.renderFrame() to draw the current scenegraph, rather than the old one?

New source code…

import sys
import wx

from direct.showbase import DirectObject
from direct.gui.OnscreenText import OnscreenText
from direct.showbase.ShowBase import ShowBase
from direct.task.Task import Task

from panda3d.core import TextNode
from panda3d.core import AmbientLight,DirectionalLight,LightAttrib
from panda3d.core import Point3,Vec3,Vec4,BitMask32,Mat4
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue,CollisionRay

from pandac.PandaModules import WindowProperties



BLACK = Vec4(0,0,0,1)
WHITE = Vec4(1,1,1,1)
HIGHLIGHT = Vec4(0.5,0.5,0.5,1)
NxN = 8


class Mouse(DirectObject.DirectObject):
    def __init__(self):
        self.accept('mouse1',self.printHello)
    def printHello(self):
        print 'Hello!'

def SquarePos(i):
    return Point3((i%NxN) - (NxN/2.0 - 0.5), int(i/NxN) - (NxN/2.0 - 0.5), 0)

#Helper function for determining wheter a square should be white or black
#The modulo operations (%) generate the every-other pattern of a chess-board
def SquareColor(i):
    if (i + ((i/NxN)%2))%2: return BLACK
    else: return WHITE

# Function to put instructions on the screen.
def addInstructions(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
                        pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)

# Function to put title on the screen.
def addTitle(text):
    return OnscreenText(text=text, style=1, fg=(1,1,1,1),
                        pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)


class PandaPanel(wx.Panel, DirectObject.DirectObject):
    _label          = 'Panda3D'
    def __init__(self, parent, network, *args, **kwargs):
        wx.Panel.__init__(self, parent, *args, **kwargs)

        self.Bind(wx.EVT_SIZE, self.OnResize)
        self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
        self.Bind(wx.EVT_MOTION, self.OnMouseMove)

        self.base = ShowBase()
        self.network = network
        self.initialize()
        self.drawScene()
        wx.CallLater(30, self.timedFunc)
        self.count = 0
        self.Refresh()
        self.mousePos = None

    def OnKeyDown(self, event):
        print 'on keypress'

    def OnMouseMove(self, event=None, *a, **k):
        print "on mouse move"

    def Refresh(self):
        self.refreshNow = True
        self.count = 0

    def timedFunc(self, *a, **k):
        try:
            if self.refreshNow is True:
                self.refreshNow = False
                base.win.setActive(True)
                #base.graphicsEngine.renderFrame()
                base.graphicsEngine.renderFrame()
                base.win.setActive(False)
            else:
                taskMgr.step()
        except:
            pass
        wx.CallLater(33, self.timedFunc)

    def initialize(self):
        assert self.GetHandle() != 0
        self.base.disableMouse()
        wp = WindowProperties()
        wp.setOrigin(0,0)
        w, h = self.GetParent().GetClientSizeTuple()
        wp.setSize(w, h)
        wp.setParentWindow(self.GetHandle())
        self.base.openDefaultWindow(props = wp, gsg = None)
        self.base.win.addPythonEventHandler(self.OnMouseMove, 'mouse1')

        #taskMgr.remove('igLoop')

    def OnResize(self, event):
        frame_size = self.GetParent().GetClientSizeTuple()
        wp = WindowProperties()
        wp.setOrigin(0,0)
        w, h = self.GetParent().GetClientSizeTuple()
        wp.setParentWindow(self.GetHandle())
        wp.setSize(w, h)
        try:
            self.base.win.requestProperties(wp)
        except:
            from traceback import print_exc
            print 'error in requesting window properties'
            print_exc()
        self.Parent.Refresh()

    def drawScene(self):
        self.prepareGrid()

    def prepareGrid(self):
        self.title = addTitle("Panda3D Picking test")

        self.accept('escape', sys.exit)              #Escape quits
        camera.setPosHpr(0, -13.75, 6, 0, -25, 0)
        self.setupLights()

        self.picker = CollisionTraverser()            #Make a traverser
        self.pq     = CollisionHandlerQueue()         #Make a handler
        self.pickerNode = CollisionNode('mouseRay')
        self.pickerNP = camera.attachNewNode(self.pickerNode)
        self.pickerNode.setFromCollideMask(BitMask32.bit(1))
        self.pickerRay = CollisionRay()               #Make our ray
        self.pickerNode.addSolid(self.pickerRay)      #Add it to the collision node
        self.picker.addCollider(self.pickerNP, self.pq)

        self.squareRoot = render.attachNewNode("squareRoot")

        #For each square
        self.squares = [None for i in range(NxN * NxN)]
        for i in range(NxN*NxN):
            #Load, parent, color, and position the model (a single square polygon)
            self.squares[i] = self.base.loader.loadModel('square')
            self.squares[i].reparentTo(self.squareRoot)
            self.squares[i].setPos(SquarePos(i))
            self.squares[i].setColor(SquareColor(i))
            #Set the model itself to be collideable with the ray. If this model was
            #any more complex than a single polygon, you should set up a collision
            #sphere around it instead. But for single polygons this works fine.
            self.squares[i].find("**/polygon").node().setIntoCollideMask(
                BitMask32.bit(1))
            #Set a tag on the square's node so we can look up what square this is
            #later during the collision pass
            self.squares[i].find("**/polygon").node().setTag('square', str(i))

        self.mouseTask = taskMgr.add(self.mouseTask, 'mouseTask')
        self.hiSq = False

    def mouseTask(self, task):
        #This task deals with the highlighting and dragging based on the mouse
        if base.mouseWatcherNode.hasMouse():
            #get the mouse position
            mpos = base.mouseWatcherNode.getMouse()
            if self.mousePos == mpos:
                pass
            else:
                # we have mouse and it moved...
                # save last object highlighed.
                oldhiSq = self.hiSq
                self.hiSq = False
                # find new object under mouse...
                self.pickerRay.setFromLens(base.camNode, mpos.getX(), mpos.getY())

                #Do the actual collision pass (Do it only on the squares for
                #efficiency purposes)
                self.picker.traverse(self.squareRoot)
                if self.pq.getNumEntries() > 0:
                    #if we have hit something, sort the hits so that the closest
                    #is first, and highlight that node
                    self.pq.sortEntries()
                    i = int(self.pq.getEntry(0).getIntoNode().getTag('square'))
                    #Set the highlight on the picked square
                    self.squares[i].setColor(HIGHLIGHT)
                    self.hiSq = i

                # if not same obj, refresh
                if self.hiSq is not oldhiSq:
                    if oldhiSq is not False:
                        # erase last highlighted square
                        self.squares[oldhiSq].setColor(SquareColor(oldhiSq))
                    self.Refresh()

        else:
            # if we dont have the mouse,
            # and something is highlighted, stop its highlight
            if self.hiSq is not False:
                self.squares[self.hiSq].setColor(SquareColor(self.hiSq))
                self.hiSq = False
                self.Refresh()

        return Task.cont


    def setupLights(self):    #This function sets up some default lighting
        ambientLight = AmbientLight( "ambientLight" )
        ambientLight.setColor( Vec4(.8, .8, .8, 1) )
        directionalLight = DirectionalLight( "directionalLight" )
        directionalLight.setDirection( Vec3( 0, 45, -45 ) )
        directionalLight.setColor( Vec4( 0.2, 0.2, 0.2, 1 ) )
        self.base.render.setLight(render.attachNewNode( directionalLight ) )
        self.base.render.setLight(render.attachNewNode( ambientLight ) )


class PandaFrame(wx.Frame):
    def __init__(self, *args, **kwargs):
        title = kwargs.pop('title', '')
        wx.Frame.__init__(self, title=title, *args, **kwargs)
        self.Show(True)
        self.pandapanel = PandaPanel(self, *args, **kwargs)
        #self.pandapanel.initialize() # this is called from inside the panda panel init method

class PandaApp(wx.App):
    def __init__(self, *args, **kw):
        wx.App.__init__(self)
        self.frame = PandaFrame(None, wx.ID_ANY, title='Panda App', *args, **kw)
        self.frame.Bind(wx.EVT_CLOSE, self.quit)

    def onDestroy(self, event=None):
        self.Exit()
        pass

    def quit(self, event=None):
        self.onDestroy(event)
        try:
            base
        except NameError:
            sys.exit()
        base.userExit()

if __name__ == '__main__':
    app = PandaApp(size=(600, 300))
    app.MainLoop()

Got it…

It seems that renderFrame draws into the back buffer and does not switch the buffers. We force switching using…

base.graphicsEngine.flipFrame()

Flip happens at the beginning of the next frame. Calling renderFrame twice would have sufficed, too.

Ah… that’s what was different. I had my own small renderer which flips at the end of a render pass and I assumed that it would be same here.

Just out of curiosity… is this the usual way game engines flip?

Different game engines have different opinions as to whether it is better to flip at the beginning or at the end of the frame; and in fact, Panda’s behavior here is configurable. Flipping at the beginning is just the default.

There are pros and cons to either approach, but the advantage to flipping at the beginning of the frame is that it allows for greater parallelization between your Python code and the GPU. Your code can start processing the next frame while the GPU is still rendering the previous frame; and this improves overall frame rate.

David