388 lines
16 KiB
Python
388 lines
16 KiB
Python
# -*- 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 <http://www.gnu.org/licenses/>.
|
|
|
|
Note that Johngoss725's original contributions were published under a
|
|
Creative Commons 1.0 Universal License (CC0-1.0) located at
|
|
<https://github.com/Johngoss725/Mixamo-To-Godot>.
|
|
'''
|
|
|
|
# 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.")
|