diff options
Diffstat (limited to 'csv_anim_bck_export.py')
-rw-r--r-- | csv_anim_bck_export.py | 314 |
1 files changed, 314 insertions, 0 deletions
diff --git a/csv_anim_bck_export.py b/csv_anim_bck_export.py new file mode 100644 index 0000000..a1eeaa3 --- /dev/null +++ b/csv_anim_bck_export.py @@ -0,0 +1,314 @@ +''' +CSV exporter for the BCK animation type +CSV to be used with the j3d animation editor program +''' + +import bpy, math +from mathutils import Matrix + +# Notes (AFAIK): + +# - position/rotation/scaling values of a bone in an animation +# must be the ones that are relative to its parent bone +# - Extrinsic Euler XYZ system is the one being used for rotation values. +# - all animations must start in Frame 0 (starting frame). + +################################################ +# ShowMessageBox function +# show message on screen for errors or warnnings +# copied this code from a page I saw it in :) +################################################ +def ShowMessageBox(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) + + +###################################### +# write_csv_bck (MAIN FUNCTION) +# function to write a CSV file with +# data for the BCK animation type +# to be used with J3D Animation Editor +###################################### +def write_csv_bck(context, filepath, loop_mode): + + # loop mode variable declaration + loop_number = 0 + + if (loop_mode == "OPT_A"): + loop_number = 0 + elif (loop_mode == "OPT_B"): + loop_number = 1 + elif (loop_mode == "OPT_C"): + loop_number = 2 + elif (loop_mode == "OPT_D"): + loop_number = 3 + elif (loop_mode == "OPT_E"): + loop_number = 4 + + print("Selecting Armature object...") + + # Get Armature object + armature_count = 0 + for object in bpy.data.objects: + if (object.type == "ARMATURE"): + armature = object + armature_count = armature_count + 1 + + # more than 2 armature objects, print error and end exporter + if (armature_count > 1): + print("\n### ERROR ###") + print("2 or more Armature Objects detected. Animations will only be extracted from one.") + print("### ERROR ###\n") + ShowMessageBox("2 or more Armature Objects detected. Animations will only be extracted from one.", "Error Exporting CSV File (for BCK)", 'ERROR') + return {'FINISHED'} + + print("Creating CSV file (for BCK)...") + + # open file to write + f = open(filepath, 'w', encoding='utf-8') + + scene = bpy.context.scene + + print("Writing CSV header...") + + # write the "header" of the CSV animation table (for BCK) + f.write("%d,%d,%d,%s" % (loop_number, scene.frame_end, 0, ".bck")) + f.write("\n") + + f.write("Bone Name,Tangent Interpolation,Component") + + for frame in range(scene.frame_end + 1): + f.write(",Frame %d" % (frame)) + f.write("\n") + + ######################################## + # read animation data for one bone + # then dump the animation data for + # said bone on the CSV file (one by one) + + # i will be used to track the bones + # j to track the frames of the animation + ######################################## + + # for loop to iterate throught each bone + for i in range(len(armature.pose.bones)): + + # get current bone + bone = armature.pose.bones[i] + + print("Extracting/Writing animation data for bone: %s" % (bone.name)) + + # bone_anim_data will hold the animation data for bone as follows + # the row length of bone_anim_data will match the number + # of frames in the animation + + # row 1 --> X scaling + # row 2 --> Y scaling + # row 3 --> Z scaling + # row 4 --> X rotation + # row 5 --> Y rotation + # row 6 --> Z rotation + # row 7 --> X translation + # row 8 --> Y translation + # row 9 --> Z translation + + # already initialized with 9 rows as I don't need more than that + bone_anim_data = [[], [], [], [], [], [], [], [], []] + + # get the animation length + animation_length = scene.frame_end + 1 + + ########################## + # read bone animation data + ########################## + + # for loop to iterate throught each frame + for j in range(animation_length): + # + # set scene frame + scene.frame_set(j) + + # data will be rounded as follows + # - scaling to 2 decimals + # - rotation to 1 decimal + # - translation to 1 decimal + + # Note: bone.matrix returns the final transformation matrix of a + # pose.bone with respect to the armature origin and as I + # need the bone's transformation matrix relative to its + # parent (bone.parent.matrix.inverted() * bone.matrix) + # gives the desired result (the bone transformation + # matrix relative to parent) + + # first bone must be the outermost bone + # and therefore it has no parent + if (i == 0): + + # -90 degree rotation matrix + rot_mat = Matrix(( [1, 0, 0, 0], + [0 , math.cos(-(math.pi/2)), -math.sin(-(math.pi/2)), 0], + [0 , math.sin(-(math.pi/2)), math.cos(-(math.pi/2)), 0], + [0, 0, 0, 1] + )) + + # bone_relative_to_parent_matrix of the first bone is rotated + # -90 degrees in the X axis as the main bone will use the + # Z axis as the UP axis in the animation instead of Y + # (applying said rotation is done to avoid the issue) + # pose.bone.matrix_basis is equal to rot_mat * bone.matrix (AFAIK) + + bone_relative_to_parent_matrix = rot_mat * bone.matrix + + # for any other bone + else: + bone_relative_to_parent_matrix = bone.parent.matrix.inverted() * bone.matrix + + # bone scaling + bone_scale = bone_relative_to_parent_matrix.to_scale() + # bone rotation (stored in radians, extrinsic XYZ Euler) + bone_rotation = bone_relative_to_parent_matrix.to_euler("XYZ") + # bone translation (multiplied by 100 because 1 GU is 100 meters) + bone_translation = 100 * bone_relative_to_parent_matrix.to_translation() + + # scaling data + bone_anim_data[0].append(round(bone_scale[0], 2)) + bone_anim_data[1].append(round(bone_scale[1], 2)) + bone_anim_data[2].append(round(bone_scale[2], 2)) + + # rotation data (must be in degrees!) + bone_anim_data[3].append(round(math.degrees(bone_rotation[0]), 1)) + bone_anim_data[4].append(round(math.degrees(bone_rotation[1]), 1)) + bone_anim_data[5].append(round(math.degrees(bone_rotation[2]), 1)) + + # position data + bone_anim_data[6].append(round(bone_translation[0], 1)) + bone_anim_data[7].append(round(bone_translation[1], 1)) + bone_anim_data[8].append(round(bone_translation[2], 1)) + # + + #################################### + # write bone animation data into CSV + # read bone_anim_data + #################################### + for k in range(9): + # + # first 3 rows are scaling data + # the next 3 rows are rotation data + # the final 3 rows are translation data + + # the start of the first row for a bone + # animation data (Scale X) contains the bone name + if (k == 0): + f.write("%s,Linear,Scale X:" % (bone.name)) + + # other rows just contain the animation properties + elif(k == 1): + f.write(",Linear,Scale Y:") + elif(k == 2): + f.write(",Linear,Scale Z:") + elif(k == 3): + f.write(",Linear,Rotation X:") + elif(k == 4): + f.write(",Linear,Rotation Y:") + elif(k == 5): + f.write(",Linear,Rotation Z:") + elif(k == 6): + f.write(",Linear,Translation X:") + elif(k == 7): + f.write(",Linear,Translation Y:") + elif(k == 8): + f.write(",Linear,Translation Z:") + + # Note: ^ "Linear" is chosen as the animation timing + # has been already set in blender. + # The animation values must be interpreted linearly + # by the game so the animation plays as in Blender + + ################################### + # write animation row data + # will print values with 2 decimals + ################################### + for l in range(animation_length): + # + # get current animation value + current_value = bone_anim_data[k][l] + + # print the first value from row + if (l == 0): + f.write(",%.2f" % (current_value)) + + # compare old_value with current_value + # if equal leave blank the animation frame value otherwise write said value + # this is done to avoid repeating the same number each time + elif (old_value == current_value): + f.write(",") + else: + f.write(",%.2f" % (current_value)) + + # if the end of a row is reached write a newline char to the line + if (l == (animation_length - 1)): + f.write("\n") + + # store old animation value for the next loop + old_value = current_value + # + # + + f.close() + + print("Armature animation data extracted!") + + # exporter end + return {'FINISHED'} + + +# 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 Export_CSV_BCK(Operator, ExportHelper): + """Save a CSV file for BCK conversion (to use with J3D Animation Editor)""" + bl_idname = "export_scene.csv_bck" + bl_label = "Export CSV (for BCK)" + + filename_ext = ".csv" + + filter_glob = StringProperty( + default="*.csv", + options={'HIDDEN'}, + maxlen=255, + ) + + loop_mode = EnumProperty( + name="Loop Mode", + description="Choose the loop mode for the animation", + items=( + ('OPT_A', "Once", "Play the animation once and stop at the last frame"), + ('OPT_B', "Once and Reset", "Play the animation once and stop at the first frame"), + ('OPT_C', "Loop", "Loop the animation infinitely"), + ('OPT_D', "Mirrored Once", "Play the animation forwards and then backwards once. Stops at the first frame"), + ('OPT_E', "Mirrored Loop", "Play the animation forwards and then backwards infinitely") + ), + default='OPT_A', + ) + + def execute(self, context): + return write_csv_bck(context, self.filepath, self.loop_mode) + + +# Only needed if you want to add into a dynamic menu +def menu_export_csv_bck(self, context): + self.layout.operator(Export_CSV_BCK.bl_idname, text="CSV (for BCK) (.csv)") + +bpy.utils.register_class(Export_CSV_BCK) +bpy.types.INFO_MT_file_export.append(menu_export_csv_bck) + + +# test call +bpy.ops.export_scene.csv_bck('INVOKE_DEFAULT') |