Two days ago I started working on a way to import the face shapes from .mhx2 files into Cinema 4D, as that is my primary animation and rigging enviroment, and I don't have a clue how to use blender
I wanted to share what I came up with so far. I think it is pretty promising, however, I have run into a slight issue with it, and since I am not actually a python programmer, I thought it would be best to ask the pros what they think.
The main problem I think is that I don't seem to understand quite how the rotations 'should' work. Everything seems fine in the pitch axis, but when using a shape like MouthMoveLeft, the levator bones just don't move right.
I am very interested to see what anyone can come up with. I am using an exported collada in c4d, then import the mhx2 file by executing the script.
- Code: Select all
import c4d
import json
import gzip
import os
import math
from c4d import gui
from c4d import utils
#Welcome to the world of Python
def main():
#Undo compliant
doc.StartUndo()
#Show load file dialog
mhxfile = c4d.storage.LoadDialog(
c4d.FILESELECTTYPE_ANYTHING,
"Please select your exported mhx2 file.",
c4d.FILESELECT_LOAD,
".mhx2"
)
mhxpath = mhxfile.decode("utf-8")
#Start build proccess
build(importMhx2Json(mhxpath))
#Building is complete, We are now done
gui.MessageDialog('Done!')
#Stop Recording undo messages
doc.EndUndo()
#Force Refreash
c4d.EventAdd()
def build(struct):
#Create new object
null = c4d.BaseObject(c4d.Onull)
doc.InsertObject(null)
doc.AddUndo(c4d.UNDOTYPE_NEW, null)
#Shortcut to paths in JSON
faceposes = struct["skeleton"]["expressions"]["face-poseunits"]
bvhs = struct["skeleton"]["expressions"]["face-poseunits"]["bvh"]
#Make sure data is not missing
if len(faceposes["json"]["framemapping"]) != len(bvhs["frames"]):
gui.MessageDialog("Frame Missmatch")
#Construct UserData Sliders on Null
if struct["skeleton"]:
for key in faceposes["json"]["framemapping"]:
data = c4d.GetCustomDataTypeDefault(c4d.DTYPE_REAL)
data[c4d.DESC_NAME] = str(key)
data[c4d.DESC_UNIT] = c4d.DESC_UNIT_PERCENT
data[c4d.DESC_MIN] = 0
data[c4d.DESC_MAX] = 1
data[c4d.DESC_STEP] = 0.01
data[c4d.DESC_CUSTOMGUI] = c4d.CUSTOMGUI_REALSLIDER
null.AddUserData(data)
else:
gui.MessageDialog("No skeleton data in mhx2 file.")
#Figure out which bones actually move
includelist = checkExcludes(bvhs)
#Start creating Xpresso Tags
createXpresso(bvhs, faceposes["json"]["framemapping"], includelist, null)
def createXpresso (bvh, frame, include, controller):
lostbones = []
d2r = math.pi/180
#Loop for every bone
for bone in include:
#Bone names in collada (.dae) use _ when imported into C4D, so fix the names
obj = doc.SearchObject(bvh["joints"][bone].replace(".","_"))
#Tag proxy
xtag = c4d.BaseTag(c4d.Texpresso)
#Add any missing bones to a list for debugging, or add the xtag proxy to the joint
if obj is None:
lostbones.append(bvh["joints"][bone])
else:
obj.InsertTag(xtag)
doc.AddUndo(c4d.UNDOTYPE_NEW, xtag)
#Setup our nodes
#Constant node contains joint's rest rotation and is connected to Math Add node
#Math node will add all adjestment values that we feed it and output a final expression
#Object node local rotation gets it's value from the math nodes final output
nodemaster = xtag.GetNodeMaster()
mathnode = nodemaster.CreateNode(nodemaster.GetRoot(), c4d.ID_OPERATOR_MATH, None, 500, 0)
mathnode[c4d.GV_DYNAMIC_DATATYPE] = c4d.ID_GV_DATA_TYPE_VECTOR
objnode = nodemaster.CreateNode(nodemaster.GetRoot(), c4d.ID_OPERATOR_OBJECT, None, 650, 0)
objnode[c4d.GV_OBJECT_OBJECT_ID] = obj
objnode[c4d.GV_OBJECT_PATH_TYPE] = 2
constnode = nodemaster.CreateNode(nodemaster.GetRoot(), c4d.ID_OPERATOR_CONST, None, 300, 0)
constnode[c4d.GV_DYNAMIC_DATATYPE] = c4d.ID_GV_DATA_TYPE_VECTOR
constnode[c4d.GV_CONST_VALUE] = obj.GetRelRot()
mathnode.GetInPort(0).Connect(constnode.GetOutPort(0))
mathnode.GetOutPort(0).Connect(objnode.AddPort(c4d.GV_PORT_INPUT, c4d.ID_BASEOBJECT_REL_ROTATION))
#Also add our null to the graph for user data
controlnode = nodemaster.CreateNode(nodemaster.GetRoot(), c4d.ID_OPERATOR_OBJECT, None, 0, 0)
controlnode[c4d.GV_OBJECT_OBJECT_ID] = controller
#this is just for positioning nodes in the graph editor, but not needed
poscount = -1
#For every frame we need to connect our user data to our math node
for n,curframe in enumerate(bvh["frames"]):
#We only care about data that contains something
if bvh["frames"][n][bone] != ([0, 0, 0]):
poscount = poscount + 1
#We have to multiply our incoming values, otherwise cinema will read any value > 0.001 as 0
x = c4d.utils.Rad(bvh["frames"][n][bone][0] * 1000000)
y = c4d.utils.Rad(bvh["frames"][n][bone][1] * 1000000)
z = c4d.utils.Rad(bvh["frames"][n][bone][2] * 1000000)
#Add port on our controller null for User data
ctrlport = controlnode.AddPort(c4d.GV_PORT_OUTPUT, c4d.DescID(c4d.DescLevel(c4d.ID_USERDATA, c4d.DTYPE_SUBCONTAINER, 0), c4d.DescLevel(n+1)), message = True)
#We create a division node and constant value to divide the output back down to the correct amount, not inserting the original data into any value field.
#This "should" stop values lower than 0.001 from reading as 0
divamount = nodemaster.CreateNode(nodemaster.GetRoot(), c4d.ID_OPERATOR_CONST, None, 75, poscount * 75)
divamount[c4d.GV_DYNAMIC_DATATYPE] = c4d.ID_GV_DATA_TYPE_REAL
divamount[c4d.GV_CONST_VALUE] = 1000000
divnode = nodemaster.CreateNode(nodemaster.GetRoot(), c4d.ID_OPERATOR_MATH, None, 150, poscount * 75)
divnode[c4d.GV_DYNAMIC_DATATYPE] = c4d.ID_GV_DATA_TYPE_REAL
divnode[c4d.GV_MATH_FUNCTION_ID] = c4d.GV_DIV_NODE_FUNCTION
#Multiply our Userdata by the offset amount defined in our JSON file
multamount = nodemaster.CreateNode(nodemaster.GetRoot(), c4d.ID_OPERATOR_CONST, None, 75, poscount * 75)
multamount[c4d.GV_DYNAMIC_DATATYPE] = c4d.ID_GV_DATA_TYPE_VECTOR
multamount[c4d.GV_CONST_VALUE] = c4d.Vector(-z, x, y)
multnode = nodemaster.CreateNode(nodemaster.GetRoot(), c4d.ID_OPERATOR_MATH, None, 150, poscount * 75)
multnode[c4d.GV_DYNAMIC_DATATYPE] = c4d.ID_GV_DATA_TYPE_VECTOR
multnode[c4d.GV_MATH_FUNCTION_ID] = c4d.GV_MUL_NODE_FUNCTION
#Controller port -> Division node -> Multiply node -> Math node -> Final rotation
ctrlport.Connect(divnode.GetInPort(0))
divamount.GetOutPort(0).Connect(divnode.GetInPort(1))
multnode.GetInPort(0).Connect(divnode.GetOutPort(0))
multnode.GetInPort(1).Connect(multamount.GetOutPort(0))
multnode.GetOutPort(0).Connect(mathnode.AddPort(c4d.GV_PORT_INPUT, c4d.GV_MATH_INPUT, message = True))
def checkExcludes(bvhpath):
#Get a list of all joints that get affected
includelist = []
zerolist = [0, 0, 0]
framecount = len(bvhpath["frames"])
jointcount = len(bvhpath["joints"])
curframe = 0
curjoint = 0
for frame in bvhpath["frames"]:
for n,joint in enumerate(frame):
if joint != zerolist:
if not n in includelist:
includelist.append(n)
for joint in includelist:
print(bvhpath["joints"][joint])
return includelist
#import and load ripped strait from the blender importer
def importMhx2Json(filepath):
if os.path.splitext(filepath)[1].lower() != ".mhx2":
gui.MessageDialog("Error: Not a mhx2 file: %s" % filepath.encode('utf-8', 'strict'))
print("Error: Not a mhx2 file: %s" % filepath.encode('utf-8', 'strict'))
return
print( "Opening MHX2 file %s " % filepath.encode('utf-8', 'strict') )
gui.MessageDialog("Opening MHX2 file %s " % filepath.encode('utf-8', 'strict') )
struct = loadJson(filepath)
try:
vstring = struct["mhx2_version"]
except KeyError:
vstring = ""
if vstring:
high,low = vstring.split(".")
fileVersion = 100*int(high) + int(low)
else:
fileVersion = 0
if (fileVersion > 49 or
fileVersion < 22):
raise MhxError(
("Incompatible MHX2 versions:\n" +
"MHX2 file: %s\n" % vstring +
"Must be between\n" +
"0.%d and 0.%d" % (22, 49))
)
return struct
def loadJson(filepath):
try:
with gzip.open(filepath, 'rb') as fp:
bytes = fp.read()
except IOError:
bytes = None
if bytes:
string = bytes.decode("utf-8")
struct = json.loads(string)
else:
with open(filepath, "rU") as fp:
struct = json.load(fp)
if not struct:
print("Could not load %s" % filepath)
gui.MessageDialog("Could not load %s" % filepath)
return struct
if __name__=='__main__':
main()