diff options
Diffstat (limited to 'bck_import.py')
-rw-r--r-- | bck_import.py | 575 |
1 files changed, 575 insertions, 0 deletions
diff --git a/bck_import.py b/bck_import.py new file mode 100644 index 0000000..add03b1 --- /dev/null +++ b/bck_import.py @@ -0,0 +1,575 @@ +# "simple" importer for BCK anim files from SMG +# file format information --> https://humming-owl.neocities.org/smg-stuff/pages/tutorials/bck + +import bpy, math +from . import blender_funcs +from . import bck_funcs +from . import file_ops +from . import math_funcs +import mathutils + +# import BCK animation into the selected armature object +# creates a new "action" and writes the data into that action slot +def import_bck_func(context, filepath, import_type, angle_limit): +# + # this thing is always needed for stuff + scene = bpy.context.scene + + # if no armature is selected end the importer + if (scene.objects.active == None or scene.objects.active.type != 'ARMATURE'): + if (scene.objects.active == None): + blender_funcs.disp_msg("No Armature selected. Select one and try again.") + else: + blender_funcs.disp_msg("No Armature selected. Currently selecting: \"%s\"" + % (scene.objects.active.name)) + return {"FINISHED"} + + # open the binary file and read its data + anim = bck_funcs.read_bck_file(filepath) + print(anim) + if (anim == None): + blender_funcs.disp_msg("Animation file: \"%s\" is malformed." + % (file_ops.get_file_name(filepath))) + return {"FINISHED"} + + # select the armature object + armature = scene.objects.active + # change to object mode + blender_funcs.select_obj(armature, False, "OBJECT") + + # check if the bone count matches (the only check it can be done) + if (len(armature.data.bones) != anim.bone_count): + blender_funcs.disp_msg("Animation file \"%s\" contains incorrect number of bones" + % (file_ops.get_file_name(filepath))) + return {"FINISHED"} + + # check import_type to know what to do + file_name = file_ops.get_file_name(filepath) + + # clear rest pose and import the animation data directly + if (import_type == "OPT_A"): + # + # select the armature object and its children meshes and duplicate it + old_armature = armature + blender_funcs.duplicate_obj(scene, armature) + armature = scene.objects.active + armature.name = old_armature.name + "_cp" + + # clear the rest pose of the object (pose mode) + blender_funcs.select_obj(armature, False, "POSE") + # unlink any actions in the armature and clear the pose + armature.animation_data.action = None + bpy.ops.pose.select_all(action = "SELECT") + bpy.ops.pose.transforms_clear() + for bone in armature.pose.bones: + if (bone.parent != None): + # current bone mat = (first parent mat) * (current bone rest mat) + # first parent mat = (second parent mat) * (first parent bone rest mat) + # second parent mat = (third parent mat) * (second parent bone rest mat) + # ... + rest_mat = bone.parent.matrix.copy().inverted() * bone.matrix.copy() + bone.matrix = bone.matrix.copy() * rest_mat.copy().inverted() + else: # Y up consideration on the very first bone (has no parents) + bone.matrix = math_funcs.calc_rotation_matrix(math.pi / 2, 0, 0) * mathutils.Matrix.Identity(4) + + # apply visual transform to all meshes + # (could the error this makes be fixed? question for future me) + for child in armature.children: + if (child.type == "MESH"): # only to meshes + blender_funcs.select_obj(child, False, "OBJECT") + bpy.ops.object.convert(target = "MESH") + + # apply pose as rest pose + blender_funcs.select_obj(armature, False, "POSE") + bpy.ops.pose.armature_apply() + + # reassign the armature modifiers to all meshes + for mesh in armature.children: + blender_funcs.select_obj(mesh, False, "OBJECT") + bpy.ops.object.modifier_add(type = "ARMATURE") + mesh.modifiers["Armature"].object = armature + mesh.modifiers["Armature"].use_vertex_groups = True + + # select the armature again + blender_funcs.select_obj(armature, False, "OBJECT") + + # create a new animation action for the skeleton + + # if it already exists, eliminate it and create a new one with the same name + for action in bpy.data.actions: + if (action.name == file_name): + bpy.data.actions.remove(action) + break + action = bpy.data.actions.new(name = file_name) + # armature has no animations, create new one, assign created action to it + if (armature.animation_data == None): + armature.animation_data_create() + armature.animation_data.action = action + + # find the lowest and greatest value for the time in the bck animation + lowest_anim_frame = 0 + for i in range(anim.bone_count): + for j in range(9): + if (anim.anim_data[i].comp[j].time[0] != None): + if (anim.anim_data[i].comp[j].time[0] < lowest_anim_frame): + lowest_anim_frame = anim.anim_data[i].comp[j].time[0] + greatest_anim_frame = 0 + for i in range(anim.bone_count): + for j in range(9): + for k in range(anim.anim_data[i].comp[j].kf_count): + if (anim.anim_data[i].comp[j].time[k] != None + and anim.anim_data[i].comp[j].time[k] > greatest_anim_frame): + greatest_anim_frame = anim.anim_data[i].comp[j].time[k] + + # assign the frame_start and frame_end blender scene variables + scene.frame_preview_start = lowest_anim_frame + scene.frame_current = lowest_anim_frame + scene.frame_preview_end = greatest_anim_frame + + # change the rotation mode of the pose bones + for bone in armature.pose.bones: + bone.rotation_mode = "XYZ" + + # get the animation data into all the bones + for i in range(anim.bone_count): + # the bone and its animation data + bone = armature.data.bones[i] # "data.bones" contains the right index order + bone_anim = anim.anim_data[i] + for j in range(9): # scale/rot/transl x/y/z + comp_string = None + comp_index = None + if (j == 0 or j == 3 or j == 6): # scale + comp_string = "scale" + comp_index = int(j / 3) + elif (j == 1 or j == 4 or j == 7): # rot + comp_string = "rotation_euler" + comp_index = int((j - 1) / 3) + elif (j == 2 or j == 5 or j == 8): # transl + comp_string = "location" + comp_index = int((j - 2) / 3) + + # create the component fcurve + data_path = "pose.bones[\"%s\"].%s" % (bone.name, comp_string) + fcurve = action.fcurves.new(data_path, comp_index, bone.name) + + # assign the keyframes + for k in range(bone_anim.comp[j].kf_count): + # convert translation values to blenxy's coordinate space + comp_value = bone_anim.comp[j].value[k] + if (j == 2 or j == 5 or j == 8): + comp_value /= 100 + # create the keyframe + if (bone_anim.comp[j].time[k] == None): # consider 1 keyframe animation tracks + fcurve.keyframe_points.insert(lowest_anim_frame, comp_value) + else: + fcurve.keyframe_points.insert(bone_anim.comp[j].time[k], comp_value) + + # adjust slopes (in one keyframe point look left and right) + for k in range(bone_anim.comp[j].kf_count): + + # change the keyframe point handles type + fcurve.keyframe_points[k].handle_left_type = "FREE" + fcurve.keyframe_points[k].handle_right_type = "FREE" + + # transform the translation into the right system + comp_in_slope = bone_anim.comp[j].in_slope[k] + comp_out_slope = bone_anim.comp[j].out_slope[k] + if (j == 2 or j == 5 or j == 8): + if (bone_anim.comp[j].kf_count > 1): + comp_in_slope = comp_in_slope / 100 + comp_out_slope = comp_out_slope / 100 + + # in slope, assign in-slopes only to keyframes after the first one + if (k != 0): + # get the time difference between this frame and the previous one + time_dif = fcurve.keyframe_points[k].co[0] - fcurve.keyframe_points[k - 1].co[0] + # compute the in slope vector angle + x_comp = -(time_dif / 3) + y_comp = x_comp * comp_in_slope + vec_slope = fcurve.keyframe_points[k].co.copy() + mathutils.Vector((x_comp, y_comp)) + fcurve.keyframe_points[k].handle_left = vec_slope + + # out slope, assign out-slopes only to keyframes before the last one + if (k != bone_anim.comp[j].kf_count - 1): + # get the time difference between this frame and the next one + time_dif = fcurve.keyframe_points[k + 1].co[0] - fcurve.keyframe_points[k].co[0] + # compute the out slope vector angle + x_comp = time_dif / 3 + y_comp = x_comp * comp_out_slope + vec_slope = fcurve.keyframe_points[k].co.copy() + mathutils.Vector((x_comp, y_comp)) + fcurve.keyframe_points[k].handle_right = vec_slope + + # mantain rest pose and import the animation data respect to rest pose + # "OPT_B" --> sample everything + # "OPT_C" --> sample everything and find "best" interpolator fit + elif (import_type == "OPT_B" or import_type == "OPT_C"): + + # its matrix time + + # create a new animation action for the skeleton + # if it already exists, eliminate it and create a new one with the same name + for action in bpy.data.actions: + if (action.name == file_name): + bpy.data.actions.remove(action) + break + action = bpy.data.actions.new(name = file_name) + # armature has no animations, create new one, assign created action to it + if (armature.animation_data == None): + armature.animation_data_create() + armature.animation_data.action = action + + # find the lowest and greatest value for the time in the bck animation + lowest_anim_frame = 0 + greatest_anim_frame = 0 + for i in range(anim.bone_count): + for j in range(9): + if (anim.anim_data[i].comp[j].kf_count > 1): + if (anim.anim_data[i].comp[j].time[0] < lowest_anim_frame): + lowest_anim_frame = anim.anim_data[i].comp[j].time[0] + if (anim.anim_data[i].comp[j].time[-1] > greatest_anim_frame): + greatest_anim_frame = anim.anim_data[i].comp[j].time[-1] + + # round with ceil (hopefully the best choice? might think about this later) + lowest_anim_frame = math.ceil(lowest_anim_frame) + greatest_anim_frame = math.ceil(greatest_anim_frame) + + # assign the frame_start and frame_end blender scene variables + scene.frame_preview_start = lowest_anim_frame + scene.frame_current = lowest_anim_frame + scene.frame_preview_end = greatest_anim_frame + + # change the rotation mode of the pose bones + for pose_bone in armature.pose.bones: + pose_bone.rotation_mode = "XYZ" + + # get the animation data into all the bones + for i in range(anim.bone_count): + # the bone and its animation data + data_bone = armature.data.bones[i] # "data.bones" contains the right index order + pose_bone = armature.pose.bones[data_bone.name] # pose bones contain the data from the rest pose + bone_anim = anim.anim_data[i] + + # get the transformation matrix from parent to child (rest mat) + # do not use the matrix from pose bones because that is the one that will be animated + rest_mat = None + if (pose_bone.parent != None): + # current bone mat = (first parent mat) * (current bone rest mat) + # first parent mat = (second parent mat) * (first parent bone rest mat) + # second parent mat = (third parent mat) * (second parent bone rest mat) + # ... + rest_mat = data_bone.parent.matrix_local.copy().inverted() * data_bone.matrix_local.copy() + else: # Y up consideration on the very first bone (has no parents) + rest_mat = mathutils.Matrix.Identity(4) + + # need to include keyframes not for each component but rather for each of the XYZ groups + # the coordinate system change can turn a simple X translation into a XYZ translation in the other system + + # get the last animation frame in which values actually change + first_anim_frame = 0 + last_anim_frame = 0 + for j in range(9): + # one keyframe animations skipped + if (bone_anim.comp[j].kf_count > 1): + if (bone_anim.comp[j].time[0] < first_anim_frame): + first_anim_frame = bone_anim.comp[j].time[0] + if (bone_anim.comp[j].time[-1] > last_anim_frame): + last_anim_frame = bone_anim.comp[j].time[-1] + # will adjust the keyframe times to be integers + # the method (for now) is to round to the nearest integer with math.ceil + last_anim_frame = math.ceil(last_anim_frame) + print("first and last keyframe frames: %s %s" % (first_anim_frame, last_anim_frame)) + + # iterate over the actual animation frames + # get the initial animation values + scale = [bone_anim.comp[0].value[0], bone_anim.comp[3].value[0], bone_anim.comp[6].value[0]] + rot = [bone_anim.comp[1].value[0], bone_anim.comp[4].value[0], bone_anim.comp[7].value[0]] + transl = [bone_anim.comp[2].value[0] / 100, + bone_anim.comp[5].value[0] / 100, + bone_anim.comp[8].value[0] / 100] + # declare the current bone's fcurves list + fcurves = [None, None, None, # scale XYZ + None, None, None, # rot XYZ + None, None, None] # transl XYZ + + # iterate over the frames of the animation + for j in range(lowest_anim_frame, last_anim_frame + 1): + + # check which keyframe components need to be added + is_scale_anim = False + is_rot_anim = False + is_transl_anim = False + new_scale = None + new_rot = None + new_transl = None + + # get the animation data for the current bone + + # get the animation transformation matrix + if (j == lowest_anim_frame): # all the animation components on the first frame are set + is_scale_anim = True + is_rot_anim = True + is_transl_anim = True + # create the components fcurves (all of them) + data_path = "pose.bones[\"%s\"].scale" % (data_bone.name) + fcurves[0] = action.fcurves.new(data_path, 0, data_bone.name) + fcurves[1] = action.fcurves.new(data_path, 1, data_bone.name) + fcurves[2] = action.fcurves.new(data_path, 2, data_bone.name) + data_path = "pose.bones[\"%s\"].rotation_euler" % (data_bone.name) + fcurves[3] = action.fcurves.new(data_path, 0, data_bone.name) + fcurves[4] = action.fcurves.new(data_path, 1, data_bone.name) + fcurves[5] = action.fcurves.new(data_path, 2, data_bone.name) + data_path = "pose.bones[\"%s\"].location" % (data_bone.name) + fcurves[6] = action.fcurves.new(data_path, 0, data_bone.name) + fcurves[7] = action.fcurves.new(data_path, 1, data_bone.name) + fcurves[8] = action.fcurves.new(data_path, 2, data_bone.name) + + # for any other frame + else: + + # check if the bone has more frames to animate in the 9 components + for k in range(9): + + # interpolation result + result = 0 + + # 1 keyframe animations are skipped + if (bone_anim.comp[k].kf_count == 1): + continue + + # iterate over the keyframes on the component + for l in range(bone_anim.comp[k].kf_count): + # frame is exactly the last keyframe of the component + if (l == bone_anim.comp[k].kf_count - 1): + result = bone_anim.comp[k].value[-1] + break + # frame to convert is in between the keyframes + elif (j >= bone_anim.comp[k].time[l] + and j < bone_anim.comp[k].time[l + 1]): + # interpolate to find the result and add it as a new keyframe + p0 = bone_anim.comp[k].value[l] + m0 = bone_anim.comp[k].out_slope[l] + m1 = bone_anim.comp[k].in_slope[l + 1] + p1 = bone_anim.comp[k].value[l + 1] + t0 = bone_anim.comp[k].time[l] + tf = bone_anim.comp[k].time[l + 1] + t = j + result = math_funcs.cubic_hermite_spline_time_general(p0, m0, m1, p1, t0, tf, t) + break + # animation for this particular component has ended + elif (j > bone_anim.comp[k].time[-1]): + result = None + break + + # ~ print("interpolation result: %s" % (result)) + + # assign to the respective component + if (result != None): + if (k == 0 or k == 3 or k == 6): # scale + is_scale_anim = True + scale[int((k - 0) / 3)] = result + elif (k == 1 or k == 4 or k == 7): # rot + is_rot_anim = True + rot[int((k - 1) / 3)] = result + elif (k == 2 or k == 5 or k == 8): # transl + is_transl_anim = True + transl[int((k - 2) / 3)] = result / 100 + + # ~ print("frame S R T: %d %s %s %s" %(j, is_scale_anim, is_rot_anim, is_transl_anim)) + # ~ print(scale) + # ~ print(rot) + # ~ print(transl) + # convert the just got frame anim values to be rest pose relative + mat = rest_mat.inverted() * math_funcs.calc_transf_mat(scale, rot, transl) + # extract the new animation data + new_scale = mat.to_scale() + new_rot = mat.to_euler("XYZ") + new_transl = mat.to_translation() + + # check if new data must be added + if (is_scale_anim == False + and is_rot_anim == False + and is_transl_anim == False): + break + + # assign the keyframes were needed and set the bezier handles to 0 + if (is_scale_anim == True): + for k in range(3): + tmp = fcurves[k].keyframe_points.insert(j, new_scale[k]) + tmp.handle_left_type = "FREE" + tmp.handle_right_type = "FREE" + tmp.handle_left = tmp.co + tmp.handle_right = tmp.co + if (is_rot_anim == True): + for k in range(3): + tmp = fcurves[3 + k].keyframe_points.insert(j, new_rot[k]) + tmp.handle_left_type = "FREE" + tmp.handle_right_type = "FREE" + tmp.handle_left = tmp.co + tmp.handle_right = tmp.co + if (is_transl_anim == True): + for k in range(3): + tmp = fcurves[6 + k].keyframe_points.insert(j, new_transl[k]) + tmp.handle_left_type = "FREE" + tmp.handle_right_type = "FREE" + tmp.handle_left = tmp.co + tmp.handle_right = tmp.co + + # reset the just checked variables for the next frame + is_scale_anim = False + is_rot_anim = False + is_transl_anim = False + + # remove the fcurve keyframe points that are constant (some cleanup by yours truly) + # don't want to use blender's built-in action.clean() method cuz it resets the bezier keyframe handles + for fcurve in action.fcurves: + if (len(fcurve.keyframe_points) == 1): # nothing to clean (are you sure about that?) + continue + else: + # check if a keyframe has the same value as its 2 closest keyframes + old_value = None + new_value = None + j = 0 + while (j < len(fcurve.keyframe_points)): + if (fcurve.keyframe_points[j].co[0] == lowest_anim_frame): + old_value = fcurve.keyframe_points[j].co[1] + else: + cur_value = fcurve.keyframe_points[j].co[1] + # remove the current keyframe if the next one is the same + # or if it is the last keyframe on the fcurve + if (cur_value == old_value): + if ((j < len(fcurve.keyframe_points) - 1 and cur_value == fcurve.keyframe_points[j + 1].co[1]) + or (j == len(fcurve.keyframe_points) - 1)): + fcurve.keyframe_points.remove(fcurve.keyframe_points[j]) + j -= 1 + old_value = cur_value + j += 1 + + # now, if this has not been hard enough... + # find the "best" interpolator fits to be able to simplify the keyframe count + if (import_type == "OPT_C"): + + # lets start + for fcurve in action.fcurves: + + # skip 1 frame animations + if (len(fcurve.keyframe_points) == 1): + continue + + # get all the sampled points data into a list and then pass it into the "find best fit" function + values = [] + for i in range(lowest_anim_frame, greatest_anim_frame + 1): + values.append(fcurve.evaluate(i)) + print(fcurve.data_path) + new_kfs = math_funcs.find_best_cubic_hermite_spline_fit(lowest_anim_frame, values, angle_limit) + print(new_kfs) + + # remove all keyframe points and add the new ones + while (len(fcurve.keyframe_points) != 0): + fcurve.keyframe_points.remove(fcurve.keyframe_points[0]) + + # add the new keyframe points + for i in range(new_kfs.kf_count): + fcurve.keyframe_points.insert(new_kfs.time[i], new_kfs.value[i]) + + # manage the handles + for i in range(len(fcurve.keyframe_points)): + kf_point = fcurve.keyframe_points[i] + kf_point.handle_left_type = "FREE" + kf_point.handle_right_type = "FREE" + # assign the handles + left = kf_point.co.copy() + right = kf_point.co.copy() + # left handle + if (i > 0): + time_dif = new_kfs.time[i] - new_kfs.time[i - 1] + x_comp = -(time_dif / 3) + y_comp = x_comp * new_kfs.in_slope[i] + left = kf_point.co.copy() + mathutils.Vector((x_comp, y_comp)) + # right handle + if (i < new_kfs.kf_count - 1): + time_dif = new_kfs.time[i + 1] - new_kfs.time[i] + x_comp = (time_dif / 3) + y_comp = x_comp * new_kfs.out_slope[i] + right = kf_point.co.copy() + mathutils.Vector((x_comp, y_comp)) + + # assign the handles + kf_point.handle_left = left + kf_point.handle_right = right + + # return to object mode + bpy.ops.object.mode_set(mode = "OBJECT") + + # store the loop mode in a custom property of the armature object + if ("loop_mode" in armature.data): + # check if nothing touched the type of this property + import idprop + if (type(armature.data["loop_mode"]) != idprop.types.IDPropertyGroup): + armature.data["loop_mode"] = {file_ops.get_file_name(filepath) : anim.loop_mode} + else: + armature.data["loop_mode"][file_ops.get_file_name(filepath)] = anim.loop_mode + else: + armature.data["loop_mode"] = {file_ops.get_file_name(filepath) : anim.loop_mode} + + # display some message + blender_funcs.disp_msg("Animation file \"%s\" imported." % (file_ops.get_file_name(filepath))) + # done! + return {"FINISHED"} + +# Stuff down is for the menu appending +# of the importer to work plus some setting stuff +# comes from a Blender importer template + +from bpy_extras.io_utils import ExportHelper +from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty, IntProperty +from bpy.types import Operator + +# import_bck class +class import_bck(Operator, ExportHelper): + """Import the animation data from a SMG BCK file""" + # stuff for blender + bl_idname = "import_scene.bck" + bl_label = "Import BCK (SMG)" + filename_ext = ".bck" + filter_glob = StringProperty(default = "*.bck", + options = {"HIDDEN"}, + maxlen = 255) + + # importer options + + # import mode + import_type = EnumProperty( + name = "Import Mode", + description = "Way in which the animation will be imported", + default = "OPT_C", + items = ( + ("OPT_A", "Ignore Rest Pose", "Selected armature will be duplicated and its rest pose cleared. Animation data will be applied to it exactly as described by the BCK. For some reason, some model meshes will be deformed weirdly (Floating point limitations? Mesh vertex deform in a non reversible way?)"), + ("OPT_B", "Keep Rest Pose - Sample Everything", "Animation will be written into the selected armature respect to its rest pose and it will be sampled for each frame of the animation. Some cleanup will be done at the end"), + ("OPT_C", "Keep Rest Pose - Find Best Interpolator", "Same as \"Keep Rest Pose and Sample everything\" but after sampling everything there will be attempts to find the best interpolator expressions for all animation tracks. This will take more time") + ) + ) + angle_limit = FloatProperty( + name = "Derivative angle limit", + description = "Value used to specify a keyframe generation at curve points at which sudden slope changes occur. Useful to adjust several straight lines. The angle comes from scaling the vertical axis of the animation track so that the \"visual derivative changes\" become visible. For Import Mode \"Keep Rest Pose - Find Best Interpolator\"", + default = 45, + min = 0, + max = 180, + ) + + # what the importer actually does + def execute(self, context): + return import_bck_func(context, + self.filepath, + self.import_type, + self.angle_limit) + +# stuff to append the item to the File -> Import/Export menu +def menu_import_bck(self, context): + self.layout.operator(import_bck.bl_idname, text = "BCK from SMG (.bck)") + +bpy.utils.register_class(import_bck) +bpy.types.INFO_MT_file_import.append(menu_import_bck) + +# test call +bpy.ops.import_scene.bck('INVOKE_DEFAULT') + + |