From 03f9bf8a147b6f808f1f125cdcbfb48eb0b4b65f Mon Sep 17 00:00:00 2001 From: TigerKat Date: Fri, 9 Apr 2021 00:48:45 +0930 Subject: [PATCH] Added support for writing .anim files, along with exporting both animation and skeletons. --- __init__.py | 12 +-- anim.py | 197 +++++++++++++++++++++++++++++++++++++++++++-- export_anim.py | 212 +++++++++++++++++++++++++++++++++++++++++++++++++ export_skel.py | 12 +++ util.py | 2 + 5 files changed, 422 insertions(+), 13 deletions(-) create mode 100644 export_anim.py create mode 100644 export_skel.py diff --git a/__init__.py b/__init__.py index 28084f9..8214d1d 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "City of Heroes (.geo)", "author": "TigerKat", - "version": (0, 2, 7), + "version": (0, 2, 8), "blender": (2, 80, 0), "location": "File > Import/Export,", "description": "City of Heroes (.geo)", @@ -138,7 +138,7 @@ class ExportSkel(bpy.types.Operator, ExportHelper): keywords = self.as_keywords(ignore=("filter_glob", "check_existing", )) - return export_skel.save(self, context, 1.0, **keywords) + return export_skel.save_skel(self, context, 1.0, **keywords) class ExportAnim(bpy.types.Operator, ExportHelper): bl_idname = "export_scene.geo_anim" @@ -168,10 +168,10 @@ def menu_func_export(self, context): 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)") + 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""" diff --git a/anim.py b/anim.py index 1f911ac..3cdb46d 100644 --- a/anim.py +++ b/anim.py @@ -79,11 +79,18 @@ class BoneLink: return if next is None: (self.child, self.next, self.boneid) = data_or_child.decode(" 1.0: + require_full = True + self.flags = self.flags & ~POSITION_MASK + #Set flags and endode data + self.position_data = b"" + if require_full: + self.flags = self.flags | POSITION_UNCOMPRESS + for pos in self.positions: + self.position_data += struct.pack(" 1.0: + # require_full = True + self.flags = self.flags & ~ROTATION_MASK + #Set flags and endode data + self.rotation_data = b"" + if require_full: + self.flags = self.flags | ROTATION_UNCOMPRESS + for rot in self.rotattions: + self.rotation_data += struct.pack("= len(chn): + rot_s = chn[-1].copy() + else: + rot_s = chn[index].copy() + else: + rot_s = Quaternion() + rot_p.rotate(rot_s) + return rot_p + #rot_s.rotate(rot_p) + #return rot_s + + +def convert_animation(context, arm_obj, arm_data, nla_track, anim, save_skel): + bone_tracks = {} + for nla_strip in nla_track.strips: + #todo: what's the proper way to handle multiple strips? + #Presently later strips will overwrite earlier strips. + #>>> [x.data_path for x in bpy.data.objects['fem'].animation_data.nla_tracks['run'].strips[0].action.fcurves] + #>>> [x.array_index for x in bpy.data.objects['fem'].animation_data.nla_tracks['run'].strips[0].action.fcurves] + for crv in nla_strip.action.fcurves: + #>>> [y.co for x in bpy.data.objects['fem'].animation_data.nla_tracks['run'].strips[0].action.fcurves for y in x.sampled_points] + dp = crv.data_path + idx = crv.array_index + print("crv: %s : %s" % (dp, idx)) + #Naively convert data_path into bone, and transform type + parts_a = dp.split('["') + parts_b = parts_a[1].split('"].') + bone_name = parts_b[0] + transform = parts_b[1] + if bone_name not in bone_tracks: + bone_tracks[bone_name] = {} + bone_track = bone_tracks[bone_name] + if transform == "location": + data_type = Vector + elif transform == "rotation_quaternion": + data_type = Quaternion + else: + #todo: + raise + data_type = None + if transform not in bone_track: + bone_track[transform] = [] + bone_track_channel = bone_track[transform] + for pnt in crv.sampled_points: + k = int(math.floor(pnt.co[0] + 0.5)) + if k < 0: + #ignore samples before 0 + continue + while len(bone_track_channel) <= k: + bone_track_channel.append(data_type()) + bone_track_channel[k][idx] = pnt.co[1] + #print("bone_tracks: %s" % bone_tracks) + print("bone_tracks['Head']: %s" % bone_tracks.get("Head", None)) + #todo: convert FCurve data to track positions and rotations + + #todo: Get bones required for export. + if save_skel: + #If we need to save the skeleton, ensure we have values for the T-pose loaded into bones that haven't been referenced yet. + for bn in arm_data.bones.keys(): + if bn not in bone_tracks: + bone_tracks[bn] = { + "location": [Vector()], + "rotation_quaternion": [Quaternion()], + } + + #todo: trim back tracks that have duplicates on their tail. + for bn, bt in bone_tracks.items(): + #ensure missing channels have a T-pose value. + if "location" not in bt: + bt["location"] = [Vector()] + if "rotation_quaternion" not in bt: + bt["rotation_quaternion"] = [Quaternion()] + #Trim back duplicates at the end of a track. + for cn in ("location", "rotation_quaternion"): + chn = bt[cn] + while len(chn) >= 2: + if chn[-1] == chn[-2]: + chn.pop() + else: + break + + #Convert track positions and rotations into a more convenient form. + for bn, bt in bone_tracks.items(): + bone = arm_data.bones[bn] + #Get position of bone, relative to parent (or armature origin for root bones). + if bone.parent is None: + bone_position = bone.head + else: + bone_position = bone.head + (bone.parent.tail - bone.parent.head) + #print("parent[%s]: %s %s" % (bone.parent.name, bone.parent.head, bone.parent.tail)) + print("bone_position[%s(%s)]: %s" % (bn, bone.name, bone_position)) + bt["net_location"] = [bone_position] + bt["net_rotation_quaternion"] = [] + rot_chn = bt["rotation_quaternion"] + for i in range(len(rot_chn)): + bt["net_rotation_quaternion"].append(getBoneRotation(bone, bone_tracks, i)) + for i in range(1, len(bt["location"])): + if i >= len(bt["net_rotation_quaternion"]): + rot = bt["net_rotation_quaternion"][-1] + else: + rot = bt["net_rotation_quaternion"][i] + pos = bone_position + bt["location"][i] + print(" pos[%s]: %s" % (i, pos)) + pos.rotate(rot) + bt["net_location"].append(pos) + + #print("bone_tracks: %s" % bone_tracks) + + #Store bone track information in the Anim + for bn, bt in bone_tracks.items(): + bat = BoneAnimTrack() + bat.bone_id = BONES_LOOKUP[bn] + bat.rotations = [export_fix_quaternion(x) for x in bt["rotation_quaternion"]] + bat.positions = [export_fix_coord(x) for x in bt["net_location"]] + anim.bone_tracks.append(bat) + + #Save the skeleton (if flagged). + if save_skel: + anim.skeleton_hierarchy = SkeletonHierarchy() + for bn in BONES_LIST: + if bn in arm_data.bones: + bone = arm_data.bones[bn] + if bone.parent is None: + parent_id = None + else: + parent_id = BONES_LOOKUP[bone.parent.name] + bone_id = BONES_LOOKUP[bn] + anim.skeleton_hierarchy.addBone(parent_id, bone_id) + + anim.dump() + pass + +def save(operator, context, scale = 1.0, filepath = "", global_matrix = None, use_mesh_modifiers = True, save_skeleton = False): + #todo: prefer armature name that matches import? + + #Choose the first selected armature as the armature to attach to. + armature = None + for ob in context.selected_objects: + if ob.type == "ARMATURE": + armature_obj = ob + armature = ob.data + break + else: + #If none found try again with all objects. + for ob in bpy.data.objects: + if ob.type == "ARMATURE": + armature_obj = ob + armature = ob.data + break + #todo: + track = None + skel_track = None + #todo: error/warning if multiple tracks are selected. + #todo: error/warning if multiple skel_ tracks are found + #todo: error if no skel_tracks are found + for t in armature_obj.animation_data.nla_tracks: + if t.select: + track = t + if t.name.lower().startswith("skel_"): + skel_track = t + if save_skeleton: + skel_track = track + arm_name = armature_obj.name + track_name = bpy.path.display_name_from_filepath(filepath) + skel_track_name = skel_track.name + + #Get name and base name + anim_name = "%s/%s" % (arm_name, track_name) + anim_base_name = "%s/%s" % (arm_name, skel_track_name) + #todo: warning if anim_name doesn't match file path + + anim = Anim() + anim.header_name = anim_name + anim.header_base_anim_name = anim_base_name + + save_skel = save_skeleton or anim_name == anim_base_name + convert_animation(context, armature_obj, armature, track, anim, save_skel) + + data = anim.saveToData() + fh = open(filepath, "wb") + fh.write(data) + fh.close() + + return {'FINISHED'} diff --git a/export_skel.py b/export_skel.py new file mode 100644 index 0000000..a365c7d --- /dev/null +++ b/export_skel.py @@ -0,0 +1,12 @@ +import bpy.path +import bpy + +try: + from .export_anim import * +except: + from export_anim import * + + + +def save_skel(operator, context, scale = 1.0, filepath = "", global_matrix = None, use_mesh_modifiers = True): + return save(operator, context, scale = scale, filepath = filepath, global_matrix = global_matrix, use_mesh_modifiers = use_mesh_modifiers, save_skeleton = True) diff --git a/util.py b/util.py index 43240d1..6a36384 100644 --- a/util.py +++ b/util.py @@ -1,6 +1,8 @@ import struct import sys +ZERO_BYTE = struct.pack("B", 0) + if sys.version_info[0] < 3: byte = chr unbyte = ord