#!BPY



"""

Name: 'Motion Capture (.bvh)...'

Blender: 236

Group: 'Import'

Tip: 'Import a (.bvh) motion capture file'

"""



__author__ = "Campbell Barton"

__url__ = ("blender", "elysiun", "http://jmsoler.free.fr/util/blenderfile/py/bvh_import.py")

__version__ = "1.0.2 04/12/28"



__bpydoc__ = """\

This script imports BVH motion capture data to Blender.



Supported: Poser 3.01<br>



Missing:<br>



Known issues:<br>



Notes:<br>

   Jean-Michel Soler improved importer to support Poser 3.01 files;<br>

   Jean-Baptiste Perin wrote a script to create an armature out of the

Empties created by this importer, it's in the Scripts window -> Scripts -> Animation menu.

"""



# $Id: bvh_import.py,v 1.6 2005/05/17 07:17:52 ianwill Exp $

#



#===============================================#

# BVH Import script 1.03 patched by Campbell    #

# Small optimizations and scale input           #

# 01/01/2005,                                   #  

#===============================================#



#===============================================#

# BVH Import script 1.02 patched by Jm Soler    #

# to the Poser 3.01 bvh file                    # 

# 28/12/2004,                                   #  

#===============================================#



#===============================================#

# BVH Import script 1.0 by Campbell Barton      #

# 25/03/2004, euler rotation code taken from    #

# Reevan Mckay's BVH import script v1.1         #

# if you have any questions about this script   #

# email me ideasman@linuxmail.org               #

#===============================================#



#===============================================#

# TODO:                                         #

# * Create bones when importing                 #

# * Make an IPO jitter removal script           #

# * Work out a better naming system             #

#===============================================#



# -------------------------------------------------------------------------- 

# BVH Import v0.9 by Campbell Barton (AKA Ideasman) 

# -------------------------------------------------------------------------- 

# ***** BEGIN GPL LICENSE BLOCK ***** 

# 

# 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 2 

# 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, write to the Free Software Foundation, 

# Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. 

# 

# ***** END GPL LICENCE BLOCK ***** 

# -------------------------------------------------------------------------- 





import string

import math

import Blender

from Blender import Window, Object, Scene, Ipo, Draw

from Blender.Scene import Render





# # PSYCO IS CRASHING ON MY SYSTEM

# # Attempt to load psyco, speed things up

# try:

#   print 'using psyco to speed up BVH importing'

#   import psyco

#   psyco.full()

#  

# except:

#   print 'psyco is not present on this system'



# Default scale

scale = 0.01



# Update as we load?

debug = 0



# Get the current scene.

scn = Scene.GetCurrent()

context = scn.getRenderingContext()



# Here we store the Ipo curves in the order they load.

channelCurves = []



# Object list

# We need this so we can loop through the objects and edit there IPO's 

# Chenging there rotation to EULER rotation

objectList = []



def getScale():

	return Draw.PupFloatInput('BVH Scale: ', 0.01, 0.001, 10.0, 0.1, 3)



def MAT(m):

	if len(m) == 3:

		return Blender.Mathutils.Matrix(m[0], m[1], m[2])

	elif len(m) == 4:

		return Blender.Mathutils.Matrix(m[0], m[1], m[2], m[3])







#===============================================#

# eulerRotation: converts X, Y, Z rotation      #

# to eular Rotation. This entire function       #

# is copied from Reevan Mckay's BVH script      #

#===============================================#

# Vars used in eular rotation funtcion

DEG_TO_RAD = math.pi/180.0

RAD_TO_DEG = 180.0/math.pi

PI=3.14159



