From 4f8e162b9141fc4994f437d0c3be516067cfb85b Mon Sep 17 00:00:00 2001 From: TigerKat Date: Fri, 2 Apr 2021 18:39:30 +0930 Subject: [PATCH] Added support for importing skeletons from skel_*.anim files. --- __init__.py | 99 ++++++++++++++++++++++++++++++++++++++------- anim.py | 69 +++++++++++++++++++++++-------- import_skel.py | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 31 deletions(-) create mode 100644 import_skel.py diff --git a/__init__.py b/__init__.py index cb536bc..49499c5 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "City of Heroes (.geo)", "author": "TigerKat", - "version": (0, 2, 4), + "version": (0, 2, 5), "blender": (2, 80, 0), "location": "File > Import/Export,", "description": "City of Heroes (.geo)", @@ -10,18 +10,29 @@ bl_info = { "warning": "", "category": "Import-Export"} -if "bpy" in locals(): - import importlib - if "import_geo" in locals(): - importlib.reload(import_geo) - if "export_geo" in locals(): - importlib.reload(export_geo) - if "geo" in locals(): - importlib.reload(geo) - if "geomesh" in locals(): - importlib.reload(geomesh) - if "vec_math" in locals(): - importlib.reload(vec_math) +def check_reload(): + if "bpy" in locals(): + import importlib + if "import_geo" in locals(): + importlib.reload(import_geo) + if "import_skel" in locals(): + importlib.reload(import_skel) + if "import_anim" in locals(): + importlib.reload(import_anim) + if "export_geo" in locals(): + importlib.reload(export_geo) + if "export_skel" in locals(): + importlib.reload(export_skel) + if "export_anim" in locals(): + importlib.reload(export_anim) + if "geo" in locals(): + importlib.reload(geo) + if "geomesh" in locals(): + importlib.reload(geomesh) + if "vec_math" in locals(): + importlib.reload(vec_math) +check_reload() + ## Python doesn't reload package sub-modules at the same time as __init__.py! #import os.path #import imp, sys @@ -68,6 +79,28 @@ class ImportGeoMetric(bpy.types.Operator, ImportHelper): keywords = self.as_keywords(ignore=("filter_glob",)) return import_geo.load(self, context, 0.30480000376701355, **keywords) +class ImportSkel(bpy.types.Operator, ImportHelper): + bl_idname = "import_scene.geo_skel" + bl_label = "Import GEO Skeleton" + + filename_ext = ".anim" + filter_glob = StringProperty(default="skel_*.anim", options={'HIDDEN'}) + def execute(self, context): + from . import import_skel + keywords = self.as_keywords(ignore=("filter_glob",)) + return import_skel.load(self, context, 1.0, **keywords) + +class ImportAnim(bpy.types.Operator, ImportHelper): + bl_idname = "import_scene.geo_anim" + bl_label = "Import GEO Animation" + + filename_ext = ".anim" + filter_glob = StringProperty(default="*.anim", options={'HIDDEN'}) + def execute(self, context): + from . import import_anim + keywords = self.as_keywords(ignore=("filter_glob",)) + return import_skel.load(self, context, 1.0, **keywords) + class ExportGeo(bpy.types.Operator, ExportHelper): bl_idname = "export_scene.geo" bl_label = "Export GEO" @@ -94,17 +127,51 @@ class ExportGeoMetric(bpy.types.Operator, ExportHelper): )) return export_geo.save(self, context, 1.0 / 0.30480000376701355, **keywords) +class ExportSkel(bpy.types.Operator, ExportHelper): + bl_idname = "export_scene.geo_skel" + bl_label = "Export GEO Skeleton" + + filename_ext = ".anim" + filter_glob = StringProperty(default="skel_*.anim", options={'HIDDEN'}) + def execute(self, context): + from . import export_skel + keywords = self.as_keywords(ignore=("filter_glob", + "check_existing", + )) + return export_skel.save(self, context, 1.0, **keywords) + +class ExportAnim(bpy.types.Operator, ExportHelper): + bl_idname = "export_scene.geo_anim" + bl_label = "Export GEO Animation" + + filename_ext = ".anim" + filter_glob = StringProperty(default="*.anim", options={'HIDDEN'}) + def execute(self, context): + from . import export_anim + keywords = self.as_keywords(ignore=("filter_glob", + "check_existing", + )) + return export_anim.save(self, context, 1.0, **keywords) + def menu_func_import(self, context): self.layout.operator(ImportGeo.bl_idname, text="City of Heroes (Feet) (.geo)") self.layout.operator(ImportGeoMetric.bl_idname, text="City of Heroes (Meters) (.geo)") + self.layout.operator(ImportSkel.bl_idname, + text="City of Heroes Skeleton (skel_*.anim)") + #self.layout.operator(ImportAnim.bl_idname, + # text="City of Heroes Animation (.anim)") def menu_func_export(self, context): self.layout.operator(ExportGeo.bl_idname, text="City of Heroes (Feet) (.geo)") self.layout.operator(ExportGeoMetric.bl_idname, text="City of Heroes (Meters) (.geo)") + #self.layout.operator(ExportSkel.bl_idname, + # text="City of Heroes Skeleton (skel_*.anim)") + #self.layout.operator(ExportAnim.bl_idname, + # text="City of Heroes Animation (.anim)") def make_annotations(cls): """Converts class fields to annotations if running with Blender 2.8""" @@ -122,8 +189,12 @@ def make_annotations(cls): classes = ( ImportGeo, ImportGeoMetric, + ImportSkel, + ImportAnim, ExportGeo, - ExportGeoMetric + ExportGeoMetric, + ExportSkel, + ExportAnim, ) def register(): #bpy.utils.register_module(__name__) diff --git a/anim.py b/anim.py index 6fa2661..1f911ac 100644 --- a/anim.py +++ b/anim.py @@ -3,7 +3,7 @@ import sys try: from .bones import * - from .util import Data + from .util import * from .geomesh import GeoMesh, GeoFace, GeoVertex from .compression_anim import * except: @@ -46,7 +46,7 @@ except: #20 - #SkeletonHeirarchy -#0 4 (int) heirarchy_root ?? First bone in the heirarchy. +#0 4 (int) heirarchy_root ?? First bone in the hierarchy. #4 12*100 (BoneLink[BONES_ON_DISK]) #1204 - #BONES_ON_DISK = 100 @@ -93,13 +93,16 @@ class SkeletonHierarchy: def __init__(self): self.root = -1 self.bones = [] - def parseData(self, data): + def parseData(self, data, size = None): self.root = data.decode(" 0: - self.data.seek(self.header_skeleton_heirarchy_offset) - self.skeleton_heirarchy = SkeletonHierarchy() - self.skeleton_heirarchy.parseData(self.data) + self.skeleton_hierarchy = None + if self.header_skeleton_hierarchy_offset > 0: + self.data.seek(self.header_skeleton_hierarchy_offset) + self.skeleton_hierarchy = SkeletonHierarchy() + if self.header_skeleton_hierarchy_offset > self.header_bone_tracks_offset: + skel_size = self.header_size - self.header_skeleton_hierarchy_offset + else: + skel_size = self.header_bone_tracks_offset - self.header_skeleton_hierarchy_offset + self.skeleton_hierarchy.parseData(self.data, skel_size) pass def encode(self): self.data = None @@ -281,7 +288,7 @@ class Anim: print("header_bone_track_count: %s" % (self.header_bone_track_count, )) print("header_rotation_compression_type: %s" % (self.header_rotation_compression_type, )) print("header_position_compression_type: %s" % (self.header_position_compression_type, )) - print("header_skeleton_heirarchy_offset: %s" % (self.header_skeleton_heirarchy_offset, )) + print("header_skeleton_hierarchy_offset: %s" % (self.header_skeleton_hierarchy_offset, )) print("header_backup_anim_track: %s" % (self.header_backup_anim_track, )) print("header_load_state: %s" % (self.header_load_state, )) print("header_last_time_used: %s" % (self.header_last_time_used, )) @@ -291,11 +298,39 @@ class Anim: print("Bone Tracks:") for bt in self.bone_tracks: bt.dump() - if self.skeleton_heirarchy is not None: - self.skeleton_heirarchy.dump() + if self.skeleton_hierarchy is not None: + self.skeleton_hierarchy.dump() pass - + def checkSkeletonHierarchy(self): + if self.skeleton_hierarchy is None: + return False + return True + def checkSkeletonBones(self): + if self.skeleton_hierarchy is None: + return False + seen = [] + if self.checkSkeletonBonesBody(self.skeleton_hierarchy.root, seen): + return True + return False + def checkSkeletonBonesBody(self, bone_id, seen): + if bone_id == -1: + return True + if bone_id < -1: + return False + if bone_id in seen: + return False + seen.append(bone_id) + bones = self.skeleton_hierarchy.bones + if bone_id >= len(bones): + return False + if bones[bone_id].boneid != bone_id: + return False + if not self.checkSkeletonBonesBody(bones[bone_id].next, seen): + return False + if not self.checkSkeletonBonesBody(bones[bone_id].child, seen): + return False + return True if __name__ == "__main__": if len(sys.argv) <= 1 or len(sys.argv) > 3: print("Usage:") diff --git a/import_skel.py b/import_skel.py new file mode 100644 index 0000000..7787f64 --- /dev/null +++ b/import_skel.py @@ -0,0 +1,107 @@ +import bpy.path +import bpy +from mathutils import Vector, Quaternion +try: + from .anim import * + from .bones import * +except: + from anim import * + from bones import * + +def import_fix_coord(v): + return Vector((-v[0], -v[2], v[1])) +def import_fix_normal(v): + return Vector(( v[0], v[2], -v[1])) +def import_fix_quaternion(quat): + return Quaternion((quat[3], -quat[0], -quat[2], quat[1])) + +TAIL_LENGTH = 0.05 #0.204377 +TAIL_VECTOR = Vector((0, 1, 0)) + +def getTposeOffset(anim, bone_id): + offset = Vector((0, 0, 0)) + for bt in anim.bone_tracks: + if bt.bone_id == bone_id: + return import_fix_coord(bt.positions[0]) + return offset + +def convertBone(anim, arm_obj, arm_data, bone_id, parent, parent_position, tail_nub): + bone_link = anim.skeleton_hierarchy.bones[bone_id] + #if bone_link.boneid != bone_id: + # raise Exception("") + bone_name = BONES_LIST[bone_id] + + # Create the (edit)bone. + new_bone = arm_data.edit_bones.new(name=bone_name) + new_bone.select = True + new_bone.name = bone_name + + bone_offset = getTposeOffset(anim, bone_id) + bone_position = parent_position + bone_offset + if parent is None: + if tail_nub: + #new_bone.head = bone_offset + TAIL_VECTOR * TAIL_LENGTH + #new_bone.tail = bone_offset + new_bone.head = bone_position + new_bone.tail = bone_position + TAIL_VECTOR * TAIL_LENGTH + else: + raise + pass + else: + new_bone.parent = parent + if tail_nub: + #new_bone.head = bone_offset - TAIL_VECTOR * TAIL_LENGTH + #new_bone.tail = bone_offset + new_bone.head = bone_position + new_bone.tail = bone_position + TAIL_VECTOR * TAIL_LENGTH + else: + raise + + #visit children + if bone_link.next != -1: + convertBone(anim, arm_obj, arm_data, bone_link.next, parent, parent_position, tail_nub) + if bone_link.child != -1: + convertBone(anim, arm_obj, arm_data, bone_link.child, new_bone, bone_position, tail_nub) + + +def convertSkeleton(context, anim): + full_name = anim.header_name.decode("utf-8") + skeleton_name = full_name.split("/")[0] + + #create armature + arm_data = bpy.data.armatures.new(name=skeleton_name) + arm_obj = bpy.data.objects.new(name=skeleton_name, object_data=arm_data) + + # instance in scene + context.view_layer.active_layer_collection.collection.objects.link(arm_obj) + arm_obj.select_set(True) + + # Switch to Edit mode. + context.view_layer.objects.active = arm_obj + is_hidden = arm_obj.hide_viewport + arm_obj.hide_viewport = False # Can't switch to Edit mode hidden objects... + bpy.ops.object.mode_set(mode='EDIT') + + #Traverse tree, creating bones, and position heads and tails. + convertBone(anim, arm_obj, arm_data, anim.skeleton_hierarchy.root, None, Vector(), True) + + #Switch to Object mode. + bpy.ops.object.mode_set(mode='OBJECT') + arm_obj.hide_viewport = is_hidden + + +def load(operator, context, scale = 1.0, filepath = "", global_matrix = None, use_mesh_modifiers = True, ignore_lod = True): + #Load .anim file + fh_in = open(filepath, "rb") + anim = Anim() + anim.loadFromFile(fh_in) + fh_in.close() + + + #Check for a skeleton hierarchy + if not anim.checkSkeletonHierarchy(): + raise Exception("Animation does not have a skeleton. (Or it has errors.)") + #todo: check for a T-pose + convertSkeleton(context, anim) + + return {'FINISHED'}