From ef4d0e08b2d312bcf3034aa3ae48436f5d8b56a5 Mon Sep 17 00:00:00 2001 From: Owl Date: Thu, 21 Aug 2025 20:07:13 -0400 Subject: all the stuff --- .python-version | 1 + README.md | 9 +- __init__.py | 11 +- basic_settings.py | 34 ++- bck_export.py | 230 ++++++++++++++ bck_funcs.py | 730 +++++++++++++++++++++++++++++++++++++++++++++ bck_import.py | 575 +++++++++++++++++++++++++++++++++++ blender_funcs.py | 45 ++- bmd_bdl_funcs.py | 0 collada_superbmd_export.py | 54 ++-- collada_superbmd_import.py | 56 ++-- file_ops.py | 239 +++++++++++++++ math_funcs.py | 503 ++++++++++++++----------------- obj_kcl_export.py | 24 +- obj_neokclcreate_import.py | 4 +- required_modules.py | 2 +- smg_common.py | 75 +++++ test.bin | Bin 0 -> 364 bytes test.py | 26 ++ 19 files changed, 2252 insertions(+), 366 deletions(-) create mode 100644 .python-version create mode 100644 bck_export.py create mode 100644 bck_funcs.py create mode 100644 bck_import.py create mode 100644 bmd_bdl_funcs.py create mode 100644 file_ops.py create mode 100644 smg_common.py create mode 100644 test.bin create mode 100644 test.py diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..475ba51 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.7 diff --git a/README.md b/README.md index 556e897..d21b3dd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ Epic Blender 2.79 template for Super Mario Galaxy modeling (honest work). # TODO -- Redo the whole logic of the CSV BCK importer/exporter +- Be aware of future BCK importer bugs. +- Import "Inherit Parent Scale" data from BMD/BDL models into an already imported DAE model. +- Make a BCK exporter. - ... # Features @@ -14,6 +16,9 @@ Epic Blender 2.79 template for Super Mario Galaxy modeling (honest work). - Reference axis `SMG axis` shows on all 3d models to guide the visualization of a mesh in the Super Mario Galaxy coordinate system (Y up). - Custom mesurement unit `Galaxy Unit` (or GU in short) that is used to mimic the game's original modelling environment (1 GU is 1 centimeter). - Epic Mario model like the default cube in Blender when starting the template (truly the most important part of the template >:]). +- OBJ importer for NeoKCLCreate OBJs. +- OBJ exporter for KCL conversion. +- BCK animation importer (with some importing options, read them). - ... # Installation @@ -40,4 +45,6 @@ If you want to contribute or report an issue open a new issue in **Issues**. I w # Changelog +- Added BCK importer. +- Added OBJ importer/exporter. - Collada importer/exporter updated. It still relies in Blender's default Collada importer/exporter. It is consistent enough to keep it this way for a long time. diff --git a/__init__.py b/__init__.py index 5f5b5fc..7873723 100644 --- a/__init__.py +++ b/__init__.py @@ -24,12 +24,13 @@ def set_blenxy_env(dummy): # "dummy" is a variable that is somehow # bpy.app.handlers.load_post.append from . import required_modules # install needed modules for bundled python (awful) from . import basic_settings # settings blenxy has - from . import collada_superbmd_import # "custom" importer for SuperBMD collada files - from . import collada_superbmd_export # "custom" exporter for SuperBMD collada files - from . import obj_kcl_export # "custom" exporter for OBJ files (Collision) - from . import obj_neokclcreate_import # "custom" importer for OBJ files (Colllision, NeoKCLCreate) + from . import collada_superbmd_import # custom importer for SuperBMD collada files + from . import collada_superbmd_export # custom exporter for SuperBMD collada files + from . import obj_kcl_export # custom exporter for OBJ files (Collision) + from . import obj_neokclcreate_import # custom importer for OBJ files (Colllision, NeoKCLCreate) + from . import bck_import # custom importer for SMG BCK files + # ~ from . import bck_export # custom exporter for SMG BCK files # ~ from . import csv_anim_bck_export # exporter for CSV files for BCK conversion - # ~ from . import csv_anim_bck_import # importer for CSV files from BCK animation # more scripts can be added here diff --git a/basic_settings.py b/basic_settings.py index 1b16e45..96b9b07 100644 --- a/basic_settings.py +++ b/basic_settings.py @@ -3,12 +3,13 @@ import bpy # SMG was made under the centimeter unit # SuperBMD assumes everything is in meters so I will scale down the models when being imported +# the holy variable scene = bpy.context.scene print("Setting length/rotation units...") -scene.unit_settings.system = 'METRIC' +scene.unit_settings.system = "METRIC" scene.unit_settings.scale_length = 0.01 -bpy.ops.scene.units_length_preset_add(name="Galaxy Unit") +bpy.ops.scene.units_length_preset_add(name = "Galaxy Unit") scene.unit_settings.system_rotation = 'DEGREES' scene.unit_settings.use_separate = False @@ -19,7 +20,7 @@ scene.unit_settings.use_separate = False print("Scaling 3D View grid...") for area in bpy.context.screen.areas: - if (area.type == 'VIEW_3D'): + if (area.type == "VIEW_3D"): view_3d_area = area.spaces.active view_3d_area.grid_scale = 0.01 view_3d_area.grid_lines = 400 @@ -34,11 +35,11 @@ for area in bpy.context.screen.areas: # get blenxy template location for that and select it # but first delete all objects in scene so it is clean -bpy.ops.object.select_all(action='SELECT') +bpy.ops.object.select_all(action = "SELECT") bpy.ops.object.delete() print("Importing Mario's Model...") -blenxy_path = bpy.utils.user_resource('SCRIPTS', "startup/bl_app_templates_user/blenxy/") +blenxy_path = bpy.utils.user_resource("SCRIPTS", "startup/bl_app_templates_user/blenxy/") bpy.ops.wm.collada_import(filepath = blenxy_path + "Mario.dae") Mario = bpy.data.objects.get("Mario") Mario.select = True @@ -58,9 +59,9 @@ Mario.select = True override = bpy.context.copy() override['area'] = area bpy.ops.transform.create_orientation(override, name = "SMG Axis", use = True) -bpy.context.scene.orientations[-1].matrix = [[ 1.0, 0.0, 0.0], - [ 0.0, 0.0, 1.0], - [ 0.0, -1.0, 0.0]] +scene.orientations[-1].matrix = [[ 1.0, 0.0, 0.0], + [ 0.0, 0.0, 1.0], + [ 0.0, -1.0, 0.0]] # set initial 3d viewport camera location @@ -76,12 +77,17 @@ view_3d_area.region_3d.view_distance = 4 print("Extra stuff to set...") # set environment lightnning (material display suggestion) -bpy.context.scene.world.light_settings.use_environment_light = True +scene.world.light_settings.use_environment_light = True # set framerate (SMG runs at 59.94 fps in THP videos) -bpy.context.scene.render.fps = 60 -bpy.context.scene.render.fps_base = 1.001 -# set start frame at 0 -bpy.data.scenes["Scene"].frame_start = 0 -bpy.data.scenes["Scene"].frame_current = 0 +scene.render.fps = 60 +scene.render.fps_base = 1.001 +# set frames to be able to go negative and allow them to be shown in scene +bpy.context.user_preferences.edit.use_negative_frames = True +scene.use_preview_range = True +scene.frame_preview_start = 0 +scene.frame_current = 0 +scene.frame_preview_end = 100 +bpy.context.user_preferences.view.use_mouse_depth_navigate = True +bpy.context.user_preferences.view.use_zoom_to_mouse = True print("Done with the basic settings!\n") 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') + + diff --git a/bck_funcs.py b/bck_funcs.py new file mode 100644 index 0000000..8ea2c72 --- /dev/null +++ b/bck_funcs.py @@ -0,0 +1,730 @@ +import os, struct, math + +# python file to read the important information out of a BCK file +# will try its best to decode the information either on big/little endian +# https://humming-owl.neocities.org/smg-stuff/pages/tutorials/bck + +# what this file will do is the following: +# pre_read_bck_file() takes the first look into the BCK and it checks if a BCK is correct. +# On the way it assigns important variables to the final bck_raw_info (smg_bck class). +# If the file is correct, then read_bck_file() will assign the remaining variables, if any +# and return the respective structure (a copy of it, not the reference). +# +# in case of any error pre_read_bck_file() returns a string that can +# be read by a human to identify what it is wrong with the BCK file +# if all is good it will return exactly that (as a string) + +# all the raw variables on a BCK file +class smg_bck_raw: + def __init__(self): + self.endian = None + self.header = self.header() + self.ank1 = self.ank1() + + def __str__(self): + rtn = "### SMG_BCK_RAW - START\n" + rtn += "Endian: %s\n" % (self.endian) + rtn += self.header.__str__() + rtn += self.ank1.__str__() + rtn += "### SMG_BCK_RAW - END\n" + return rtn + + # header + class header: + def __init__(self): + self.magic = None + self.ftype = None + self.file_size = None + self.section_count = None + self.unknown1 = None + + def __str__(self): + rtn = " ### HEADER START\n" + rtn += " Magic: %s\n" % (self.magic.__str__()) + rtn += " File type: %s\n" % (self.ftype.__str__()) + rtn += " File size: %s\n" % (self.file_size.__str__()) + rtn += " Section count: %s\n" % (self.section_count.__str__()) + rtn += " Unknown 1: %s\n" % (self.unknown1.__str__()) + rtn += " ### HEADER END\n" + return rtn + + # ank1 + class ank1: + def __init__(self): + self.magic = None + self.size = None + self.loop_mode = None + self.rot_lshift = None + self.anim_length = None + self.bone_count = None + self.scale_arr_length = None + self.rot_arr_length = None + self.transl_arr_length = None + self.anim_data_offset = None + self.scale_arr_offset = None + self.rot_arr_offset = None + self.transl_arr_offset = None + self.anim_data = [] # list of length bone_count + self.scale_arr = [] # list of length scale_arr_length + self.rot_arr = [] # list of length rot_arr_length + self.transl_arr = [] # list of length transl_arr_length + + def __str__(self): + rtn = " ### ANK1 - START\n" + rtn += " Magic: %s\n" % (self.magic.__str__()) + rtn += " Section size: %s\n" % (self.size.__str__()) + rtn += " Loop mode: %s\n" % (self.loop_mode.__str__()) + rtn += " Rotation left shift: %s\n" % (self.rot_lshift.__str__()) + rtn += " Anim length: %s\n" % (self.anim_length.__str__()) + rtn += " Bone count: %s\n" % (self.bone_count.__str__()) + rtn += " Scale array length: %s\n" % (self.scale_arr_length.__str__()) + rtn += " Rotation array length: %s\n" % (self.rot_arr_length.__str__()) + rtn += " Translation array length: %s\n" % (self.transl_arr_length.__str__()) + rtn += " Anim data offset: %s\n" % (self.anim_data_offset.__str__()) + rtn += " Scale array offset: %s\n" % (self.scale_arr_offset.__str__()) + rtn += " Rotation array offset: %s\n" % (self.rot_arr_offset.__str__()) + rtn += " Translation array offset: %s\n" % (self.transl_arr_offset.__str__()) + rtn += " Animation data:\n" + for i in range(len(self.anim_data)): + rtn += " Bone %d\n" % (i) + rtn += "%s" % (self.anim_data[i].__str__()) + rtn += " Scale array: %s\n" % (self.scale_arr.__str__()) + rtn += " Rotation array: %s\n" % (self.rot_arr.__str__()) + rtn += " Translation array: %s\n" % (self.transl_arr.__str__()) + rtn += " ### ANK1 - END\n" + return rtn + + # bone animation data table + class anim_data: + def __init__(self): + self.comp = [self.comp(), # scale x + self.comp(), # rot x + self.comp(), # transl x + self.comp(), # scale y + self.comp(), # rot y + self.comp(), # transl y + self.comp(), # scale z + self.comp(), # rot z + self.comp()] # transl z + + def __str__(self): + rtn = " Scale X: %s" % (self.comp[0].__str__()) + rtn += " Rot X: %s" % (self.comp[1].__str__()) + rtn += " Transl X: %s" % (self.comp[2].__str__()) + rtn += " Scale Y: %s" % (self.comp[3].__str__()) + rtn += " Rot Y: %s" % (self.comp[4].__str__()) + rtn += " Transl Y: %s" % (self.comp[5].__str__()) + rtn += " Scale Z: %s" % (self.comp[6].__str__()) + rtn += " Rot Z: %s" % (self.comp[7].__str__()) + rtn += " Transl Z: %s" % (self.comp[8].__str__()) + return rtn + + + # the animation data for each of the animation components + class comp: + def __init__(self): + self.keyframe_count = None + self.anim_data_index = None + self.interpolation_mode = None + + def __str__(self): + return "%s %s %s\n" % (self.keyframe_count.__str__(), + self.anim_data_index.__str__(), + self.interpolation_mode.__str__()) + +# the actually useful information encoded in the BCK file +class smg_bck_anim: + def __init__(self): + self.loop_mode = None + self.anim_length = None + self.bone_count = None + self.anim_data = [] # list of length bone_count + + def __str__(self): + rtn = "### SMG_BCK_ANIM START\n" + rtn += " Loop mode: %s\n" % (self.loop_mode.__str__()) + rtn += " Animation length: %s\n" % (self.anim_length.__str__()) + rtn += " Bone count: %s\n" % (self.bone_count.__str__()) + rtn += " Animation data:\n" + for i in range(len(self.anim_data)): + rtn += " Bone %d\n" % (i) + rtn += "%s" % (self.anim_data[i].__str__()) + rtn += "### SMG_BCK_ANIM END" + return rtn + + # anim_data + class anim_data: + def __init__(self): + self.comp = [self.comp(), # scale x + self.comp(), # rot x + self.comp(), # transl x + self.comp(), # scale y + self.comp(), # rot y + self.comp(), # transl y + self.comp(), # scale z + self.comp(), # rot z + self.comp()] # transl z + + def __str__(self): + rtn = " Scale X: %s" % (self.comp[0].__str__()) + rtn += " Rot X: %s" % (self.comp[1].__str__()) + rtn += " Transl X: %s" % (self.comp[2].__str__()) + rtn += " Scale Y: %s" % (self.comp[3].__str__()) + rtn += " Rot Y: %s" % (self.comp[4].__str__()) + rtn += " Transl Y: %s" % (self.comp[5].__str__()) + rtn += " Scale Z: %s" % (self.comp[6].__str__()) + rtn += " Rot Z: %s" % (self.comp[7].__str__()) + rtn += " Transl Z: %s" % (self.comp[8].__str__()) + return rtn + + # anim_data + class comp: + def __init__(self): + self.kf_count = None + self.interp_mode = None + self.time = [] # list of length kf_count (if kf_count > 1) + self.value = [] # list of length kf_count (the only list available to read if kf_count == 1) + self.in_slope = [] # list of length kf_count (if kf_count > 1) + self.out_slope = [] # list of length kf_count (if kf_count > 1) + + def __str__(self): + rtn = "%s %s\n" % (self.kf_count, self.interp_mode) + rtn += " time: %s\n" % (self.time.__str__()) + rtn += " value: %s\n" % (self.value.__str__()) + rtn += " in slope: %s\n" % (self.in_slope.__str__()) + rtn += " out slope: %s\n" % (self.out_slope.__str__()) + return rtn + + +# create a global variable to hold temporal information +bck_raw_info = None +bck_error_str = "bck-error: " +bck_anim_error_str = "bck-anim-error: " +pad_str = "This is padding data to alignme" +f = None + +# main function +# will read and will check while reading +def read_bck_file(filepath): + + # make global variables editable + global f + global bck_raw_info + # "pre read" the file + result_str = pre_read_bck_file(filepath) + print(result_str) + + # all good + bck_anim_info = None + if (result_str == bck_error_str + "all good"): + # construct the data structure that is easier to deal with + print(bck_raw_info) + bck_anim_info = smg_bck_anim() + + # assign the easy variables + bck_anim_info.loop_mode = bck_raw_info.ank1.loop_mode + bck_anim_info.anim_length = bck_raw_info.ank1.anim_length + bck_anim_info.bone_count = bck_raw_info.ank1.bone_count + + # construct the animation tracks + + # iterate over each bone + for i in range(bck_anim_info.bone_count): + bck_anim_info.anim_data.append(smg_bck_anim.anim_data()) + # iterate over each animation component + for j in range(9): + bck_anim_info.anim_data[i].comp[j].kf_count = bck_raw_info.ank1.anim_data[i].comp[j].keyframe_count + bck_anim_info.anim_data[i].comp[j].interp_mode = bck_raw_info.ank1.anim_data[i].comp[j].interpolation_mode + data_index = bck_raw_info.ank1.anim_data[i].comp[j].anim_data_index + arr = None + + # select the array and the items per read + rot_mult = 1 # rotation consideration (convert to radians) + if (j == 0 or j == 3 or j == 6): # scale + arr = bck_raw_info.ank1.scale_arr + elif (j == 1 or j == 4 or j == 7): # rotation + arr = bck_raw_info.ank1.rot_arr + rot_mult = (math.pi * math.pow(2, bck_raw_info.ank1.rot_lshift)) / 0x7FFF + elif (j == 2 or j == 5 or j == 8): # translation + arr = bck_raw_info.ank1.transl_arr + + # assign the data + + # 1 keyframe + if (bck_anim_info.anim_data[i].comp[j].kf_count == 1): + bck_anim_info.anim_data[i].comp[j].time.append(None) + bck_anim_info.anim_data[i].comp[j].value.append(arr[data_index] * rot_mult) + bck_anim_info.anim_data[i].comp[j].in_slope.append(None) + bck_anim_info.anim_data[i].comp[j].out_slope.append(None) + # more than 1 + elif (bck_anim_info.anim_data[i].comp[j].kf_count > 1): + for k in range(bck_anim_info.anim_data[i].comp[j].kf_count): + # time + bck_anim_info.anim_data[i].comp[j].time.append(arr[data_index]) + data_index += 1 + # value + bck_anim_info.anim_data[i].comp[j].value.append(arr[data_index] * rot_mult) + data_index += 1 + # in slope + bck_anim_info.anim_data[i].comp[j].in_slope.append(arr[data_index] * rot_mult) + data_index += 1 + # out slope + if (bck_anim_info.anim_data[i].comp[j].interp_mode == 1): + bck_anim_info.anim_data[i].comp[j].out_slope.append(arr[data_index] * rot_mult) + data_index += 1 + else: + bck_anim_info.anim_data[i].comp[j].out_slope.append(bck_anim_info.anim_data[i].comp[j].in_slope[-1]) + + # done! + f.close() + f = None + bck_raw_info = None + return bck_anim_info + +# function to check a BCK file before getting its full information out +def pre_read_bck_file(filepath): + + # check its size first + if (os.path.getsize(filepath) <= 32): + return bck_error_str + "file size" + + # make global variables editable + global f + global bck_raw_info + + # open the file + f = open(filepath, "rb") + + # holder for variables + bck_raw_info = smg_bck_raw(); + + ######## + # header + + # magic + bck_raw_info.header.magic = f.read(4).decode("ascii") + if (bck_raw_info.header.magic == "J3D1"): + bck_raw_info.endian = "BIG" + elif (bck_raw_info.header.magic == "1D3J"): + bck_raw_info.endian = "LITTLE" + else: + return bck_error_str + "magic" + bck_raw_info.header.magic = "J3D1" + + # variable to set for struct.unpack byte order reading + endian_ch = ">" # big endian + if (bck_raw_info.endian == "LITTLE"): + endian_ch = "<" + + # ftype + bck_raw_info.header.ftype = f.read(4).decode("ascii") + if ((bck_raw_info.header.ftype == "bck1" and bck_raw_info.endian != "BIG") + and (bck_raw_info.header.ftype == "1kcb" and bck_raw_info.endian != "LITTLE")): + return bck_error_str + "ftype" + bck_raw_info.header.ftype = "bck1" + + # file size + bck_raw_info.header.file_size = struct.unpack(endian_ch + "I", f.read(4))[0] + if (bck_raw_info.header.file_size != os.path.getsize(filepath)): + return bck_error_str + "file size" + + # section count + bck_raw_info.header.section_count = struct.unpack(endian_ch + "I", f.read(4))[0] + if (bck_raw_info.header.section_count != 1): + return bck_error_str + "section count" + + # unknown 1 + bck_raw_info.header.unknown1 = list(f.read(16)) + for i in range(16): + if (bck_raw_info.header.unknown1[i] != 0xFF): + return bck_error_str + "unknown 1" + + ############## + # ank1 section + + # magic + bck_raw_info.ank1.magic = f.read(4).decode("ascii") + if ((bck_raw_info.ank1.magic == "ANK1" and bck_raw_info.endian != "BIG") + and (bck_raw_info.ank1.magic == "1KNA" and bck_raw_info.endian != "LITTLE")): + return bck_error_str + "ank1 magic" + bck_raw_info.ank1.magic = "ANK1" + + # size + bck_raw_info.ank1.size = struct.unpack(endian_ch + "I", f.read(4))[0] + if (bck_raw_info.ank1.size != bck_raw_info.header.file_size - 32): + return bck_error_str + "ank1 size" + + # loop mode + bck_raw_info.ank1.loop_mode = struct.unpack(endian_ch + "B", f.read(1))[0] + if (bck_raw_info.ank1.loop_mode > 0x04): + return bck_error_str + "ank1 loop mode" + + # rotation left shift + bck_raw_info.ank1.rot_lshift = struct.unpack(endian_ch + "B", f.read(1))[0] + # animation length + bck_raw_info.ank1.anim_length = struct.unpack(endian_ch + "H", f.read(2))[0] + # bone count + bck_raw_info.ank1.bone_count = struct.unpack(endian_ch + "H", f.read(2))[0] + # scale array length + bck_raw_info.ank1.scale_arr_length = struct.unpack(endian_ch + "H", f.read(2))[0] + # rotation array length + bck_raw_info.ank1.rot_arr_length = struct.unpack(endian_ch + "H", f.read(2))[0] + # translation array length + bck_raw_info.ank1.transl_arr_length = struct.unpack(endian_ch + "H", f.read(2))[0] + + # offsets + + # bone animation data offset + bck_raw_info.ank1.anim_data_offset = struct.unpack(endian_ch + "I", f.read(4))[0] + if (bck_raw_info.ank1.anim_data_offset + + (bck_raw_info.ank1.bone_count * 9 * 6) > bck_raw_info.ank1.size): + return bck_error_str + "ank1 bone animation data offset" + # scale array offset + bck_raw_info.ank1.scale_arr_offset = struct.unpack(endian_ch + "I", f.read(4))[0] + if (bck_raw_info.ank1.scale_arr_offset + + (bck_raw_info.ank1.scale_arr_length * 4) > bck_raw_info.ank1.size): + return bck_error_str + "ank1 scale array offset" + # rotation array offset + bck_raw_info.ank1.rot_arr_offset = struct.unpack(endian_ch + "I", f.read(4))[0] + if (bck_raw_info.ank1.rot_arr_offset + + (bck_raw_info.ank1.rot_arr_length * 2) > bck_raw_info.ank1.size): + return bck_error_str + "ank1 rotation array offset" + # translation array offset + bck_raw_info.ank1.transl_arr_offset = struct.unpack(endian_ch + "I", f.read(4))[0] + if (bck_raw_info.ank1.transl_arr_offset + + (bck_raw_info.ank1.transl_arr_length * 4) > bck_raw_info.ank1.size): + return bck_error_str + "ank1 translation array offset" + + ######################################################################## + # refer to the offsets to read the animation data always (SMG does this) + + # bone animation data + f.seek(32 + bck_raw_info.ank1.anim_data_offset) + + # iterate over the bones + for i in range(bck_raw_info.ank1.bone_count): + # append a new item in the empty list + bck_raw_info.ank1.anim_data.append(smg_bck_raw.ank1.anim_data()) + # iterate over the animation components + for j in range(9): + bck_raw_info.ank1.anim_data[i].comp[j].keyframe_count = struct.unpack(endian_ch + "H", f.read(2))[0] + bck_raw_info.ank1.anim_data[i].comp[j].anim_data_index = struct.unpack(endian_ch + "H", f.read(2))[0] + bck_raw_info.ank1.anim_data[i].comp[j].interpolation_mode = struct.unpack(endian_ch + "H", f.read(2))[0] + # temporal shortcuts for the variables above + kf_count = bck_raw_info.ank1.anim_data[i].comp[j].keyframe_count + data_index = bck_raw_info.ank1.anim_data[i].comp[j].anim_data_index + interp_mode = bck_raw_info.ank1.anim_data[i].comp[j].interpolation_mode + + # store this file position + old_file_pos = f.tell() + + # check the interpolation mode and if nothing overflows + if (interp_mode > 1): + return bck_error_str + "ank1 interpolation mode" + + # variables to be used later + item_read_size = None + arr_offset = None + item_read_type = None + # scale or translation + if (j == 0 or j == 3 or j == 6): + item_read_size = 4 + arr_offset = bck_raw_info.ank1.scale_arr_offset + item_read_type = "f" + # rotation + elif (j == 1 or j == 4 or j == 7): + item_read_size = 2 + arr_offset = bck_raw_info.ank1.rot_arr_offset + item_read_type = "h" + # translation + elif (j == 2 or j == 5 or j == 8): + item_read_size = 4 + arr_offset = bck_raw_info.ank1.transl_arr_offset + item_read_type = "f" + + # how many items are read from the component arrays + # (depends on the interpolation mode and on the keyframe count) + number_of_items_per_kf = 1 + if (kf_count > 1 and interp_mode == 0): # soft interpolation + number_of_items_per_kf = 3 + elif (kf_count > 1 and interp_mode == 1): # custom interpolation + number_of_items_per_kf = 4 + + # check overflow + if (arr_offset + (item_read_size * data_index) + + (item_read_size * kf_count * number_of_items_per_kf) > bck_raw_info.ank1.size): + return bck_error_str + "ank1 anim data overflow" + + # read the respective arrays to check time consistency + old_time = None + cur_time = None + value = None + slope = None + + # go to that part of the file and read the respective animation data + f.seek(32 + arr_offset + (item_read_size * data_index)) + + # read the animation data + if (number_of_items_per_kf == 1): # single value read + # value + value = struct.unpack(endian_ch + item_read_type, f.read(item_read_size))[0] + else: # (3 or 4) * kf_count value reads + # iterate over the number of keyframes + for k in range(kf_count): + # time + cur_time = struct.unpack(endian_ch + item_read_type, f.read(item_read_size))[0] + if (old_time != None): + if (old_time >= cur_time): + return bck_error_str + "ank1 keyframe time" + old_time = cur_time + # value + value = struct.unpack(endian_ch + item_read_type, f.read(item_read_size))[0] + # slope + slope = struct.unpack(endian_ch + item_read_type, f.read(item_read_size))[0] + if (number_of_items_per_kf == 4): + slope = struct.unpack(endian_ch + item_read_type, f.read(item_read_size))[0] + + # return to the bone animation data table + f.seek(old_file_pos) + + # scale, rotation and translation arrays data + f.seek(32 + bck_raw_info.ank1.scale_arr_offset) + for i in range(bck_raw_info.ank1.scale_arr_length): + bck_raw_info.ank1.scale_arr.append(struct.unpack(endian_ch + "f", f.read(4))[0]) + f.seek(32 + bck_raw_info.ank1.rot_arr_offset) + for i in range(bck_raw_info.ank1.rot_arr_length): + bck_raw_info.ank1.rot_arr.append(struct.unpack(endian_ch + "h", f.read(2))[0]) + f.seek(32 + bck_raw_info.ank1.transl_arr_offset) + for i in range(bck_raw_info.ank1.transl_arr_length): + bck_raw_info.ank1.transl_arr.append(struct.unpack(endian_ch + "f", f.read(4))[0]) + + # finally done bruh + return bck_error_str + "all good" + +# check if a smg_bck_anim structure is good +def check_smg_bck_anim(anim): + + # check if the information in the smg_bck_anim struct is valid + + # the only stuff I can check is time consistency (frame -3 goes before frame 2) + # and that the lengths of the arrays are all good (also the variable types) + if (type(anim) != smg_bck_anim): + return bck_anim_error_str + "smg_bck_anim struct" + if(type(anim.anim_data) != list or len(anim.anim_data) != anim.bone_count): + return bck_anim_error_str + "number of bones or anim_data list" + + # iterate over the bones + for bone in anim.anim_data: + if (type(bone.comp) != list or len(bone.comp) != 9): + return bck_anim_error_str + "number of components or comp list" + + # iterate over the anim components + for comp in bone.comp: + + # check object types and integer data + if (type(comp) != smg_bck_anim.anim_data): + return bck_anim_error_str + "anim_data struct" + if (type(comp.kf_count) != int or comp.kf_count <= 0): + return bck_anim_error_str + "keyframe count" + if (type(comp.interp_mode) != int or (comp.kf_count != 0 and comp.kf_count != 1)): + return bck_anim_error_str + "interpolation mode" + if (type(comp.time) != list or len(comp.time) != comp.kf_count): + return bck_anim_error_str + "time list" + if (type(comp.values) != list or len(comp.values) != comp.kf_count): + return bck_anim_error_str + "values list" + if (type(comp.in_slope) != list or len(comp.in_slope) != comp.kf_count): + return bck_anim_error_str + "in_slope list" + if (type(comp.out_slope) != list or len(comp.out_slope) != comp.kf_count): + return bck_anim_error_str + "out_slope list" + + # check time consistency + for i in range(comp.kf_count): + if (i == 0): + continue + if (comp.time[i - 1] >= comp.time[i]): + return bck_anim_error_str + "time inconsistency" + + # all is good + return bck_anim_error_str + "all good" + +# create smg_bck_raw from smg_bck_anim +def create_smg_bck_raw(anim): + + # calls check_smg_bck_anim() + result = check_smg_bck_anim(anim) + print(result) + if (result != bck_anim_error_str + "all good"): + return None + + # build a new raw structure and return it + raw = smg_bck_raw() + raw.endian = "BIG" + + # header + raw.header.magic = "J3D1" + raw.header.ftype = "btp1" + raw.header.file_size = 32 # update later + raw.header.section_count = 1 + raw.header.unknown1 = [0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF] + + # find the file size (bytes) + # find if there can be merged tracks (same value animation tracks) + # find out the rot_lshift number + + # ank1 section + raw.ank1.magic = "ANK1" + raw.ank1.size = 36 # update at the end + raw.ank1.loop_mode = anim.loop_mode + raw.ank1.rot_lshift = 0 # update now + raw.ank1.anim_length = anim.anim_length + raw.ank1.bone_count = anim.bone_count + + # check all the rotation lists and get the average value + avg_angle_mag = 0 + angle_count = 0 + for i in range(anim.bone_count): + for j in range(3): + for k in range(len(anim.anim_data[i].comp[(j * 3) + 1].kf_count)): + max_angle_mag += abs(anim.anim_data[i].comp[(j * 3) + 1].values[k]) + angle_count += 1 + # calculate rot_lshift so that this value can be represented + # (shit can go crazy if this value is very large) + avg_angle_mag = avg_angle_mag / angle_count + raw.ank1.rot_lshift = int(math.ceil(math.log2(avg_angle_mag / math.pi))) + # ceil of it because I am forcing the average to be represented + # as 0x7FFF which cannot be done as rot_lshift needs to be an integer + + # start writing the animation data + + # iterate over the bones + for i in range(anim.bone_count): + # add a section for a bone + raw.ank1.anim_data.append(smg_bck_raw.anim_data()) + + # iterate over the animation components + for j in range(9): + # in case the same dataset can be found already + # written in one of the animation data arrays + match_found = True + arr = None + # scale + if (j == 0 or j == 3 or j == 6): + arr = raw.ank1.scale_arr + # rot + elif (j == 1 or j == 4 or j == 7): + arr = raw.ank1.rot_arr + # translate + elif (j == 2 or j == 5 or j == 8): + arr = raw.ank1.transl_arr + cur_index = len(arr) + + # keyframe_count, interpolation_mode + raw.ank1.anim_data[i].comp[j].keyframe_count = anim.anim_data[i].comp[j].kf_count + raw.ank1.anim_data[i].comp[j].interpolation_mode = anim.anim_data[i].comp[j].interp_mode + # number of items to write on each animation array + number_of_items = 1 + if (anim.anim_data[i].comp[j].kf_count > 1): + if (raw.ank1.anim_data[i].comp[j].interpolation_mode == 0): + number_of_items = 3 + elif (raw.ank1.anim_data[i].comp[j].interpolation_mode == 1): + number_of_items = 4 + + # check if it is a single keyframe value + if (number_of_items == 1): + # check if there is an equivalent value around the already written data + match_found = False + k = 0 + while (k < len(arr)): + if (anim.anim_data[i].comp[j].values[0] == arr[k]): + raw.ank1.anim_data[i].comp[j].anim_data_index = k + match_found = True + break + k += 1 + # something was found, index was already assigned + if (match_found == True): + continue + # otherwise, update the array + arr.append(anim.anim_data[i].comp[j].values[0]) + + # or if it is more + else: + # iterate over all the other values in the scale array to see if a match is found + k = 0 + while ((k + (anim.anim_data[i].comp[j].kf_count * number_of_items)) < len(arr)): + # check coincidence + l = 0 + match_found = True + while (l < anim.anim_data[i].comp[j].kf_count): + # check value equality + if ((anim.anim_data[i].comp[j].time[l] != arr[k + l + 0]) + or (anim.anim_data[i].comp[j].value[l] != arr[k + l + 1]) + or (anim.anim_data[i].comp[j].in_slope[l] != arr[k + l + 2])): + match_found = False + break + # interpolation mode == 1 + if ((anim.anim_data[i].comp[j].interp_mode == 1) + and (anim.anim_data[i].comp[j].out_slope[l] != arr[k + l + 3])): + match_found = False + break + l += number_of_items + # something was found + if (match_found == True): + raw.ank1.anim_data[i].comp[j].anim_data_index = k + break + # continue to next loop + k += 1 + # something was found, index was already assigned + if (match_found == True): + continue + # else append the new data + raw.ank1.anim_data[i].comp[j].anim_data_index = cur_index + # iterate over the frames and assign the scale values + k = 0 + while (k < anim.anim_data[i].comp[j].kf_count): + arr.append(anim.anim_data[i].comp[j].time[k]) + arr.append(anim.anim_data[i].comp[j].value[k]) + arr.append(anim.anim_data[i].comp[j].in_slope[k]) + if (anim.anim_data[i].comp[j].interp_mode == 1): + arr.append(anim.anim_data[i].comp[j].out_slope[k]) + k += 1 + + # update the animation arrays + # scale + if (j == 0 or j == 3 or j == 6): + raw.ank1.scale_arr = arr + # rot + elif (j == 1 or j == 4 or j == 7): + raw.ank1.rot_arr = arr + # translate + elif (j == 2 or j == 5 or j == 8): + raw.ank1.transl_arr = arr + + # assign these variables now + raw.ank1.scale_arr_length = len(raw.ank1.scale_arr) + raw.ank1.rot_arr_length = len(raw.ank1.rot_arr) + raw.ank1.transl_arr_length = len(raw.ank1.transl_arr) + raw.ank1.anim_data_offset = 0x40 + raw.ank1.scale_arr_offset = None + raw.ank1.rot_arr_offset = None + raw.ank1.transl_arr_offset = None + raw.ank1.anim_data = [] # list of length bone_count + raw.ank1.scale_arr = [] # list of length scale_arr_length + raw.ank1.rot_arr = [] # list of length rot_arr_length + raw.ank1.transl_arr = [] # list of length transl_arr_length + + return raw + +# write smg_bck_raw +def write_smg_bck_raw(raw, filepath, endian_ch): + + # assumes smg_bck_raw struct is correct so don't even + # attempt in making one yourself, use create_smg_bck_raw() + # use struct.pack() to write in different endian orders + # dont be crazy with it an assign the data tables to the "standard offsets" + + global f + f = open(filepath, "wb") + f.close() 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') + + diff --git a/blender_funcs.py b/blender_funcs.py index d9eb7a4..e18431e 100644 --- a/blender_funcs.py +++ b/blender_funcs.py @@ -1,4 +1,5 @@ import bpy, numpy +import time # file that contains useful blender some functions that I don't # want to re-write on each individual .py file @@ -31,19 +32,39 @@ def disp_msg(string): print(string) bpy.ops.message.messagebox('INVOKE_DEFAULT', message = string) -# select object and its children -def select_obj(scene, obj, recursive): +# select object and its children in the interaction mode specified +def select_obj(obj, recursive, interact_mode): - # select an area - bpy.ops.object.select_all(action='DESELECT') - scene.objects.active = None - obj.select = True + # get the object scene + scene = obj.users_scene[0] + + # make it the only object selected and active + # multiple objects can be "selected" but only one can be "active" + # so deselect all objects + for o in scene.objects: + o.select = False + + # select the new object and set it to the selected mode 2 times lol scene.objects.active = obj + obj.select = True + bpy.ops.object.mode_set(mode = interact_mode) + bpy.ops.object.mode_set(mode = interact_mode) - # select object and its children + # select the children objects as well, if on object mode if (recursive == True): - bpy.ops.object.select_grouped(type = 'CHILDREN_RECURSIVE') - obj.select = True + for child in obj.children: + child.select = True + + +# duplicate a selected object with its children objects +def duplicate_obj(scene, obj): + + # select the object in object mode + select_obj(obj, True, "OBJECT") + # do the following lmao + bpy.ops.object.duplicate(linked = True) + bpy.ops.object.make_single_user(type = 'SELECTED_OBJECTS', object = True, obdata = True) + select_obj(scene.objects.active, False, "OBJECT") # transf_apply_recurse function # transform_apply the parent and its child meshes @@ -51,18 +72,18 @@ def select_obj(scene, obj, recursive): def transf_apply_recurse(scene, obj, loc, rot, sca): # select obj - select_obj(scene, obj, False) + select_obj(obj, False, "OBJECT") bpy.ops.object.transform_apply(location = loc, rotation = rot, scale = sca) # armature child mesh scaling if (len(obj.children) != 0): for child in obj.children: # select child and apply transform - select_obj(scene, child, False) + select_obj(child, False, "OBJECT") bpy.ops.object.transform_apply(location = loc, rotation = rot, scale = sca) # select parent object at the end - select_obj(scene, obj, False) + select_obj(obj, False, "OBJECT") # set a bone bind matrix def set_bone_bind_mat(bone, mat): diff --git a/bmd_bdl_funcs.py b/bmd_bdl_funcs.py new file mode 100644 index 0000000..e69de29 diff --git a/collada_superbmd_export.py b/collada_superbmd_export.py index ad1363b..9ef08b2 100644 --- a/collada_superbmd_export.py +++ b/collada_superbmd_export.py @@ -15,26 +15,25 @@ def write_bmd_bdl_collada(context, filepath, triangulate): scene = bpy.context.scene # if nothing is selected end the exporter - if (scene.objects.active == None or scene.objects.active.type != 'ARMATURE'): + 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'} + return {"FINISHED"} # get armature object armature = scene.objects.active print("\nArmature found: \"%s\"" % (armature.name)) - # change to object mode - bpy.ops.object.mode_set(mode='OBJECT') + # select it + blender_funcs.select_obj(armature, False, "OBJECT") # check if the armature contains only mesh objects inside - for obj in armature.children: - if (obj.type != 'MESH'): - blender_funcs.disp_msg("\"%s\": contains non-mesh object (%s)." - % (armature.name, obj.name)) - return {'FINISHED'} + for child in armature.children: + if (child.type != "MESH"): + blender_funcs.disp_msg("\"%s\": contains non-mesh object (%s)." % (armature.name, child.name)) + return {"FINISHED"} # check if all meshes have an armature modifier that is # assigned to the armature object and it is binded to @@ -43,18 +42,16 @@ def write_bmd_bdl_collada(context, filepath, triangulate): for mesh in armature.children: if("Armature" not in mesh.modifiers): blender_funcs.disp_msg("\"%s\": has no armature modifier. Adding one..." % (mesh.name)) - blender_funcs.select_obj(scene, mesh, False) - bpy.ops.object.modifier_add(type = 'ARMATURE') + 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 else: # ensure bind is to a vertex group and that the bind is to the actual parent armature if (mesh.modifiers["Armature"].use_vertex_groups == False): - blender_funcs.disp_msg("\"%s\": armature modifier wasn't binded to vertex groups" - % (mesh.name)) + blender_funcs.disp_msg("\"%s\": armature modifier wasn't binded to vertex groups" % (mesh.name)) mesh.modifiers["Armature"].use_vertex_groups = True if (mesh.modifiers["Armature"].object != armature): - blender_funcs.disp_msg("\"%s\": armature modifier was binded to another armature object" - % (mesh.name)) + blender_funcs.disp_msg("\"%s\": armature modifier was binded to another armature object" % (mesh.name)) mesh.modifiers["Armature"].object = armature; # check if all the vertex groups in each mesh correspond to the name of a skeleton bone @@ -65,16 +62,16 @@ def write_bmd_bdl_collada(context, filepath, triangulate): for mesh in armature.children: for v_group in mesh.vertex_groups: if (v_group.name not in bone_name_list): - blender_funcs.disp_msg(("\"%s\": contains non-valid vert group \"%s\"." - % (mesh.name, v_group.name)) + " Unable to continue.") - return {'FINISHED'} + blender_funcs.disp_msg(("\"%s\": contains non-valid vert group \"%s\"." % (mesh.name, v_group.name)) + + " Unable to continue.") + return {"FINISHED"} # vertex weight check for mesh in armature.children: for vertex in mesh.data.vertices: if (len(vertex.groups) == 0): blender_funcs.disp_msg(("\"%s\": contains unweighted vertices." % (mesh.name)) + " Unable to continue.") - return {'FINISHED'} + return {"FINISHED"} # get the object names obj_name = armature.name @@ -83,13 +80,12 @@ def write_bmd_bdl_collada(context, filepath, triangulate): child_names.append(child.name) # change to object view and make a copy of the armature object (with its children) - bpy.ops.object.mode_set(mode='OBJECT') - blender_funcs.select_obj(scene, armature, True) + blender_funcs.select_obj(armature, True, "OBJECT") bpy.ops.object.duplicate(linked = True) - bpy.ops.object.make_single_user(type = 'SELECTED_OBJECTS', object = True, obdata = True) + bpy.ops.object.make_single_user(type = "SELECTED_OBJECTS", object = True, obdata = True) old_armature = armature armature = scene.objects.active - blender_funcs.select_obj(scene, armature, False) + blender_funcs.select_obj(armature, False, "OBJECT") # apply the transformations to the armature armature.rotation_euler[0] = math.radians(-90) @@ -98,9 +94,9 @@ def write_bmd_bdl_collada(context, filepath, triangulate): # apply the transformations to the meshes inside the armature for i in range(0, len(armature.children)): - blender_funcs.select_obj(scene, armature.children[i], False) + blender_funcs.select_obj(armature.children[i], False, "OBJECT") bpy.ops.object.transform_apply(location = True, rotation = True, scale = True) - blender_funcs.select_obj(scene, armature, False) + blender_funcs.select_obj(armature, False, "OBJECT") # handle the object names so that they match the original model names armature.name = obj_name @@ -112,10 +108,10 @@ def write_bmd_bdl_collada(context, filepath, triangulate): # export the object bpy.ops.wm.collada_export(filepath = filepath, use_blender_profile = False, selected = True, include_children = True, - triangulate = triangulate) + triangulate = triangulate) # delete the duplicate object - blender_funcs.select_obj(scene, armature, True) + blender_funcs.select_obj(armature, True, "OBJECT") bpy.ops.object.delete(use_global = False) # re-assign the original object names @@ -127,9 +123,9 @@ def write_bmd_bdl_collada(context, filepath, triangulate): armature.children[i].data.name = child_names[i] # done! - blender_funcs.select_obj(scene, armature, False) + blender_funcs.select_obj(armature, False, "OBJECT") blender_funcs.disp_msg("Armature \"%s\" exported!" % (armature.name)) - return {'FINISHED'} + return {"FINISHED"} # Stuff down is for the menu appending # of the exporter to work plus some setting stuff diff --git a/collada_superbmd_import.py b/collada_superbmd_import.py index 39e7ce6..2fa7ef0 100644 --- a/collada_superbmd_import.py +++ b/collada_superbmd_import.py @@ -4,7 +4,7 @@ from . import collada_funcs from . import blender_funcs from . import math_funcs -# Comments (spanish) (reescribir): +# Comments (español) (reescribir): # Bind Pose o Pose de Vínculo (aplica a huesos): es la posición (transformación) que tienen los huesos de un modelo en 3D cuando asignas los huesos a las mallas de dicho modelo. Cualquier otra posición deforma las mallas del modelo. A las matrices que llevan esta información se les denomina matriz de vínculo o bind matrix. En los archivos collada las matrices de vínculo (inversas) de los huesos nombrados en el elemento pueden obtenerse del elemento (que viene después de ) en el elemento de dicho archivo. Hay que invertir las matrices en esta sección para obtener las que se van a palicar a los huesos. Estas transformaciones son con respecto al sistema del objeto armadura (no son locales!). @@ -53,19 +53,23 @@ def import_collada_superbmd(context, filepath): # remove the temporal file os.remove(filepath + ".xml") - ######################################## # reconstruct the skeleton for bind pose # get the armature object and start doing math shidge - armature = bpy.data.objects[-1] # last object imported + armature = None + for obj in scene.objects: + if (obj.select == True and obj.type == "ARMATURE"): + armature = obj + break + # ^ the collada importer selects the armature and its children + # last children is scene's active object - # apply transform and select only the object - blender_funcs.select_obj(scene, armature, False) + # apply transform and select only the object in edit mode + blender_funcs.select_obj(armature, False, "EDIT") # Showtime (bind pose, meshes are already in their correct position) # delete all bones in the armature - bpy.ops.object.mode_set(mode = 'EDIT') - bpy.ops.armature.select_all(action = 'SELECT') + bpy.ops.armature.select_all(action = "SELECT") bpy.ops.armature.delete() # get the bones bind matrices @@ -92,7 +96,6 @@ def import_collada_superbmd(context, filepath): break # get the bone rest pose information - bpy.ops.object.mode_set(mode='EDIT') jnts_same_level = [jnt_root_node] is_first_jnt = True while (jnts_same_level != []): @@ -136,8 +139,8 @@ def import_collada_superbmd(context, filepath): else: bone.matrix = rest_matrices[bone.name] - # get to pose mode - bpy.ops.object.mode_set(mode='POSE') + # get to pose mode and deform the meshes + blender_funcs.select_obj(armature, False, "POSE") # set current pose to be rest pose and rest pose to be rest pose for bone in armature.pose.bones: @@ -147,21 +150,18 @@ def import_collada_superbmd(context, filepath): bone.matrix = rest_matrices[bone.name] # apply visual transform to all meshes - bpy.ops.object.mode_set(mode='OBJECT') for child in armature.children: - blender_funcs.select_obj(scene, child, False) - bpy.ops.object.convert(target='MESH') + blender_funcs.select_obj(child, False, "OBJECT") + bpy.ops.object.convert(target = "MESH") # apply pose to rest pose - blender_funcs.select_obj(scene, armature, False) - bpy.ops.object.mode_set(mode='POSE') + blender_funcs.select_obj(armature, False, "POSE") bpy.ops.pose.armature_apply() # reassign the armature modifiers to all meshes - bpy.ops.object.mode_set(mode='OBJECT') for child in armature.children: - blender_funcs.select_obj(scene, child, False) - bpy.ops.object.modifier_add(type = 'ARMATURE') + blender_funcs.select_obj(child, False, "OBJECT") + bpy.ops.object.modifier_add(type = "ARMATURE") child.modifiers["Armature"].object = armature child.modifiers["Armature"].use_vertex_groups = True @@ -169,6 +169,22 @@ def import_collada_superbmd(context, filepath): armature.scale = (0.01, 0.01, 0.01) blender_funcs.transf_apply_recurse(scene, armature, True, True, True) + # change the rotation mode of the pose bones + for bone in armature.pose.bones: + bone.rotation_mode = "XYZ" + + # remove doubles + for mesh in armature.children: + if (mesh.type == "MESH"): + blender_funcs.select_obj(mesh, False, "EDIT") + bpy.ops.mesh.select_all(action = "SELECT") + bpy.ops.mesh.remove_doubles() + bpy.ops.mesh.select_all(action = "DESELECT") + blender_funcs.select_obj(mesh, False, "OBJECT") + + # re-select the armature object + blender_funcs.select_obj(armature, False, "OBJECT") + # done! blender_funcs.disp_msg("SuperBMD Collada file imported!") return {'FINISHED'} @@ -193,6 +209,10 @@ class import_superbmd_collada(Operator, ExportHelper): # ExportHelper mixin class uses this filename_ext = ".dae" filter_glob = StringProperty(default = "*.dae", options = {'HIDDEN'}, maxlen = 255) + bruh = BoolProperty(name = "Bruh?", + description = "LMAOOOOO I made you read", + default = False) + # execute function def execute(self, context): return import_collada_superbmd(context, self.filepath) diff --git a/file_ops.py b/file_ops.py new file mode 100644 index 0000000..b9b3f6f --- /dev/null +++ b/file_ops.py @@ -0,0 +1,239 @@ +# my functions for file creation/deletion operations +# also for file path string stuff +import os, shutil + +# function to format a path string correctly: +# - no duplicate slashes +# - no backward slashes +# - nothing ends with a slash +def get_path_str(path): + + # check params + if (type(path) != str): + return "" + + # else work on the string + rtn_str = "" + while (path != ""): + if (path[0] in "/\\"): + rtn_str += '/' + while (len(path) > 0 and path[0] in "/\\"): + path = path[1:] + if (len(path) > 0): + rtn_str += path[0] + path = path[1:] + + # check if there is an ending slash + if (rtn_str[-1] == '/'): + rtn_str = rtn_str[:-1] + + # done! + return rtn_str + +# function to check if a file/folder path exists or not +def f_exists(path): + path = get_path_str(path) + return os.path.exists(path) + +# function to check if a file path actually points to a file +def is_file(path): + path = get_path_str(path) + return os.path.isfile(path) + +# function to check if a folder path actually points to a folder +def is_folder(path): + path = get_path_str(path) + return os.path.isdir(path) + +# function to get the file name portion of a file path +def get_file_name(path): + + # check params + if (type(path) != str): + return None + path = get_path_str(path) + + # get the full path of the file/folder + if (f_exists(path)): + path = os.path.abspath(path) + + # start reading path string + string = "" + for i in range(len(path) - 1, -1, -1): + if (path[i] == '/'): + for j in range(i + 1, len(path)): + string += path[j] + break + + # done! + return string + +# function to get the base path of a file or folder +def get_base_path(path, is_file): + + # check params + if ((type(path) != str) or (type(is_file) != bool)): + return None + path = get_path_str(path) + + # get the full path of the file/folder + if (f_exists(path)): + path = get_path_str(os.path.abspath(path)) + + # work on path + if (is_file): + while (path != ""): + if (path[-1] == '/'): + path = path[:-1] + break + path = path[:-1] + return path + +# function to get the base path of a file or folder +def get_base_folder_name(path, is_file, folder_as_file): + + # check params + if ((type(path) != str) or (type(is_file) != bool) or (type(folder_as_file) != bool)): + return None + path = get_path_str(path) + + # reuse get_base_path() and get_file_name() + if ((is_file) or (is_file == False and folder_as_file)): + return get_file_name(get_base_path(path, True)) + return get_file_name(get_base_path(path, False)) + +# function to copy a file to another location +def cp_file(src, dest): + + # check params + if ((f_exists(src) == False) or (is_file(src) == False)): + return False + + # depending on dest 2 things can happen + # - dest defines a new name for the file to create/replace + # - dest is just a folder + if (f_exists(dest) == False): # copy as a new file in that location + # get the base path of dest + base_path = get_base_path(dest, True) + # create the base path if it does not exist + if (f_exists(base_path) == False): + os.makedirs(base_path) + # copy the file to the directory + shutil.copy(src, base_path + "/" + get_file_name(dest)) + elif (f_exists(dest) == True and is_file(dest)): + # get the base path of dest + base_path = get_base_path(dest, True) + # remove the file at dest + os.remove(dest) + # copy the file in src to dest + shutil.copy(src, base_path + "/" + get_file_name(dest)) + elif (f_exists(dest) == True and is_folder(dest)): + # copy the file in src to dest + shutil.copy(src, dest + "/" + get_file_name(src)) + + # done! + return True + +# function to return a folder tree (file/folders) +def list_folder_tree(src): + + # check params + if (type(src) != str or f_exists(src) == False): + return [] + + # get the list of all files inside the mod folder first (files/folders) + # infinite loop moment + f_list = [src] + tmp_list = [] + while (True): + # reset tmp_list + tmp_list = [] + # add the new files in f_list folders + # that are not present in it to tmp_list + for f in f_list: + # add new folder/file + if (f not in tmp_list): + tmp_list.append(f) + # if f is a folder check its contents so they can be added to tmp_list + if (is_folder(f)): + for int_f in os.listdir(f): + # add new folder/file + if ((f + "/" + int_f) not in tmp_list): + tmp_list.append(f + "/" + int_f) + # when there is nothing more new added to tmp_list end the infinite loop + if (f_list == tmp_list): + break + # otherwise keep adding the mising files + f_list = tmp_list + # ^ I honestly wasn't expecting this loop to work at first lolll + return f_list + +# function to copy a folder to another folder (a file like copy) +def cp_folder(src, dest, treat_as_file): + + # check params + if ((f_exists(src) == False) or (is_folder(src) == False) + or (type(dest) != str) or (type(treat_as_file) != bool)): + return False + + # check the existence of dest + if (f_exists(dest) == False): + os.makedirs(dest) + + # check if src is to be treated as a file (i.e. copy the folder as well and not just its contents) + if (treat_as_file): + dest = dest + "/" + get_base_folder_name(src, False, True) + if (f_exists(dest) == False): + os.makedirs(dest) + + # get the list of all files inside the mod folder first (files/folders) + # infinite loop moment + f_list = list_folder_tree(src) + + # start copying the files + for f in f_list: + dest_f_path = dest + "/" + f.replace(src, "") + if (is_file(f)): + cp_file(f, dest_f_path) + elif (is_folder(f) and (is_folder(dest_f_path) == False)): + os.makedirs(dest_f_path) + + # done! + return True + +# function to delete a file +def rm_file(path): + + # check params + if (type(path) != str or f_exists(path) == False or is_file(path) == False): + return False + + # remove the file + os.remove(path) + return True + +# function to delete a folder with/without contents, don't care if the folder does not exist +def rm_folder(path): + + # check params + if (type(path) != str or f_exists(path) == False or is_folder(path) == False): + return False + + # remove the folder (get the tree list) + f_list = list_folder_tree(path) + for i in range(len(f_list) - 1, -1, -1): + if (is_file(f_list[i])): + rm_file(f_list[i]) + elif (is_folder(f_list[i])): + os.rmdir(f_list[i]) + + # done! + return True + +# function to get the size of a file +def get_file_size(path): + + # check params + if (is_file(path) == False): + return 0 + return os.path.getsize(get_path_str(path)) diff --git a/math_funcs.py b/math_funcs.py index 8f92bef..ec7f7de 100644 --- a/math_funcs.py +++ b/math_funcs.py @@ -1,319 +1,280 @@ -import math -from mathutils import Matrix +import math, numpy +import mathutils +import warnings +warnings.simplefilter("ignore", numpy.RankWarning) # file containning math related functions # function to return a 0 filled matrix 3x3 def get_zero_mat3x3(): - - return Matrix(([0, 0, 0], [0, 0, 0], [0, 0, 0])) + return mathutils.Matrix(([0, 0, 0], [0, 0, 0], [0, 0, 0])) # function to return a 0 filled matrix 4x4 def get_zero_mat4x4(): - - return Matrix(([0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0])) + return mathutils.Matrix(([0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0])) # function to return an identity matrix 3x3 def get_id_mat3x3(): - - return Matrix(([1, 0, 0], [0, 1, 0], [0, 0, 1])) + return mathutils.Matrix(([1, 0, 0], [0, 1, 0], [0, 0, 1])) # function to return an identity matrix 4x4 def get_id_mat4x4(): - - return Matrix(([1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1])) + return mathutils.Matrix(([1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1])) # calc_scale_matrix function # function to build the scale matrix -def calc_scale_matrix(x_scale, y_scale, z_scale): +def calc_scale_matrix(sx, sy, sz): - scale_matrix = Matrix(( [x_scale, 0, 0, 0], - [0, y_scale, 0, 0], - [0, 0, z_scale, 0], - [0, 0, 0, 1] - )) - - return scale_matrix + mat = mathutils.Matrix(([sx, 0, 0, 0], + [0, sy, 0, 0], + [0, 0, sz, 0], + [0, 0, 0, 1])) + return mat # calc_rotation_matrix function # function to calculate the rotation matrix # for a Extrinsic Euler XYZ system (radians) -def calc_rotation_matrix(x_angle, y_angle, z_angle): - - x_rot_mat = Matrix(([1, 0, 0, 0], - [0, math.cos(x_angle), -math.sin(x_angle), 0], - [0, math.sin(x_angle), math.cos(x_angle), 0], - [0, 0, 0, 1])) - - y_rot_mat = Matrix(([math.cos(y_angle), 0, math.sin(y_angle), 0], - [0, 1, 0, 0], +def calc_rotation_matrix(rx, ry, rz): - [-math.sin(y_angle), 0, math.cos(y_angle), 0], - [0, 0, 0, 1])) - - z_rot_mat = Matrix(([math.cos(z_angle), -math.sin(z_angle), 0, 0], - [math.sin(z_angle), math.cos(z_angle), 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1])) - - return z_rot_mat * y_rot_mat * x_rot_mat + x_rot = mathutils.Matrix(([1, 0, 0, 0], + [0, math.cos(rx), -math.sin(rx), 0], + [0, math.sin(rx), math.cos(rx), 0], + [0, 0, 0, 1])) + y_rot = mathutils.Matrix(([math.cos(ry), 0, math.sin(ry), 0], + [0, 1, 0, 0], + [-math.sin(ry), 0, math.cos(ry), 0], + [0, 0, 0, 1])) + z_rot = mathutils.Matrix(([math.cos(rz), -math.sin(rz), 0, 0], + [math.sin(rz), math.cos(rz), 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1])) + return z_rot * y_rot * x_rot # calc_translation_matrix function # function to build the translation matrix -def calc_translation_matrix(x_translation, y_translation, z_translation): +def calc_translation_matrix(tx, ty, tz): - translation_matrix = Matrix(( [1, 0, 0, x_translation], - [0, 1, 0, y_translation], - [0, 0, 1, z_translation], - [0, 0, 0, 1] - )) - - return translation_matrix - -# interpolate function -# used to find a value in an interval with the specified mode. -# So that it is clear that the values are points that have 2 -# coordinates I will treat the input as they are (x,y) points -# the function will either return m_x or m_y depending on -# which of the middle point values is provided to the function -# (set None to the variable going to be returned by the -# function). Only linear interpolation is supported for now. -# -# l_x (float) --> left point X axis component -# l_y (float) --> left point Y axis component -# r_x (float) --> right point X axis component -# r_y (float) --> right point Y axis component -# m_x (float) --> middle point X axis component -# m_y (float) --> middle point Y axis component -# interp_type (string) --> "linear" for linear interpolation -def interpolate(l_x, l_y, r_x, r_y, m_x, m_y, interp_type): - - # variable to be returned - result = 0 - - # if right point does not exist (special case) - # return l_y as the interpolation result - if (r_x == None or r_y == None): - result = l_y - return result - - # m_x is the one to be returned - if (m_x == None): - - # linear interpolation - if (interp_type == "linear"): - m_x = (((r_x - l_x) / (r_y - l_y)) * (m_y - r_y)) + r_x - result = m_x - - # m_y is the one to be returned - if (m_y == None): - - # linear interpolation - if (interp_type == "linear"): - m_y = (((r_y - l_y) / (r_x - l_x)) * (m_x - r_x)) + r_y - result = m_y - - return result + mat = mathutils.Matrix(([1, 0, 0, tx], + [0, 1, 0, ty], + [0, 0, 1, tz], + [0, 0, 0, 1])) + return mat -# find_left_right function -# find the values and positions of the elements at the left and the -# right of the element in position pos on the anim_array -# elements will be used in the interpolate() function later -# -# anim_array (array of floats) --> anim property frame array its -# length must the animation length -# pos (int) --> position of the animation property value to be -# later interpolated in the anim_array array -def find_left_right(anim_array, pos): +# calculate transformation matrix for blender +def calc_transf_mat(scale, rotation, translation): + mat = calc_translation_matrix(translation[0], translation[1], translation[2]) + mat *= calc_rotation_matrix(rotation[0], rotation[1], rotation[2]) + mat *= calc_scale_matrix(scale[0], scale[1], scale[2]) + return mat - # create left/right variables - l_val = 0 - l_val_pos = 0 - r_val = 0 - r_val_pos = 0 +# calculate a value of a cubic hermite interpolation +# it is assumed t0 and tf are 0 and 1 +# t is the parametrization variable +# https://humming-owl.neocities.org/smg-stuff/pages/tutorials/model3#cubic-hermite-spline +def cubic_hermite_spline_time_unitary(p0, m0, m1, p1, t): + # interpolation not defined here bruh + if (t < 0 or t > 1): + return None + t_pow3 = math.pow(t, 3) + t_pow2 = math.pow(t, 2) + t_pow1 = t + interp_result = p0 * ((+2 * t_pow3) - (3 * t_pow2) + 1) + interp_result += m0 * ((+1 * t_pow3) - (2 * t_pow2) + t_pow1) + interp_result += m1 * ((+1 * t_pow3) - (1 * t_pow2) + 0) + interp_result += p1 * ((-2 * t_pow3) + (3 * t_pow2) + 0) + return interp_result - # find near left value (has to exist) - # read array from right to left - for i in range(len(anim_array), -1, -1): - # - # skip elements at the right of - # pos in anim_array - if (i >= pos): - continue - - # left value is found - if (anim_array[i] != None): - l_val = anim_array[i] - l_val_pos = i - break - # +# same as the above function but t0 and tf can be any time value (tf > t0) +def cubic_hermite_spline_time_general(p0, m0, m1, p1, t0, tf, t): + # interpolation not defined here bruh + if (t < t0 or t > tf): + return None + interp_result = cubic_hermite_spline_time_unitary(p0, m0 * (tf - t0), m1 * (tf - t0), p1, (t - t0) / (tf - t0)) + return interp_result - ############## - # special case - # if pos is the last element position on - # the array r_val and r_val_pos do not exist - if (pos == (len(anim_array) - 1)): - return [l_val_pos, l_val, None, None] +# structure to be returned by the function below +class best_chs_fits: - # find near right value (might not exist) - # read array from left to right - for i in range(len(anim_array)): - # skip elements at the left of - # pos in anim_array - if (i <= pos): - continue - - # right value is found - if (anim_array[i] != None): - r_val = anim_array[i] - r_val_pos = i - break - - # if no value is found at the end of - # the anim_array r_val and r_val_pos do not exist - # (value does not change between the left value - # found and the end of the animation) - if (i == (len(anim_array) - 1)): - return [l_val_pos, l_val, None, None] + def __init__(self): + self.kf_count = 0 + self.time = [] + self.value = [] + self.in_slope = [] + self.out_slope = [] - # if all values are found, return them - return [l_val_pos, l_val, r_val_pos, r_val] + def __str__(self): + rtn = "keyframe count: %s\n" % (self.kf_count) + rtn += "times: %s\n" % (self.time) + rtn += "values: %s\n" % (self.value) + rtn += "in slopes: %s\n" % (self.in_slope) + rtn += "out slopes: %s" % (self.out_slope) + return rtn -# convert_angle_to_180 function -# function used by the convert_anim_rot_to_180 function -# to convert a single angle in its representation on -# the -180/+180 degree range (angles passed to the -# function that are already in this range will be -# returned without conversion) -# -# angle (float) --> angle to convert to the -180/+180 -# degree range (angle is expected to -# be in degrees) -def convert_angle_to_180(angle): -# - # check if the angle really needs to be processed - # i.e. is already inside the -180/+180 degree range - if (angle >= -180 and angle <= 180): - return angle - - # convert it otherwise +# function to calculate the best cubic hermite spline interpolator fits +# for a given a set of points. The set of points is expected to be at +# each frame of the animation (start_frame indicates the start frame, integer) +# the function will return the above structure +# it will be the generic cubic hermite spline form +def find_best_cubic_hermite_spline_fit(start_frame, values, angle_limit): - # check if it is positive or negative - # and set the opposite direction of the angle - # if the angle is > 0 then its mesurement is clockwise (opposite is counter-clockwise) - # if the angle is < 0 then its mesurement is counter-clockwise (opposite is clockwise) - if (angle > 0): - opposite_spin_dir = -1 - else: # it is negative - opposite_spin_dir = 1 + # check + if ((type(start_frame) != int) + or (type(values) != list) + or (len(values) == 0)): + return None - # decrease the angle by 360 degrees until it - # is in the -180/+180 degree interval - while (abs(angle) > 180): - angle = angle + (opposite_spin_dir * 360) + # create the object to return + rtn = best_chs_fits() - return angle - -# convert_anim_rot_to_180 function -# used to re-calculate a rotation animation on an axis -# so that angles used lay in between -180/180 degrees -# done to avoid rotation animation data loss when extracting -# said angles from a transformation matrix -# this function calls the convert_angle_to_180() function -# at the end of the function csv_keyframe_numbers is updated -# with the new frames to be injected into the animation -# -# example: -# -# Original keyframes: -# Frame 0 Frame 21 (2 keyframes) -# 0º 360º -# -# Processed keyframes: -# Frame 0 Frame 10 Frame 11 Frame 21 (4 keyframes) -# 0º 171.4º -171.5º 0º -# -# rot_array (array of floats) --> bone rotation animation data -# for a single axis -# csv_keyframe_numbers (array of ints) --> original keyframes -# of the animation -# -# Note: function will have problems interpreting keyframes with -# high rotation diferences if the number of frames in -# between said keyframes in lower than 2 times the spins -# done in between those keyframe angles (thinking a fix) -def convert_rot_anim_to_180(rot_array, csv_keyframe_numbers): + # the cubic hermite spline is just a sub-family of 3rd degree bezier curves + # that makes this curve to be "able" to represent 3rd degree polynomials + # these polinomials can have a maximum of 2 concavity changes + # to avoid a greater data loss I will just make the best fit + # when single concavity change is reached - # temp rot array to store calculated values and keyframe position - rot_array_cp = [[], []] + # special cases + if (len(values) == 1): # single keyframe values like in BCK + rtn.kf_count = 1 + rtn.time = [None] + rtn.value = values + rtn.in_slope = [None] + rtn.out_slope = [None] + return rtn + elif (len(values) == 2): # 2 keyframe values (force linear interpolation) + rtn.kf_count = 2 + rtn.time = [start_frame, start_frame + 1] + rtn.value = values + rtn.in_slope = [None, (rtn.value[1] - rtn.value[0]) / (rtn.time[1] - rtn.time[0])] + rtn.out_slope = [(rtn.value[1] - rtn.value[0]) / (rtn.time[1] - rtn.time[0]), None] + return rtn - # find the frames in which rot_array has values defined - rot_array_kf = [] - for i in range(len(rot_array)): - if (rot_array[i] != None): - rot_array_kf.append(i) + # get the lowest and highest value of the set of values given + # will be used to scale the points to be able to do derivative difference checking + # also get the average + # all of them positive + lowest_value = 0 + highest_value = 0 + avg_value = 0 + for i in range(len(values)): + if (abs(values[i]) < lowest_value): + lowest_value = values[i] + if (abs(values[i]) > highest_value): + highest_value = values[i] + avg_value += abs(values[i]) + scale_factor = (1 / highest_value) * (len(values) - start_frame) + avg_value /= len(values) - # loop through each consecutive pair of keyframes of the rot_array_kf array - for i in range(len(rot_array_kf) - 1): - # - # get left/right keyframe values and positions - l_kf_pos = rot_array_kf[i] - l_kf_val = rot_array[l_kf_pos] - r_kf_pos = rot_array_kf[i + 1] - r_kf_val = rot_array[r_kf_pos] - - # append l_kf_val to rot_array_cp (converted) - rot_array_cp[0].append(l_kf_pos) - rot_array_cp[1].append(convert_angle_to_180(l_kf_val)) + # lets do this bruv + start_index = 0 + old_value = None + cur_value = None + new_value = None + left_first_derivative = None + right_first_derivative = None + old_concavity = None + new_concavity = None + generate_keyframe = None + small_time_dif = 0.000001 + # generate the keyframes + i = 0 + while (i < len(values)): + # first keyframe + if (i == 0): + rtn.kf_count += 1 + rtn.time.append(start_frame) + rtn.value.append(values[0]) + rtn.in_slope.append(None) + rtn.out_slope.append(None) - # get the rotation direction - if (r_kf_val > l_kf_val): # clockwise - rot_direction = 1 - else: # counter-clockwise - rot_direction = -1 + # last keyframe / concavity never changes + elif (i == len(values) - 1): + # add the last keyframe of the animation + poly_deg = 3 + if (i - start_index == 1): + poly_deg = 1 + elif(i - start_index == 2): + poly_deg = 2 + rtn.kf_count += 1 + rtn.time.append(start_frame + i) + rtn.value.append(values[-1]) + poly = numpy.polyfit(list(range(start_index, i + 1)), + values[start_index : i + 1], poly_deg) + p0 = numpy.polyval(poly, start_index) + pa = numpy.polyval(poly, start_index + small_time_dif) + m0 = (pa - p0) / small_time_dif + pb = numpy.polyval(poly, i - small_time_dif) + p1 = numpy.polyval(poly, i) + m1 = (p1 - pb) / small_time_dif + rtn.in_slope.append(m1) + rtn.out_slope[-1] = m0 + rtn.out_slope.append(None) - # advance -180/+180 (depending on the rotation direction) - # and generate the middle keyframe values - # angle is fixed to the l_kf_val's closest 360 degree multiple - angle_val = l_kf_val - convert_angle_to_180(l_kf_val) - while (abs(r_kf_val - angle_val) > 180): - # - angle_val = angle_val + (rot_direction * 180) + # the rest + else: - # find the frame (float) in which this value exists - angle_pos = interpolate(l_kf_pos, l_kf_val, r_kf_pos, r_kf_val, None, angle_val, "linear") + # get the values + old_value = values[i - 1] + cur_value = values[i] + new_value = values[i + 1] + + # determine the first derivative on both sides + left_first_derivative = cur_value - old_value + right_first_derivative = new_value - cur_value + new_concavity = right_first_derivative - left_first_derivative - # find the 2 frames (integer) that are lower - # and upper limits of this angle_pos - lower_frame = int(angle_pos) - upper_frame = int(angle_pos + 1) + # check if this is the first time getting the concavity + if (old_concavity == None): + old_concavity = new_concavity - # interpolate to find the values on lower_frame and upper_frame - lower_frame_value = convert_angle_to_180(interpolate(l_kf_pos, l_kf_val, r_kf_pos, r_kf_val, lower_frame, None, "linear")) - upper_frame_value = convert_angle_to_180(interpolate(l_kf_pos, l_kf_val, r_kf_pos, r_kf_val, upper_frame, None, "linear")) + # check concavity changes + if (numpy.sign(old_concavity) != numpy.sign(new_concavity)): + generate_keyframe = True - # append results to rot_array_cp + # scale the values up and check if the slope change is too violent with vector angles + # the scaling is done to be able to visually see the angle changes, it is the same thing + # we do with curves when zoomin in or out (either horizontally or vertically) + left_first_derivative_scaled = left_first_derivative * scale_factor + right_first_derivative_scaled = right_first_derivative * scale_factor + left_vec = mathutils.Vector((1, left_first_derivative_scaled)) + right_vec = mathutils.Vector((1, right_first_derivative_scaled)) + angle = abs(math.degrees(left_vec.angle(right_vec))) + if (angle > angle_limit): + generate_keyframe = True - # keyframes - rot_array_cp[0].append(lower_frame) - rot_array_cp[0].append(upper_frame) - # values - rot_array_cp[1].append(lower_frame_value) - rot_array_cp[1].append(upper_frame_value) - - # add the new keyframe values to rot_array - # on their respective frame position - for i in range(len(rot_array_cp[0])): - rot_array[rot_array_cp[0][i]] = rot_array_cp[1][i] + # store the old concavity for the next loop + old_concavity = new_concavity + + # check if a new keyframe should be added + if (generate_keyframe == True): + # find the best 3rd degree polynomial "fit" with the set of points studied + poly_deg = 3 + if (i - start_index == 1): + poly_deg = 1 + elif(i - start_index == 2): + poly_deg = 2 + poly = numpy.polyfit(list(range(start_index, i + 1)), + values[start_index : i + 1], poly_deg) + print(poly) + p0 = numpy.polyval(poly, start_index) + pa = numpy.polyval(poly, start_index + small_time_dif) + m0 = (pa - p0) / small_time_dif + pb = numpy.polyval(poly, i - small_time_dif) + p1 = numpy.polyval(poly, i) + m1 = (p1 - pb) / small_time_dif + # write the data into the structure + rtn.kf_count += 1 + rtn.time.append(start_frame + i) + rtn.value.append(p1) + rtn.in_slope.append(m1) + rtn.out_slope[-1] = m0 + rtn.out_slope.append(None) + + # reset these variables for the next iteration + start_index = i + generate_keyframe = False + + # increment i + i += 1 - # update the keyframes on csv_keyframe_numbers to store - # the calculated keyframes numbers from rot_array_cp[0] - # append those at the end of csv_keyframe_numbers - for i in range(len(rot_array_cp[0])): - value_found = False - for j in range(len(csv_keyframe_numbers)): - if (rot_array_cp[0][i] == csv_keyframe_numbers[j]): - value_found = True - break - if (value_found == False): - csv_keyframe_numbers.append(rot_array_cp[0][i]) + # done! + return rtn diff --git a/obj_kcl_export.py b/obj_kcl_export.py index b106869..56b6d18 100644 --- a/obj_kcl_export.py +++ b/obj_kcl_export.py @@ -12,29 +12,27 @@ def write_kcl_obj(context, filepath): scene = bpy.context.scene # if nothing is selected end the exporter - if (scene.objects.active == None or scene.objects.active.type != 'ARMATURE'): + 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'} + blender_funcs.disp_msg("No Armature selected. Currently selecting: \"%s\"" % (scene.objects.active.name)) + return {"FINISHED"} # get armature object armature = scene.objects.active print("\nArmature found: \"%s\"" % (armature.name)) - # change to object mode - bpy.ops.object.mode_set(mode='OBJECT') + # select it well + blender_funcs.select_obj(armature, False, "OBJECT") # check if the armature contains only mesh objects inside for obj in armature.children: - if (obj.type != 'MESH'): - blender_funcs.disp_msg("\"%s\": contains non-mesh object (%s)." - % (armature.name, obj.name)) - return {'FINISHED'} + if (obj.type != "MESH"): + blender_funcs.disp_msg("\"%s\": contains non-mesh object (%s)." % (armature.name, obj.name)) + return {"FINISHED"} # select all the meshes inside the armature - blender_funcs.select_obj(scene, armature, True) + blender_funcs.select_obj(armature, True, "OBJECT") # export the object bpy.ops.export_scene.obj(filepath = filepath, axis_up = "Y", axis_forward = "-Z", @@ -42,9 +40,9 @@ def write_kcl_obj(context, filepath): use_materials = True, use_normals = True) # done! - blender_funcs.select_obj(scene, armature, False) + blender_funcs.select_obj(armature, False, "OBJECT") blender_funcs.disp_msg("Meshes from \"%s\" exported!" % (armature.name)) - return {'FINISHED'} + return {"FINISHED"} # Stuff down is for the menu appending # of the exporter to work plus some setting stuff diff --git a/obj_neokclcreate_import.py b/obj_neokclcreate_import.py index 1b63633..975112d 100644 --- a/obj_neokclcreate_import.py +++ b/obj_neokclcreate_import.py @@ -21,7 +21,7 @@ def import_obj_neokclcreate(context, filepath): if (o.select == True): obj = o break - blender_funcs.select_obj(scene, obj, False) + blender_funcs.select_obj(obj, False, "OBJECT") obj.scale = obj.scale / 100 bpy.ops.object.transform_apply(location = True, rotation = True, scale = True) @@ -35,6 +35,7 @@ def import_obj_neokclcreate(context, filepath): cur_mat_name = re.sub("^(.*?)\| ", "", mat.name) # iterate over all the mesh faces for face in obj.data.polygons: + print(face) face_mat = obj.material_slots[face.material_index] face_mat_name = re.sub("^(.*?)\| ", "", face_mat.name) # material with the same name found @@ -65,7 +66,6 @@ from bpy.props import StringProperty, BoolProperty, EnumProperty from bpy.types import Operator class import_neokclcreate_obj(Operator, ExportHelper): - """Import a Collada file from SuperBMD (SuperBMD only)""" bl_idname = "import_scene.neokclcreate_obj" bl_label = "Import NeoKCLCreate OBJ (.OBJ)" diff --git a/required_modules.py b/required_modules.py index ae2b44b..069a1b7 100644 --- a/required_modules.py +++ b/required_modules.py @@ -34,5 +34,5 @@ print("lxml %s is installed!" % (lxml.__version__)) # hacky way to do it but it is the only way that works if (numpy.__version__ != "1.18.5"): file_ops.rm_folder(numpy.__path__[0]) # remove the installation folder - run(pip_install + ["numpy==1.18.5"]) # install the newer version + run(pip_install + ["numpy==1.18.5"]) # install a newer version print("numpy %s is installed!" % (numpy.__version__)) diff --git a/smg_common.py b/smg_common.py new file mode 100644 index 0000000..c278382 --- /dev/null +++ b/smg_common.py @@ -0,0 +1,75 @@ +# paddng string +padding = + +# name tables + +# read name tables found in SMG binary files + +# ~ # structure raw +# ~ class smg_name_table_raw: + # ~ # functions + # ~ def __init__(self): + # ~ self.name_count = None + # ~ self.unknown1 = None + # ~ self.name_info = [] # of name_count length + # ~ self.names = [] # of name_count length + # ~ def __str__(self): + # ~ rtn = "Name count: %s\n" % (self.name_count) + # ~ rtn += "Unknown 1: %04X\n" % (self.unknown1) + # ~ rtn += "Names information:\n" + # ~ for i in range(self.name_count): + # ~ rtn += " Name %d:\n" % (i) + # ~ rtn += self.name_info[i].__str__() + # ~ rtn += "Names:\n" + # ~ for i in range(self.name_count): + # ~ rtn += " Index %d -> %s\n" % (i, names[i]) + # ~ return rtn + + # ~ # information about the names + # ~ class smg_name_info: + # ~ # functions + # ~ def __init__(self): + # ~ self.hash = None + # ~ self.offset = None + # ~ def __str__(self): + # ~ rtn = " Hash: %04X\n" % (self.hash) + # ~ rtn += " Offset: %s\n" % (self.offset) + # ~ return rtn + +# ~ # actual structure to use +# ~ class smg_name_table_info: + # ~ # functions + # ~ def __init__(self): + # ~ self.name_count = None + # ~ self.names = [] # of length name_count + # ~ def __init__(self): + # ~ rtn = "Name count: %s\n" % (self.name_count) + # ~ rtn += "Names:\n" + # ~ for i in range(self.name_count): + # ~ rtn += " Index %d -> %s\n" % (names[i]) + # ~ return rtn + +# ~ # create a global variable to hold temporal information +# ~ name_table_raw_info = None +# ~ name_table_error_str = "name_table-error: " +# ~ f = None + +# ~ # main function +# ~ # will read and will check while reading +# ~ def read_name_table(byte_array): + + # ~ # make global variables editable + # ~ global f + # ~ global name_table_raw_info + # ~ # "pre read" the file + # ~ result_str = pre_read_name_table(byte_array) + # ~ print(result_str) + # ~ return None + +# ~ # function to check an encoded name table before getting its full information out +# ~ def pre_read_name_table(byte_array): + + + + # ~ return name_table_error_str + "all good" + diff --git a/test.bin b/test.bin new file mode 100644 index 0000000..9f543a3 Binary files /dev/null and b/test.bin differ diff --git a/test.py b/test.py new file mode 100644 index 0000000..1d74b42 --- /dev/null +++ b/test.py @@ -0,0 +1,26 @@ +import smg_name_table_funcs + +# I think I am making my own way to handle bytes +# ~ class owl_byte(int): + # ~ def __str__(self): + # ~ return "%0X" % (self) + +# I think I am making my own way to print bytes +# the default printing is awful to look at and I need rapid inspection +class owl_bytes(bytearray): + def __str__(self): + rtn = "" + for i in range(len(self)): + rtn += "%02X" % self[i] + rtn += " " + return rtn + +f = open("test.bin", "rb") +byte_arr = owl_bytes(b"") +byte = f.read(1) +while (byte != b""): + byte_arr += byte + byte = f.read(1) +f.close() + +print(byte_arr) -- cgit v1.2.3-70-g09d2