Skip to content

Instantly share code, notes, and snippets.

@Greatness7
Last active December 9, 2023 01:17
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Greatness7/85fffb2bd16566501304d35bb8ff2664 to your computer and use it in GitHub Desktop.
Save Greatness7/85fffb2bd16566501304d35bb8ff2664 to your computer and use it in GitHub Desktop.
morrowind_mixamo_helper.py
bl_info = {
"name": "Morrowind Mixamo Helper",
"version": (0, 1, 3),
"blender": (3, 6, 5),
"location": "Search > Morrowind Mixamo Helper",
"description": "Morrowind Mixamo Helper",
"category": "Animation",
}
import bpy
class Paths(bpy.types.PropertyGroup):
fbx: bpy.props.StringProperty(
subtype="FILE_PATH",
options={'HIDDEN'},
name="animation.fbx",
description="Path to a mixamo fbx file",
default="c:\\",
)
nif: bpy.props.StringProperty(
subtype="FILE_PATH",
options={'HIDDEN'},
name="xbase_anim.nif",
description="Path to 'xbase_anim.nif'' file",
default="c:\\morrowind\\data files\\meshes\\xbase_anim.nif",
)
class Panel(bpy.types.Panel):
bl_label = "Morrowind Mixamo Helper"
bl_idname = "SCENE_PT_morrowind_mixamo_helper"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Tool"
def draw(self, context):
self.layout.prop(context.scene.morrowind_mixamo_helper, "fbx")
self.layout.prop(context.scene.morrowind_mixamo_helper, "nif")
self.layout.operator("scene.morrowind_mixamo_helper")
class Operator(bpy.types.Operator):
bl_idname = "scene.morrowind_mixamo_helper"
bl_options = {'REGISTER', 'UNDO'}
bl_label = "Import Animation"
bl_description = "Import Animation"
@classmethod
def poll(self, context):
return context.mode == "OBJECT"
def execute(self, context):
fbx_path = context.scene.morrowind_mixamo_helper.fbx
nif_path = context.scene.morrowind_mixamo_helper.nif
bpy.ops.object.select_all(action='DESELECT')
# import the nif
bpy.ops.import_scene.mw(filepath=nif_path)
dst = next(ob for ob in context.selected_objects if ob.type == "ARMATURE")
# import the fbx
bpy.ops.import_scene.fbx(
filepath=fbx_path,
use_manual_orientation=True,
ignore_leaf_bones=True,
axis_forward="Z",
anim_offset=0,
)
src = next(ob for ob in context.selected_objects if ob.type == "ARMATURE")
# set frame ends
context.scene.frame_start = 0
context.scene.frame_end = int(max(
kp.co[0] for fc in src.animation_data.action.fcurves for kp in fc.keyframe_points
))
# make constraints
for bone in dst.pose.bones:
if bone.name in src.pose.bones:
c = bone.constraints.new('COPY_TRANSFORMS')
c.target = src
c.subtarget = bone.name
# switch to pose mode
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = dst
dst.select_set(True)
bpy.ops.object.mode_set(mode="POSE")
# select all bones
bpy.ops.pose.select_all(action="SELECT")
# backup idle pose
try:
bpy.ops.poselib.create_pose_asset(pose_name="Idle", activate_new_action=False)
except AttributeError:
pass
# bake constraints
args = dict(
frame_start=context.scene.frame_start,
frame_end=context.scene.frame_end,
step=1,
only_selected=True,
visual_keying=True,
clear_constraints=True,
clear_parents=False,
use_current_action=False,
clean_curves=True,
bake_types={'POSE'}
)
try:
bpy.ops.nla.bake(**args)
except TypeError:
del args["clean_curves"]
bpy.ops.nla.bake(**args)
# optimize fcurves
area_type = context.area.type
context.area.type = 'GRAPH_EDITOR'
bpy.ops.graph.clean(threshold=0.0001)
bpy.ops.graph.decimate(mode='ERROR', remove_error_margin=0.0001)
context.area.type = area_type
# prep scene cleanup
bpy.ops.object.mode_set(mode="OBJECT")
bpy.ops.object.select_all(action='DESELECT')
# select src objects
context.view_layer.objects.active = src
src.select_set(True)
bpy.ops.object.select_grouped(type='CHILDREN_RECURSIVE', extend=True)
# keep only required
for child in self.children_recursive(dst):
if (child.type == "MESH") and ("Shadow" not in child.name):
child.select_set(True)
# delete/purge orphans
bpy.data.actions.remove(src.animation_data.action)
bpy.ops.object.delete()
try:
bpy.ops.outliner.orphans_purge(do_recursive=True)
except TypeError:
bpy.ops.outliner.orphans_purge() # legacy compat
# add text keys
pose_markers = dst.animation_data.action.pose_markers
pose_markers.new("Idle9: Start").frame = context.scene.frame_start
pose_markers.new("Idle9: Stop").frame = context.scene.frame_end
# set action name
file_name = bpy.path.basename(context.scene.morrowind_mixamo_helper.fbx)
dst.animation_data.action.name = file_name[:-4]
return {'FINISHED'}
@classmethod
def children_recursive(cls, node):
for child in node.children:
yield child
yield from cls.children_recursive(child)
def register():
bpy.utils.register_class(Paths)
bpy.utils.register_class(Panel)
bpy.utils.register_class(Operator)
bpy.types.Scene.morrowind_mixamo_helper = bpy.props.PointerProperty(type=Paths, options={"HIDDEN"})
def unregister():
bpy.utils.unregister_class(Operator)
bpy.utils.unregister_class(Panel)
bpy.utils.unregister_class(Paths)
del bpy.types.Scene.morrowind_mixamo_helper
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment