diff options
author | Isaac <isaclien9752@gmail.com> | 2024-08-14 22:48:16 -0400 |
---|---|---|
committer | Isaac <isaclien9752@gmail.com> | 2024-08-14 22:48:16 -0400 |
commit | 17fd2304270b2ef619cceb980b9f49cb656a6d13 (patch) | |
tree | decd5572cf766c781c2bb6a604aa66155041cdb4 | |
parent | b907bcc414eb742f45e2a0c2ea0232256c226a92 (diff) | |
download | blenxy-17fd2304270b2ef619cceb980b9f49cb656a6d13.tar.gz blenxy-17fd2304270b2ef619cceb980b9f49cb656a6d13.zip |
updated Collada importer/exporter for SuperBMD
-rw-r--r-- | Mario.dae | 4 | ||||
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | __init__.py | 18 | ||||
-rw-r--r-- | basic_settings.py (renamed from blenxy_settings_stuff.py) | 63 | ||||
-rw-r--r-- | blender_funcs.py | 97 | ||||
-rw-r--r-- | collada_bmd_bdl_export.py | 316 | ||||
-rw-r--r-- | collada_bmd_bdl_import.py | 1130 | ||||
-rw-r--r-- | collada_funcs.py | 43 | ||||
-rw-r--r-- | collada_superbmd_export.py | 168 | ||||
-rw-r--r-- | collada_superbmd_import.py | 202 | ||||
-rw-r--r-- | math_funcs.py (renamed from my_functions.py) | 109 |
11 files changed, 577 insertions, 1595 deletions
@@ -7,7 +7,7 @@ </contributor> <created>2023-07-04T13:47:18</created> <modified>2023-07-04T13:47:18</modified> - <unit name="meter" meter="100"/> + <unit name="meter" meter="0.01"/> <up_axis>Z_UP</up_axis> </asset> <library_images/> @@ -68,4 +68,4 @@ <scene> <instance_visual_scene url="#Scene"/> </scene> -</COLLADA>
\ No newline at end of file +</COLLADA> @@ -6,22 +6,16 @@ https://humming-owl.neocities.org/smg-stuff/pages/tutorials/t8.html # TODO -- CSV Exporter has issues with equivalent keyframes/rotation angles close to -180/+180 degrees (think a fix). -- DAE importer to apply textures to model on import. -- Think on the possibility of a BTP Importer/Exporter. -- Add a OBJ exporter/importer for collision models (for conversion to KCL and PA). -- Rely less on blender's DAE importer/exporter (basically make my own, but I have no clue how cuz currently I have skill issue) +- Redo the whole logic of the CSV BCK importer/exporter - ... # Features -- Custom DAE Exporter for BMD/BDL conversion with SuperBMD. -- Custom DAE Importer for DAEs that come from SuperBMD (only from SuperBMD). -- CSV Exporter for the BCK animation type (to use with the JAE, linear interpolation only). -- CSV Importer for the BCK animation type from JAE (linear interpolation only). -- Reference axis `SMG axis` shows on all 3d models to guide the visualization of a mesh in the Super Mario Galaxy reference system. -- Custom mesurement unit `Galaxy Unit` (or GU) that is used to facilitate DAE import/export without the need to re-scale manually all the time (1 GU is 100 meters). -- Epic Mario model as the default cube in Blender when starting the template (truly the most important part of the template >:]). +- Custom Collada importer for SuperBMD Collada files. +- Custom Collada exporter for conversion with SuperBMD. +- 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 >:]). - ... # Installation @@ -43,8 +37,8 @@ If you want to contribute or report an issue open a new issue in **Issues**. I w - Gabbo for pointing me out the Blender Python API reference. - RenolY2 for J3DView and SuperBMD. - The authors of the material I could read about BMD/BDL. -- The many stackoverflow threads (or similar) in which Blender Python API issues are discussed. +- The many stackoverflow threads (or similar forums/pages) in which Blender Python API issues/facts are discussed. # Changelog -- +- 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 4131210..5d783d2 100644 --- a/__init__.py +++ b/__init__.py @@ -13,22 +13,21 @@ https://web.archive.org/web/20210925181415/https://blenderbrew.com/custom-applic import bpy from bpy.app.handlers import persistent -####################################################### +################################################# # define function that sets all the blenxy stuff: # galaxy unit, custom collada exporter/importer # custom CSV animation file (for BCK) exporter/importer -####################################################### @persistent def set_blenxy_env(dummy): # "dummy" is a variable that is somehow # passed to set_blenxy_env when called by # bpy.app.handlers.load_post.append - from . import blenxy_settings_stuff # settings blenxy has - from . import collada_bmd_bdl_export # custom exporter for DAE files - from . import collada_bmd_bdl_import # custom importer for DAE 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 + 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 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 @@ -36,15 +35,14 @@ def set_blenxy_env(dummy): # "dummy" is a variable that is somehow ############################# # register/unregister stuff # for set_blenxy_env function -############################# def register(): print("\nWelcome to Blenxy!\n") - print("Setting Blenxy Environment...\n") + print("Setting environment...") bpy.app.handlers.load_post.append(set_blenxy_env) def unregister(): - print("\nSee you later!\n") + print("See you later!") bpy.app.handlers.load_post.remove(set_blenxy_env) if __name__ == "__main__": diff --git a/blenxy_settings_stuff.py b/basic_settings.py index 9636522..1b16e45 100644 --- a/blenxy_settings_stuff.py +++ b/basic_settings.py @@ -1,48 +1,35 @@ import bpy -# SMG1/2 was made with Autodesk Maya -# and Maya uses centimeters as default unit -# so supposedly SMG1/2 original units are in centimeters - -# however... - -# SuperBMD assumes everything is in meters so -# using the Mario.dae obtained from the Mario.bdl file -# in the template I will set 1 Galaxy Unit as 100 Meters -# this can be easily modified in the future in case -# I get how this unit thing works fully -# might open an issue on the SuperBMD repo to ask about it +# SMG was made under the centimeter unit +# SuperBMD assumes everything is in meters so I will scale down the models when being imported scene = bpy.context.scene -print("Setting Galaxy Unit...") - -scene.unit_settings.scale_length = 100 +print("Setting length/rotation units...") scene.unit_settings.system = 'METRIC' +scene.unit_settings.scale_length = 0.01 bpy.ops.scene.units_length_preset_add(name="Galaxy Unit") scene.unit_settings.system_rotation = 'DEGREES' scene.unit_settings.use_separate = False - # scale grid to fit new length unit # Reference used: # https://blender.stackexchange.com/a/210749 -print("\nScaling 3D View grid...") +print("Scaling 3D View grid...") for area in bpy.context.screen.areas: if (area.type == 'VIEW_3D'): - view_3d_area = area.spaces.active # declare to use later - area.spaces.active.grid_scale = 100 - area.spaces.active.grid_lines = 300 - area.spaces.active.show_axis_x = False - area.spaces.active.show_axis_y = False - area.spaces.active.show_axis_z = False - area.spaces.active.clip_start = 0.1 - area.spaces.active.clip_end = 10000 + view_3d_area = area.spaces.active + view_3d_area.grid_scale = 0.01 + view_3d_area.grid_lines = 400 + view_3d_area.show_axis_x = True + view_3d_area.show_axis_y = True + view_3d_area.show_axis_z = True + view_3d_area.clip_start = 0.001 + view_3d_area.clip_end = 1000000 break - # import Mario's model from DAE file included in the template # get blenxy template location for that and select it # but first delete all objects in scene so it is clean @@ -50,14 +37,12 @@ for area in bpy.context.screen.areas: bpy.ops.object.select_all(action='SELECT') bpy.ops.object.delete() -print("\nImporting Mario's Model...") - +print("Importing Mario's Model...") 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 - # set a SMG axis for reference when doing models # it is just a visual queue to get how the model will appear on game # select a model --> create transform orientation --> modify its matrix @@ -67,40 +52,36 @@ Mario.select = True # for context incorrect bypass: # https://blender.stackexchange.com/a/6105 -print("\nSetting SMG Axis Reference...") +print("Setting SMG Axis Reference...") 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] ] +bpy.context.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 # just a rotation and a camera view distance change -print("\nAdjusting 3D Viewport camera location...") - +print("Adjusting 3D Viewport camera location...") # set rotation (quaternions) view_3d_area.region_3d.view_rotation = (0.8001, 0.4619, 0.1913, 0.3314) # set camera distance to origin -view_3d_area.region_3d.view_distance = 4 - +view_3d_area.region_3d.view_distance = 4 # extra stuff to set >:] -print("\nExtra stuff to set...") - +print("Extra stuff to set...") # set environment lightnning (material display suggestion) bpy.context.scene.world.light_settings.use_environment_light = True - # set framerate (SMG runs at 59.94 fps in THP videos) -# BTP animations do run (or at least ideally run) at an aproximate of 60fps 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 -print("\nDone with the main settings!\n") +print("Done with the basic settings!\n") diff --git a/blender_funcs.py b/blender_funcs.py new file mode 100644 index 0000000..d9eb7a4 --- /dev/null +++ b/blender_funcs.py @@ -0,0 +1,97 @@ +import bpy, numpy + +# file that contains useful blender some functions that I don't +# want to re-write on each individual .py file + +# show message on screen copied the code from here +# https://b3d.interplanety.org/en/creating-pop-up-panels-with-user-ui-in-blender-add-on/ +class MessageBox(bpy.types.Operator): + bl_idname = "message.messagebox" + bl_label = "INFORMATION:" + + message = bpy.props.StringProperty( + name = "message", + description = "message", + default = '' + ) + + def execute(self, context): + return {'FINISHED'} + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width = 400) + def draw(self, context): + self.layout.label("") + self.layout.label(self.message) + self.layout.label("") + +bpy.utils.register_class(MessageBox) + +# function to display a message using the above class +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 an area + bpy.ops.object.select_all(action='DESELECT') + scene.objects.active = None + obj.select = True + scene.objects.active = obj + + # select object and its children + if (recursive == True): + bpy.ops.object.select_grouped(type = 'CHILDREN_RECURSIVE') + obj.select = True + +# transf_apply_recurse function +# transform_apply the parent and its child meshes +# selects the parent object at the end +def transf_apply_recurse(scene, obj, loc, rot, sca): + + # select obj + select_obj(scene, obj, False) + 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) + bpy.ops.object.transform_apply(location = loc, rotation = rot, scale = sca) + + # select parent object at the end + select_obj(scene, obj, False) + +# set a bone bind matrix +def set_bone_bind_mat(bone, mat): + + # custom property matrix bind_mat is column written + temp_array = [mat[0][0], mat[1][0], mat[2][0], mat[3][0], + mat[0][1], mat[1][1], mat[2][1], mat[3][1], + mat[0][2], mat[1][2], mat[2][2], mat[3][2], + mat[0][3], mat[1][3], mat[2][3], mat[3][3]] + + # convert temp_array to the right type with numpy + temp_array = numpy.array(temp_array, dtype = 'f') + bone["bind_mat"] = temp_array + # if I don't convert temp_array to the right type + # the exported matrices will contain weird data + return + +# set a bone rest matrix +def set_bone_rest_mat(bone, mat): + + # custom property matrix rest_mat is column written + temp_array = [mat[0][0], mat[1][0], mat[2][0], mat[3][0], + mat[0][1], mat[1][1], mat[2][1], mat[3][1], + mat[0][2], mat[1][2], mat[2][2], mat[3][2], + mat[0][3], mat[1][3], mat[2][3], mat[3][3]] + + # convert temp_array to the right type with numpy + temp_array = numpy.array(temp_array, dtype = 'f') + bone["rest_mat"] = temp_array + # if I don't convert temp_array to the right type + # the exported matrices will contain weird data + return diff --git a/collada_bmd_bdl_export.py b/collada_bmd_bdl_export.py deleted file mode 100644 index 30bca40..0000000 --- a/collada_bmd_bdl_export.py +++ /dev/null @@ -1,316 +0,0 @@ -''' -just a DAE exporter. -it was supposed to only rotate the meshes around the X axis by -90 degrees -but above that it now checks for armature and vertex group stuff -only exports an armature object selection -''' - -import bpy, math -from .my_functions import * - -#################################################### -# transform_apply_parent_child function -# transform_apply the parent and its child meshes -# selects the parent object at the end -# -# scene --> Blender scene in which the object exists -# parent_object --> object that has child objects -# to which the transform apply -# operator will be applied -# loc (bool) --> apply location -# rot (bool) --> apply rotation -# sca (bool) --> apply scaling -#################################################### -def transform_apply_parent_child(scene, parent_object, loc, rot, sca): -# - # parent object must be selected and active for the function to work - bpy.ops.object.transform_apply(location = loc, rotation = rot, scale = sca) - - # armature child mesh scaling - for child_mesh in parent_object.children: - - # empty selection and active object - bpy.ops.object.select_all(action = 'DESELECT') - scene.objects.active = None - # select and activate child mesh - child_mesh.select = True - scene.objects.active = child_mesh - # apply scale and rotation - bpy.ops.object.transform_apply(location = loc, rotation = rot, scale = sca) - - # select parent_object - bpy.ops.object.select_all(action='DESELECT') - scene.objects.active = None - parent_object.select = True - scene.objects.active = parent_object -# - - -################################################ -# write_bmd_bdl_collada function (MAIN FUNCTION) -# function to write custom collada file for -# SuperBMD conversion -################################################ -def write_bmd_bdl_collada(context, filepath, triangulate): -# - print("\nWriting Collada file for BMD/BDL conversion\n") - - # this thing is always needed for stuff - scene = bpy.context.scene - - # if nothing is selected end the exporter - if (scene.objects.active == None - or - scene.objects.active.type != 'ARMATURE'): - error_string = "No Armature object selected. Select one and try again." - print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n") - show_message(error_string, "Error exporting collada file", 'ERROR') - return {'FINISHED'} - - # get armature object - armature = scene.objects.active - print("Armature found: %s" % armature.name) - print() - - # change to object view - bpy.ops.object.mode_set(mode='OBJECT') - - # deselect everything (to be sure nothing weird happens) - bpy.ops.object.select_all(action='DESELECT') - scene.objects.active = None - - # re-select the armature object only - armature.select = True - scene.objects.active = armature - - # start mesh stuff - print("\n### Mesh stuff ###") - - ################################## - # check the meshes on the armature - for object in armature.children: - # - if (object.type == "MESH"): - # - print() - print("Mesh: \"" + object.name + "\"") - print() - - #################################### - # select the mesh and make it active - object.select = True - scene.objects.active = object - - # change mesh to object view (in case it isn't) - bpy.ops.object.mode_set(mode='OBJECT') - - # reset scale, rotation and position of the mesh - bpy.ops.object.transform_apply(location = True, rotation = True, scale = True) - - ############################## - # check mesh armature modifier - has_arm_mod = False - for modifiers in object.modifiers: - if (modifiers.name == "Armature"): - has_arm_mod = True - - if (has_arm_mod): - print("Mesh \"" + object.name + "\" has an Armature Modifier") - if (object.modifiers["Armature"].object != armature): - print("But not assigned to the right Armature Object. Re-assigning...") - object.modifiers["Armature"].object = armature - else: - print("And it is assigned to the right Armature Object: \"" + armature.name + "\"") - else: - print("Mesh \"" + object.name + "\" does not have an Armature Modifier") - print("Creating Modifier...") - bpy.ops.object.modifier_add(type='ARMATURE') - print("Assigning Modifier object to: \"" + armature.name + "\"") - object.modifiers["Armature"].object = armature - - # check if the mesh contains vertex groups - # if not, assign one that links to the main armature bone - # and weight all vertex in the mesh to it, otherwise leave - # the mesh in peace (mesh might not be weighted) - - # probably make a weight checker for each mesh so the exporter is complete - is_weighted = True # variable used later - - if (object.vertex_groups.active == None): - print("Mesh \"" + object.name + "\" does not have any Vertex Group assigned.") - - # change to edit mode (for vertex selection stuff) - bpy.ops.object.mode_set(mode='EDIT') - - # get vertex group name (first armature bone name) - ver_group_name = armature.data.bones[0].name - print("Creating Vertex Group: \"" + ver_group_name + "\"") - - # create new vertex group for mesh - object.vertex_groups.new(name = ver_group_name) - print("Assigning all vertex in mesh to Vertex Group...") - - # select all vertex in mesh - bpy.ops.mesh.select_all(action='SELECT') - #get vertex group created - ver_group = object.vertex_groups[ver_group_name] - # set active vertex group in mesh to new vertex group created - object.vertex_groups.active = ver_group - # assign all vertex of the mesh to said vertex group - bpy.ops.object.vertex_group_assign() - - print("Vertex assigned.") - - # go back to object mode - bpy.ops.object.mode_set(mode='OBJECT') - - else: - - # if the mesh contains vertex groups then they might not - # be correctly linked to the bones on an armature, - # in other words, the mesh might contain a vertex group which name - # isn't the name of a bone in the armature object - # (this will throw a SuperBMD error) - # Assimp.AssimpException: Error importing file: Collada: [DAE filename].dae - Invalid contents in element "n". - - if (len(object.vertex_groups) > len(armature.data.bones)): - error_string = "Mesh has a vertex group that isn't related to any bone of the Armature.\nCheck the meshes and try again." - print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n") - show_message(error_string, "Error exporting collada file", 'ERROR') - return {'FINISHED'} - - else: - for ver_group in object.vertex_groups: - valid_vertex_group = False - for bone in armature.data.bones: - if (ver_group.name == bone.name): - valid_vertex_group = True - break - - if (valid_vertex_group == False): - error_string = "Mesh has a vertex group that isn't related to any bone of the Armature.\nCheck the meshes and try again." - print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n") - show_message(error_string, "Error exporting collada file", 'ERROR') - return {'FINISHED'} - - # also the group might be valid but the vertex in - # the mesh might not be weigthed in the vertex group - # print message saying that the mesh might not be weighted - # even if it has a vertex group (set is_weighted to False) - - ######################################## - # make loop to check for weights (later) - ######################################## - - print("Mesh \"" + object.name + "\" has a Vertex Group assigned.") - print("Mesh might not be weighted.") - is_weighted = False - - # deselect mesh at the end of each loop - object.select = False - scene.objects.active = None - # - # - - print("\n### Mesh stuff done ###") - print() - - ################################################################# - # final part of the collada export - # print messages on vertex weight given by the is_weight variable - if (is_weighted): - print("All meshes are weighted") - else: - print("All meshes are probably weighted") - - print("Finalizing export...\n") - - # select armature object and make it active - # do the -90 degree rotation on X axis and scale it to 100 - # then do transform_apply, do transform_apply on the meshes - # inside the armature and finally export the result into a Collada file - - # the 100 scaling is done because SuperBMD isn't able - # to take the scaling factor on the DAE model and apply it to - # the mesh on the BMD/BDL conversion (it just ignores it) - - armature.select = True - scene.objects.active = armature - - # somehow the 2 statements before this comment change - # the armature object into pose mode (???) - # bpy.ops.object.mode_set(mode='OBJECT') - - # armature scale/rotation - print("Rotating Armature -90 degrees on the X axis...") - armature.rotation_euler[0] = math.radians(-90) - print("Scaling to 100...") - armature.scale = (100, 100, 100) - - # transform_apply the armature and its meshes - transform_apply_parent_child(scene, armature, False, True, True) - - # export model (selection only) - print("Exporting Model...") - bpy.ops.wm.collada_export(filepath = filepath, - use_blender_profile = False, - selected = True, - include_children = True, - triangulate = triangulate) - print("Collada file (DAE) exported.") - - # after export reset object rotation and scaling to original state - # i.e. apply a 90 degree rotation on X axis and scale to 0.01 - # then do transform_apply - - # armature scale/rotation - print("Reversing the -90 degrees rotation on the X axis...") - armature.rotation_euler[0] = math.radians(90) - print("Reversing the 100 scaling...") - armature.scale = (0.01, 0.01, 0.01) - - # transform_apply the armature and its meshes - transform_apply_parent_child(scene, armature, False, True, True) - - print("\n### Done! ###\n") - return {'FINISHED'} -# - - -################################################# -# Stuff down is for the menu appending -# of the exporter 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 -from bpy.types import Operator - -class Export_BMD_BDL_Collada(Operator, ExportHelper): -# - """Save a Collada file for BMD/BDL conversion with SuperBMD""" - bl_idname = "export_scene.collada_bmd_bdl" - bl_label = "Export Collada (for BMD/BDL)" - filename_ext = ".dae" - filter_glob = StringProperty( default="*.dae", - options={'HIDDEN'}, - maxlen=255, - ) - - triangulate = BoolProperty( name = "Triangulate meshes", - description = "Triangulate meshes inside armatures", - default = False, - ) - def execute(self, context): - return write_bmd_bdl_collada(context, self.filepath, self.triangulate) -# - -def menu_export_bmd_bdl_collada(self, context): - self.layout.operator(Export_BMD_BDL_Collada.bl_idname, text="Collada (for BMD/BDL) (.dae)") - -bpy.utils.register_class(Export_BMD_BDL_Collada) -bpy.types.INFO_MT_file_export.append(menu_export_bmd_bdl_collada) - -# test call -bpy.ops.export_scene.collada_bmd_bdl('INVOKE_DEFAULT') diff --git a/collada_bmd_bdl_import.py b/collada_bmd_bdl_import.py deleted file mode 100644 index 8894af6..0000000 --- a/collada_bmd_bdl_import.py +++ /dev/null @@ -1,1130 +0,0 @@ -''' -just a DAE importer for SuperBMD DAEs -it loads the said DAE files with the default importer and the extension re-builds -the armature and mesh skinning. Done because Blender's 2.79 Collada importer -can miss armature stuff when importing (not all the time). It also lets -you select the type of pose you want in the armature when imported in Blender - -For my mental sanity this importer is only for SuperBMD files -Blender 2.79 deals with armature data on Collada files in an -extremely poorly way. Hopefully in the future I can make my own -Collada importer (epik project >:]). -''' - -import bpy, math, re -from mathutils import Matrix, Vector -from numpy import array -# ^ needed for converting an array to the correct type - -# Notes (AFAIK): - -# - bind shape matrix or BSM: matrix that tells the position of -# a mesh with respect to the armature origin so it is in the -# correct position for the bone it is assigned to. -# - inverse bind matrix or IBM: matrix located at the skin section of -# the collada file. The inverse of this matrix, the bind matrix, -# describes a bone position with respect to the armature origin when the -# meshes were assigned to the bones (shows on Blender in the -# bind_mat custom property for the bones of an armature). Bind matrices -# are the ones that show on Blender when loading a model. This matrix -# is the reference matrix used to when vertex weighted agaisnt a bone -# are moved by it in an animation -# - rest matrix: matrix found on the armature section of the collada -# file. It describes a bone position with respect to its parent bone -# at the moment the armature was edited (after the skinning) to get -# the model rest pose (t-pose stuff, for animation purposes). Shows -# on Blender on the rest_mat custom property for the bones of an armature. -# - All matrices read by Blender assume that the rotation system located in -# each 4x4 matrix is in the extrinsic Euler XYZ system - -################################################ -# get_bone_rest_matrix function -# function to: -# - calculate final rest matrix of a single bone -################################################ -def calculate_rest_matrix(rest_matrices, row_pos, bone_parenting_diagram): - - # acumulate transformation matrices while reading bone_parenting_diagram - # reading is done from the child bone to the ultimate parent bone - # final matrix will be initialized with current bone matrix from rest_matrices - final_matrix = rest_matrices[row_pos] - - # bone position on bone_parenting_diagram will be found in the next for loop - bone_parenting_pos = 0 - - for number in range(len(bone_parenting_diagram[row_pos])): - if (bone_parenting_diagram[row_pos][number] == 1): - bone_parenting_pos = number - break - - # with bone position on bone_parenting_diagram now it is time to read said - # array from bottom to top searching for each consecutive parent of said bone - # for a single bone, its parent is on the row the first "1" is found - # that is 1 position left from said bone in its row - - # example: - # | - # v - # bone 1 [1, 0, 0] row 1 - # bone 2 [0, 1, 0] row 2 - # bone 3 [0, 1 ,0] row 3 - # ^ - # | - # bone 3's parent is bone 1 - - # set 2 for loops to read the array from down to top and right to left >:] - # first loop on each for loop is skipped - - # variable to be used on inner loop - current_row = row_pos - - for parenting_pos in range(bone_parenting_pos, -1, -1): - - # first loop is skipped - if (parenting_pos == bone_parenting_pos): - continue - - for row_number in range(current_row, -1, -1): - - # first loop is skipped - if (row_number == row_pos): - continue - - # if element found is 0 go up one row - # if element found is 1 grab transformation matrix from rest_matrices - # perform multiplication, store row_number for the next inner loop - # then break out of the inner loop - if (bone_parenting_diagram[row_number][parenting_pos] == 0): - continue - - # bone_parenting_diagram[row_number][parenting_pos] == 1 returns True - else: - # acumulate transformation matrices - final_matrix = rest_matrices[row_number] * final_matrix - - # set new row for the next time the inner loop is executed - current_row = row_number - break - # - - return final_matrix - - -############################################### -############################################### -# read_bmd_bdl_collada function (MAIN FUNCTION) -# read superbmd/blenxy collada file -# used to import the collada file into Blender -############################################### -############################################### -def read_bmd_bdl_collada(context, filepath, debug_messages, show_armature_pose): - - ################### - # importing section - ################### - - # scene variable is always needed for something - scene = bpy.context.scene - - print("\nImporting Collada file used in BMD/BDL conversion...\n") - - # import collada in the standard way (blender importer, with no flags) - bpy.ops.wm.collada_import(filepath=filepath) - - print("\nCollada file (DAE) imported.") - - # initialize armature variable - armature = scene.objects.active - - # search the armature of the imported model - # Collada importer selects a child of the armature - # if only an armature is immported then the Collada - # importer selection is already the armature object - if (scene.objects.active.type != "ARMATURE"): - armature = scene.objects.active.parent - - # deselect everything - bpy.ops.object.select_all(action='DESELECT') - scene.objects.active = None - - # select and make active the armature - armature.select = True - scene.objects.active = armature - - - ########################### - # get armature data section - # from Collada file - ########################### - - print("Getting Armature data...") - - # open DAE file - f = open(filepath, 'r', encoding='utf-8') - - # I need the bind matrices and rest matrices from the armature bones - # also the bind shape matrix for each mesh - - # bind matrices are in the library_controllers section of the Collada file - # the inverse of them to be specific - # each mesh that is assigned to a vertex group (and consequently to a bone) - # has an inverse bind matrix for said bone. By inversing that matrix I get the - # bind matrix for the bone. This matrix is the one that should be applied to - # the bones on Blender. - - # rest matrices are in the library_visual_scenes section (matrices relative - # to each bone) and they are used in the animation process. - # If a bone does not have an inverse bind matrix on any mesh then - # the matrix used to position the bone on blender is pulled from here - # (the multiplication of the parent's rest matrices + the bone rest matrix). - - - ######################################################### - # infinite loop to get to the skin section of the collada - ######################################################### - while True: - line = f.readline() - - # find() returns -1 if it does not find anything on a string - # 0 is equivalent to False - if (line.find("<library_controllers>") + 1): - break - - - ############################# - # library_visual_scenes start - ############################# - - # controller_data will be used to store the mesh id, its bind shape matrix - # and bone names with their inverse bind pose matrix one mesh per row - # as follows: - - # row 1: mesh 1 id, BSM, bone 1 name, IBM of bone 1, bone 2 name, IBM of bone 2, ... - # row 2: mesh 2 id, BSM, bone a name, IBM of bone a, bone b name, IBM of bone b, ... - # . - # . - # . - - controller_data = [[]] - - # variables to keep track of the crontroller data length - i = -1 # for number of rows - - ####################################################### - # infinite loop to read the library_controllers section - ####################################################### - while True: - - line = f.readline() - - ######################### - # controller start - # the skin stuff for each - # mesh is defined there - ######################### - if (line.find("<controller") + 1): - - i = i + 1 - - # for the first mesh there is already a row in controller_data - if (i != 0): - controller_data.append([]) - - # get mesh id - mesh_id = re.search('id="(.+?)"', line).group(1) - controller_data[i].append(mesh_id) - - - ############################################## - # bind shape matrix from current mesh on row i - ############################################## - elif (line.find("<bind_shape_matrix>") + 1): - k = 0 - temp_rest_matrices = [] - - while (k < 4): - temp_array = [0, 0, 0, 0] - line = f.readline() - temp_array = line.split() - temp_array = [float(string) for string in temp_array] - l = 0 - - # fill temp_rest_matrices with a row - while (l < 4): - temp_rest_matrices.append(temp_array[l]) - l = l + 1 - - k = k + 1 - - # created "a" so the next matrix assignation is more readable - a = temp_rest_matrices - matrix = Matrix(( [a[0], a[1], a[2], a[3]], - [a[4], a[5], a[6], a[7]], - [a[8], a[9], a[10], a[11]], - [a[12], a[13], a[14], a[15]] - )).normalized() - - controller_data[i].append(matrix) - - - ######################################################## - # Name_array start - # the names of the bones to which the mesh was assigned - # to are here. Next to this section a few lines down the - # collada file there is the float_array section that has - # the inverse bind matrices for each bone of Name_array - ######################################################## - elif (line.find("<Name_array") + 1): - - # line were the bone names are - line = f.readline() - - # store bone names in temp_bones - temp_bones = [] - temp_bones = line.split() - - # skip the lines until float_array appears - while True: - line = f.readline() - if(line.find("<float_array") + 1): - break - - - ################### - # float_array start - ################### - - # read a 4x4 matrix, store it on temp_array_matrices - # then create a matrix and append the matrix with the - # respetive bone on controller_data[i] - - # to store matrices - temp_matrices = [] - - while True: - - # to store a matrix in the form of an array - temp_array_matrix = [] - - # will store the numbers as strings first on temp_array_matrix - k = 0 - while (k < 4): - line = f.readline() - - for number in line.split(): - temp_array_matrix.append(float(number)) - - k = k + 1 - - # created "a" so the next matrix assignation is more readable - a = temp_array_matrix - # build matrix - temp_matrix = Matrix(( [a[0], a[1], a[2], a[3]], - [a[4], a[5], a[6], a[7]], - [a[8], a[9], a[10],a[11]], - [a[12], a[13],a[14],a[15]] - )).normalized() - - # append matrix to temp_matrices - temp_matrices.append(temp_matrix) - - # after reading a matrix there is either - # - a blank line (another matrix starts next) - # - the end of float_array - - line = f.readline() - - if (line.find("</float_array>") + 1): - - # assign all bone names and its matrices to controller_data[i] - for k in range(len(temp_bones)): - controller_data[i].append(temp_bones[k]) - controller_data[i].append(temp_matrices[k]) - - # end of the Name_array and float_array section - break - - - #################################### - # end of library_controllers section - #################################### - elif (line.find("</library_controllers>") + 1): - break - - - ###################### - # controller data view - ###################### - - if (debug_messages): - for mesh in controller_data: - print("\nrow start") - for element in mesh: - print(element) - print() - - - ############################# - # library_visual_scenes start - ############################# - - line = f.readline() - - if not (line.find("<library_visual_scenes>") + 1): - print("DAE file contains an incompatible skin section structure for this importer. Check the file and try again.") - # end importer - return {'FINISHED'} - - line = f.readline() # contains visual_scene start - - # the next 2 lines have the armature name and tranformation matrix - # respectively, so I will store those separately from the bone's data - - line = f.readline() - armature_name = name = re.search('name="(.+?)"', line).group(1) - - line = f.readline() - armature_matrix_string = re.search('<matrix.*>(.*)</matrix>', line).group(1).split() - - # created "a" so the next matrix assignation is more readable - a = [float(string) for string in armature_matrix_string] - armature_matrix = Matrix(( [a[0], a[4], a[8], a[12]], - [a[1], a[5], a[9], a[13]], - [a[2], a[6], a[10], a[14]], - [a[3], a[7], a[11], a[15]] - )).normalized() - - ########### - # bone data - ########### - - # infinite loop to read this section - # all bone names will be stored in bones_array (as they show on the DAE file) - # all the matrices will be stored in rest_matrices (as they show on the DAE file) - # in this way the bones_array[i] bone rest matrix will be rest_matrices[i] - # to check tag start/end bone_tag_check will be used - # when <node> tag is reached the bone name related to that tag will be stored - # in bone_tag_check, then, if </node> tag is reached whatever bone is at the - # end of the bone_tag_check gets banished (like a stack) - # in this way I can get parent bones more easily for complex bone structures - - bones_array = [] - rest_matrices = [] - bone_tag_check = [] - - # the following array will be used to "draw" bone parenting - # each row will represent a bone and it will be filled with zeros from left to right - # until a position (position defined by the bone name position in bones_array) in - # which there will be a 1. In this way I can get a visual diagram to get the bone - # parenting for if I need to rebuild it later in the program - - bone_parenting_diagram = [[]] - - i = -1 # position of the last element on bones_array and rest_matrices - j = -1 # position of the last non_empty element on bone_tag_check - - # ^ initialized to -1 so the while loop from below is more convinient - - ######################################################### - # infinite loop section - # exit condition is when j = -1 (bone_tag_check is empty) - ######################################################### - - while True: - - line = f.readline() - - - ############ - # node start - ############ - if (line.find("<node ") + 1): - - # new bone, new matrix, new element in bone_tag_check - i = i + 1 - j = j + 1 - - # bone name - bone_name = re.search('name="(.+?)"', line).group(1) - bones_array.append(bone_name) - - # bone tag check - if (j >= len(bone_tag_check)): - bone_tag_check.append(bone_name) - else: - bone_tag_check[j] = bone_name - - # bone parenting diagram - if (i == 0): - bone_parenting_diagram[0] = [1] - else: - bone_parenting_diagram.append([]) - for k in range(j): - bone_parenting_diagram[i].append(0) - bone_parenting_diagram[i].append(1) - - - ################## - # matrix from node - ################## - elif (line.find("<matrix ") + 1): - - # store contents of matrix tag in an array - # transform array to float type then to a 4x4 matrix - bone_matrix_string_array = re.search('<matrix.*>(.*)</matrix>', line).group(1).split() - # created "a" so the next matrix assignation is more readable - a = [float(string) for string in bone_matrix_string_array] - bone_matrix = Matrix(([a[0], a[1], a[2], a[3]], - [a[4], a[5], a[6], a[7]], - [a[8], a[9], a[10], a[11]], - [a[12], a[13], a[14], a[15]] - )).normalized() - if (bone_matrix.determinant() == 0): - bone_matrix = Matrix(([1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1])) - rest_matrices.append(bone_matrix) - - - ########## - # node end - ########## - elif (line.find("</node>") + 1): - - # empty element bone_tag_check[j] - # decrease j by 1 - bone_tag_check[j] = "" - j = j - 1 - - - ##################################### - # what kind of DAE file I am reading? - ##################################### - else: - - # if the collada file contains a different structure than the usual - # for the armature section just end the importer and print text error text - print("DAE file contains an incompatible armature section structure for this importer. Check the file and try again.") - return {'FINISHED'} - - - ########### - # exit loop - ########### - if (j == -1): - break - - - ############## - # post process - - # finalize filling with zeros the rows of the bone_parenting_diagram - final_row_length = len(bone_tag_check) - - for row in bone_parenting_diagram: - for l in range(final_row_length - len(row)): - row.append(0) - - - #################### - # armature data view - #################### - - if (debug_messages): - print("Bone Names") - print() - print(bones_array) - print() - print("Rest Matrices") - print() - for rest_mat in rest_matrices: - print(rest_mat) - print() - print("Bone tag check (empty = good)") - print("number of columns must match the bone\nparenting diagram number of columns") - print() - print(bone_tag_check) - print() - print("Bone Parenting Diagram") - print() - for bone in bone_parenting_diagram: - print(bone) - print() - - - ########################## - # re-ordering data section - ########################## - - # this section is to fill 2 big and important arrays - # with all the data read from above - # - bind_matrices will hold all bone bind matrices for each bone (from the skin read) - # the inverse of the ones read to be specific. Will use the rest matrix from the - # armature section of the Collada if the bone does not have an IBM defined - # - final_rest_matrices will hold all bone rest matrices (armature section of - # the collada) matrices will be calculated with calculate_rest_matrix() - # function for each bone - - # the matrices will be in the ordered as the bone names on bone_names - - bind_matrices = [] - final_rest_matrices = [] - - # will be used to tell if a bone had defined a bind matrix and a rest matrix - # checking if a bone has a bind matrix is enought - has_bind_rest = [] - - # rest_matrices will be filled first and bind_matrices after - - ##################### - # final_rest_matrices - for i in range(len(bones_array)): - final_rest_matrices.append(calculate_rest_matrix(rest_matrices, i, bone_parenting_diagram)) - - ############### - # bind_matrices - for i in range(len(bones_array)): - - # loop through each element of the controller_data array - # to find all the bone's inverse bind matrices available - - bone_found = False - - for row in range(len(controller_data)): - for col in range(len(controller_data[row])): - if (bones_array[i] == controller_data[row][col]): - bind_matrices.append(controller_data[row][col + 1].inverted()) - has_bind_rest.append(True) - bone_found = True - break - - if (bone_found): - break - - # if a bone has not been found - # assign final rest matrix of said bone from final_rest_matrices - if not (bone_found): - bind_matrices.append(final_rest_matrices[i]) - has_bind_rest.append(False) - - - ############################################## - # bind, rest and final rest matrices data view - ############################################## - - if (debug_messages): - print("Bone Names") - print() - print(bones_array) - - print("\nBind Matrices\n") - for bind_mat in bind_matrices: - print(bind_mat) - - print("\nRest Matrices\n") - for rest_mat in rest_matrices: - print(rest_mat) - - print("\nFinal Rest Matrices\n") - for final_rest_mat in final_rest_matrices: - print(final_rest_mat) - - print("\nBone has both Matrices defined in Collada") - print() - print(bones_array) - print(has_bind_rest) - print() - - ########################## - # Rebuild armature section - ########################## - - # for this I will create a different armature object with all the bones - # and then link all the meshes from the imported model into this armature - # at the end the original imported armature will be deleted. - - print("Attempting re-build...") - - # change to object mode - bpy.ops.object.mode_set(mode='OBJECT') - - # deselect everything - bpy.ops.object.select_all(action='DESELECT') - scene.objects.active = None - - # store old armature object - armature_old = armature - - # create new armature - bpy.ops.object.armature_add(location=(0.0, 0.0, 0.0)) - armature = scene.objects.active - # change its name - armature.name = armature_old.name + "_new" - - # apply transformation matrix of armature old into new armature - armature.matrix_world = armature_matrix - - # enter edit mode - bpy.ops.object.mode_set(mode='EDIT') - - # create, apply transformation matrix and parent all bones in a - # column of the bone_parenting_diagram array (those at the "same parenting level") - # elements in bone_parenting_diagram are bone_parenting_diagram[i][j] - - for j in range(len(bone_parenting_diagram[0])): - for i in range(len(bone_parenting_diagram)): - - # first bone (position [0][0]) is already created - # I assume there is one bone that is the main bone - if (i == 0): - armature.data.edit_bones[0].name = bones_array[0] - armature.data.edit_bones[0].matrix = bind_matrices[0] - continue - - ################################## - # for the columns next to column 0 - ################################## - - # current position does not have a bone - if (bone_parenting_diagram[i][j] == 0): - continue - - # bone_parenting_diagram[i][j] == 1 returns True - # bone found, create it and assign name - bpy.ops.armature.bone_primitive_add(name = bones_array[i]) - - # find parent bone - # read the rows above the bone row to find its parent - # it is located at the column next to it (one column left) - # second argument in range() is -1 as I need a loop on k = 0 - - for k in range(i, -1, -1): - - # make k 1 less than i (row adjustment) - if (k >= i): - continue - - # start reading the column next to bone column (left column) - # Find the nearest 1 on said column (parent bone) - if (bone_parenting_diagram[k][j - 1] == 0): - continue - - # bone_parenting_diagram[k][j - 1] == 1 returns True --> parent bone found - - # assign bone parent - armature.data.edit_bones[bones_array[i]].parent = armature.data.edit_bones[bones_array[k]] - - # and more length to the bone (visual stuff, non-important) - armature.data.edit_bones[bones_array[i]].length = 10 - - # apply transformation matrix (bind matrix) - armature.data.edit_bones[bones_array[i]].matrix = bind_matrices[i] - - break - - - # leave edit mode - bpy.ops.object.mode_set(mode='OBJECT') - - # assign bone custom properties only to bones that have - # both bind and rest matrices defined in the collada file - for i in range(len(bones_array)): - - # only check bones that have a bind matrix - if (has_bind_rest[i]): - - # Note: custom property matrices bind_mat and rest_mat are column written - - # create bind_mat custom property - # Note: by using Blender's FloatVectorProperty I get - # a custom property that can't be edited through GUI - # (API Defined, but it works well on the bind/rest_mat export) - # can be edited if exporting the model as DAE and then - # re-importing after closing and re-openning Blender - - # create rest_mat custom property (Blender's FloatVectorProperty) - # bpy.types.Bone.rest_mat = bpy.props.FloatVectorProperty( - # name="rest_mat", - # size=16, - # subtype="MATRIX" - # ) - - # bind matrix for bone - a = bind_matrices[i].transposed() - temp_array = [a[0][0], a[0][1], a[0][2], a[0][3], - a[1][0], a[1][1], a[1][2], a[1][3], - a[2][0], a[2][1], a[2][2], a[2][3], - a[3][0], a[3][1], a[3][2], a[3][3]] - - # convert temp_array to the right type with numpy - temp_array = array(temp_array, dtype = 'f') - - armature.data.bones[bones_array[i]]["bind_mat"] = temp_array - - # rest matrix for bone - a = rest_matrices[i].transposed() - temp_array = (a[0][0], a[0][1], a[0][2], a[0][3], - a[1][0], a[1][1], a[1][2], a[1][3], - a[2][0], a[2][1], a[2][2], a[2][3], - a[3][0], a[3][1], a[3][2], a[3][3]) - - # convert temp_array to the right type with numpy - temp_array = array(temp_array, dtype = 'f') - - armature.data.bones[bones_array[i]]["rest_mat"] = temp_array - - # Note: ^ if I don't convert temp_array to the right type and - # just assign it to the custom property, the exported matrices - # will contain weird data (no clue why that happens) - - - # armature has finished being built - print("Re-build done!") - - # assign meshes from old armature into new armature - # also reconect mesh's armature modifiers to the new armature - for child_mesh in armature_old.children: - child_mesh.parent = armature - child_mesh.modifiers["Armature"].object = armature - - # rename new armature with old armature's name - armature.name = armature_old.name - - - ############### - # final section - ############### - - # change back to object mode - bpy.ops.object.mode_set(mode='OBJECT') - - # deselect everything - bpy.ops.object.select_all(action='DESELECT') - scene.objects.active = None - - # select old armature - armature_old.select = True - scene.objects.active = armature_old - - # delete old armature - # it has been an honor :saluting_face: - bpy.ops.object.delete(use_global = False) - - # select new armature again - armature.select = True - scene.objects.active = armature - - ######################## - # apply pose to armature - ######################## - - print("Generating Armature poses...") - - # already have done the bind pose from the steps above - # so end the importer if the bind pose is the pose - # is the one wanted to be applied to the bones of the armature - if (show_armature_pose == "OPT_A"): - print("Bind Pose done!") - - # ^ if that is not the case then I have to build again the - # armature for the rest pose and the ignore rest pose - # but this time I have to deform manually the meshes so they are - # correct for the animation imports - - # Note: for some reason Mario's mesh has a deformation - # in the Hip zone after these vertex transformation - - # rest pose/ignore rest pose build - else: - - # change to edit mode - bpy.ops.object.mode_set(mode='EDIT') - - # build the rest pose/ignore rest pose on the bones - # of the armature (edit_bones, does not matter applying order) - # use final_rest_matrices for rest pose - # use rot_90_deg_mat for ignore rest pose - for i in range(len(armature.data.edit_bones)): - # - # get edit_bone - edit_bone = armature.data.edit_bones[i] - - if (show_armature_pose == "OPT_B"): - # rotate matrix (use ) - matrix = final_rest_matrices[i] - - elif (show_armature_pose == "OPT_C"): - # rotate matrix (all of the bones on ignore - # rest pose are in the same place) - matrix = Matrix(( [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1] - )) - - # assign matrix - edit_bone.matrix = matrix - # - - # deform mesh according to the rest pose - # of the armature a vertex at a time - for mesh in armature.children: - # - # get all vertex groups on the mesh - # its index and name - mesh_vertex_groups = [[], []] - for vertex_group in mesh.vertex_groups: - mesh_vertex_groups[0].append(vertex_group.index) - mesh_vertex_groups[1].append(vertex_group.name) - - for vertex in mesh.data.vertices: - # - # get vertex in armature's reference system - # and as a vector to be able to be multiplied by matrices - vertex_vector = Vector(([ vertex.co[0], - vertex.co[1], - vertex.co[2]] - )) - - # final vertex position variable - final_vertex_vector = Vector(([0, 0, 0])) - - # get the sum of the weights of this vertex on all - # vertex groups, the individual weights on each group - # and the vertex group index - # leave extra row on vertex_vertex_groups to store - # the vertex group names for later - weight_sum = 0 - vertex_vertex_groups = [[], [], []] - for vertex_group in vertex.groups: - vertex_vertex_groups[0].append(vertex_group.group) - vertex_vertex_groups[1].append(vertex_group.weight) - weight_sum = weight_sum + vertex_group.weight - - # iterate through each bone this vertex is linked to - # and apply the "skinning formula" AFAIK - - # this formula is as follows: - - # for a bone with a bind matrix (the matrix that describes the bone position - # without deforming the mesh, respect to armature) and a pose matrix (the - # position the bone has when the mesh deforms, with respect to armature) - # that influences a vertex with armature coordinates v and relative - # weight w for said bone the final position of the vertex for said - # bone movement is - - # v_final_for_bone = inverse_bind_mat * bone_pose_matrix * v * w - - # were w is the fraction of the weight of the vertex on the vertex group - # that is linked to the bone divided by the sum of all the weights the vertex - # has on all vertex groups it is assigned to - - # v_final_for_all_bones (the final trasnformation of the vertex that is - # got by all the bones that have influence on said vertex) is found by adding - # all individual v_final_for_bone vectors (one per bone, all the bones that - # influence the vertex) - - # find the bone names that affect this vertex - # and add those to the 3rd row of vertex_vertex_groups - # i will be used to go throught a mesh_vertex_groups row - # j will ve used to go through a vertex_vertex_groups row - i = 0 - j = 0 - while (j != len(vertex_vertex_groups[0])): - # - # when vertex group is located in mesh_vertex_groups - # append vertex group name in vertex_vertex_groups[2] (bone name) - # start reading mesh_vertex_groups again to search for the next bone - if (mesh_vertex_groups[0][i] == vertex_vertex_groups[0][j]): - vertex_vertex_groups[2].append(mesh_vertex_groups[1][i]) - i = 0 - j = j + 1 - - i = i + 1 - # - - # iterate through each bone that affects this vertex - for i in range(len(vertex_vertex_groups[0])): - # - # get bone and its index - bone = armature.data.bones[vertex_vertex_groups[2][i]] - bone_index = 0 - for j in range(len(armature.data.bones)): - if (vertex_vertex_groups[2][i] == armature.data.bones[j].name): - bone_index = j - break - - # do the proper multiplication of the influence of - # each bone on the vertex and sum the resulting vectors - # inverse_bind_mat is got from bind_matrices - # bone_pose_matrix will be the final_rest_matrices (for rest pose) - # or the identity matrix (for ignore rest pose) - bind_matrix = bind_matrices[bone_index] - inv_bind_matrix = bind_matrix.inverted() - - if (show_armature_pose == "OPT_B"): - - bone_pose_matrix = final_rest_matrices[bone_index] - - elif (show_armature_pose == "OPT_C"): - - bone_pose_matrix = Matrix(( [1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, 1, 0], - [0, 0, 0, 1] - )) - - final_vertex_vector = final_vertex_vector + (inv_bind_matrix * bone_pose_matrix * vertex_vector * (vertex_vertex_groups[1][i] / weight_sum)) - # - - # apply final_vertex_vector to vertex - # in armature reference system - vertex.co = final_vertex_vector - # - # - - # scale down and rotate the armature - armature.scale = (0.01, 0.01, 0.01) - armature.rotation_euler[0] = math.radians(90) - - # apply scale to armature - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) - - # apply scale to all meshes on armature - for mesh in armature.children: - mesh.select = True - scene.objects.active = mesh - bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) - mesh.select = False - scene.objects.active = None - - # select armature again - armature.select = True - scene.objects.active = armature - - if (show_armature_pose == "OPT_B"): - print("Rest Pose done!") - elif (show_armature_pose == "OPT_C"): - print("Ignore Rest Pose done!") - - - # ~ #################### - # ~ # add model textures - # ~ #################### - - # ~ # will search for textures in the same place the DAE file is - # ~ # if they are not there the program won't do anything - - # ~ print("Getting Texture data...") - # ~ mat_json_path = filepath.split(".dae")[0] + "_materials.json" - # ~ print(mat_json_path) - - # ~ # open materials JSON - # ~ f = open(mat_json_path, 'r', encoding='utf-8') - - # ~ for i in range(len(armature.children)): - # ~ while True: - # ~ line = f.readline() - # ~ # material data start - # ~ if (line.find("\"Name\":") + 1): - - # ~ # get mat name - # ~ mat_name = re.search(': "(.+?)",', line).group(1) - - # ~ # find material along the - - - # ~ # get textures associated with this material - # ~ while True: - # ~ line = f.readline() - # ~ tex_name = "dummy" - # ~ if (line.find("\"TextureNames\":") + 1): - # ~ while (tex_name != ""): - # ~ tex_name = re.search('"(.+?)",', f.readline()).group(1) - # ~ break - # ~ break - # ~ break - - - # ~ for mesh in armature.children: - # ~ for i in range(len(mesh.data.materials)): - # ~ mesh.active_material_index = i - # ~ tex_slot = mesh.data.materials[i].texture_slots.add() - # ~ bpy.ops.texture.new() # create new texture - # ~ tex_slot.texture = bpy.data.textures[-1] - - # ~ while True: - - # ~ line = f.readline() - - # ~ ######################### - # ~ # controller start - # ~ # the skin stuff for each - # ~ # mesh is defined there - # ~ ######################### - # ~ if (line.find("<controller") + 1): - - # end importer - print("Done importing!") - return {'FINISHED'} - - -################################################# -# Stuff down is for the menu appending -# of the importer to work plus some setting stuff -# comes from a Blender importer template -################################################# - -# ExportHelper is a helper class, defines filename and -# invoke() function which calls the file selector. -from bpy_extras.io_utils import ExportHelper -from bpy.props import StringProperty, BoolProperty, EnumProperty -from bpy.types import Operator - - -class Import_BMD_BDL_Collada(Operator, ExportHelper): - """Import a Collada file from SuperBMD (SuperBMD only)""" - bl_idname = "import_scene.collada_bmd_bdl" - bl_label = "Import Collada (from SuperBMD)" - - # ExportHelper mixin class uses this - filename_ext = ".dae" - - filter_glob = StringProperty( - default="*.dae", - options={'HIDDEN'}, - maxlen=255, - ) - - # option to show messages on terminal of the bone importing process - debug_messages = BoolProperty( name = "Display debug messages", - description = "Display armature data as it is read", - default = False, - ) - - # option to allow users define the pose of the armature that is shown on Blender - show_armature_pose = EnumProperty( - name="Armature Pose", - description="Pose to load on Blender for the imported armature", - items=( ('OPT_A', "Bind Pose", "Show Armature on Bind Pose"), - ('OPT_B', "Rest Pose", "Show Armature on Rest Pose"), - ('OPT_C', "Ignore Rest Pose", "Show Armature as on SMG when an empty BCK animation is applied to it. WARNNING: for complex armature models bone deformations on mesh wont be accurate (it is unknown why)")), - default='OPT_B', - ) - - def execute(self, context): - return read_bmd_bdl_collada(context, self.filepath, self.debug_messages, self.show_armature_pose) - - -# Only needed if you want to add into a dynamic menu -def menu_import_bmd_bdl_collada(self, context): - self.layout.operator(Import_BMD_BDL_Collada.bl_idname, text="Collada (from SuperBMD) (.dae)") - -bpy.utils.register_class(Import_BMD_BDL_Collada) -bpy.types.INFO_MT_file_import.append(menu_import_bmd_bdl_collada) - - -# test call -bpy.ops.import_scene.collada_bmd_bdl('INVOKE_DEFAULT') diff --git a/collada_funcs.py b/collada_funcs.py new file mode 100644 index 0000000..ba96c0f --- /dev/null +++ b/collada_funcs.py @@ -0,0 +1,43 @@ +from lxml import etree +from mathutils import Matrix + +# function to check if a collada file is valid +# for now if it wil well formatted +# it will also remove namespaces +def check_collada(xml_path): + + # check for errors + try: + xml = etree.parse(xml_path) + except: + return None + + # function to remove namespaces + def strip_namespace(element): + if element.tag.startswith("{"): + element.tag = element.tag.split("}")[1] + for child in element: + strip_namespace(child) + return element + + # return the clean xml element + strip_namespace(xml.getroot()) + return xml + +# function to return the matrix contained in a matrix element +def get_text_mat4x4(text, offset): + + # remove the starting whitespace characters + while (text[0] in " \n\t\r"): + text = text[1:] + text = text.split() + a = [] + for num in text: + a.append(float(num)) + for i in range(0, offset): + a.pop(0) + mat = Matrix(([a[0], a[1], a[2], a[3]], + [a[4], a[5], a[6], a[7]], + [a[8], a[9], a[10], a[11]], + [a[12], a[13], a[14], a[15]])) + return mat diff --git a/collada_superbmd_export.py b/collada_superbmd_export.py new file mode 100644 index 0000000..ba721d5 --- /dev/null +++ b/collada_superbmd_export.py @@ -0,0 +1,168 @@ +import bpy, math +from . import blender_funcs + +# just a DAE exporter. +# it was supposed to only rotate the meshes around the X axis by -90 degrees +# but above that it now checks for armature and vertex group stuff +# only exports the armature object selected + +# write_bmd_bdl_collada function +# function to write custom collada file for +# SuperBMD conversion +def write_bmd_bdl_collada(context, filepath, triangulate): + + # this thing is always needed for stuff + 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): + 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'} + + # get armature object + armature = scene.objects.active + print("\nArmature found: \"%s\"" % (armature.name)) + + # 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'} + + # check if all meshes have an armature modifier that is + # assigned to the armature object and it is binded to + # vertex groups if none of those are just create them, + # I don't think it is bad to automate that + 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') + mesh.modifiers["Armature"].object = armature + mesh.modifiers["Armature"].use_vertex_groups = True + else: # ensure bind is to a vertex group + if (mesh.modifiers["Armature"].use_vertex_groups == False): + blender_funcs.disp_msg("\"%s\": armature modifier wasn't binded to vertex groups" + % (mesh.name)) + mesh.modifiers["Armature"].use_vertex_groups = True + + # check if all the vertex groups in each mesh correspond to the name of a skeleton bone + bone_name_list = [] + for bone in armature.data.bones: + bone_name_list.append(bone.name) + # vertex group check + 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'} + # 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'} + + # get the object names + obj_name = armature.name + child_names = [] + for child in armature.children: + 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) + bpy.ops.object.duplicate(linked = 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) + + # apply the transformations + armature.rotation_euler[0] = math.radians(-90) + armature.scale = 100 * armature.scale + bpy.ops.object.transforms_to_deltas(mode = 'ALL') + + # handle the object names so that they match the original model names + armature.name = obj_name + armature.data.name = obj_name + for i in range(0, len(armature.children)): + armature.children[i].name = child_names[i] + armature.children[i].data.name = child_names[i] + + # store the new bind_mat and rest_mat (the user might want to update them) + for data_bone in armature.data.bones: + # related pose bone + pose_bone = armature.pose.bones[data_bone.name] + # bind matrix + bpy.data.armatures[armature.data.name].pose_position = 'REST' + bpy.context.scene.update() # update scene + blender_funcs.set_bone_bind_mat(data_bone, pose_bone.matrix) + # rest matrix + bpy.data.armatures[armature.data.name].pose_position = 'POSE' + bpy.context.scene.update() # update scene + mat = None + if (pose_bone.parent != None): + mat = pose_bone.parent.matrix.inverted() * pose_bone.matrix + else: + mat = pose_bone.matrix + blender_funcs.set_bone_rest_mat(data_bone, mat) + + # export the object + bpy.ops.wm.collada_export(filepath = filepath, use_blender_profile = False, + selected = True, include_children = True, + triangulate = triangulate) + + # delete the duplicate object + blender_funcs.select_obj(scene, armature, True) + bpy.ops.object.delete(use_global = False) + + # re-assign the original object names + armature = old_armature + armature.name = obj_name + armature.data.name = obj_name + for i in range(0, len(armature.children)): + armature.children[i].name = child_names[i] + armature.children[i].data.name = child_names[i] + + # done! + blender_funcs.select_obj(scene, armature, False) + blender_funcs.disp_msg("Armature \"%s\" exported!" % (armature.name)) + return {'FINISHED'} + +# Stuff down is for the menu appending +# of the exporter 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 +from bpy.types import Operator + +class export_superbmd_collada(Operator, ExportHelper): + """Export a Collada file for SuperBMD (SuperBMD only)""" + bl_idname = "export_scene.superbmd_collada" + bl_label = "Export SuperBMD Collada (.DAE)" + filename_ext = ".dae" + filter_glob = StringProperty(default = "*.dae", options = {'HIDDEN'}, maxlen = 255) + + triangulate = BoolProperty(name = "Triangulate meshes", + description = "Triangulate meshes inside armatures", + default = False) + def execute(self, context): + return write_bmd_bdl_collada(context, self.filepath, self.triangulate) + +# Only needed if you want to add into a dynamic menu +def menu_export_superbmd_collada(self, context): + self.layout.operator(export_superbmd_collada.bl_idname, text="SuperBMD Collada (.dae)") + +bpy.utils.register_class(export_superbmd_collada) +bpy.types.INFO_MT_file_export.append(menu_export_superbmd_collada) + +# test call +bpy.ops.export_scene.superbmd_collada('INVOKE_DEFAULT') diff --git a/collada_superbmd_import.py b/collada_superbmd_import.py new file mode 100644 index 0000000..7e8de8b --- /dev/null +++ b/collada_superbmd_import.py @@ -0,0 +1,202 @@ +import bpy, os +from lxml import etree +from . import collada_funcs +from . import blender_funcs +from . import math_funcs + +# Comments (spanish) (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 <Name_array> pueden obtenerse del elemento <float_array> (que viene después de <Name_array>) en el elemento <skin> 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!). + +# Bind Shape Matrix o Matriz Forma de Vínculo (aplica a mallas): es la matriz de transformación que se le aplica a las mallas del modelo en 3D para que dichas mallas esten en la posición correcta para ser usadas por los huesos de dicho modelo. De otra forma, las mallas tienen una posición (transformación) original y estas matrices la modifican para entonces tener los vertices de dicha malla en la posición deseada para ser movidos por los huesos del modelo. Esta posición original creo que no tiene que ver con el proceso de vínculo (huesos con la malla) o con la pose de descanso, creo que es algo separado a ambos. + +# Rest Pose o Pose de Descanso (aplica a huesos): o pose de descanso, es la posición (transformación) asignada a los huesos luego de enlazar (vincular) los huesos a la malla del modelo en 3D. Por su descripción, puede ser igual o diferente a las transformaciones de hueso en la pose de vínculo. Es la transformación que se usa como base para las animaciones de esqueleto (no se usan para describir la posición de los huesos con las mallas!!!). Estas transformaciones son con respecto al origen del hueso padre (no son locales!). + +# Voy a suponer que las mallas con su matriz de forma de vinculo estan ya en la configuración para la pose de vinculo con los huesos (¿será por eso que se llama bind shape matrix?) + +# lo lógico es entonces: +# - Cargar las mallas con el importador de collada de blender (Mallas con su BSM) +# - Poner los huesos en su posición de vínculo con la información en las mallas (Huesos en su pose para empezar a deformar las mallas). +# - Crear una pose de descanso para el esqueleto en el modo pose de blender (Huesos en su pose para animar las mallas) +# - Almacenar la informacion de las poses de vínculo y descanso en las propieades de los huesos del esqueleto de blender + +# import_collada_superbmd() function +# read superbmd/blenxy collada file +# used to import the collada file into Blender +def import_collada_superbmd(context, filepath): + + # scene variable is always needed for something + scene = bpy.context.scene + + # make a temporal collada file with the "unit" and "up_axis" elements modified + # and import that file (elements inside the assets element) + # also fix the dumb Z:\ paths that happen when using wine + xml = collada_funcs.check_collada(filepath) + if (xml == None): + blender_funcs.disp_msg("Collada file isn't well formatted") + return {'FINISHED'} + + # get asset's unit element + root = xml.getroot() + unit_elem = root.find("asset").find("unit") + unit_elem.attrib["meter"] = "0.01" + + # texture files path + if (os.name == "posix"): + for image in root.find("library_images"): + tmp = image.find("init_from").text + while (tmp[0] != '\\'): + tmp = tmp[1:] + image.find("init_from").text = tmp.replace("\\", "/") + + # save the new collada file and load it into blender + xml.write(filepath + ".xml", pretty_print = True) + bpy.ops.wm.collada_import(filepath = filepath + ".xml") + # 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 + + # apply transform and select only the object + blender_funcs.select_obj(scene, armature, False) + + # 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.delete() + + # get the bones bind matrices + # traverse through the <Name_array> and + # <float_array> elements with the bone bind pose data + bind_matrices = {} + for controller in root.find("library_controllers").findall("controller"): + bone_names = controller[0][1][0].text.split() + bone_matrices = controller[0][2][0].text + for i in range(0, len(bone_names)): + mat = collada_funcs.get_text_mat4x4(bone_matrices, 16 * i).inverted() + dict_entry = {bone_names[i] : mat} + bind_matrices.update(dict_entry) + + # get the bones rest matrices and in parallel reconstruct the armature + # traverse through the <node> elements in <visual_scene> + rest_matrices = {} + # search for the node named "skeleton_root" inside the <visual_scene> + # element which is inside the <library_visual_scenes> element + jnt_root_node = root.find("library_visual_scenes").find("visual_scene") + for node in jnt_root_node: + if (node.attrib["name"] == "skeleton_root"): + jnt_root_node = node.find("node") + 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 != []): + + # get the joints for the next iteration + next_jnts = [] + for jnt in jnts_same_level: + for elem in jnt.getchildren(): + if (elem.tag == "node"): + next_jnts.append(elem) + + # get the current joint's matrix information + for jnt in jnts_same_level: + jnt_name = jnt.attrib["name"] + # create the bone + bpy.ops.armature.bone_primitive_add(name = jnt_name) + armature.data.edit_bones[jnt_name].length = 10 + mat = collada_funcs.get_text_mat4x4(jnt.find("matrix").text, 0) + # check if this is the root joint of the skeleton + if (is_first_jnt == False): + parent_name = jnt.getparent().attrib["name"] + # parent the bone + armature.data.edit_bones[jnt_name].parent = armature.data.edit_bones[parent_name] + # check if the current bone matrix is not zero filled + if (mat.determinant() == 0): + mat = math_funcs.get_id_mat4x4() + # add the respective matrix to the dictionary and assign it to the bone + rest_matrices.update({jnt_name : mat}) + + jnts_same_level = next_jnts + is_first_jnt = False + + # apply the bind matrices + for i in range(0, len(armature.data.edit_bones)): + bone = armature.data.edit_bones[i] + if (bone.name in bind_matrices): + bone.matrix = bind_matrices[bone.name] + else: + if (bone.parent != None): + bone.matrix = bone.parent.matrix * rest_matrices[bone.name] + else: + bone.matrix = rest_matrices[bone.name] + + # get to pose mode + bpy.ops.object.mode_set(mode='POSE') + + # set the "pose position" to be the rest position (for animations) + # and keep the "rest position" as the bind position (cannot be altered) + for bone in armature.pose.bones: + if (bone.parent != None): + bone.matrix = bone.parent.matrix * rest_matrices[bone.name] + else: + bone.matrix = rest_matrices[bone.name] + + # store the bind_mat and rest_mat custom properties on each bone + bpy.ops.object.mode_set(mode='OBJECT') + for bone in armature.data.bones: + if ((bone.name in bind_matrices) and (bone.name in rest_matrices)): + blender_funcs.set_bone_bind_mat(bone, bind_matrices[bone.name]) + else: + blender_funcs.set_bone_bind_mat(bone, rest_matrices[bone.name]) + blender_funcs.set_bone_rest_mat(bone, rest_matrices[bone.name]) + + # scale down the model and apply transformations + bpy.ops.object.mode_set(mode='OBJECT') + armature.scale = (0.01, 0.01, 0.01) + bpy.ops.object.transforms_to_deltas(mode = 'ALL') + + # done! + blender_funcs.disp_msg("SuperBMD Collada file imported!") + return {'FINISHED'} + + +################################################# +# Stuff down is for the menu appending +# of the importer to work plus some setting stuff +# comes from a Blender importer template + +# ExportHelper is a helper class, defines filename and +# invoke() function which calls the file selector. +from bpy_extras.io_utils import ExportHelper +from bpy.props import StringProperty, BoolProperty, EnumProperty +from bpy.types import Operator + +class import_superbmd_collada(Operator, ExportHelper): + """Import a Collada file from SuperBMD (SuperBMD only)""" + bl_idname = "import_scene.superbmd_collada" + bl_label = "Import SuperBMD Collada (.DAE)" + + # ExportHelper mixin class uses this + filename_ext = ".dae" + filter_glob = StringProperty(default = "*.dae", options = {'HIDDEN'}, maxlen = 255) + # execute function + def execute(self, context): + return import_collada_superbmd(context, self.filepath) + +# Only needed if you want to add into a dynamic menu +def menu_import_superbmd_collada(self, context): + self.layout.operator(import_superbmd_collada.bl_idname, text="SuperBMD Collada (.dae)") + +bpy.utils.register_class(import_superbmd_collada) +bpy.types.INFO_MT_file_import.append(menu_import_superbmd_collada) + +# test call +bpy.ops.import_scene.superbmd_collada('INVOKE_DEFAULT') diff --git a/my_functions.py b/math_funcs.py index 49dc87d..8f92bef 100644 --- a/my_functions.py +++ b/math_funcs.py @@ -1,35 +1,32 @@ -''' -file that contains some functions that I don't -want to re-write on each individual .py file -''' - -import bpy, math, re +import math from mathutils import Matrix +# file containning math related functions -################################################ -# show message on screen for errors or warnnings -# copied this code from a page I saw it in :) -################################################ -def show_message(message = "", title = "Message Box", icon = 'INFO'): -# - def draw(self, context): - self.layout.label(text=message) - - bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) -# +# function to return a 0 filled matrix 3x3 +def get_zero_mat3x3(): + + return 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])) +# function to return an identity matrix 3x3 +def get_id_mat3x3(): + + return 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])) -####################################### # calc_scale_matrix function # function to build the scale matrix -# -# x_scale (float) --> scaling in X axis -# y_scale (float) --> scaling in Y axis -# z_scale (float) --> scaling in Z axis -####################################### def calc_scale_matrix(x_scale, y_scale, z_scale): -# + scale_matrix = Matrix(( [x_scale, 0, 0, 0], [0, y_scale, 0, 0], [0, 0, z_scale, 0], @@ -37,20 +34,12 @@ def calc_scale_matrix(x_scale, y_scale, z_scale): )) return scale_matrix -# - -################################################### # calc_rotation_matrix function # function to calculate the rotation matrix -# for a Extrinsic Euler XYZ system -# -# x_angle (float) --> rot angle in X axis (radians) -# y_angle (float) --> rot angle in Y axis (radians) -# z_angle (float) --> rot angle in > axis (radians) -################################################### +# 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], @@ -68,19 +57,11 @@ def calc_rotation_matrix(x_angle, y_angle, z_angle): [0, 0, 0, 1])) return z_rot_mat * y_rot_mat * x_rot_mat -# - -################################################# # calc_translation_matrix function # function to build the translation matrix -# -# x_translation (float) --> translation in X axis -# y_translation (float) --> translation in Y axis -# z_translation (float) --> translation in Z axis -################################################# def calc_translation_matrix(x_translation, y_translation, z_translation): -# + translation_matrix = Matrix(( [1, 0, 0, x_translation], [0, 1, 0, y_translation], [0, 0, 1, z_translation], @@ -88,11 +69,7 @@ def calc_translation_matrix(x_translation, y_translation, z_translation): )) 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 @@ -109,20 +86,17 @@ def calc_translation_matrix(x_translation, y_translation, z_translation): # 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): @@ -131,7 +105,6 @@ def interpolate(l_x, l_y, r_x, r_y, m_x, m_y, interp_type): 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): @@ -141,10 +114,7 @@ def interpolate(l_x, l_y, r_x, r_y, m_x, m_y, interp_type): result = m_y return result -# - -#################################################################### # 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 @@ -154,17 +124,14 @@ def interpolate(l_x, l_y, r_x, r_y, m_x, m_y, interp_type): # 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): -# - ############################# + # create left/right variables l_val = 0 l_val_pos = 0 r_val = 0 r_val_pos = 0 - ##################################### # find near left value (has to exist) # read array from right to left for i in range(len(anim_array), -1, -1): @@ -183,17 +150,14 @@ def find_left_right(anim_array, pos): ############## # 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] - ######################################### # 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): @@ -211,14 +175,10 @@ def find_left_right(anim_array, pos): # found and the end of the animation) if (i == (len(anim_array) - 1)): return [l_val_pos, l_val, None, None] - # # if all values are found, return them return [l_val_pos, l_val, r_val_pos, r_val] -# - -####################################################### # convert_angle_to_180 function # function used by the convert_anim_rot_to_180 function # to convert a single angle in its representation on @@ -229,7 +189,6 @@ def find_left_right(anim_array, pos): # 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 @@ -254,10 +213,7 @@ def convert_angle_to_180(angle): angle = angle + (opposite_spin_dir * 360) 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 @@ -286,9 +242,8 @@ def convert_angle_to_180(angle): # 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): -# + # temp rot array to store calculated values and keyframe position rot_array_cp = [[], []] @@ -298,7 +253,6 @@ def convert_rot_anim_to_180(rot_array, csv_keyframe_numbers): if (rot_array[i] != None): rot_array_kf.append(i) - ########################################################################### # loop through each consecutive pair of keyframes of the rot_array_kf array for i in range(len(rot_array_kf) - 1): # @@ -318,7 +272,6 @@ def convert_rot_anim_to_180(rot_array, csv_keyframe_numbers): else: # counter-clockwise rot_direction = -1 - ############################################################## # 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 @@ -330,18 +283,15 @@ def convert_rot_anim_to_180(rot_array, csv_keyframe_numbers): # 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") - ############################################ # 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) - ############################################################### # 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")) - ################################ # append results to rot_array_cp # keyframes @@ -350,16 +300,12 @@ def convert_rot_anim_to_180(rot_array, csv_keyframe_numbers): # 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] - ####################################################### # 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 @@ -371,4 +317,3 @@ def convert_rot_anim_to_180(rot_array, csv_keyframe_numbers): break if (value_found == False): csv_keyframe_numbers.append(rot_array_cp[0][i]) -# |