You are on page 1of 6

#

# BVH Importer script for Maya.


#
# Importer for .bvh files (BioVision Hierachy files).
# BVH is a common ascii motion capture data format containing skeletal and motion
data.
#
# <license>
# BVH Importer script for Maya.
# Copyright (C) 2012 Jeroen Hoolmans
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# </license>

__author__ = "Jeroen Hoolmans"


__copyright__ = "Copyright 2012, Jeroen Hoolmans"
__credits__ = ["Jeroen Hoolmans"]
__license__ = "GPL"
__version__ = "1.0.1"
__maintainer__ = "Jeroen Hoolmans"
__email__ = "jhoolmans@gmail.com"
__status__ = "Production"

import pymel.core as pm
import maya.cmds as mc
import os

# This maps the BVH naming convention to Maya


translationDict = {
"Xposition" : "translateX",
"Yposition" : "translateY",
"Zposition" : "translateZ",
"Xrotation" : "rotateX",
"Yrotation" : "rotateY",
"Zrotation" : "rotateZ"
}

class TinyDAG(object):
#
# Small helper class to keep track of parents
#

def __init__(self, obj, pObj = None):


self.obj = obj
self.pObj = pObj

def __str__(self):
# returns object name
return str(self.obj)

def _fullPath(self):
# returns full object path
if self.pObj is not None:
return "%s|%s" % (self.pObj._fullPath(), self.__str__())
return str(self.obj)

class BVHImporterDialog(object):
#
# Dialog class..
#

def __init__(self, debug=False):


# Don't use debug when importing more than 10 frames.. Otherwise it
gets messy
self._name = "bvhImportDialog"
self._title = "BVH Importer %s" % __version__

# UI related
self._textfield = ""
self._scaleField = ""
self._frameField = ""
self._rotationOrder = ""
self._reload = ""

# Other
self._rootNode = None # Used for targeting
self._debug = debug

# BVH specific stuff


self._filename = ""
self._channels = []

self.setup_ui()

def setup_ui(self):
# Creates the great dialog
win = self._name
if mc.window(win, ex=True):
mc.deleteUI(win)

# Non sizeable dialog


win = mc.window(self._name, title=self._title, w=200, rtf=True,
sizeable=False)

mc.columnLayout(adj=1, rs=5)
mc.separator()
mc.text("Options")
mc.separator()

mc.rowColumnLayout( numberOfColumns=2,
columnWidth=[(1, 80), (2, 150)],
cal=[(1, "right"), (2, "center")],
cs=[(1,5), (2,5)],
rs=[(1,5), (2,5)])

mc.text("Rig scale")
self._scaleField = mc.floatField(minValue=0.01, maxValue=2, value=1)
mc.text("Frame offset")
self._frameField = mc.intField(minValue=0)
mc.text("Rotation Order")
self._rotationOrder = mc.optionMenu()
mc.menuItem( label='XYZ' )
mc.menuItem( label='YZX' )
mc.menuItem( label='ZXY' )
mc.menuItem( label='XZY' )
mc.menuItem( label='YXZ' )
mc.menuItem( label='ZYX' )

mc.setParent("..")
mc.separator()

# Targeting UI
mc.text("Skeleton Targeting")
mc.text("(Select the hips)")
mc.separator()

mc.rowColumnLayout( numberOfColumns=2,
columnWidth=[(1, 150), (2, 80)],
cs=[(1,5), (2,5)],
rs=[(1,5), (2,5)])

self._textfield = mc.textField(editable=False)
mc.button("Select/Clear", c=self._on_select_root)

mc.setParent("..")
mc.separator()
mc.button("Import..", c=self._on_select_file)
self._reload = mc.button("Reload", enable=False, c=self._read_bvh)

# Sorry :)
mc.text("Created by Jeroen Hoolmans")

mc.window(win, e=True, rtf=True, sizeable=False)


mc.showWindow(win)

def _on_select_file(self, e):


# Without All Files it didn't work for some reason..
filter = "All Files (*.*);;Motion Capture (*.bvh)"
dialog = mc.fileDialog2(fileFilter=filter, dialogStyle=1, fm=1)

if dialog is None:
return
if not len(dialog):
return

self._filename = dialog[0]

mc.button(self._reload, e=True, enable=True)

# Action!
self._read_bvh()

def _read_bvh(self, e=False):


# Safe close is needed for End Site part to keep from setting new
parent.
safeClose = False
# Once motion is active, animate.
motion = False
# Clear channels before appending
self._channels = []

