import os from pyfbsdk import * import random import math import itertools import numpy as np # to install numpy for motionbuilder, please refer to: https://help.autodesk.com/view/MOBPRO/2022/ENU/?guid=GUID-46E090C5-34AD-4E26-872F-F7D21DC57C74 # Tutorials/code I referred to: # https://help.autodesk.com/view/MOBPRO/2019/ENU/?guid=__files_GUID_A1189AA0_3816_4350_B8F3_5383DEC25A33_htm # https://github.com/eksod/Retargeter """ ====================================================== Native Python Functions ====================================================== """ """ parsing character pool, tweaking skeleton properties, converting bvh format, etc """ def parse_all_char(data_path): filelist = list(filter(lambda x: x.endswith(".txt"), os.listdir(data_path))) char_list = [] for filename in filelist: filepath = os.path.join(data_path, filename) character = {} with open(filepath, "r") as file: for line in file: tokens = line.strip().split(",") joint_name = tokens[0].replace("_END", "_End") character[joint_name] = np.array( [float(tokens[1]), float(tokens[2]), float(tokens[3])] ) char_list.append([filename, character]) print("PARSE_ALL_CHAR :: DONE :: ", len(char_list)) return char_list def rand_scale(rand_range): # random between [1-rand_range, 1+rand_range] return (1 - rand_range) + 2 * rand_range * random.random() def tweakSkelProperty(char_skel_list, rnd_hierarchy=True, charFile=None): char_i = random.randint(0, len(char_skel_list) - 1) char_file_i, char_stat_i = char_skel_list[char_i] char_stat_i_names = list(char_stat_i.keys()) head_joint_list = [ ["Hips", "LowerBack"], ["Spine", "Spine1", "Spine2", "Spine3", "Spine4"], ["Neck", "Neck1"], ["Head", "Head_End"], ] lleg_joint_list = [ ["LeftHipJoint"], ["LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase", "LeftToeBase_End"], ] larm_joint_list = [ ["LeftShoulder", "LeftDummyShoulder"], ["LeftArm", "LeftForeArm", "LeftHand", "LeftHand_End"], ] """ @return / rnd_offset: Dict[str, [x,y,z(floats)]] @return / parent_map: Dict[str, str] @return / all_joint_list: List[str] # order matters! parents should come first (for addJoint in MB) """ spineCnt = len( [spine for spine in head_joint_list[1] if spine in char_stat_i_names] ) neckCnt = len([spine for spine in head_joint_list[2] if spine in char_stat_i_names]) useHipJoint = "LeftHipJoint" in char_stat_i_names useLowerBack = "LowerBack" in char_stat_i_names useDummyShoulder = "DummyShoulder" in char_stat_i_names ## Heuristics to augment the skeleton if rnd_hierarchy: if random.random() < 0.1: useHipJoint = True char_stat_i["LeftHipJoint"] = np.array([0, 0, 0]) if random.random() < 0.1: useLowerBack = True char_stat_i["LowerBack"] = np.array([0, 0, 0]) # print(char_stat_i_names) if random.random() < 0.05: useDummyShoulder = True char_stat_i["LeftDummyShoulder"] = [5, 0, 0] elif random.random() < 0.05: useDummyShoulder = True char_stat_i["LeftDummyShoulder"] = [0, 0, 0] # randomize spine cnt default_spineCnt = spineCnt spineCnt += max(min(int(random.gauss(0, 1.0)), 2), -2) spineCnt = max(min(spineCnt, 5), 1) # modify default spine length if spineCnt != default_spineCnt: all_spine_list = head_joint_list[1] sum_spine = np.sum( [char_stat_i[all_spine_list[si]] for si in range(default_spineCnt)], axis=0, ) if default_spineCnt < spineCnt: for si in range(default_spineCnt, spineCnt): char_stat_i[all_spine_list[si]] = sum_spine * 1 / spineCnt for si in range(default_spineCnt): char_stat_i[all_spine_list[si]] = ( char_stat_i[all_spine_list[si]] * default_spineCnt / spineCnt ) elif default_spineCnt > spineCnt: sum_spine_remain = np.sum( [char_stat_i[all_spine_list[si]] for si in range(spineCnt)], axis=0 ) ratio = np.where( np.abs(sum_spine_remain) < 1e-4, np.ones_like(sum_spine), sum_spine / sum_spine_remain, ) for si in range(spineCnt): char_stat_i[all_spine_list[si]] *= ratio for si in range(spineCnt, default_spineCnt): del char_stat_i[all_spine_list[si]] # randomize neck cnt default_neckCnt = neckCnt neckCnt += max(min(int(random.gauss(0, 1.0)), 1), -1) neckCnt = max(min(neckCnt, 2), 1) if neckCnt != default_neckCnt: all_neck_list = head_joint_list[2] sum_neck = np.sum( [char_stat_i[all_neck_list[si]] for si in range(default_neckCnt)], axis=0, ) if default_neckCnt < neckCnt: for si in range(default_neckCnt, neckCnt): char_stat_i[all_neck_list[si]] = sum_neck * 1 / neckCnt for si in range(default_neckCnt): char_stat_i[all_neck_list[si]] = ( char_stat_i[all_neck_list[si]] * default_neckCnt / neckCnt ) elif default_neckCnt > neckCnt: sum_neck_remain = np.sum( [char_stat_i[all_neck_list[si]] for si in range(neckCnt)], axis=0 ) ratio = np.where( np.abs(sum_neck_remain) < 1e-4, np.ones_like(sum_neck), sum_neck / sum_neck_remain, ) for si in range(neckCnt): char_stat_i[all_neck_list[si]] *= ratio for si in range(neckCnt, default_neckCnt): del char_stat_i[all_neck_list[si]] max_spineCnt = len(head_joint_list[1]) for i in range(max_spineCnt - spineCnt): head_joint_list[1].pop() max_neckCnt = len(head_joint_list[2]) for i in range(max_neckCnt - neckCnt): head_joint_list[2].pop() if not useLowerBack: head_joint_list[0].pop() if not useHipJoint: lleg_joint_list[0].pop() if not useDummyShoulder: larm_joint_list[0].pop() root_joint = head_joint_list[0][0] last_spine_joint = head_joint_list[1][-1] def flatten(folded_list): return list(itertools.chain(*folded_list)) # def add_prefix(namelist, prefix): return [prefix+v for v in namelist] def change_prefix(namelist, old_prefix, new_prefix): newlist = [] for v in namelist: if v.startswith(old_prefix): newlist.append(new_prefix + v[len(old_prefix) :]) else: newlist.append(v) return newlist head_joint_list = flatten(head_joint_list) lleg_joint_list = flatten(lleg_joint_list) larm_joint_list = flatten(larm_joint_list) rleg_joint_list = change_prefix(lleg_joint_list, "Left", "Right") rarm_joint_list = change_prefix(larm_joint_list, "Left", "Right") ee_joint_list = [ head_joint_list[-1], lleg_joint_list[-1], rleg_joint_list[-1], larm_joint_list[-1], rarm_joint_list[-1], ] # parent_map parent_map = {} for joint_list in [ head_joint_list, lleg_joint_list, rleg_joint_list, larm_joint_list, rarm_joint_list, ]: for i in range(1, len(joint_list)): parent_map[joint_list[i]] = joint_list[i - 1] parent_map[head_joint_list[0]] = None parent_map[lleg_joint_list[0]] = root_joint parent_map[rleg_joint_list[0]] = root_joint parent_map[larm_joint_list[0]] = last_spine_joint parent_map[rarm_joint_list[0]] = last_spine_joint # all_joint_list all_joint_list = flatten( [ head_joint_list, lleg_joint_list, rleg_joint_list, larm_joint_list, rarm_joint_list, ] ) # if there are duplicate, remove except for the first one(ex.lowerback in lleg_ and rleg_) names_used = [] for jn in all_joint_list: if jn in names_used: all_joint_list.remove(jn) names_used.append(jn) # rnd_offset :: caution: Left / Right symmetry rnd_offset = {} for jn, offset_j in char_stat_i.items(): if jn.startswith("Hip"): continue if jn.startswith("Right"): continue local_scale = np.array([rand_scale(0.2), rand_scale(0.2), rand_scale(0.2)]) rnd_offset[jn] = offset_j * local_scale if jn.startswith("Left"): rnd_offset[jn.replace("Left", "Right")] = np.array( [-rnd_offset[jn][0], rnd_offset[jn][1], rnd_offset[jn][2]] ) if jn in ee_joint_list: if random.random() < 0.05: rnd_offset[jn] = np.zeros(3) default_height = -sum([rnd_offset[lljoint][1] for lljoint in lleg_joint_list]) min_ratio = 30 / default_height max_ratio = 120 / default_height global_scale = random.uniform(min_ratio, max_ratio) if charFile is not None: charFile.write( f", {char_file_i}, {default_height}, {default_height*global_scale}\n" ) for jn in rnd_offset: rnd_offset[jn] *= global_scale height = -sum([rnd_offset[lljoint][1] for lljoint in lleg_joint_list]) height += random.random() * 3 # [0, 3]cm random offset (+y) rnd_offset[root_joint] = np.array([0, height, 0]) return rnd_offset, parent_map, all_joint_list def convert(inputPath, outputPath): """ currently, Motionbuilder scripting only supports exporting with rotation & translation for all joints while translation remains unchanged for joints except root. This function reads bvh, removes translation from joints except root, and re-save it to bvh. """ outputFile = open(outputPath, "w") outputFile.write("HIERARCHY\n") indent = 0 jointDOF = list() writeMode = 0 # 0: hierarchy, 1: metadata, 2: data prevJoint = "" offset = 0 with open(inputPath, "r") as file: for line in file: # reading each word words = line.split() if writeMode == 0: if (words[0] == "JOINT") or (words[0] == "ROOT"): prevJoint = words[1] if prevJoint.endswith("_"): prevJoint = prevJoint[:-1] if prevJoint == "Hips" or prevJoint == "Hips_": outputFile.write(" " * indent + "ROOT " + prevJoint + "\n") jointDOF.append((prevJoint, 6)) if words[0] == "JOINT": offset = 6 else: outputFile.write(" " * indent + "JOINT " + prevJoint + "\n") jointDOF.append((prevJoint, 3)) elif words[0] == "End": outputFile.write(" " * indent + "End Site\n") elif words[0] == "CHANNELS": if prevJoint == "": continue if prevJoint == "Hips" or prevJoint == "Hips_": outputFile.write( " " * indent + "CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation\n" ) else: outputFile.write( " " * indent + "CHANNELS 3 Zrotation Xrotation Yrotation\n" ) elif words[0] == "{": if prevJoint == "": continue outputFile.write(" " * indent + "{" + "\n") indent += 1 elif words[0] == "OFFSET": if prevJoint == "": continue outputFile.write( " " * indent + "OFFSET " + words[1] + " " + words[2] + " " + words[3] + "\n" ) elif words[0] == "}": indent -= 1 if indent >= 0: outputFile.write(" " * indent + "}" + "\n") elif words[0] == "MOTION": writeMode = 1 outputFile.write(line) elif writeMode == 1: outputFile.write(line) if line.startswith("Frame Time:"): writeMode = 2 elif writeMode == 2: words = line.split() offset_local = offset newLine = str() # print(len(words)) for jointName, dof in jointDOF: # print(jointName, dof, offset_local) if dof == 3: offset_local += 3 for i in range(dof): newLine += words[offset_local + i] + " " offset_local += dof newLine += "\n" outputFile.write(newLine) outputFile.close() """ ====================================================== Mobupy Functions ====================================================== """ """ create skeleton, characterize, plot animation, switch take, etc """ def generateSkel(pNamespace, rnd_offset, parent_map, all_joint_list): # Populate the skeleton skeleton = {} for jointName in all_joint_list: # add underbar to avoid conflict with reserved names jointName_ = jointName + "_" if jointName == "Reference" or jointName == "Hips": # If it is the reference node, create an FBModelRoot. joint = FBModelRoot(jointName_) else: # Otherwise, create an FBModelSkeleton. joint = FBModelSkeleton(jointName_) joint.LongName = ( pNamespace + ":" + joint.Name ) # Apply the specified namespace to each joint. joint.Color = FBColor(0.3, 0.8, 1) # Cyan joint.Size = 150 # Arbitrary size: big enough to see in viewport joint.Show = True # Make the joint visible in the scene. # Add the joint to our skeleton. skeleton[jointName] = joint def connectPlaceJoint(jointName): translation = rnd_offset[jointName] parentName = parent_map[jointName] # Only assign a parent if it exists. if parentName != None and parentName in parent_map.keys(): skeleton[jointName].Parent = skeleton[parentName] if jointName not in skeleton: print(jointName, "not in skeleton") return # The translation should be set after the parent has been assigned. skeleton[jointName].Translation = FBVector3d(translation) for jointName in reversed(all_joint_list): connectPlaceJoint(jointName) return skeleton def createSkeleton_fromdata( pNamespace, char_skel_list, rnd_hierarchy=True, charFile=None ): rnd_offset, parent_map, all_joint_list = tweakSkelProperty( char_skel_list, rnd_hierarchy, charFile ) return generateSkel(pNamespace, rnd_offset, parent_map, all_joint_list) def characterizeSkeleton(pCharacterName, pSkeleton): # Create a new character. character = FBCharacter(pCharacterName) app.CurrentCharacter = character # Add each joint in our skeleton to the character. for jointName, joint in pSkeleton.items(): slot = character.PropertyList.Find(jointName + "Link") if slot is not None: slot.append(joint) # Flag that the character has been characterized. character.SetCharacterizeOn(True) return character def deselect_all(): modelList = FBModelList() FBGetSelectedModels(modelList, None, True) for model in modelList: model.Selected = False # List of all Mobu Joints jointList = [ "Reference", "Hips", "LeftUpLeg", "LeftLeg", "LeftFoot", "LeftToeBase", "RightUpLeg", "RightLeg", "RightFoot", "RightToeBase", "Spine", "Spine1", "Spine2", "Spine3", "Spine4", "Spine5", "Spine6", "Spine7", "Spine8", "Spine9", "Neck", "Neck1", "Neck2", "Neck3", "Neck4", "Neck5", "Neck6", "Neck7", "Neck8", "Neck9", "Head", "LeftShoulder", "LeftArm", "LeftForeArm", "LeftHand", "RightShoulder", "RightArm", "RightForeArm", "RightHand", ] """ [TODO] If bvh skeleton contains joints that are not in the MotionBuilder's biped character map, append the bvh joint name to `joint_candidates[mobu joint name]` list. # joint_candidates: A dictionary mapping mobu joints to bvh joints (all candidates) # - key: mobu Joint Names # - value: [bvhJointName...] candidate List """ joint_candidates = {"LeftToeBase": ["LeftToe"], "RightToeBase": ["RightToe"]} def CharacterizeBiped(namespace): myBiped = FBCharacter(f"{namespace}: mycharacter") app.CurrentCharacter = myBiped myBiped.LongName = f"{namespace}: mycharacter" # assign Biped to Character Mapping. for mobuJoint in jointList: modelLongName = f"{namespace}:{mobuJoint}" if namespace else mobuJoint myJoint = FBFindModelByLabelName(modelLongName) if (not myJoint) and (mobuJoint in joint_candidates): for bvh_joint_candidate in joint_candidates[mobuJoint]: modelLongName = ( f"{namespace}:{bvh_joint_candidate}" if namespace else bvh_joint_candidate ) myJoint = FBFindModelByLabelName(modelLongName) if myJoint: break # print(modelLongName, myJoint) if myJoint: proplist = myBiped.PropertyList.Find(mobuJoint + "Link") proplist.append(myJoint) switchOn = myBiped.SetCharacterizeOn(True) # print "Character mapping created for " + (myBiped.LongName) return myBiped def plotAnim(char, animChar): """ Receives two characters, sets the input of the first character to the second and plot. Return ploted character. """ # if char.GetCharacterize: # switchOn = char.SetCharacterizeOn(True) plotoBla = FBPlotOptions() plotoBla.ConstantKeyReducerKeepOneKey = True plotoBla.PlotAllTakes = False plotoBla.PlotOnFrame = True plotoBla.PlotPeriod = FBTime(0, 0, 0, 1) plotoBla.PlotTranslationOnRootOnly = True plotoBla.PreciseTimeDiscontinuities = True # plotoBla.RotationFilterToApply = FBRotationFilter.kFBRotationFilterGimbleKiller plotoBla.UseConstantKeyReducer = False plotoBla.ConstantKeyReducerKeepOneKey = True char.InputCharacter = animChar char.InputType = FBCharacterInputType.kFBCharacterInputCharacter char.ActiveInput = True if not char.PlotAnimation( FBCharacterPlotWhere.kFBCharacterPlotOnSkeleton, plotoBla ): FBMessageBox( "Something went wrong", "Plot animation returned false, cannot continue", "OK", None, None, ) return False return char def SwitchTake(pTakeName): iDestName = pTakeName for iTake in system.Scene.Takes: if iTake.Name == iDestName: system.CurrentTake = iTake def skelExists(root, name): if root == None: return False if root.Name == name: return True for child in root.Children: if skelExists(child, name): return True return False """ ====================================================== MAIN ====================================================== """ # [TODO] change below variables DATA_DIR = "/home/sunminlee/workspace/SAME/data/" DATA_NAME = "sample" MOTION_DIR = os.path.join( DATA_DIR, DATA_NAME, "motion", "bvh" ) # place all bvh files here CHAR_DIR = os.path.join( DATA_DIR, DATA_NAME, "character", "joint_pos" ) # place all character files here batch_size = 3 # number of motions to retarget per one skeletal variation iter_num = 2 # number of skeletal variations per one batch merge_skel = False # whether all motions have the same skeleton (if True: no need to import multiple times) fresh_start = True # whether to start fresh, or to continue from the last point scale_stat = parse_all_char(CHAR_DIR) OUT_DIR = os.path.join(MOTION_DIR, "result") if not os.path.exists(OUT_DIR): os.makedirs(OUT_DIR) res_rel_dir = os.path.relpath(OUT_DIR, DATA_DIR) system = FBSystem() app = FBApplication() import time start_time = time.time() total_i = 0 sOptions = FBFbxOptions(False) # false = save options sOptions.SaveCharacter = True sOptions.SaveControlSet = False sOptions.SaveCharacterExtension = False sOptions.ShowFileDialog = False sOptions.ShowOptionsDialog = False app.FileNew() logFilePath = os.path.join(MOTION_DIR, "pair.txt") charFilePath = os.path.join(MOTION_DIR, "character.txt") if fresh_start: fileList = [] for dp, dn, filenames in os.walk(MOTION_DIR): if dp == OUT_DIR: continue for f in filenames: if f.endswith(".bvh"): fileList.append(os.path.join(dp, f)) random.shuffle(fileList) batch_num = math.ceil(len(fileList) / batch_size) with open(os.path.join(MOTION_DIR, "fileList.txt"), "w") as fileListFile: for bi in range(batch_num): for m_i, filePath in enumerate( fileList[bi * batch_size : (bi + 1) * batch_size] ): fileListFile.write( f"{bi}, {m_i}, {os.path.relpath(filePath, DATA_DIR)}\n" ) logFile = open(logFilePath, "w") charFile = open(charFilePath, "w") else: fileList = [] with open(os.path.join(MOTION_DIR, "fileList.txt"), "r") as fileListFile: for line in fileListFile: line_list = line.strip().split(",") fileList.append(os.path.join(DATA_DIR, line_list[-1].strip())) batch_num = math.ceil(len(fileList) / batch_size) # find continue_bi done = np.zeros((batch_num, iter_num), dtype=int) with open(logFilePath, "r") as logfile: for line in logfile: line_tokens = line.split(",") if line.strip() == "": continue bi = int(line_tokens[0].strip()) si = int(line_tokens[1].strip()) done[bi, si] += 1 for bi in range(batch_num): if not (done[bi] == batch_size).all(): break continue_bi = bi + 1 # remove log after continue_bi (partial done, so remove and redo) with open(logFilePath, "r") as logfile: lines = logfile.readlines() with open(logFilePath, "w") as logfile: for line in lines: line_tokens = line.split(",") if line.strip() == "": continue bi = int(line_tokens[0].strip()) if bi < continue_bi: logfile.write(line) else: break # remove character log after continue_bi (partial done, so remove and redo) with open(charFilePath, "r") as charFile: lines = charFile.readlines() with open(charFilePath, "w") as charFile: for line in lines: line_tokens = line.split(",") if line.strip() == "": continue fi = int(line_tokens[0].strip()) if fi < continue_bi * iter_num * batch_size: charFile.write(line) else: break logFile = open(logFilePath, "a") charFile = open(charFilePath, "a") for bi in range(batch_num): app.FileNew() animCharList = [] fileNameList = [] valid_m_num = 0 if not fresh_start and bi < continue_bi: total_i = (bi + 1) * batch_size * iter_num continue if bi != 0: logFile = open(logFilePath, "a") charFile = open(charFilePath, "a") for m_i, filePath in enumerate(fileList[bi * batch_size : (bi + 1) * batch_size]): fileName = os.path.relpath(filePath, DATA_DIR) newTake = FBTake(fileName) #'Take_'+str(m_i)) system.Scene.Takes.append(newTake) SwitchTake(fileName) newTake.ClearAllProperties(False) success = app.FileImport(filePath, merge_skel) valid_m_num += 1 FBPlayerControl().SetTransportFps(FBTimeMode.kFBTimeMode30Frames) if merge_skel: prefix = None else: prefix = "BVH" if m_i == 0 else "BVH " + str(m_i) reference_long_name = prefix + ":reference" if FBFindModelByLabelName(reference_long_name): FBFindModelByLabelName(reference_long_name).FBDelete() if (not merge_skel) or (m_i == 0): animChar = CharacterizeBiped(prefix) animChar.SelectModels(True, True, True, False) lPlayer = FBPlayerControl() lPlayer.Goto(FBTime(0, 0, 0, 0)) animCharList.append(animChar) fileNameList.append(fileName) deselect_all() for skel_i in range(iter_num): characterName = "newSkel" charFile.write(str(total_i)) skeleton = createSkeleton_fromdata( characterName, scale_stat, rnd_hierarchy=True, charFile=charFile ) character = characterizeSkeleton(characterName, skeleton) poseOptions = FBCharacterPoseOptions() poseOptions.mCharacterPoseKeyingMode = ( FBCharacterPoseKeyingMode.kFBCharacterPoseKeyingModeFullBody ) for m_i in range(valid_m_num): SwitchTake(fileNameList[m_i]) # key all frames for bvh to prevent unwanted interpolation between frames lEndTime = system.CurrentTake.LocalTimeSpan.GetStop() lEndFrame = system.CurrentTake.LocalTimeSpan.GetStop().GetFrame() lStartFrameTime = system.CurrentTake.LocalTimeSpan.GetStart() lStartFrame = system.CurrentTake.LocalTimeSpan.GetStart().GetFrame() lRange = min(int(lEndFrame) + 1, 50) lPlayer = FBPlayerControl() for i in range(lRange): lPlayer.Goto(FBTime(0, 0, 0, i)) system.Scene.Evaluate() lPlayer.Key() system.Scene.Evaluate() lPlayer.Goto(FBTime(0, 0, 0, 0)) animChar = animCharList[0] if merge_skel else animCharList[m_i] plotAnim(character, animChar) logFile.write( f"{bi}, {skel_i}, {fileNameList[m_i]}, {res_rel_dir}/{total_i}.bvh\n" ) dummyPath = os.path.join(OUT_DIR, str(total_i) + "_dummy.bvh") convPath = os.path.join(OUT_DIR, str(total_i) + ".bvh") character.SelectModels(True, True, True, True) app.FileExport(dummyPath) convert(dummyPath, convPath) os.remove(dummyPath) deselect_all() total_i += 1 ## runnable check # break # break character.FBDelete() for k, v in skeleton.items(): v.FBDelete() logFile.close() charFile.close() for animChar in animCharList: animChar.FBDelete() del animCharList