Plugin Cafe Homepage
Forum Home Forum Home > Plugin Cafe > PYTHON Development
  New Posts New Posts
  FAQ FAQ  Forum Search   Register Register  Login Login

Makehuman Face Shape importer

 Post Reply Post Reply
Author
Message
Moonlight63 View Drop Down
Member
Member


Joined: 2015 Oct 25
Online Status: Offline
Posts: 4
Post Options Post Options   Quote Moonlight63 Quote  Post ReplyReply Direct Link To This Post Topic: Makehuman Face Shape importer
    Posted: 2016 Feb 05 at 10:00pm
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()

Back to Top
S_Bach View Drop Down
Forum Moderator
Forum Moderator
Avatar

Joined: 2011 Jun 27
Online Status: Offline
Posts: 1364
Post Options Post Options   Quote S_Bach Quote  Post ReplyReply Direct Link To This Post Posted: 2016 Feb 08 at 1:28am
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
SDK Support Engineer
Back to Top
Moonlight63 View Drop Down
Member
Member


Joined: 2015 Oct 25
Online Status: Offline
Posts: 4
Post Options Post Options   Quote Moonlight63 Quote  Post ReplyReply Direct Link To This Post Posted: 2016 Feb 14 at 7:03pm
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.
Back to Top
Moonlight63 View Drop Down
Member
Member


Joined: 2015 Oct 25
Online Status: Offline
Posts: 4
Post Options Post Options   Quote Moonlight63 Quote  Post ReplyReply Direct Link To This Post Posted: 2016 Feb 14 at 7:44pm
ok, here are a few examples:

http://imgur.com/a/rhtlw

the second 2 are the easiest to see what is happening.
Back to Top
S_Bach View Drop Down
Forum Moderator
Forum Moderator
Avatar

Joined: 2011 Jun 27
Online Status: Offline
Posts: 1364
Post Options Post Options   Quote S_Bach Quote  Post ReplyReply Direct Link To This Post Posted: 2016 Feb 15 at 9:41am
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
SDK Support Engineer
Back to Top
Moonlight63 View Drop Down
Member
Member


Joined: 2015 Oct 25
Online Status: Offline
Posts: 4
Post Options Post Options   Quote Moonlight63 Quote  Post ReplyReply Direct Link To This Post Posted: 2016 Feb 15 at 4:16pm
All. Like I said, The problem is that I don't know what the problem is.

Edited by Moonlight63 - 2016 Feb 15 at 4:17pm
Back to Top
 Post Reply Post Reply

Forum Jump Forum Permissions View Drop Down

Bulletin Board Software by Web Wiz Forums® version 9.61 [Free Express Edition]
Copyright ©2001-2009 Web Wiz

This page was generated in 0.109 seconds.