diff --git a/README.md b/README.md index c8c43b0..a089fbf 100644 --- a/README.md +++ b/README.md @@ -38,13 +38,23 @@ Note: Presently LOD models are excluded from imports. ### Importing skeletons -1. From the menu select File->Import->"City of Heroes (Feet) (skel_*.anim). +1. From the menu select File->Import->"City of Heroes Skeleton (skel_*.anim). 2. Browse to the file you want to import and click "Import". Note: A new aramature will be created using the name found inside the file. Note: The import will fail if the .anim file is missing a skeleton hierarchy. +### Importing animations + +1. From the menu select File->Import->"City of Heroes Animation (*.anim). +2. Browse to the file you want to import and click "Import". + +Note: It will import as a NLA track to the currently selected armature. + +Note: If bone in the animation is missing from armature, the import will fail. +Note: If there is an existing animation in the armature with the same name as what you're trying to import, the import will fail. Rename your existing animation, and retry the import. + # Tools These command line tools allow inspection and modification of .geo files. diff --git a/__init__.py b/__init__.py index 49499c5..186d509 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "City of Heroes (.geo)", "author": "TigerKat", - "version": (0, 2, 5), + "version": (0, 2, 6), "blender": (2, 80, 0), "location": "File > Import/Export,", "description": "City of Heroes (.geo)", @@ -99,7 +99,7 @@ class ImportAnim(bpy.types.Operator, ImportHelper): def execute(self, context): from . import import_anim keywords = self.as_keywords(ignore=("filter_glob",)) - return import_skel.load(self, context, 1.0, **keywords) + return import_anim.load(self, context, 1.0, **keywords) class ExportGeo(bpy.types.Operator, ExportHelper): bl_idname = "export_scene.geo" @@ -160,8 +160,8 @@ def menu_func_import(self, context): 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)") + self.layout.operator(ImportAnim.bl_idname, + text="City of Heroes Animation (.anim)") def menu_func_export(self, context): self.layout.operator(ExportGeo.bl_idname, diff --git a/import_anim.py b/import_anim.py new file mode 100644 index 0000000..7f62373 --- /dev/null +++ b/import_anim.py @@ -0,0 +1,190 @@ +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])) + + +def getBoneLength(arm_data, bone_name): + bl = arm_data.bones[bone_name] + if bl.parent is None: + return bl.head + +def getBoneRotation(bone, bone_trk_lookup, trk_rot_list, index): + if bone is None: + #rot_p = Quaternion() + return Quaternion() + else: + rot_p = getBoneRotation(bone.parent, bone_trk_lookup, trk_rot_list, index) + if bone.name in bone_trk_lookup: + trk_index = bone_trk_lookup[bone.name] + rot_list = trk_rot_list[trk_index] + if index >= len(rot_list): + rot_s = rot_list[-1].copy() + else: + rot_s = rot_list[index].copy() + else: + rot_s = Quaternion() + rot_p.rotate(rot_s) + return rot_p + #rot_s.rotate(rot_p) + #return rot_s +def convertAnimation(context, arm_obj, arm_data, anim, rescale = True): + full_name = anim.header_name.decode("utf-8") + anim_name = full_name.split("/")[1].lstrip("skel_") + #todo: get all bones used in animation, and maximum length + max_frames = 0 + bone_ids = [] + bone_names = [] + bone_trk_lengths = [] + bone_arm_lengths = [] + bone_scales = [] + bone_trk_lookup = {} + for i, bt in enumerate(anim.bone_tracks): + #get maximum frame count + max_frames = max(max_frames, len(bt.positions), len(bt.rotations)) + #get IDs and names + bone_id = bt.bone_id + bone_name = BONES_LIST[bone_id] + bone_trk_lookup[bone_name] = i + bone_ids.append(bone_id) + bone_names.append(bone_name) + #get animation's T-pose length + bone_trk_len = Vector(bt.positions[0]).length + bone_trk_lengths.append(bone_trk_len) + #get armature's T-pose length + bone_arm_len = getBoneLength(arm_data, bone_name) + bone_arm_lengths.append(bone_arm_len) + #determine scale + bone_scale = rescale and (bone_arm_len / bone_trk_len) or (1.0) + bone_scales.append(bone_scale) + + #todo: create animation + if arm_obj.animation_data is None: + arm_obj.animation_data_create() + if anim_name in arm_obj.animation_data.nla_tracks: + #todo: resolve this + nla_track = arm_obj.animation_data.nla_tracks[anim_name] + pass + else: + nla_track = arm_obj.animation_data.nla_tracks.new() + nla_track.name = anim_name + trk_pos_list = [] + trk_rot_list = [] + for i, bt in enumerate(anim.bone_tracks): + #pos_start = (len(bt.positions) > 1) and 1 or 0 + pos_start = 0 + pos_stop = len(bt.positions) + #rot_start = (len(bt.rotations) > 1) and 1 or 0 + rot_start = 0 + rot_stop = len(bt.rotations) + pos_list = [] + rot_list = [] + for j in range(pos_start, pos_stop): + v = import_fix_coord(bt.positions[j]) + pos_list.append(v) + pass + for j in range(rot_start, rot_stop): + v = import_fix_quaternion(bt.rotations[j]) + rot_list.append(v) + pass + trk_pos_list.append(pos_list) + trk_rot_list.append(rot_list) + action = bpy.data.actions.new(anim_name) + action.use_fake_user = True + nla_strip = nla_track.strips.new(anim_name, 0, action) + nla_strip.action_frame_start = 0 + nla_strip.action_frame_end = max_frames + + for i, bt in enumerate(anim.bone_tracks): + bone_name = bone_names[i] + bone = arm_data.bones[bone_name] + pose_bone = arm_obj.pose.bones[bone_name] + #pos_start = (len(bt.positions) > 1) and 1 or 0 + pos_start = 0 + pos_stop = len(bt.positions) + #rot_start = (len(bt.rotations) > 1) and 1 or 0 + rot_start = 0 + rot_stop = len(bt.rotations) + pos_list = trk_pos_list[i] + rot_list = trk_rot_list[i] + props = [(pose_bone.path_from_id("location"), 3, bone_name), #"Location"), + (pose_bone.path_from_id("rotation_quaternion"), 4, bone_name), #"Quaternion Rotation"), + #(pose_bone.path_from_id("rotation_axis_angle"), 4, "Axis Angle Rotation"), + #(pose_bone,path_from_id("rotatin_euler"), 3, "Euler Rotation"), + #(pose_bone.path_from_id("scale"), 3, "Scale"), + ] + curves = [action.fcurves.new(prop, index = cidx, action_group = agrp) + for prop, channel_count, agrp in props + for cidx in range(channel_count)] + pos_curves = curves[0:3] + rot_curves = curves[3:7] + for j, pos in enumerate(pos_list): + #Remove the bone component from the position. + if bone.parent is None: + #Only compute it for root bones. + rot = getBoneRotation(bone.parent, bone_trk_lookup, trk_rot_list, j) + #rot.invert() + pos0 = pos_list[0].copy() + pos0.rotate(rot) + l = (pos - pos0).length + if l >= 0.001:# and (bone.name in ["Hips", "Waist","Chest"]): + print("%s, %s: %s: pos: %s : %s ::: %s : %s" % (bone.name, j, l, pos, pos0, pos_list[0], rot)) + pos = pos - pos0 + if l < 0.001: + #Distance is close to zero, force position adjustment to zero. + pos = Vector() + else: + #assume 0,0,0 correction in other nodes. + pos = Vector() + for k, crv in enumerate(pos_curves): + crv.keyframe_points.insert(j, pos[k], options={'NEEDED', 'FAST'}).interpolation = 'LINEAR' + for j, rot in enumerate(rot_list): + for k, crv in enumerate(rot_curves): + crv.keyframe_points.insert(j, rot[k], options={'NEEDED', 'FAST'}).interpolation = 'LINEAR' + for crv in curves: + crv.update() + + #todo: create animation + + #todo: iterate over bones and generate frames data + + #todo: delete mid points for simple motions? + pass + +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() + + #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 + convertAnimation(context, armature_obj, armature, anim, False) + + return {'FINISHED'}