Added support for importing skeletons from skel_*.anim files.

master
TigerKat 4 years ago
parent ada937ce82
commit 4f8e162b91

@ -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"}
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__)

@ -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("<i")[0]
self.bones = []
for i in range(70): #BONES_ON_DISK):
if size is None:
size = len(data)
count = int(math.floor((size - 4) / 12.0))
for i in range(count): #BONES_ON_DISK):
self.bones.append(BoneLink(data))
def dump(self):
print("SkeletonHierarchy: root: %s (%s)" % (self.root, BONES_LIST[self.root]))
print("SkeletonHierarchy: root: %s (%s) bones: %s" % (self.root, BONES_LIST[self.root], len(self.bones)))
for b in self.bones:
b.dump()
@ -212,7 +215,7 @@ class Anim:
self.header_bone_track_count = 0
self.header_rotation_compression_type = 0
self.header_position_compression_type = 0
self.header_skeleton_heirarchy_offset = 0
self.header_skeleton_hierarchy_offset = 0
self.header_backup_anim_track = 0
self.header_load_state = 0
self.header_last_time_used = 0
@ -220,7 +223,7 @@ class Anim:
self.header_spare_room = b""
self.bone_tracks = []
self.skeleton_heirarchy = []
self.skeleton_hierarchy = []
pass
def loadFromData(self, data):
@ -250,7 +253,7 @@ class Anim:
self.header_bone_track_count = self.data.decode("i")[0]
self.header_rotation_compression_type = self.data.decode("i")[0]
self.header_position_compression_type = self.data.decode("i")[0]
self.header_skeleton_heirarchy_offset = self.data.decode("I")[0]
self.header_skeleton_hierarchy_offset = self.data.decode("I")[0]
self.header_backup_anim_track = self.data.decode("I")[0]
self.header_load_state = self.data.decode("i")[0]
self.header_last_time_used = self.data.decode("f")[0]
@ -262,11 +265,15 @@ class Anim:
self.data.seek(self.header_bone_tracks_offset)
for i in range(self.header_bone_track_count):
self.bone_tracks.append(BoneAnimTrack(self.data.read(20), self.data))
self.skeleton_heirarchy = None
if self.header_skeleton_heirarchy_offset > 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:")

@ -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'}
Loading…
Cancel
Save