def eulerRotate(x,y,z): 

  #=================================

  def RVMatMult3 (mat1,mat2):

  #=================================

    mat3=[[0.0,0.0,0.0],[0.0,0.0,0.0],[0.0,0.0,0.0]]

    for i in range(3):

      for k in range(3):

        for j in range(3):

          mat3[i][k]=mat3[i][k]+mat1[i][j]*mat2[j][k]

    return mat3

  

  

  #=================================

  def	RVAxisAngleToMat3 (rot4):

  #	Takes a direction vector and

  #	a rotation (in rads) and

  #	returns the rotation matrix.

  #	Graphics Gems I p. 466:

  #=================================

    mat3=[[0.0,0.0,0.0],[0.0,0.0,0.0],[0.0,0.0,0.0]]

    if math.fabs(rot4[3])>0.01:

      s=math.sin(rot4[3])

      c=math.cos(rot4[3])

      t=1.0-math.cos(rot4[3])

    else:

      s=rot4[3]

      c=1.0

      t=0.0



    x=rot4[0]; y=rot4[1]; z=rot4[2]

    

    mat3[0][0]=t*x*x+c

    mat3[0][1]=t*x*y+s*z

    mat3[0][2]=t*x*z-s*y 

    

    mat3[1][0]=t*x*y-s*z

    mat3[1][1]=t*y*y+c

    mat3[1][2]=t*y*z+s*x

    

    mat3[2][0]=t*x*z+s*y

    mat3[2][1]=t*y*z-s*x

    mat3[2][2]=t*z*z+c

    

    return mat3

 

  eul = [x,y,z]

  

  for jj in range(3):

    while eul[jj] < 0:

      eul[jj] = eul[jj] + 360.0

    while eul[jj] >= 360.0:

      eul[jj] = eul[jj] - 360.0



  eul[0] = eul[0]*DEG_TO_RAD

  eul[1] = eul[1]*DEG_TO_RAD

  eul[2] = eul[2]*DEG_TO_RAD

  

  xmat=RVAxisAngleToMat3([1,0,0,eul[0]])

  ymat=RVAxisAngleToMat3([0,1,0,eul[1]])

  zmat=RVAxisAngleToMat3([0,0,1,eul[2]])

  

  mat=[[1.0,0.0,0.0],[0.0,1.0,0.0],[0.0,0.0,1.0]]  

  

  # Standard BVH multiplication order

  mat=RVMatMult3 (zmat,mat)

  mat=RVMatMult3 (xmat,mat)

  mat=RVMatMult3 (ymat,mat)

  

  

  '''

  # Screwy Animation Master BVH multiplcation order

  mat=RVMatMult3 (ymat,mat)

  mat=RVMatMult3 (xmat,mat)

  mat=RVMatMult3 (zmat,mat)

  '''

  mat = MAT(mat)

  

  eul = mat.toEuler()

  x =- eul[0]/-10

  y =- eul[1]/-10

  z =- eul[2]/-10

  

  return x, y, z # Returm euler roration values.







#===============================================#

# makeJoint: Here we use the node data          #

# from the BVA file to create an empty          #

#===============================================#

def makeJoint(name, parent, prefix, offset, channels):

  global scale

  # Make Empty, with the prefix in front of the name

  ob = Object.New('Empty', prefix + name) # New object, ob is shorter and nicer to use.

  scn.link(ob) # place the object in the current scene

  

  # Offset Empty

  ob.setLocation(offset[0]*scale, offset[1]*scale, offset[2]*scale)



  # Make me a child of another empty.

  # Vale of None will make the empty a root node (no parent)

  if parent[-1] != None:

    obParent = Object.Get(prefix + parent[-1]) # We use this a bit so refrence it here.

    obParent.makeParent([ob], 0, 1) #ojbs, noninverse, 1 = not fast.



  # Add Ipo's for necessary channels

  newIpo = Ipo.New('Object', prefix + name)

  ob.setIpo(newIpo)

  for channelType in channels:

    if channelType == 'Xposition':

      newIpo.addCurve('LocX')

      newIpo.getCurve('LocX').setInterpolation('Linear')

    if channelType == 'Yposition':

      newIpo.addCurve('LocY')

      newIpo.getCurve('LocY').setInterpolation('Linear')

    if channelType == 'Zposition':

      newIpo.addCurve('LocZ')

      newIpo.getCurve('LocZ').setInterpolation('Linear')



    if channelType == 'Zrotation':

      newIpo.addCurve('RotZ')

      newIpo.getCurve('RotZ').setInterpolation('Linear')

    if channelType == 'Yrotation':

      newIpo.addCurve('RotY')

      newIpo.getCurve('RotY').setInterpolation('Linear')

    if channelType == 'Xrotation':

      newIpo.addCurve('RotX')

      newIpo.getCurve('RotX').setInterpolation('Linear')



  # Add to object list

  objectList.append(ob)

  

  # Redraw if debugging

  if debug: Blender.Redraw()

  



#===============================================#

# makeEnd: Here we make an end node             #

# This is needed when adding the last bone      #

#===============================================#

def makeEnd(parent, prefix, offset):

  # Make Empty, with the prefix in front of the name, end nodes have no name so call it its parents name+'_end'

  ob = Object.New('Empty', prefix + parent[-1] + '_end') # New object, ob is shorter and nicer to use.

  scn.link(ob)

  

  # Dont check for a parent, an end node MUST have a parent

  obParent = Object.Get(prefix + parent[-1]) # We use this a bit so refrence it here.

  obParent.makeParent([ob], 0, 1) #ojbs, noninverse, 1 = not fast.



  # Offset Empty

  ob.setLocation(offset[0]*scale, offset[1]*scale, offset[2]*scale) 

  

  # Redraw if debugging

  if debug: Blender.Redraw()  

  







#===============================================#

# MAIN FUNCTION - All things are done from here #

#===============================================#

