Hi all, I am a Penn State undergrad doing a summer research project of designing a virtual hockey table for analysis of the human motor control system. I will be using raw input from two mice embedded in an air hockey mallet as a cheap, DIY motion capture. I want to sample the input data at something close to the 250 Hz of the mouse polling rate (for now, while I get sampling rate issues resolved, I am just using single mouse input w/ Panda’s default mouse watcher to handle mallet motion).
I found a similar topic on this forum, Threading Motion Capture at a High Rate., which suggested the use of creating additional taskchains as a kind of pseudo-threading. I made a taskchain consisting of one task which handles both mouse input and my own collision detection system. This task passes mallet position and, when a collision occurs, the calculated initial puck velocity to the tasks of the other taskchain. This other taskchain handles a onetime calculation of the position of the puck with respect to time and animates both mallet and puck accordingly. All of my code properly works when I have everything running in the default taskchain at 60 fps, but things go awry when I split my tasks between two taskchains.
The following is my code before creating taskchains:
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from pandac.PandaModules import OrthographicLens
import numpy as np
import time
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import WindowProperties, TextNode
import sys
class HockeyGame(ShowBase):
def __init__(self): # Define initialization of class HockeyGame
ShowBase.__init__(self)
self.physflag, self.collisionflag, self.counter = 0, 0, 0 # Initialize variables
base.setFrameRateMeter(True) # Display framerate
self.table = self.loader.loadModel("models/hockeytable1") # Load the table model.
self.table.reparentTo(self.render) # Reparent the table model to NodePath render.
self.puck = self.loader.loadModel("models/puck") # Load the puck model
self.puck.reparentTo(self.render) # Reparent the puck model to NodePath render.
self.puck.setScale(1, 1, 1) # Apply scale and position transforms on the puck model.
self.puck.setPos(0, 0, 0.1)
self.mallet = self.loader.loadModel("models/puck") # Load the puck model to serve as model for mallet.
self.mallet.reparentTo(self.render) # Reparent the mallet model to NodePath render.
self.mallet.setScale(1, 1, 1) # Apply scale and position transforms on the mallet model.
self.mallet.setPos(-7, 0, 0.1)
self.disableMouse() # Disables default of mouse control being used to move camera
camera.setPosHpr(0, 0, 2, 0, -90, 0) # Set the camera in a fixed position
props = WindowProperties() # Set cursor to be hidden in window
props.setCursorHidden(True)
base.win.requestProperties(props)
self.setBackgroundColor(0, 0, 0) # Set background color of scene graph to black
lens = OrthographicLens() # Set camera lens to orthographic (instead of perspective)
base.cam.node().setLens(lens)
lens.setFilmSize(16, 9) # Set ortho lens film size (cf FOV) to length units of hockey table model
taskMgr.add(self.malletmotion, "Mallet Motion") # Add task to handle motion of mallet
taskMgr.add(self.puckmotion, "Puck Motion") # Add task to handle puck motion calculation
taskMgr.add(self.puckanim, "Puck Animation") # Add task to handle puck animation
self.accept('escape', sys.exit, [0]) # Accepts input such that program ends when esc key pressed
self.deltaText = self.genLabelText("", -5) # Creates text for dx, dy by calling genLabelText method
self.finalposText = self.genLabelText(" ", -6)
self.lastMouseX, self.lastMouseY = self.mallet.getX(), self.mallet.getY() # Initialize vars for mouse pos from last frame
def malletmotion(self, task): # Define mallet motion task
w, h = self.win.getSize() # Get height, width of window
if base.mouseWatcherNode.hasMouse(): # Check that a mouse is connected
mpos = base.mouseWatcherNode.getMouse() # Get mouse pos data and assign to mpos
x, y = mpos.getX(), mpos.getY() # Get mouse pos in x, y (w/ 1,1 top right, -1,-1 bottom left of screen)
if self.lastMouseX is not None: # Check that mouse has been previously moved
dx, dy = x - self.lastMouseX, y - self.lastMouseY # Find difference in positions from last frame
else: # If mouse has not been previously moved, set change in position to zero
dx, dy = 0, 0
# Check that the mallet is touching or in puck and that a collision has not already been flagged
if ((self.puck.getY() - self.mallet.getY()) ** 2 + (self.puck.getX() - self.mallet.getX()) ** 2) ** 0.5 < 2.0 and self.collisionflag == 0:
self.collisionflag = 1 # Flag that a collision has occurred so further collision checks will not be done
print('mallet velx: ' + str((dx * 8.0) / globalClock.getDt())) # Display mallet vel in x at impact
print('mallet vely: ' + str((dy * 4.5) / globalClock.getDt())) # Display mallet vel in y at impact
malletvelx = (dx * 8.0) / globalClock.getDt() # Calculate mallet vel in x
malletvely = (dy * 4.5) / globalClock.getDt() # Calculate mallet vel in y
# Calculate unit vector for line of collision in x, y
normalx = (self.mallet.getX()-self.puck.getX())/((self.mallet.getX()-self.puck.getX())**2 + (self.mallet.getY()-self.puck.getY())**2)**0.5
normaly = (self.mallet.getY()-self.puck.getY())/((self.mallet.getX()-self.puck.getX())**2 + (self.mallet.getY()-self.puck.getY())**2)**0.5
print('normalx: ' + str(normalx)) # Display normal unit vector in x
print('normaly: ' + str(normaly)) # Display normal unit vector in y
self.velx = (normalx*malletvelx + normaly*malletvely)*normalx # Calculate initial velocity of puck in x
self.vely = (normalx*malletvelx + normaly*malletvely)*normaly # Calculate initial velocity of puck in y
print('puck velx: ' + str(self.velx))
print('puck vely: ' + str(self.vely))
self.lastMouseX, self.lastMouseY = x, y # Store current pos for next frame
else: # If mouse input not detected (outside window), keep x, y at mallet pos, let dx, dy be zero
x, y, dx, dy = self.mallet.getX(), self.mallet.getY(), 0, 0
self.deltaText.setText("Delta: {0}, {1}".format(int(dx * w), int(dy * h))) # Display dx, dy in pixels/frame
self.mallet.setX(x * 8) # Set mallet to mouse pos in x
self.mallet.setY(y * 4.5) # Set mallet to mouse pos in y
return Task.cont # Task continues infinitely
def puckmotion(self, task): # Define puck motion task
if self.collisionflag == 1 and self.physflag == 0: # Run calculations only after collision and only once
# Initialize physical properties, initial conditions of table, puck, etc., initialize variables for time
g, mu, rp, cor = 9.81, 0.4, 1, 0.9 # grav, friction coeff, puck radius (m), puck/wall restitution coeff
tbldimx, tbldimy = 8.0, 4.5 # half-length table dim in x, y
initposx, initposy = 0.0, 0.0 # initial puck pos in x, y (m)
velx, vely = self.velx, self.vely
dt = 0.001 # Set constant time step (s)
maxiter = round(2.0 * (max(abs(velx), abs(vely)) / (g * mu)) / dt) # Time to stop if no walls multiplied by coeff buffer
timeint = range(0, int(maxiter + 1)) # Time interval to theoretical max time
posx, posy = [0] * int(maxiter + 1), [0] * int(maxiter + 1) # Preallocate space for pos in x, y
posx[0], posy[0] = initposx, initposy
# Set acceleration as opp dir to vel * friction coeff * gravity
aclx, acly = ((-mu*g)/(velx**2 + vely**2)**0.5)*velx, ((-mu*g)/(velx**2 + vely**2)**0.5)*vely
self.physflag = 1 # Flag this task as complete
for k in timeint: # Iterate pos, vel, acl over timeint (Euler method)
if k > 0: # No change in puck pos for first iteration
posx[k], posy[k] = posx[(k - 1)] + velx * dt, posy[(k - 1)] + vely * dt # Calculate pos in x, y
if posx[k] < (rp - tbldimx): # Collision w/ left wall
posx[k] = (rp - tbldimx) - (posx[k] - (rp - tbldimx)) # Set dist into wall as new pos right of wall
velx, vely, aclx = -cor * velx, cor * vely, -aclx # vels * restitution coeff, change dir of vel, acl in x
elif posx[k] > (tbldimx - rp): # Collision w/ right wall
posx[k] = (tbldimx - rp) - (posx[k] - (tbldimx - rp)) # Set dist into wall as new pos left of wall
velx, vely, aclx = -cor * velx, cor * vely, -aclx # vels * restitution coeff, change dir of vel, acl in x
elif posy[k] > (tbldimy - rp): # Collision w/ top wall
posy[k] = (tbldimy - rp) - (posy[k] - (tbldimy - rp)) # Set dist into wall as new pos below wall
vely, velx, acly = -cor * vely, cor * velx, -acly # vels * restitution coeff, change dir of vel, acl in y
elif posy[k] < (rp - tbldimy): # Collision w/ bottom wall
posy[k] = (rp - tbldimy) - (posy[k] - (rp - tbldimy)) # Set dist into wall as new pos above wall
vely, velx, acly = -cor * vely, cor * velx, -acly # vels * restitution coeff, change dir of vel, acl in y
if np.sign(velx) == np.sign(velx + aclx * dt): # Check xvel does not cross zero outside collisions
velx, vely = velx + aclx * dt, vely + acly * dt # Calculate vel in x, y for next iter
else: # Copy and create values with wider scope, start timer for animation, break for loop
self.counter, self.posx, self.posy, self.animtime = k, posx, posy, time.time()
break
return Task.cont # Task continues infinitely
def puckanim(self, task): # Define puck animation task
if self.physflag == 1: # Check that puck motion calculations are complete
if round((time.time() - self.animtime), 3) * 1000 < self.counter: # Animate til zero velocity reached
self.puck.setX(self.posx[int(round((time.time() - self.animtime), 3) * 1000)])
self.puck.setY(self.posy[int(round((time.time() - self.animtime), 3) * 1000)])
else: # Display final pos in x, y once animation complete
self.finalposText.setText("Final Pos: {0}, {1}".format(round(self.puck.getX(), 3), round(self.puck.getY(), 3))) # Display final puck pos
return Task.cont # Task continues infinitely
def genLabelText(self, text, i): # Define method to generate onscreen text with uniform settings
text = OnscreenText(text = text, pos = (-1.3, .5-.05*i), fg=(0,1,0,1), align = TextNode.ALeft, scale = .05)
return text
app = HockeyGame() # Create instance of HockeyGame class
app.run() # Run class instantiation
This is my code after I created taskchains:
from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from pandac.PandaModules import OrthographicLens
import numpy as np
import time
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import WindowProperties, TextNode
import sys
class HockeyGame(ShowBase):
def __init__(self): # Define initialization of class HockeyGame
ShowBase.__init__(self)
self.physflag, self.collisionflag, self.counter = 0, 0, 0 # Initialize variables
base.setFrameRateMeter(True) # Display framerate
self.table = self.loader.loadModel("models/hockeytable1") # Load the table model.
self.table.reparentTo(self.render) # Reparent the table model to NodePath render.
self.puck = self.loader.loadModel("models/puck") # Load the puck model
self.puck.reparentTo(self.render) # Reparent the puck model to NodePath render.
self.puck.setScale(1, 1, 1) # Apply scale and position transforms on the puck model.
self.puck.setPos(0, 0, 0.1)
self.mallet = self.loader.loadModel("models/puck") # Load the puck model to serve as model for mallet.
self.mallet.reparentTo(self.render) # Reparent the mallet model to NodePath render.
self.mallet.setScale(1, 1, 1) # Apply scale and position transforms on the mallet model.
self.mallet.setPos(-7, 0, 0.1)
self.disableMouse() # Disables default of mouse control being used to move camera
camera.setPosHpr(0, 0, 2, 0, -90, 0) # Set the camera in a fixed position
props = WindowProperties() # Set cursor to be hidden in window
props.setCursorHidden(True)
base.win.requestProperties(props)
self.setBackgroundColor(0, 0, 0) # Set background color of scene graph to black
lens = OrthographicLens() # Set camera lens to orthographic (instead of perspective)
base.cam.node().setLens(lens)
lens.setFilmSize(16, 9) # Set ortho lens film size (cf FOV) to length units of hockey table model
taskMgr.setupTaskChain('outputChain', numThreads = 0, tickClock = False, frameBudget= -1)
taskMgr.add(self.malletmotion, "Mallet Motion", taskChain = 'outputChain') # Add task to handle motion of mallet
taskMgr.add(self.puckmotion, "Puck Motion", taskChain = 'outputChain') # Add task to handle puck motion calculation
taskMgr.add(self.puckanim, "Puck Animation", taskChain = 'outputChain') # Add task to handle puck animation
taskMgr.setupTaskChain('inputChain', numThreads = 1, tickClock = True, frameBudget = 0.005)
taskMgr.add(self.inputandcollision, "Input and Collision", taskChain= 'inputChain')
self.accept('escape', sys.exit, [0]) # Accepts input such that program ends when esc key pressed
self.deltaText = self.genLabelText("", -5) # Creates text for dx, dy by calling genLabelText method
self.finalposText = self.genLabelText(" ", -6)
self.lastMouseX, self.lastMouseY = self.mallet.getX(), self.mallet.getY() # Initialize vars for mouse pos from last frame
self.x, self.y = self.mallet.getX()/8.0, self.mallet.getY()/4.5
def inputandcollision(self, task):
if base.mouseWatcherNode.hasMouse(): # Check that a mouse is connected
mpos = base.mouseWatcherNode.getMouse() # Get mouse pos data and assign to mpos
self.x, self.y = mpos.getX(), mpos.getY() # Get mouse pos in x, y (w/ 1,1 top right, -1,-1 bottom left of screen)
if self.lastMouseX is not None: # Check that mouse has been previously moved
dx, dy = self.x - self.lastMouseX, self.y - self.lastMouseY # Find difference in positions from last frame
else: # If mouse has not been previously moved, set change in position to zero
dx, dy = 0, 0
# Check that the mallet is touching or in puck and that a collision has not already been flagged
if ((self.puck.getY() - (self.y * 4.5)) ** 2 + (self.puck.getX() - (self.x * 8.0)) ** 2) ** 0.5 < 2.0 and self.collisionflag == 0:
malletvelx = (dx * 8.0) / globalClock.getDt() # Calculate mallet vel in x
malletvely = (dy * 4.5) / globalClock.getDt() # Calculate mallet vel in y
# Calculate unit vector for line of collision in x, y
normalx = (self.mallet.getX()-self.puck.getX())/((self.mallet.getX()-self.puck.getX())**2 + (self.mallet.getY()-self.puck.getY())**2)**0.5
normaly = (self.mallet.getY()-self.puck.getY())/((self.mallet.getX()-self.puck.getX())**2 + (self.mallet.getY()-self.puck.getY())**2)**0.5
self.velx = (normalx*malletvelx + normaly*malletvely)*normalx # Calculate initial velocity of puck in x
self.vely = (normalx*malletvelx + normaly*malletvely)*normaly # Calculate initial velocity of puck in y
self.collisionflag = 1 # Flag that a collision has occurred so further collision checks will not be done
dt = globalClock.getDt()
print(dt)
self.lastMouseX, self.lastMouseY = self.x, self.y # Store current pos for next frame
else: # If mouse input not detected (outside window), keep x, y at mallet pos, let dx, dy be zero
self.x, self.y, dx, dy = self.mallet.getX(), self.mallet.getY(), 0, 0
return Task.cont
def malletmotion(self, task): # Define mallet motion task
self.mallet.setX(self.x * 8) # Set mallet to mouse pos in x
self.mallet.setY(self.y * 4.5) # Set mallet to mouse pos in y
return Task.cont # Task continues infinitely
def puckmotion(self, task): # Define puck motion task
if self.collisionflag == 1 and self.physflag == 0: # Run calculations only after collision and only once
# Initialize physical properties, initial conditions of table, puck, etc., initialize variables for time
g, mu, rp, cor = 9.81, 0.4, 1, 0.9 # grav, friction coeff, puck radius (m), puck/wall restitution coeff
tbldimx, tbldimy = 8.0, 4.5 # half-length table dim in x, y
initposx, initposy = 0.0, 0.0 # initial puck pos in x, y (m)
velx, vely = self.velx, self.vely
dt = 0.001 # Set constant time step (s)
maxiter = round(
2.0 * (max(abs(velx), abs(vely)) / (g * mu)) / dt) # Time to stop if no walls multiplied by coeff buffer
timeint = range(0, int(maxiter + 1)) # Time interval to theoretical max time
posx, posy = [0] * int(maxiter + 1), [0] * int(maxiter + 1) # Preallocate space for pos in x, y
posx[0], posy[0] = initposx, initposy
# Set acceleration as opp dir to vel * friction coeff * gravity
aclx, acly = ((-mu*g)/(velx**2 + vely**2)**0.5)*velx, ((-mu*g)/(velx**2 + vely**2)**0.5)*vely
self.physflag = 1 # Flag this task as complete
for k in timeint: # Iterate pos, vel, acl over timeint (Euler method)
if k > 0: # No change in puck pos for first iteration
posx[k], posy[k] = posx[(k - 1)] + velx * dt, posy[(k - 1)] + vely * dt # Calculate pos in x, y
if posx[k] < (rp - tbldimx): # Collision w/ left wall
posx[k] = (rp - tbldimx) - (posx[k] - (rp - tbldimx)) # Set dist into wall as new pos right of wall
velx, vely, aclx = -cor * velx, cor * vely, -aclx # vels * restitution coeff, change dir of vel, acl in x
elif posx[k] > (tbldimx - rp): # Collision w/ right wall
posx[k] = (tbldimx - rp) - (posx[k] - (tbldimx - rp)) # Set dist into wall as new pos left of wall
velx, vely, aclx = -cor * velx, cor * vely, -aclx # vels * restitution coeff, change dir of vel, acl in x
elif posy[k] > (tbldimy - rp): # Collision w/ top wall
posy[k] = (tbldimy - rp) - (posy[k] - (tbldimy - rp)) # Set dist into wall as new pos below wall
vely, velx, acly = -cor * vely, cor * velx, -acly # vels * restitution coeff, change dir of vel, acl in y
elif posy[k] < (rp - tbldimy): # Collision w/ bottom wall
posy[k] = (rp - tbldimy) - (posy[k] - (rp - tbldimy)) # Set dist into wall as new pos above wall
vely, velx, acly = -cor * vely, cor * velx, -acly # vels * restitution coeff, change dir of vel, acl in y
if np.sign(velx) == np.sign(velx + aclx * dt): # Check xvel does not cross zero outside collisions
velx, vely = velx + aclx * dt, vely + acly * dt # Calculate vel in x, y for next iter
else: # Copy and create values with wider scope, start timer for animation, break for loop
self.counter, self.posx, self.posy, self.animtime = k, posx, posy, time.time()
break
return Task.cont # Task continues infinitely
def puckanim(self, task): # Define puck animation task
if self.physflag == 1: # Check that puck motion calculations are complete
if round((time.time() - self.animtime), 3) * 1000 < self.counter: # Animate til zero velocity reached
self.puck.setX(self.posx[int(round((time.time() - self.animtime), 3) * 1000)])
self.puck.setY(self.posy[int(round((time.time() - self.animtime), 3) * 1000)])
else: # Display final pos in x, y once animation complete
self.finalposText.setText("Final Pos: {0}, {1}".format(round(self.puck.getX(), 3), round(self.puck.getY(), 3))) # Display final puck pos
return Task.cont # Task continues infinitely
def genLabelText(self, text, i): # Define method to generate onscreen text with uniform settings
text = OnscreenText(text = text, pos = (-1.3, .5-.05*i), fg=(0,1,0,1), align = TextNode.ALeft, scale = .05)
return text
app = HockeyGame() # Create instance of HockeyGame class
app.run() # Run class instantiation
It seems both tasks run arbitrarily fast (the framerate meter reads a ridiculously high number). The mallet moves smoothly, but as it gets close to the puck, the screen freezes for a second and then the puck animates but with an initial velocity vector much faster than the mallet’s velocity.
I’ve attached a zip file containing the egg files and textures such that anyone here has everything they need to test out my code. Please note this script also requires NumPy.
art assets.zip (983 KB)