diff options
author | “Humming-Owl” <“isaclien9752@gmail.com”> | 2023-09-02 01:01:30 -0400 |
---|---|---|
committer | “Humming-Owl” <“isaclien9752@gmail.com”> | 2023-09-02 01:01:30 -0400 |
commit | 9f152e3f380c382459793d3a2e8e6fe728fb9af2 (patch) | |
tree | 7267047f84af8cd79e370edd90bebc8f1b844e2a /csv_anim_bck_export.py | |
parent | 7abaa50dced0f03eba2011e9d605b8acb7451a91 (diff) | |
download | blenxy-9f152e3f380c382459793d3a2e8e6fe728fb9af2.tar.gz blenxy-9f152e3f380c382459793d3a2e8e6fe728fb9af2.zip |
CSV exporter update
Diffstat (limited to 'csv_anim_bck_export.py')
-rw-r--r-- | csv_anim_bck_export.py | 523 |
1 files changed, 341 insertions, 182 deletions
diff --git a/csv_anim_bck_export.py b/csv_anim_bck_export.py index a1eeaa3..9451507 100644 --- a/csv_anim_bck_export.py +++ b/csv_anim_bck_export.py @@ -3,8 +3,9 @@ CSV exporter for the BCK animation type CSV to be used with the j3d animation editor program ''' -import bpy, math +import bpy, math, re from mathutils import Matrix +from .my_functions import * # Notes (AFAIK): @@ -12,19 +13,6 @@ from mathutils import Matrix # 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) @@ -32,7 +20,10 @@ def ShowMessageBox(message = "", title = "Message Box", icon = 'INFO'): # data for the BCK animation type # to be used with J3D Animation Editor ###################################### -def write_csv_bck(context, filepath, loop_mode): +def write_csv_bck(context, filepath, loop_mode, export_mode): + + # always needed + scene = bpy.context.scene # loop mode variable declaration loop_number = 0 @@ -48,267 +39,435 @@ def write_csv_bck(context, filepath, loop_mode): 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') + # 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() + print("###############") + print("Armature found: %s" % armature.name) + print() + 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")) + ############################################### + # write the "header" of the CSV animation table + ############################################### + + animation_length = scene.frame_end + 1 + f.write("%d,%d,%d,%s" % (loop_number, animation_length - 1, 0, ".bck")) f.write("\n") f.write("Bone Name,Tangent Interpolation,Component") - for frame in range(scene.frame_end + 1): + for frame in range(animation_length): 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) + #################################################### + # get a detailed list of the keyframes used in the + # animation and the bones related to those keyframes + # + # bone_kf_data will hold the bone and keyframe position as follows: + # + # bone name --> name of the animated bone + # anim property --> Scale/Rotation/Translation + # axis --> 0/1/2 --> X/Y/Z + # + # a bone name, anim property, axis, kf 1 pos, kf 2 pos, ... + # another bone name, anim property, axis, kf 3 pos, kf 4 pos, ... + # ... + # + # Note that each animation property can have different keyframe positions in time + + bone_kf_data = [] + + for i in range(len(armature.animation_data.action.fcurves)): + # + # get curve + curve = armature.animation_data.action.fcurves[i] + # add new row for given curve in bone_kf_data + bone_kf_data.append([]) + + ################################################################ + # append bone name, animation property and axis related to curve + + # bone name + bone_kf_data[i].append(re.search('\["(.+?)"\]', curve.data_path).group(1)) + + # anim property + if (curve.data_path.find("scale") + 1): + bone_kf_data[i].append("scale") + elif (curve.data_path.find("rotation_euler") + 1): + bone_kf_data[i].append("rotation") + elif (curve.data_path.find("location") + 1): + bone_kf_data[i].append("translation") + # axis + if (curve.array_index == 0): + bone_kf_data[i].append("x") + elif (curve.array_index == 1): + bone_kf_data[i].append("y") + elif (curve.array_index == 2): + bone_kf_data[i].append("z") + + # store keyframe data + for j in range(len(curve.keyframe_points)): + keyframe = curve.keyframe_points[j] + bone_kf_data[i].append(int(keyframe.co[0])) # keyframe pos is an integer (frame) + # + + # print bone_kf_data to terminal + print() + for row in bone_kf_data: + print(row) + + ############################################################ + # get the armature bones that contain 2 or more keyframes + # defined (read bone_kf_data) and store at the side of + # each bone name the last keyframe position of its animation + # + # bone_last_kf_pos will contain data as follows: + # + # bone1 name, last bone1 keyframe position, bone2 name, last bone2 keyframe position, ... + # + bone_last_kf_pos = [] + for i in range(len(bone_kf_data)): + # + if (len(bone_last_kf_pos) != 0): + # if the last bone name on bone_last_kf_pos is the same as the + # one on bone_kf_data[i][0] go to the column element + if (bone_last_kf_pos[len(bone_last_kf_pos) - 2] == bone_kf_data[i][0]): + # check if the keyframe position of the bone is larger and store the larger value + if (bone_last_kf_pos[len(bone_last_kf_pos) - 1] < bone_kf_data[i][len(bone_kf_data[i]) - 1]): + bone_last_kf_pos[len(bone_last_kf_pos) - 1] = bone_kf_data[i][len(bone_kf_data[i]) - 1] + continue + + # bone animation row has more than 1 keyframe on an anim property + # append bone name and last keyframe position in time + if (len(bone_kf_data[i]) > 4): + bone_last_kf_pos.append(bone_kf_data[i][0]) + bone_last_kf_pos.append(bone_kf_data[i][len(bone_kf_data[i]) - 1]) + # + + # print bones_with_kf to terminal + print() + print(bone_last_kf_pos) + print() - # i will be used to track the bones - # j to track the frames of the animation - ######################################## + ################################################################### + # read animation data for one bone then dump the animation data for + # said bone on the CSV file + ################################################################### - # for loop to iterate throught each bone + ######################## + # loop through each bone for i in range(len(armature.pose.bones)): - - # get current bone + + # get bone bone = armature.pose.bones[i] + # print bone going to be processed on terminal + print("Processing animation for bone: %s" % (bone.name)) - 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 + # store the animation data scale/rotation/translation X/Y/Z on + # bone_anim_data which will have 9 rows and each row will be the + # length of the animation + 1 (including Frame 0 values) - # 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 + # row 1 --> Scale X + # row 2 --> Scale Y + # row 3 --> Scale Z + # row 4 --> Rotation X + # row 5 --> Rotation Y + # row 6 --> Rotation Z + # row 7 --> Translation X + # row 8 --> Translation Y + # row 9 --> Translation Z - # 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 - ########################## + ############################################################### + # fill bone_anim_data so the rows have the animation length + 1 + for j in range(len(bone_anim_data)): + for k in range(animation_length): + bone_anim_data[j].append("") - # 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) + ########################################################################### + # check if the bone has 2 or more keyframes (bone has an animation) + # and store the animation length of that bone (read bone_last_kf_pos, + # first frame counts on the animation length!, to use later) + bone_has_anim = False + anim_length_for_bone = 0 + 1 + for j in range(int(len(bone_last_kf_pos) / 2)): + # + if (bone_last_kf_pos[2 * j] == bone.name): + bone_has_anim = True + anim_length_for_bone = bone_last_kf_pos[(2 * j) + 1] + 1 + break + # + + print("Bone has animation? ", end = "") + print(bone_has_anim) + print("Bone animation length: %d" % (anim_length_for_bone)) + + ################################################################### + # if export mode is "only keyframes" define current_bone_kf_data + # and get from it all the bone keyframes in each animation property + # keyframes on bone_kfs won't necessarily be on order + # (do it on bones that have animation) + current_bone_kf_data = [] + bone_kfs = [] + if (export_mode == "OPT_B" and bone_has_anim == True): + # + ########################################################## + # store the rows of bone_kf_data in which the current bone + # appears in current_bone_kf_data (to use later) + current_bone_kf_data = [] + for j in range(len(bone_kf_data)): + if (bone_kf_data[j][0] == bone.name): + current_bone_kf_data.append(bone_kf_data[j]) - # first bone must be the outermost bone - # and therefore it has no parent - if (i == 0): + ################################################# + # read current_bone_kf_data to get all the unique + # keyframe positions of the bone animation + for j in range(len(current_bone_kf_data)): + # + # store the keyframes found on the first row + # of current_bone_kf_data in bone_kfs + if (j == 0): + for k in range(len(current_bone_kf_data[0])): + # make k equal to 3 + if (k < 3): + continue + bone_kfs.append(current_bone_kf_data[j][k]) + + # other rows + for k in range(len(current_bone_kf_data[j])): + # + if (k < 3): # make k equal to 3 + continue + + # loop through bone_kfs to check for new keyframe positions + keyframe_exists = False + for l in range(len(bone_kfs)): + if (current_bone_kf_data[j][k] == bone_kfs[l]): + keyframe_exists = True + break + + if (keyframe_exists == False): + bone_kfs.append(current_bone_kf_data[j][k]) + # + # + # ~ print(current_bone_kf_data) + print("Bone's keyframes position:") + print(bone_kfs) + # + + print() + + # if bone_has_anim equals False only store its first frame animation values + # if bone_has_anim equals True store (depending on the export mode) its + # keyframes/all frame animation values (until the last keyframe) + + ######################################## + # loop through the animation of the bone + # k is used to go through bone_kfs if + # export mode is "only keyframes" + k = 0 + for j in range(anim_length_for_bone): + # + ########################################## + # set scene frame depending on export mode + if (export_mode == "OPT_B" and bone_has_anim == True and j != 0): + # + # check k in case bone_kfs end is reached + if (k == len(bone_kfs)): + break + + frame = bone_kfs[k] + scene.frame_set(frame) + k = k + 1 + # + else: + # + frame = j + scene.frame_set(frame) + # + # first bone must be the outermost bone and therefore it + # has no parent (batman moment, sorry batman u epik >:]) + 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 + rot_mat = calc_rotation_matrix(math.radians(-90), 0, 0) + # the first bone has to be rotated -90 degrees + # on X to get correct animation values for it + bone_rel_to_parent_mat = rot_mat * bone.matrix + else: # for any other bone + bone_rel_to_parent_mat = bone.parent.matrix.inverted() * bone.matrix - # for any other bone - else: - bone_relative_to_parent_matrix = bone.parent.matrix.inverted() * bone.matrix + ########################################## + # extract bone_rel_to_parent_mat anim data # bone scaling - bone_scale = bone_relative_to_parent_matrix.to_scale() + bone_scale = bone_rel_to_parent_mat.to_scale() # bone rotation (stored in radians, extrinsic XYZ Euler) - bone_rotation = bone_relative_to_parent_matrix.to_euler("XYZ") + bone_rotation = bone_rel_to_parent_mat.to_euler("XYZ") # bone translation (multiplied by 100 because 1 GU is 100 meters) - bone_translation = 100 * bone_relative_to_parent_matrix.to_translation() + bone_translation = 100 * bone_rel_to_parent_mat.to_translation() + ########################################################## + # store frame animation values of bone into bone_anim_data + # 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)) + bone_anim_data[0][frame] = round(bone_scale[0], 2) + bone_anim_data[1][frame] = round(bone_scale[1], 2) + bone_anim_data[2][frame] = 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)) + bone_anim_data[3][frame] = round(math.degrees(bone_rotation[0]), 2) + bone_anim_data[4][frame] = round(math.degrees(bone_rotation[1]), 2) + bone_anim_data[5][frame] = round(math.degrees(bone_rotation[2]), 2) # 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)) + bone_anim_data[6][frame] = round(bone_translation[0], 2) + bone_anim_data[7][frame] = round(bone_translation[1], 2) + bone_anim_data[8][frame] = round(bone_translation[2], 2) + + # ^ a lot of values can be repeated in each row. + # When writing the animation data to the CSV is when + # an optimization method will be done # + # ~ for row in bone_anim_data: + # ~ print(row) + # ~ print() + #################################### # write bone animation data into CSV # read bone_anim_data #################################### - for k in range(9): + for j in range(9): # # first 3 rows are scaling data # the next 3 rows are rotation data - # the final 3 rows are translation 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): + if (j == 0): f.write("%s,Linear,Scale X:" % (bone.name)) - - # other rows just contain the animation properties - elif(k == 1): + elif(j == 1): f.write(",Linear,Scale Y:") - elif(k == 2): + elif(j == 2): f.write(",Linear,Scale Z:") - elif(k == 3): + elif(j == 3): f.write(",Linear,Rotation X:") - elif(k == 4): + elif(j == 4): f.write(",Linear,Rotation Y:") - elif(k == 5): + elif(j == 5): f.write(",Linear,Rotation Z:") - elif(k == 6): + elif(j == 6): f.write(",Linear,Translation X:") - elif(k == 7): + elif(j == 7): f.write(",Linear,Translation Y:") - elif(k == 8): + elif(j == 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): - # + for k in range(animation_length): + # # get current animation value - current_value = bone_anim_data[k][l] + current_value = bone_anim_data[j][k] - # 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): + # write the first value from row + if (k == 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 (to save file size) + elif (old_value == current_value or 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)): + if (k == (animation_length - 1)): f.write("\n") # store old animation value for the next loop - old_value = current_value - # + if (current_value != ""): + 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. +################################################# +# 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_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, - ) +# + """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" - 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) + 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' + ) + export_mode = EnumProperty( + name="Export Mode", + description="Choose the method used to export the model animation data", + items=( ('OPT_A', "All Frames", "Export animation values for each frame of the animation. Slow, higher CSV file size, more accurate animation"), + ('OPT_B', "Only Keyframes", "Only export the animation keyframe values of each bone. Fast, lower CSV file size, least accurate animation"), + ), default='OPT_A' + ) + + def execute(self, context): + return write_csv_bck(context, self.filepath, self.loop_mode, self.export_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)") + self.layout.operator(Export_CSV_BCK.bl_idname, text="CSV Animation Table (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') |