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:

    http://imgur.com/a/rhtlw

    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.


Log in to reply