# Scale the entire rig and animation


rigScale = mc.floatField(self._scaleField, q=True, value=True)
frame = mc.intField(self._frameField, q=True, value=True)
rotOrder = mc.optionMenu(self._rotationOrder, q=True, select=True) - 1

with open(self._filename) as f:
# Check to see if the file is valid (sort of)
if not f.next().startswith("HIERARCHY"):
mc.error("No valid .bvh file selected.")
return False

if self._rootNode is None:
# Create a group for the rig, easier to scale. (Freeze
transform when ungrouping please..)
mocapName = os.path.basename(self._filename)
grp = pm.group(em=True,name="_mocap_%s_grp" % mocapName)
grp.scale.set(rigScale, rigScale, rigScale)

# The group is now the 'root'


myParent = TinyDAG(str(grp), None)
else:
myParent = TinyDAG(str(self._rootNode), None)
self._clear_animation()

for line in f:
line = line.replace(" "," ") # force spaces
if not motion:
# root joint
if line.startswith("ROOT"):
# Set the Hip joint as root
if self._rootNode:
myParent = TinyDAG(str(self._rootNode),
None)
else:
myParent = TinyDAG(line[5:].rstrip(),
myParent)

if "JOINT" in line:
jnt = line.split(" ")
# Create the joint
myParent = TinyDAG(jnt[-1].rstrip(), myParent)

if "End Site" in line:


# Finish up a hierarchy and ignore a closing
bracket
safeClose = True

if "}" in line:
# Ignore when safeClose is on
if safeClose:
safeClose = False
continue
# Go up one level
if myParent is not None:
myParent = myParent.pObj
if myParent is not None:
mc.select(myParent._fullPath())

if "CHANNELS" in line:
chan = line.strip().split(" ")
if self._debug:
print chan

# Append the channels that are animated


for i in range(int(chan[1]) ):
self._channels.append("%s.%s" %
(myParent._fullPath(), translationDict[chan[2 + i]] ) )

if "OFFSET" in line:
offset = line.strip().split(" ")
if self._debug:
print offset
jntName = str(myParent)

# When End Site is reached, name it "_tip"


if safeClose:
jntName += "_tip"

# skip if exists
if mc.objExists(myParent._fullPath()):
jnt = pm.PyNode(myParent._fullPath())
jnt.rotateOrder.set(rotOrder)
jnt.translate.set([float(offset[1]),
float(offset[2]), float(offset[3])])
continue

# Build the joint and set its properties


jnt = pm.joint(name=jntName, p=(0,0,0))
jnt.translate.set([float(offset[1]),
float(offset[2]), float(offset[3])])
jnt.rotateOrder.set(rotOrder)

if "MOTION" in line:
# Animate!
motion = True

if self._debug:
if myParent is not None:
print "parent: %s" % myParent._fullPath()

else:
# We don't really need to use Framecount and
time(since Python handles file reads nicely)
if "Frame" not in line:
data = line.split(" ")
if len(data) > 0:
if data[0] == "": data.pop(0)

if self._debug:
print "Animating.."
print "Data size: %d" % len(data)
print "Channels size: %d" %
len(self._channels)
# Set the values to channels
for x in range(0, len(data) - 1 ):
if self._debug:
print "Set Attribute: %s %f" %
(self._channels[x], float(data[x]))
mc.setKeyframe(self._channels[x],
time=frame, value=float(data[x]))

frame = frame + 1

def _clear_animation(self):
# select root joint
pm.select(str(self._rootNode), hi=True)
nodes = pm.ls(sl=True)

trans_attrs = ["translateX", "translateY", "translateZ"]


rot_attrs = ["rotateX", "rotateY", "rotateZ"]
for node in nodes:
for attr in trans_attrs:
connections = node.attr(attr).inputs()
pm.delete(connections)
for attr in rot_attrs:
connections = node.attr(attr).inputs()
pm.delete(connections)
node.attr(attr).set(0)

def _on_select_root(self, e):


# When targeting, set the root joint (Hips)
selection = pm.ls(sl=True, type="joint")
if len(selection) == 0:
self._rootNode = None
mc.textField(self._textfield, e=True, text="")
else:
self._rootNode = selection[0]
mc.textField(self._textfield, e=True, text=str(self._rootNode))

if __name__ == "__main__":
dialog = BVHImporterDialog()

You might also like