Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush Python API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Makehuman Face Shape importer

    Scheduled Pinned Locked Moved PYTHON Development
    6 Posts 0 Posters 710 Views
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • H Offline
      Helper
      last edited by

      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()
        
      
      
      1 Reply Last reply Reply Quote 0
      • H Offline
        Helper
        last edited by

        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

        1 Reply Last reply Reply Quote 0
        • H Offline
          Helper
          last edited by

          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.

          1 Reply Last reply Reply Quote 0
          • H Offline
            Helper
            last edited by

            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.

            1 Reply Last reply Reply Quote 0
            • H Offline
              Helper
              last edited by

              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

              1 Reply Last reply Reply Quote 0
              • H Offline
                Helper
                last edited by

                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.

                1 Reply Last reply Reply Quote 0
                • First post
                  Last post