C4D Python face shape importer

MakeHuman python API, python plugins, etc

Moderator: joepal

C4D Python face shape importer

Postby Moonlight » Sun Jan 10, 2016 7:09 am

So right off the bat I will say I am not %100 sure that this thread is where this topic should go, but it is a python scripting thread.
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 :lol:
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()

Moonlight
 
Posts: 1
Joined: Sun Jan 10, 2016 6:53 am

Return to Python scripts

Who is online

Users browsing this forum: No registered users and 1 guest