"""DIRECT Animation Control Panel"""
__all__ = ['AnimPanel', 'ActorControl']
### SEE END OF FILE FOR EXAMPLE USEAGE ###
# Import Tkinter, Pmw, and the floater code from this directory tree.
from panda3d.core import Filename, getModelPath, ClockObject
from direct.tkwidgets.AppShell import AppShell
from direct.task import Task
from direct.task.TaskManagerGlobal import taskMgr
from tkinter.simpledialog import askfloat
from tkinter.filedialog import askopenfilename
import Pmw
import os
import tkinter as tk
FRAMES = 0
SECONDS = 1
[docs]class AnimPanel(AppShell):
# Override class variables
appname = 'Anim Panel'
frameWidth = 675
frameHeight = 250
usecommandarea = 0
usestatusarea = 0
index = 0
[docs] def __init__(self, aList = [], parent = None, session = None, **kw):
INITOPT = Pmw.INITOPT
if isinstance(aList, (list, tuple)):
kw['actorList'] = aList
else:
kw['actorList'] = [aList]
optiondefs = (
('title', self.appname, None),
('actorList', [], None),
('Actor_label_width', 12, None),
)
self.defineoptions(kw, optiondefs)
# direct session that spawned me, if any, used
# for certain interactions with the session such
# as being able to see selected objects/actors
self.session = session
self.frameHeight = 60 + (50 * len(self['actorList']))
self.playList = []
self.id = 'AnimPanel_%d' % AnimPanel.index
AnimPanel.index += 1
# current index used for creating new actor controls
self.actorControlIndex = 0
# Initialize the superclass
AppShell.__init__(self)
# Execute option callbacks
self.initialiseoptions(AnimPanel)
# We need to know when AnimPanel is closed
self.destroyCallBack = None
[docs] def createInterface(self):
# Handle to the toplevels interior
interior = self.interior()
menuBar = self.menuBar
menuBar.addmenu('AnimPanel', 'Anim Panel Operations')
# Actor control status
menuBar.addcascademenu('AnimPanel', 'Control Status',
'Enable/disable actor control panels')
menuBar.addmenuitem('Control Status', 'command',
'Enable all actor controls',
label = 'Enable all',
command = self.enableActorControls)
menuBar.addmenuitem('Control Status', 'command',
'Disable all actor controls',
label = 'Disable all',
command = self.disableActorControls)
# Frame Slider units
menuBar.addcascademenu('AnimPanel', 'Display Units',
'Select display units')
menuBar.addmenuitem('Display Units', 'command',
'Display frame counts', label = 'Frame count',
command = self.displayFrameCounts)
menuBar.addmenuitem('Display Units', 'command',
'Display seconds', label = 'Seconds',
command = self.displaySeconds)
# Reset all actor controls
menuBar.addmenuitem('AnimPanel', 'command',
'Set actor controls to t = 0.0',
label = 'Jump all to zero',
command = self.resetAllToZero)
menuBar.addmenuitem('AnimPanel', 'command',
'Set Actor controls to end time',
label = 'Jump all to end time',
command = self.resetAllToEnd)
# Add some buttons to update all Actor Controls
self.fToggleAll = 1
b = self.createcomponent(
'toggleEnableButton', (), None,
tk.Button, (self.menuFrame,),
text = 'Toggle Enable',
command = self.toggleAllControls)
b.pack(side = tk.RIGHT, expand = 0)
b = self.createcomponent(
'showSecondsButton', (), None,
tk.Button, (self.menuFrame,),
text = 'Show Seconds',
command = self.displaySeconds)
b.pack(side = tk.RIGHT, expand = 0)
b = self.createcomponent(
'showFramesButton', (), None,
tk.Button, (self.menuFrame,),
text = 'Show Frames',
command = self.displayFrameCounts)
b.pack(side = tk.RIGHT, expand = 0)
self.actorFrame = None
self.createActorControls()
# Create a frame to hold the playback controls
controlFrame = tk.Frame(interior)
self.toStartButton = self.createcomponent(
'toStart', (), None,
tk.Button, (controlFrame,),
text = '<<',
width = 4,
command = self.resetAllToZero)
self.toStartButton.pack(side = tk.LEFT, expand = 1, fill = tk.X)
self.toPreviousFrameButton = self.createcomponent(
'toPreviousFrame', (), None,
tk.Button, (controlFrame,),
text = '<',
width = 4,
command = self.previousFrame)
self.toPreviousFrameButton.pack(side = tk.LEFT, expand = 1, fill = tk.X)
self.playButton = self.createcomponent(
'playButton', (), None,
tk.Button, (controlFrame,),
text = 'Play', width = 8,
command = self.playActorControls)
self.playButton.pack(side = tk.LEFT, expand = 1, fill = tk.X)
self.stopButton = self.createcomponent(
'stopButton', (), None,
tk.Button, (controlFrame,),
text = 'Stop', width = 8,
command = self.stopActorControls)
self.stopButton.pack(side = tk.LEFT, expand = 1, fill = tk.X)
self.toNextFrameButton = self.createcomponent(
'toNextFrame', (), None,
tk.Button, (controlFrame,),
text = '>',
width = 4,
command = self.nextFrame)
self.toNextFrameButton.pack(side = tk.LEFT, expand = 1, fill = tk.X)
self.toEndButton = self.createcomponent(
'toEnd', (), None,
tk.Button, (controlFrame,),
text = '>>',
width = 4,
command = self.resetAllToEnd)
self.toEndButton.pack(side = tk.LEFT, expand = 1, fill = tk.X)
self.loopVar = tk.IntVar()
self.loopVar.set(0)
self.loopButton = self.createcomponent(
'loopButton', (), None,
tk.Checkbutton, (controlFrame,),
text = 'Loop', width = 8,
variable = self.loopVar)
self.loopButton.pack(side = tk.LEFT, expand = 1, fill = tk.X)
# add actors and animations, only allowed if a direct
# session has been specified since these currently require
# interaction with selected objects
if self.session:
menuBar.addmenuitem('File', 'command',
'Set currently selected group of objects as actors to animate.',
label = 'Set Actors',
command = self.setActors)
menuBar.addmenuitem('File', 'command',
'Load animation file',
label = 'Load Anim',
command = self.loadAnim)
controlFrame.pack(fill = tk.X)
[docs] def createActorControls(self):
# Create a frame to hold all the actor controls
self.actorFrame = tk.Frame(self.interior())
# Create a control for each actor
self.actorControlList = []
for actor in self['actorList']:
anims = actor.getAnimNames()
print("actor animnames: %s"%anims)
topAnims = []
if 'neutral' in anims:
i = anims.index('neutral')
del anims[i]
topAnims.append('neutral')
if 'walk' in anims:
i = anims.index('walk')
del anims[i]
topAnims.append('walk')
if 'run' in anims:
i = anims.index('run')
del anims[i]
topAnims.append('run')
anims.sort()
anims = topAnims + anims
if len(anims) == 0:
# no animations set for this actor, don't
# display the control panel
continue
# currComponents = self.components()
# if 'actorControl%d' % index in currComponents:
# self.destroycomponent('actorControl%d' % index)
# ac = self.component('actorControl%d' % index)
# if ac is None:
ac = self.createcomponent(
'actorControl%d' % self.actorControlIndex, (), 'Actor',
ActorControl, (self.actorFrame,),
animPanel = self,
text = actor.getName(),
animList = anims,
actor = actor)
ac.pack(expand = 1, fill = tk.X)
self.actorControlList.append(ac)
self.actorControlIndex = self.actorControlIndex + 1
# Now pack the actor frame
self.actorFrame.pack(expand = 1, fill = tk.BOTH)
[docs] def clearActorControls(self):
if self.actorFrame:
self.actorFrame.forget()
self.actorFrame.destroy()
self.actorFrame = None
[docs] def setActors(self):
self.stopActorControls()
actors = self.session.getSelectedActors()
# make sure selected objects are actors, if not don't
# use?
aList = []
for currActor in actors:
aList.append(currActor)
self['actorList'] = aList
self.clearActorControls()
self.createActorControls()
[docs] def loadAnim(self):
# bring up file open box to allow selection of an
# animation file
animFilename = askopenfilename(
defaultextension = '.mb',
filetypes = (('Maya Models', '*.mb'),
('All files', '*')),
initialdir = '/i/beta',
title = 'Load Animation',
parent = self.component('hull')
)
if not animFilename or animFilename == 'None':
# no file selected, canceled
return
# add directory where animation was loaded from to the
# current model path so any further searches for the file
# can find it
fileDirName = os.path.dirname(animFilename)
fileBaseName = os.path.basename(animFilename)
fileBaseNameBase = os.path.splitext(fileBaseName)[0]
fileDirNameFN = Filename(fileDirName)
fileDirNameFN.makeCanonical()
getModelPath().prependDirectory(fileDirNameFN)
for currActor in self['actorList']:
# replace all currently loaded anims with specified one
# currActor.unloadAnims(None, None, None)
currActor.loadAnims({fileBaseNameBase:fileBaseNameBase})
self.clearActorControls()
self.createActorControls()
[docs] def playActorControls(self):
self.stopActorControls()
self.lastT = ClockObject.getGlobalClock().getFrameTime()
self.playList = self.actorControlList[:]
taskMgr.add(self.play, self.id + '_UpdateTask')
[docs] def play(self, task):
if not self.playList:
return Task.done
fLoop = self.loopVar.get()
currT = ClockObject.getGlobalClock().getFrameTime()
deltaT = currT - self.lastT
self.lastT = currT
for actorControl in self.playList:
# scale time by play rate value
actorControl.play(deltaT * actorControl.playRate, fLoop)
return Task.cont
[docs] def stopActorControls(self):
taskMgr.remove(self.id + '_UpdateTask')
[docs] def getActorControlAt(self, index):
return self.actorControlList[index]
[docs] def enableActorControlAt(self, index):
self.getActorControlAt(index).enableControl()
[docs] def toggleAllControls(self):
if self.fToggleAll:
self.disableActorControls()
else:
self.enableActorControls()
self.fToggleAll = 1 - self.fToggleAll
[docs] def enableActorControls(self):
for actorControl in self.actorControlList:
actorControl.enableControl()
[docs] def disableActorControls(self):
for actorControl in self.actorControlList:
actorControl.disableControl()
[docs] def disableActorControlAt(self, index):
self.getActorControlAt(index).disableControl()
[docs] def displayFrameCounts(self):
for actorControl in self.actorControlList:
actorControl.displayFrameCounts()
[docs] def displaySeconds(self):
for actorControl in self.actorControlList:
actorControl.displaySeconds()
[docs] def resetAllToZero(self):
for actorControl in self.actorControlList:
actorControl.resetToZero()
[docs] def resetAllToEnd(self):
for actorControl in self.actorControlList:
actorControl.resetToEnd()
[docs] def nextFrame(self):
for actorControl in self.actorControlList:
actorControl.nextFrame()
[docs] def previousFrame(self):
for actorControl in self.actorControlList:
actorControl.previousFrame()
[docs] def setDestroyCallBack(self, callBack):
self.destroyCallBack = callBack
[docs] def destroy(self):
# First clean up
taskMgr.remove(self.id + '_UpdateTask')
if self.destroyCallBack is not None:
self.destroyCallBack()
self.destroyCallBack = None
AppShell.destroy(self)
[docs]class ActorControl(Pmw.MegaWidget):
[docs] def __init__(self, parent = None, **kw):
INITOPT = Pmw.INITOPT
DEFAULT_FONT = (('MS', 'Sans', 'Serif'), 12, 'bold')
DEFAULT_ANIMS = ('neutral', 'run', 'walk')
animList = kw.get('animList', DEFAULT_ANIMS)
if len(animList) > 0:
initActive = animList[0]
else:
initActive = DEFAULT_ANIMS[0]
optiondefs = (
('text', 'Actor', self._updateLabelText),
('animPanel', None, None),
('actor', None, None),
('animList', DEFAULT_ANIMS, None),
('active', initActive, None),
('sLabel_width', 5, None),
('sLabel_font', DEFAULT_FONT, None),
)
self.defineoptions(kw, optiondefs)
# Initialize the superclass
Pmw.MegaWidget.__init__(self, parent)
# Handle to the toplevels hull
interior = self.interior()
interior.configure(relief = tk.RAISED, bd = 2)
# Instance variables
self.fps = 24
self.offset = 0.0
self.maxSeconds = 1.0
self.currT = 0.0
self.fScaleCommand = 0
self.fOneShot = 0
# Create component widgets
self._label = self.createcomponent(
'label', (), None,
tk.Menubutton, (interior,),
font=('MSSansSerif', 14, 'bold'),
relief = tk.RAISED, bd = 1,
activebackground = '#909090',
text = self['text'])
# Top level menu
labelMenu = tk.Menu(self._label, tearoff = 0)
# Menu to select display mode
self.unitsVar = tk.IntVar()
self.unitsVar.set(FRAMES)
displayMenu = tk.Menu(labelMenu, tearoff = 0)
displayMenu.add_radiobutton(label = 'Frame count',
value = FRAMES,
variable = self.unitsVar,
command = self.updateDisplay)
displayMenu.add_radiobutton(label = 'Seconds',
value = SECONDS,
variable = self.unitsVar,
command = self.updateDisplay)
# Items for top level menu
labelMenu.add_cascade(label = 'Display Units', menu = displayMenu)
# labelMenu.add_command(label = 'Set Offset', command = self.setOffset)
labelMenu.add_command(label = 'Jump To Zero',
command = self.resetToZero)
labelMenu.add_command(label = 'Jump To End Time',
command = self.resetToEnd)
# Now associate menu with menubutton
self._label['menu'] = labelMenu
self._label.pack(side = tk.LEFT, fill = tk.X)
# Combo box to select current animation
self.animMenu = self.createcomponent(
'animMenu', (), None,
Pmw.ComboBox, (interior,),
labelpos = tk.W, label_text = 'Anim:',
entry_width = 12, selectioncommand = self.selectAnimNamed,
scrolledlist_items = self['animList'])
self.animMenu.selectitem(self['active'])
self.animMenu.pack(side = 'left', padx = 5, expand = 0)
# Combo box to select frame rate
playRateList = ['1/24.0', '0.1', '0.5', '1.0', '2.0', '5.0', '10.0']
playRate = '%0.1f' % self['actor'].getPlayRate(self['active'])
if playRate not in playRateList:
playRateList.append(playRate)
playRateList.sort(key=lambda s:eval(s))
playRateMenu = self.createcomponent(
'playRateMenu', (), None,
Pmw.ComboBox, (interior,),
labelpos = tk.W, label_text = 'Play Rate:',
entry_width = 4, selectioncommand = self.setPlayRate,
scrolledlist_items = playRateList)
playRateMenu.selectitem(playRate)
playRateMenu.pack(side = tk.LEFT, padx = 5, expand = 0)
# Scale to control animation
frameFrame = tk.Frame(interior, relief = tk.SUNKEN, bd = 1)
self.minLabel = self.createcomponent(
'minLabel', (), 'sLabel',
tk.Label, (frameFrame,),
text = 0)
self.minLabel.pack(side = tk.LEFT)
self.frameControl = self.createcomponent(
'scale', (), None,
tk.Scale, (frameFrame,),
from_ = 0, to = 24, resolution = 1.0,
command = self.goTo,
orient = tk.HORIZONTAL, showvalue = 1)
self.frameControl.pack(side = tk.LEFT, expand = 1)
self.frameControl.bind('<Button-1>', self.__onPress)
self.frameControl.bind('<ButtonRelease-1>', self.__onRelease)
self.maxLabel = self.createcomponent(
'maxLabel', (), 'sLabel',
tk.Label, (frameFrame,),
text = 24)
self.maxLabel.pack(side = tk.LEFT)
frameFrame.pack(side = tk.LEFT, expand = 1, fill = tk.X)
# Checkbutton to enable/disable control
self.frameActiveVar = tk.IntVar()
self.frameActiveVar.set(1)
frameActive = self.createcomponent(
'checkbutton', (), None,
tk.Checkbutton, (interior,),
variable = self.frameActiveVar)
frameActive.pack(side = tk.LEFT, expand = 1)
# Execute option callbacks
self.initialiseoptions(ActorControl)
self.playRate = 1.0
self.updateDisplay()
def _updateLabelText(self):
self._label['text'] = self['text']
[docs] def updateDisplay(self):
actor = self['actor']
active = self['active']
self.fps = actor.getFrameRate(active)
if self.fps is None:
# there was probably a problem loading the
# active animation, set default anim properties
print("unable to get animation fps, zeroing out animation info")
self.fps = 24
self.duration = 0
self.maxFrame = 0
self.maxSeconds = 0
else:
self.duration = actor.getDuration(active)
self.maxFrame = actor.getNumFrames(active) - 1
self.maxSeconds = self.offset + self.duration
# switch between showing frame counts and seconds
if self.unitsVar.get() == FRAMES:
# these are approximate due to discrete frame size
fromFrame = 0
toFrame = self.maxFrame
self.minLabel['text'] = fromFrame
self.maxLabel['text'] = toFrame
self.frameControl.configure(from_ = fromFrame,
to = toFrame,
resolution = 1.0)
else:
self.minLabel['text'] = '0.0'
self.maxLabel['text'] = "%.2f" % self.duration
self.frameControl.configure(from_ = 0.0,
to = self.duration,
resolution = 0.01)
def __onPress(self, event):
# Enable slider command
self.fScaleCommand = 1
def __onRelease(self, event):
# Disable slider command
self.fScaleCommand = 0
[docs] def selectAnimNamed(self, name):
# Update active anim
self['active'] = name
# Reset play rate
self.component('playRateMenu').selectitem('1.0')
self.setPlayRate('1.0')
# Move slider to zero
self.resetToZero()
[docs] def setPlayRate(self, rate):
# set play rate on the actor, although for the AnimPanel
# purpose we don't use the actor's play rate, but rather
# the self.playRate value since we drive the animation
# playback ourselves
self['actor'].setPlayRate(eval(rate), self['active'])
self.playRate = eval(rate)
self.updateDisplay()
[docs] def setOffset(self):
newOffset = askfloat(parent = self.interior(),
title = self['text'],
prompt = 'Start offset (seconds):')
if newOffset is not None:
self.offset = newOffset
self.updateDisplay()
[docs] def enableControl(self):
self.frameActiveVar.set(1)
[docs] def disableControl(self):
self.frameActiveVar.set(0)
[docs] def displayFrameCounts(self):
self.unitsVar.set(FRAMES)
self.updateDisplay()
[docs] def displaySeconds(self):
self.unitsVar.set(SECONDS)
self.updateDisplay()
[docs] def play(self, deltaT, fLoop):
if self.frameActiveVar.get():
# Compute new time
self.currT = self.currT + deltaT
if fLoop and self.duration:
# If its looping compute modulo
loopT = self.currT % self.duration
self.goToT(loopT)
else:
if self.currT > self.maxSeconds:
# Clear this actor control from play list
self['animPanel'].playList.remove(self)
else:
self.goToT(self.currT)
else:
# Clear this actor control from play list
self['animPanel'].playList.remove(self)
[docs] def goToF(self, f):
if self.unitsVar.get() == FRAMES:
self.frameControl.set(f)
else:
self.frameControl.set(f / self.fps)
[docs] def goToT(self, t):
if self.unitsVar.get() == FRAMES:
self.frameControl.set(t * self.fps)
else:
self.frameControl.set(t)
[docs] def goTo(self, t):
# Convert scale value to float
t = float(t)
# Now convert t to seconds for offset calculations
if self.unitsVar.get() == FRAMES:
t = t / self.fps
# Update currT
if self.fScaleCommand or self.fOneShot:
self.currT = t
self.fOneShot = 0
# Now update actor (pose specifed as frame count)
self['actor'].pose(self['active'],
min(self.maxFrame, int(t * self.fps)))
[docs] def resetToZero(self):
# This flag forces self.currT to be updated to new value
self.fOneShot = 1
self.goToT(0)
[docs] def resetToEnd(self):
# This flag forces self.currT to be updated to new value
self.fOneShot = 1
self.goToT(self.duration)
[docs] def nextFrame(self):
"""
There needed to be a better way to select an exact frame number
as the control slider doesn't have the desired resolution
"""
self.fOneShot = 1
self.goToT((self.currT+(1/self.fps))%self.duration)
[docs] def previousFrame(self):
"""
There needed to be a better way to select an exact frame number
as the control slider doesn't have the desired resolution
"""
self.fOneShot = 1
self.goToT((self.currT-(1/self.fps))%self.duration)
"""
# EXAMPLE CODE
from direct.actor import Actor
import AnimPanel
a = Actor.Actor({250:{"head":"phase_3/models/char/dogMM_Shorts-head-250",
"torso":"phase_3/models/char/dogMM_Shorts-torso-250",
"legs":"phase_3/models/char/dogMM_Shorts-legs-250"},
500:{"head":"phase_3/models/char/dogMM_Shorts-head-500",
"torso":"phase_3/models/char/dogMM_Shorts-torso-500",
"legs":"phase_3/models/char/dogMM_Shorts-legs-500"},
1000:{"head":"phase_3/models/char/dogMM_Shorts-head-1000",
"torso":"phase_3/models/char/dogMM_Shorts-torso-1000",
"legs":"phase_3/models/char/dogMM_Shorts-legs-1000"}},
{"head":{"walk":"phase_3/models/char/dogMM_Shorts-head-walk", \
"run":"phase_3/models/char/dogMM_Shorts-head-run"}, \
"torso":{"walk":"phase_3/models/char/dogMM_Shorts-torso-walk", \
"run":"phase_3/models/char/dogMM_Shorts-torso-run"}, \
"legs":{"walk":"phase_3/models/char/dogMM_Shorts-legs-walk", \
"run":"phase_3/models/char/dogMM_Shorts-legs-run"}})
a.attach("head", "torso", "joint-head", 250)
a.attach("torso", "legs", "joint-hips", 250)
a.attach("head", "torso", "joint-head", 500)
a.attach("torso", "legs", "joint-hips", 500)
a.attach("head", "torso", "joint-head", 1000)
a.attach("torso", "legs", "joint-hips", 1000)
a.drawInFront("joint-pupil?", "eyes*", -1, lodName=250)
a.drawInFront("joint-pupil?", "eyes*", -1, lodName=500)
a.drawInFront("joint-pupil?", "eyes*", -1, lodName=1000)
a.setLOD(250, 250, 75)
a.setLOD(500, 75, 15)
a.setLOD(1000, 15, 1)
a.fixBounds()
a.reparentTo(render)
a2 = Actor.Actor({250:{"head":"phase_3/models/char/dogMM_Shorts-head-250",
"torso":"phase_3/models/char/dogMM_Shorts-torso-250",
"legs":"phase_3/models/char/dogMM_Shorts-legs-250"},
500:{"head":"phase_3/models/char/dogMM_Shorts-head-500",
"torso":"phase_3/models/char/dogMM_Shorts-torso-500",
"legs":"phase_3/models/char/dogMM_Shorts-legs-500"},
1000:{"head":"phase_3/models/char/dogMM_Shorts-head-1000",
"torso":"phase_3/models/char/dogMM_Shorts-torso-1000",
"legs":"phase_3/models/char/dogMM_Shorts-legs-1000"}},
{"head":{"walk":"phase_3/models/char/dogMM_Shorts-head-walk", \
"run":"phase_3/models/char/dogMM_Shorts-head-run"}, \
"torso":{"walk":"phase_3/models/char/dogMM_Shorts-torso-walk", \
"run":"phase_3/models/char/dogMM_Shorts-torso-run"}, \
"legs":{"walk":"phase_3/models/char/dogMM_Shorts-legs-walk", \
"run":"phase_3/models/char/dogMM_Shorts-legs-run"}})
a2.attach("head", "torso", "joint-head", 250)
a2.attach("torso", "legs", "joint-hips", 250)
a2.attach("head", "torso", "joint-head", 500)
a2.attach("torso", "legs", "joint-hips", 500)
a2.attach("head", "torso", "joint-head", 1000)
a2.attach("torso", "legs", "joint-hips", 1000)
a2.drawInFront("joint-pupil?", "eyes*", -1, lodName=250)
a2.drawInFront("joint-pupil?", "eyes*", -1, lodName=500)
a2.drawInFront("joint-pupil?", "eyes*", -1, lodName=1000)
a2.setLOD(250, 250, 75)
a2.setLOD(500, 75, 15)
a2.setLOD(1000, 15, 1)
a2.fixBounds()
a2.reparentTo(render)
ap = AnimPanel.AnimPanel([a, a2])
# Alternately
ap = a.animPanel()
ap2 = a2.animPanel()
"""