# -*- coding: utf-8 -*- ''' Copyright (C) 2022 Richard Perry Copyright (C) Average Godot Enjoyer (Johngoss725) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Note that Johngoss725's original contributions were published under a Creative Commons 1.0 Universal License (CC0-1.0) located at . ''' # Original Script Created By: Average Godot Enjoyer (Johngoss725) # Bone Renaming Modifications, File Handling, And Addon By: Richard Perry import bpy import os import logging from pathlib import Path log = logging.getLogger(__name__) # in future remove_prefix should be renamed to rename prefix and a target prefix should be specifiable via ui def fixBones(remove_prefix=False, name_prefix="mixamorig:"): bpy.ops.object.mode_set(mode = 'OBJECT') if not bpy.ops.object: log.warning('[Mixamo Root] Could not find amature object, please select the armature') bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.context.object.show_in_front = True if remove_prefix: for rig in bpy.context.selected_objects: if rig.type == 'ARMATURE': for mesh in rig.children: for vg in mesh.vertex_groups: new_name = vg.name new_name = new_name.replace(name_prefix,"") rig.pose.bones[vg.name].name = new_name vg.name = new_name for bone in rig.pose.bones: bone.name = bone.name.replace(name_prefix,"") for action in bpy.data.actions: fc = action.fcurves for f in fc: f.data_path = f.data_path.replace(name_prefix,"") def scaleAll(): bpy.ops.object.mode_set(mode='OBJECT') prev_context=bpy.context.area.type bpy.ops.object.mode_set(mode='POSE') bpy.ops.pose.select_all(action='SELECT') bpy.context.area.type = 'GRAPH_EDITOR' bpy.context.space_data.dopesheet.filter_text = "Location" bpy.context.space_data.pivot_point = 'CURSOR' bpy.context.space_data.dopesheet.use_filter_invert = False bpy.ops.anim.channels_select_all(action='SELECT') bpy.ops.transform.resize(value=(1, 0.01, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, True, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) def copyHips(root_bone_name="Root", hip_bone_name="mixamorig:Hips", name_prefix="mixamorig:"): bpy.context.area.ui_type = 'FCURVES' #SELECT OUR ROOT MOTION BONE bpy.ops.pose.select_all(action='DESELECT') bpy.context.object.pose.bones[name_prefix + root_bone_name].bone.select = True # SET FRAME TO ZERO bpy.ops.graph.cursor_set(frame=0.0, value=0.0) #ADD NEW KEYFRAME bpy.ops.anim.keyframe_insert_menu(type='Location') #SELECT ONLY HIPS AND LOCTAIUON GRAPH DATA bpy.ops.pose.select_all(action='DESELECT') bpy.context.object.pose.bones[hip_bone_name].bone.select = True bpy.context.area.ui_type = 'DOPESHEET' bpy.context.space_data.dopesheet.filter_text = "Location" bpy.context.area.ui_type = 'FCURVES' #COPY THE LOCATION VALUES OF THE HIPS AND DELETE THEM bpy.ops.graph.copy() bpy.ops.graph.select_all(action='DESELECT') myFcurves = bpy.context.object.animation_data.action.fcurves for i in myFcurves: hip_bone_fcvurve = 'pose.bones["'+hip_bone_name+'"].location' if str(i.data_path)==hip_bone_fcvurve: myFcurves.remove(i) bpy.ops.pose.select_all(action='DESELECT') bpy.context.object.pose.bones[name_prefix + root_bone_name].bone.select = True bpy.ops.graph.paste() bpy.context.area.ui_type = 'VIEW_3D' bpy.ops.object.mode_set(mode='OBJECT') def fix_bones_nla(remove_prefix=False, name_prefix="mixamorig:"): bpy.ops.object.mode_set(mode = 'OBJECT') if not bpy.ops.object: log.warning('[Mixamo Root] Could not find amature object, please select the armature') bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.context.object.show_in_front = True def scale_all_nla(armature): bpy.ops.object.mode_set(mode='OBJECT') # prev_context=bpy.context.area.type for track in [x for x in armature.animation_data.nla_tracks]: bpy.context.active_nla_track = track for strip in track.strips: bpy.context.active_nla_strip = strip print(bpy.context.active_nla_strip) bpy.ops.object.mode_set(mode='POSE') bpy.ops.pose.select_all(action='SELECT') bpy.context.area.type = 'GRAPH_EDITOR' bpy.context.space_data.dopesheet.filter_text = "Location" bpy.context.space_data.pivot_point = 'CURSOR' bpy.context.space_data.dopesheet.use_filter_invert = False bpy.ops.anim.channels_select_all(action='SELECT') bpy.ops.transform.resize(value=(1, 0.01, 1), orient_type='GLOBAL', orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='GLOBAL', constraint_axis=(False, True, False), mirror=True, use_proportional_edit=False, proportional_edit_falloff='SMOOTH', proportional_size=1, use_proportional_connected=False, use_proportional_projected=False) def copy_hips_nla(root_bone_name="Root", hip_bone_name="mixamorig:Hips", name_prefix="mixamorig:"): hip_bone_name="Ctrl_Hips" bpy.ops.object.mode_set(mode='POSE') previous_context = bpy.context.area.ui_type bpy.ops.pose.select_all(action='DESELECT') while False: #SELECT OUR ROOT MOTION BONE # bpy.context.object.pose.bones[name_prefix + root_bone_name].bone.select = True # bpy.ops.nla.tweakmode_enter() # bpy.context.area.ui_type = 'FCURVES' # # SET FRAME TO ZERO # bpy.ops.graph.cursor_set(frame=0.0, value=0.0) # #ADD NEW KEYFRAME # bpy.ops.anim.keyframe_insert_menu(type='Location') # #SELECT ONLY HIPS AND LOCTAIUON GRAPH DATA # bpy.ops.pose.select_all(action='DESELECT') # bpy.context.object.pose.bones[hip_bone_name].bone.select = True # bpy.context.area.ui_type = 'DOPESHEET' # bpy.context.space_data.dopesheet.filter_text = "Location" # bpy.context.area.ui_type = 'FCURVES' # #COPY THE LOCATION VALUES OF THE HIPS AND DELETE THEM # bpy.ops.graph.copy() # bpy.ops.graph.select_all(action='DESELECT') # myFcurves = bpy.context.object.animation_data.action.fcurves # for i in myFcurves: # hip_bone_fcvurve = 'pose.bones["'+hip_bone_name+'"].location' # if str(i.data_path)==hip_bone_fcvurve: # myFcurves.remove(i) # bpy.ops.pose.select_all(action='DESELECT') # bpy.context.object.pose.bones[name_prefix + root_bone_name].bone.select = True # bpy.ops.graph.paste() # for animation data in object # for pass for track in bpy.context.object.animation_data.nla_tracks: bpy.context.object.animation_data.nla_tracks.active = track for strip in track.strips: bpy.context.object.pose.bones[name_prefix + root_bone_name].bone.select = True bpy.context.area.ui_type = 'NLA_EDITOR' bpy.ops.nla.tweakmode_enter() bpy.context.area.ui_type = 'FCURVES' hip_curves = [fc for fc in strip.fcurves if hip_bone_name in fc.data_path and fc.data_path.startswith('location')] # Copy Hips to root ## Insert keyframe for root bone start_frame = strip.action.frame_range[0] # frame sets the x axis cursor (determines the frame, and value the y axis cursor, which is the amplitude of the curve) bpy.ops.graph.cursor_set(frame=start_frame, value=0.0) bpy.ops.anim.keyframe_insert_menu(type='Location') bpy.ops.pose.select_all(action='DESELECT') ## Copy Location fcruves bpy.context.object.pose.bones[hip_bone_name].bone.select = True bpy.context.area.ui_type = 'DOPESHEET' bpy.context.space_data.dopesheet.filter_text = "Location" bpy.context.area.ui_type = 'FCURVES' bpy.ops.graph.copy() bpy.ops.graph.select_all(action='DESELECT') ## We want to delete the hips locations allFcurves = strip.fcurves for fc in hip_curves: allFcurves.remove(fc) ## Paste location fcurves to the root bone bpy.ops.pose.select_all(action='DESELECT') bpy.context.object.pose.bones[name_prefix + root_bone_name].bone.select = True bpy.ops.graph.paste() loc_fcurves = [fc for fc in strip.fcurves if root_bone_name in fc.data_path and fc.data_path.startswith('location')] # Update Root Bone # set z of root to min 0 (not negative). for fc in loc_fcurves: # Z axis location curve if fc.array_index == 2: for kp in fc.keyframe_points: kp.co.z = min(0, abs(kp.co.z)) # Delete rotation curves for x(0) and y(1) axis. Should we delet Z rotation too? # rot_fcurves = [fc for fc in strip.fcurves if root_bone_name in fc.data_path and fc.data_path.startswith('rotation') and (fc.array_index == 0 or fc.array_index == 1)] # for fc in rot_fcurves: # strip.fcurves.remove(fc) # while(rot_fcurves): # fc = rot_fcurves.pop() # strip.fcurves.remove(fc) bpy.context.area.ui_type = 'NLA_EDITOR' bpy.ops.nla.tweakmode_exit() bpy.context.area.ui_type = previous_context bpy.ops.object.mode_set(mode='OBJECT') def deleteArmature(imported_objects=set()): armature = None if bpy.context.selected_objects: armature = bpy.context.selected_objects[0] if imported_objects == set(): log.warning("[Mixamo Root] No armature imported, nothing to delete") else: bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.select_all(action='DESELECT') for obj in imported_objects: bpy.data.objects[obj.name].select_set(True) bpy.ops.object.delete(use_global=False, confirm=False) if bpy.context.selected_objects: bpy.context.view_layer.objects.active = armature def import_armature(filepath, root_bone_name="Root", hip_bone_name="mixamorig:Hips", remove_prefix=False, name_prefix="mixamorig:", insert_root=False, delete_armatures=False): old_objs = set(bpy.context.scene.objects) if insert_root: bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) bpy.ops.import_scene.fbx(filepath = filepath)#, automatic_bone_orientation=True) else: bpy.ops.import_scene.fbx(filepath = filepath)#, automatic_bone_orientation=True) imported_objects = set(bpy.context.scene.objects) - old_objs imported_actions = [x.animation_data.action for x in imported_objects if x.animation_data] print("[Mixamo Root] Now importing: " + str(filepath)) imported_actions[0].name = Path(filepath).resolve().stem # Only reads the first animation associated with an imported armature if insert_root: add_root_bone(root_bone_name, hip_bone_name, remove_prefix, name_prefix) def add_root_bone(root_bone_name="Root", hip_bone_name="mixamorig:Hips", remove_prefix=False, name_prefix="mixamorig:"): armature = bpy.context.selected_objects[0] bpy.ops.object.mode_set(mode='EDIT') root_bone = armature.data.edit_bones.new(name_prefix + root_bone_name) root_bone.tail.y = 30 armature.data.edit_bones[hip_bone_name].parent = armature.data.edit_bones[name_prefix + root_bone_name] bpy.ops.object.mode_set(mode='OBJECT') fixBones(remove_prefix=remove_prefix, name_prefix=name_prefix) scaleAll() copyHips(root_bone_name=root_bone_name, hip_bone_name=hip_bone_name, name_prefix=name_prefix) def add_root_bone_nla(root_bone_name="Root", hip_bone_name="mixamorig:Hips", name_prefix="mixamorig:"):#remove_prefix=False, name_prefix="mixamorig:"): armature = bpy.context.selected_objects[0] bpy.ops.object.mode_set(mode='EDIT') # Add root bone to edit bones root_bone = armature.data.edit_bones.new(name_prefix + root_bone_name) root_bone.tail.z = .25 armature.data.edit_bones[hip_bone_name].parent = armature.data.edit_bones[name_prefix + root_bone_name] bpy.ops.object.mode_set(mode='OBJECT') # fix_bones_nla(remove_prefix=remove_prefix, name_prefix=name_prefix) # scale_all_nla() copy_hips_nla(root_bone_name=root_bone_name, hip_bone_name=hip_bone_name, name_prefix=name_prefix) def push(obj, action, track_name=None, start_frame=0): # Simulate push : # * add a track # * add an action on track # * lock & mute the track # * remove active action from object tracks = obj.animation_data.nla_tracks new_track = tracks.new(prev=None) if track_name: new_track.name = track_name strip = new_track.strips.new(action.name, start_frame, action) obj.animation_data.action = None def get_all_anims(source_dir, root_bone_name="Root", hip_bone_name="mixamorig:Hips", remove_prefix=False, name_prefix="mixamorig:", insert_root=False, delete_armatures=False): files = os.listdir(source_dir) num_files = len(files) current_context = bpy.context.area.ui_type old_objs = set(bpy.context.scene.objects) for file in files: print("file: " + str(file)) try: filepath = source_dir+"/"+file import_armature(filepath, root_bone_name, hip_bone_name, remove_prefix, name_prefix, insert_root, delete_armatures) imported_objects = set(bpy.context.scene.objects) - old_objs if delete_armatures and num_files > 1: deleteArmature(imported_objects) num_files -= 1 except Exception as e: log.error("[Mixamo Root] ERROR get_all_anims raised %s when processing %s" % (str(e), file)) return -1 bpy.context.area.ui_type = current_context bpy.context.scene.frame_start = 0 bpy.ops.object.mode_set(mode='OBJECT') def apply_all_anims(delete_applied_armatures=False, control_rig=None, push_nla=False): if control_rig and control_rig.type == 'ARMATURE': bpy.ops.object.mode_set(mode='OBJECT') imported_objects = set(bpy.context.scene.objects) imported_armatures = [x for x in imported_objects if x.type == 'ARMATURE' and x.name != control_rig.name] for obj in imported_armatures: action_name = obj.animation_data.action.name bpy.context.scene.mix_source_armature = obj bpy.context.view_layer.objects.active = control_rig bpy.ops.mr.import_anim_to_rig() bpy.context.view_layer.objects.active = control_rig selected_action = control_rig.animation_data.action selected_action.name = 'ctrl_' + action_name # created_actions.append(selected_action) if push_nla: push(control_rig, selected_action, None, int(selected_action.frame_start)) if delete_applied_armatures: bpy.context.view_layer.objects.active = control_rig deleteArmature(set([obj])) if __name__ == "__main__": dir_path = "" # If using script in place please set this before running. get_all_anims(dir_path) print("[Mixamo Root] Run as plugin, or copy script in text editor while setting parameter defaults.")