From ef4d0e08b2d312bcf3034aa3ae48436f5d8b56a5 Mon Sep 17 00:00:00 2001 From: Owl Date: Thu, 21 Aug 2025 20:07:13 -0400 Subject: all the stuff --- bck_export.py | 230 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 bck_export.py (limited to 'bck_export.py') diff --git a/bck_export.py b/bck_export.py new file mode 100644 index 0000000..9d8576c --- /dev/null +++ b/bck_export.py @@ -0,0 +1,230 @@ +# "simple" exporter 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 + +# export BCK animation from the selected armature object +def export_bck_func(options, context): + + # this thing is always needed for stuff + scene = context.scene + + # checking stage + + # if no armature is selected + if (scene.objects.active == None): + blender_funcs.disp_msg("No Armature selected. Select one and try again.") + return {"FINISHED"} + elif (scene.objects.active.type != "ARMATURE"): + blender_funcs.disp_msg("No Armature selected. Currently selecting: \"%s\"" % (scene.objects.active.name)) + return {"FINISHED"} + + # select the armature object + armature = scene.objects.active + blender_funcs.select_obj(armature, False, "OBJECT") + + # if the armature has no bones (lmao) + if (len(armature.data.bones) == 0): + blender_funcs.disp_msg("Armature selected \"%s\" does not have any bones." % (armature.name)) + return {"FINISHED"} + + # if the armature has no animation data + if (armature.animation_data == None + or armature.animation_data.action == None): + blender_funcs.disp_msg("Armature selected \"%s\" does not have an animation active." % (armature.name)) + return {"FINISHED"} + + # start gathering the animation information + bck_anim = bck_funcs.smg_bck_anim() + + # loop mode (dark python string magic - ascii math) + bck_anim.loop_mode = options.loop_mode.encode()[-1] - "A".encode()[0] + bck_anim.anim_length = options.anim_length + bck_anim.bone_count = len(armature.data.bones) + for i in range(bck_anim.bone_count): + # append the bone component animation data + bck_anim.anim_data.append(bck_funcs.smg_bck_anim.anim_data()) + + # start getting the actual animation data + for i in range(len(armature.data.bones)): + data_bone = armature.data.bones[i] # correct bone index order + pose_bone = armature.pose.bones[data_bone.name] # pose matrix is got from here + + # check if the bone has animation data (1 or more keyframes) + # if not just add its rest pose value to the structure + + # gather the existing fcurves for a bone + bone_fcurves = [None, None, None, None, None, None, None, None, None] + # ^ sx, rx, tx, sy (24!), ry, ty, sz, rz and tz in that order (bck order) + bone_data_path_str = "pose.bones[\"%s\"]." % (data_bone.name) + for fcurve in armature.animation_data.action: + if (fcurve.data_path == bone_data_path_str + "scale"): + bone_fcurves[0 + fcurve.array_index] = fcurve + elif (fcurve.data_path == bone_data_path_str + "rotation_euler"): + bone_fcurves[3 + fcurve.array_index] = fcurve + elif (fcurve.data_path == bone_data_path_str + "location"): + bone_fcurves[6 + fcurve.array_index] = fcurve + + # generate all the animation points, interpolation stuff will be done later + + # get the rest pose matrix + rest_mat = data_bone.matrix_local.copy() + if (pose_bone.parent != None): + rest_mat = data_bone.parent.matrix_local.copy().inverted() * mat.copy() + + # get the points on all frames, only the points + for j in range(bck_anim.anim_length): + # find the values respect to rest pose + scale = [1, 1, 1] + rot = [0, 0, 0] + transl = [0, 0, 0] + + # all components + for k in range(9): + # components with fcurve + if (len(bone_fcurves[k].keyframe_points) >= 1): + value = bone_fcurves[k].evaluate(options.first_frame + j) + # check which is the component to get + if (k == 0 or k == 3 or k == 6): + scale[k / 3] = value + elif (k == 1 or k == 4 or k == 7): + rot[(k - 1) / 3] = value + elif (k == 2 or k == 5 or k == 8): + trans[(k - 2) / 3] = value + + # convert the values to be respect to parent + new_mat = math_funcs.calc_transf_mat(scale, rot, transl).copy() * rest_mat.copy() + for k in range(9): + value = None + # check which is the component to get + if (k == 0 or k == 3 or k == 6): + value = new_mat.to_scale()[k / 3] + elif (k == 1 or k == 4 or k == 7): + value = new_mat.to_euler("XYZ")[(k - 1) / 3] + elif (k == 2 or k == 5 or k == 8): + value = new_mat.to_translation()[(k - 2) / 3] + bck_anim.anim_data[i].comp[k].values.append(value) + + # got all the animation points, now to decide what to do with them + + # keep all the samples intact and calculate the slopes + # using linear interpolation between consecutive frames + if (options.export_type == "OPT_A"): + # assign the rest of the variables + for i in range(bck_anim.bone_count): + for j in range(9): + bck_anim.anim_data[i].comp[j].kf_count = len(bck_anim.anim_length) + bck_anim.anim_data[i].comp[j].interp_mode = 1 # has to be like this + for k in range(bck_anim.anim_length): + bck_anim.anim_data[i].comp[j].time.append(k) + in_slope = 0 + out_slope = 0 + if (k > 0): + in_slope = bck_anim.anim_data[i].comp[j].values[k] - bck_anim.anim_data[i].comp[j].values[k - 1] + if (k < bck_anim.anim_length - 1): + out_slope = bck_anim.anim_data[i].comp[j].values[k + 1] - bck_anim.anim_data[i].comp[j].values[k] + bck_anim.anim_data[i].comp[j].in_slope.append(in_slope) + bck_anim.anim_data[i].comp[j].out_slope.append(out_slope) + + # find "best" interpolator fits for the samples + elif (options.export_type == "OPT_B"): + print() + + + # create a raw bck struct and write the BCK file + raw = bck_funcs.create_smg_bck_raw(bck_anim) + print(raw) + endian_ch = ">" # big endian character for struct.unpack() + if (options.endian == "OPT_B"): # little character + endian_ch = "<" + bck_funcs.write_smg_bck_raw(raw, filepath, endian_ch) + + # done! + blender_funcs.disp_msg("BCK animation \"%s\" written" % (file_ops.get_filename(filepath))) + 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 + +# export_bck class +class export_bck(Operator, ExportHelper): + """Export the animation data from an Armature into a SMG BCK file""" + # stuff for blender + bl_idname = "export_scene.bck" + bl_label = "Export BCK (SMG)" + filename_ext = ".bck" + filter_glob = StringProperty(default = "*.bck", options = {"HIDDEN"}, maxlen = 255) + + # exporter options + export_type = EnumProperty( + name = "Export Mode", + description = "Way in which the animation will be exported", + default = "OPT_B", + items = ( + ("OPT_A", "Sample Everything", "Animation will be written completely sampled doing linear interpolation between all the frames of the animation. Some cleanup will be done while reading. Fast and accurate but takes a lot of space"), + ("OPT_B", "Find Best Interpolator", "Will find the best interpolator fits for all the animation curves involved in the animation. Slow and not that accurate but can take less space") + ) + ) + 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", + default = 45, + min = 0, + max = 180, + ) + first_frame = IntProperty( + name = "First frame", + description = "Value used to specify the first frame of the animation.", + default = 0, + ) + anim_length = IntProperty( + name = "Animation length", + description = "Value used to specify the number of frames of the BCK animation after the first frame specified.", + default = 30, + ) + loop_mode = EnumProperty( + name = "Loop mode", + description = "Way in which the animation be played in-game", + default = "OPT_C", + items = ( + ("OPT_A", "Play once - Stop at last frame", "Animation will start playing forwards and, when the animation data finishes, the last frame will be kept loaded into the model."), + ("OPT_B", "Play once - Stop at first frame", "Animation will start playing forwards and, when the animation data finishes, the first frame will be kept loaded into the model."), + ("OPT_C", "Repeat - Play forwards always", "Animation will start playing forwards and, when the animation data finishes, will play again from the beginning forwards."), + ("OPT_D", "Play once - First forwards then backwards", "Animation will start playing forwards and, when the animation data finishes, the animation will be played backwards. This occurs only once."), + ("OPT_E", "Repeat - Play forwards then backwards always", "Animation will start playing forwards and, when the animation data finishes, the animation will be played backwards. This repeats infinitely.") + ) + ) + endian = EnumProperty( + name = "Endian order", + description = "Way in which the animation data will be written", + default = "OPT_A", + items = ( + ("OPT_A", "Big", "Write data in the big endian byte ordering"), + ("OPT_B", "Little", "Write data in the little endian byte ordering") + ) + ) + # what the importer actually does + def execute(self, context): + return export_bck_func(self, context) + +# stuff to append the item to the File -> Import/Export menu +def menu_export_bck(self, context): + self.layout.operator(export_bck.bl_idname, text = "BCK for SMG (.bck)") + +bpy.utils.register_class(export_bck) +bpy.types.INFO_MT_file_export.append(menu_export_bck) + +# test call +bpy.ops.export_scene.bck('INVOKE_DEFAULT') + + -- cgit v1.2.3-70-g09d2