def loadBVH(filename):

  global scale

  print ''

  print 'BVH Importer 1.0 by Campbell Barton (Ideasman) - ideasman@linuxmail.org'

  alpha='abcdefghijklmnopqrstuvewxyz'

  ALPHA=alpha+alpha.upper()

  ALPHA+=' 0123456789+-{}. '  

  time1 = Blender.sys.time()

  tmpScale = getScale()

  if tmpScale != None:

    scale = tmpScale

  

  # File loading stuff

  # Open the file for importing

  file = open(filename, 'r')  

  fileData = file.readlines()

  # Make a list of lines

  lines = []

  for fileLine in fileData:

    fileLine=fileLine.replace('..','.')

    newLine = string.split(fileLine)

    if newLine != []:

      t=[]

      for n in newLine:

         for n0 in n:

           if n0 not in ALPHA:

              n=n.replace(n0,'')  

         t.append(n)

      lines.append(t)



    

  del fileData

  # End file loading code



  # Call object names with this prefix, mainly for scenes with multiple BVH's - Can imagine most partr names are the same

  # So in future

  #prefix = str(len(lines)) + '_'

  

  prefix = '_'

  

  # Create Hirachy as empties

  if lines[0][0] == 'HIERARCHY':

    print 'Importing the BVH Hierarchy for:', filename

  else:

    return 'ERROR: This is not a BVH file'

  

  # A liniar list of ancestors to keep track of a single objects heratage

  # at any one time, this is appended and removed, dosent store tree- just a liniar list.

  # ZERO is a place holder that means we are a root node. (no parents)

  parent = [None]  

  

  #channelList [(<objectName>, [channelType1, channelType2...]),  (<objectName>, [channelType1, channelType2...)]

  channelList = []

  channelIndex = -1



  



  lineIdx = 1 # An index for the file.

  while lineIdx < len(lines) -1:

    #...

    if lines[lineIdx][0] == 'ROOT' or lines[lineIdx][0] == 'JOINT':

      if lines[lineIdx][0] == 'JOINT' and len(lines[lineIdx])>2:

         for j in range(2,len(lines[lineIdx])) :

             lines[lineIdx][1]+='_'+lines[lineIdx][j]



      # MAY NEED TO SUPPORT MULTIPLE ROOT's HERE!!!, Still unsure weather multiple roots are possible.??



      print len(parent) * '  ' + 'node:',lines[lineIdx][1],' parent:',parent[-1]

      print lineIdx

      name = lines[lineIdx][1]

      print name,lines[lineIdx+1],lines[lineIdx+2]

      lineIdx += 2 # Incriment to the next line (Offset)

      offset = ( float(lines[lineIdx][1]), float(lines[lineIdx][2]), float(lines[lineIdx][3]) )

      lineIdx += 1 # Incriment to the next line (Channels)

      

      # newChannel[Xposition, Yposition, Zposition, Xrotation, Yrotation, Zrotation]

      # newChannel has Indecies to the motiondata,

      # -1 refers to the last value that will be added on loading at a value of zero

      # We'll add a zero value onto the end of the MotionDATA so this is always refers to a value.

      newChannel = [-1, -1, -1, -1, -1, -1] 

      for channel in lines[lineIdx][2:]:

        channelIndex += 1 # So the index points to the right channel

        if channel == 'Xposition':

          newChannel[0] = channelIndex

        elif channel == 'Yposition':

          newChannel[1] = channelIndex

        elif channel == 'Zposition':

          newChannel[2] = channelIndex

        elif channel == 'Xrotation':

          newChannel[3] = channelIndex

        elif channel == 'Yrotation':

          newChannel[4] = channelIndex

        elif channel == 'Zrotation':

          newChannel[5] = channelIndex

      

      channelList.append(newChannel)

      

      channels = lines[lineIdx][2:]

      

      # Call funtion that uses the gatrhered data to make an empty.

      makeJoint(name, parent, prefix, offset, channels)

      

      # If we have another child then we can call ourselves a parent, else 

      parent.append(name)



    # Account for an end node

    if lines[lineIdx][0] == 'End' and lines[lineIdx][1] == 'Site': # There is somtimes a name afetr 'End Site' but we will ignore it.

      lineIdx += 2 # Incriment to the next line (Offset)

      offset = ( float(lines[lineIdx][1]), float(lines[lineIdx][2]), float(lines[lineIdx][3]) )

      makeEnd(parent, prefix, offset)



      # Just so we can remove the Parents in a uniform way- End end never has kids

      # so this is a placeholder

      parent.append(None)



    if lines[lineIdx] == ['}']:

      parent = parent[:-1] # Remove the last item





    #=============================================#

    # BVH Structure loaded, Now import motion     #

    #=============================================#    

    if lines[lineIdx] == ['MOTION']:

      print '\nImporting motion data'

      lineIdx += 3 # Set the cursor to the forst frame

      

      #=============================================#

      # Loop through frames, each line a frame      #

      #=============================================#      

      currentFrame = 1

      print 'frames: ',

      

      

      #=============================================#

      # Add a ZERO keyframe, this keeps the rig     #

      # so when we export we know where all the     #

      # joints start from                           #

      #=============================================#  

      obIdx = 0

      while obIdx < len(objectList) -1:

        if channelList[obIdx][0] != -1:

          objectList[obIdx].getIpo().getCurve('LocX').addBezier((currentFrame,0))

        if channelList[obIdx][1] != -1:

          objectList[obIdx].getIpo().getCurve('LocY').addBezier((currentFrame,0))

        if channelList[obIdx][2] != -1:

          objectList[obIdx].getIpo().getCurve('LocZ').addBezier((currentFrame,0))

        if channelList[obIdx][3] != '-1' or channelList[obIdx][4] != '-1' or channelList[obIdx][5] != '-1':

          objectList[obIdx].getIpo().getCurve('RotX').addBezier((currentFrame,0))

          objectList[obIdx].getIpo().getCurve('RotY').addBezier((currentFrame,0))

          objectList[obIdx].getIpo().getCurve('RotZ').addBezier((currentFrame,0))

        obIdx += 1

      

      while lineIdx < len(lines):

        

        # Exit loop if we are past the motiondata.

        # Some BVH's have extra tags like 'CONSTRAINTS and MOTIONTAGS'

        # I dont know what they do and I dont care, they'll be ignored here.

        if len(lines[lineIdx]) < len(objectList):

          print '...ending on unknown tags'

          break

        

        

        currentFrame += 1 # Incriment to next frame

                

        #=============================================#

        # Import motion data and assign it to an IPO  #

        #=============================================#

        lines[lineIdx].append('0') # Use this as a dummy var for objects that dont have a rotate channel.

        obIdx = 0

        if debug: Blender.Redraw() 

        while obIdx < len(objectList) -1:

          if channelList[obIdx][0] != -1:

            VAL0=lines[lineIdx][channelList[obIdx][0]]  

            if VAL0.find('.')==-1:

               VAL0=VAL0[:len(VAL0)-6]+'.'+VAL0[-6:] 

            objectList[obIdx].getIpo().getCurve('LocX').addBezier((currentFrame, scale * float(VAL0)))



          if channelList[obIdx][1] != -1:

            VAL1=lines[lineIdx][channelList[obIdx][0]]  

            if VAL1.find('.')==-1:

               VAL1=VAL1[:len(VAL1)-6]+'.'+VAL1[-6:] 

            objectList[obIdx].getIpo().getCurve('LocY').addBezier((currentFrame, scale * float(VAL1)))



          if channelList[obIdx][2] != -1:

            VAL2=lines[lineIdx][channelList[obIdx][0]]  

            if VAL2.find('.')==-1:

               VAL2=VAL2[:len(VAL2)-6]+'.'+VAL2[-6:] 

            objectList[obIdx].getIpo().getCurve('LocZ').addBezier((currentFrame, scale * float(VAL2)))

          

          if channelList[obIdx][3] != '-1' or channelList[obIdx][4] != '-1' or channelList[obIdx][5] != '-1':

            VAL3=lines[lineIdx][channelList[obIdx][3]]  

            if VAL3.find('.')==-1:

               VAL3=VAL3[:len(VAL3)-6]+'.'+VAL3[-6:]

 

            VAL4=lines[lineIdx][channelList[obIdx][4]]

            if VAL4.find('.')==-1:

               VAL4=VAL4[:len(VAL4)-6]+'.'+VAL4[-6:]



            VAL5=lines[lineIdx][channelList[obIdx][5]] 

            if VAL5.find('.')==-1:

               VAL5=VAL5[:len(VAL5)-6]+'.'+VAL5[-6:]



            x, y, z = eulerRotate(float(VAL3), float(VAL4), float(VAL5))



            objectList[obIdx].getIpo().getCurve('RotX').addBezier((currentFrame, x))

            objectList[obIdx].getIpo().getCurve('RotY').addBezier((currentFrame, y))

            objectList[obIdx].getIpo().getCurve('RotZ').addBezier((currentFrame, z))



          obIdx += 1

          # Done importing motion data #

        

        # lines[lineIdx] = None # Scrap old motion data, save some memory?

        lineIdx += 1

      # We have finished now

      print currentFrame, 'done.'

     

      # No point in looking further, when this loop is done

      # There is nothine else left to do      

      print 'Imported ', currentFrame, ' frames'

      break

      

    # Main file loop

    lineIdx += 1

  print "bvh import time: ", Blender.sys.time() - time1



Blender.Window.FileSelector(loadBVH, "Import BVH")

