Using taskchains for high sampling rate motion capture

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)