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
    #Show load file dialog
    mhxfile =
        "Please select your exported mhx2 file.",
    mhxpath = mhxfile.decode("utf-8")
    #Start build proccess
    #Building is complete, We are now done
    #Stop Recording undo messages
    #Force Refreash
def build(struct) :
    #Create new object
    null = c4d.BaseObject(c4d.Onull)
    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
        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:
            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.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
                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:
    for joint in includelist:
    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'))
    print( "Opening MHX2 file %s " % filepath.encode('utf-8', 'strict') )
    gui.MessageDialog("Opening MHX2 file %s " % filepath.encode('utf-8', 'strict') )
    struct = loadJson(filepath)
        vstring = struct["mhx2_version"]
    except KeyError:
        vstring = ""
    if vstring:
        high,low = vstring.split(".")
        fileVersion = 100*int(high) + int(low)
        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) :
        with, 'rb') as fp:
            bytes =
    except IOError:
        bytes = None
    if bytes:
        string = bytes.decode("utf-8")
        struct = json.loads(string)
        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__':

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,

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_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:


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,

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.