Makehuman Face Shape importer
-
On 05/02/2016 at 22:00, xxxxxxxx wrote:
A little while back I started working on a python script to import the face shapes than makehuman can export to a file format called .mhx2. I am sure that someone here has used makehuman, and you probably no just how powerful it can be. I wanted to start using the more advance facial features available from the rigs that can be exported, but there arn't any mhx2 importers for c4d yet. So I figured, hey, it can't be that hard to import the data and use it to drive xpresso. So I did that. 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 for example when using a shape like MouthMoveLeft, the levator bones just don't move correctly. 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.
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()
-
On 08/02/2016 at 01:28, xxxxxxxx wrote:
Hello and welcome,
can you specificity your question? What exactly does not work as expected and what part of your code is relevant for your question?
best wishes,
Sebastian -
On 14/02/2016 at 19:03, xxxxxxxx wrote:
Hi, sorry for the late response. The problem is, well, I don't really know what the problem is. Everything "should" work, and I think it all does. The issue I run into is that the mhx2 file specifies very small degrees in rotation for some bones in certain expressions or poses. The best example I can give is if you import a Makehuman model into blender using the mhx2 importer and change MouthMoveLeft, and do the same with my c4d importer, the result is pretty different. I think it hase to do with this potion of the code:
#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)I have tried all variations of the x,y,z rotations.
I will upload images to better show what I am talking about.
-
On 14/02/2016 at 19:44, xxxxxxxx wrote:
ok, here are a few examples:
the second 2 are the easiest to see what is happening.
-
On 15/02/2016 at 09:41, xxxxxxxx wrote:
Hello,
without knowing what the problem is or what is going wrong it is pretty hard to say anything. Is it a problem that appears only with a certain asset or does it happen with all assets?
Best wishes,
Sebastian -
On 15/02/2016 at 16:16, xxxxxxxx wrote:
All. Like I said, The problem is that I don't know what the problem is.