''' CSV exporter for the BCK animation type CSV to be used with the j3d animation editor program ''' import bpy, math, re from mathutils import Matrix from .my_functions import * # 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). ###################################### # 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, export_mode): # always needed scene = bpy.context.scene # 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 # 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') print("Writing CSV header...") ############################################### # 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(animation_length): f.write(",Frame %d" % (frame)) f.write("\n") #################################################### # 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() ################################################################### # read animation data for one bone then dump the animation data for # said bone on the CSV file ################################################################### ######################## # loop through each bone for i in range(len(armature.pose.bones)): # get bone bone = armature.pose.bones[i] # print bone going to be processed on terminal print("Processing animation for bone: %s" % (bone.name)) # 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 --> 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 bone_anim_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("") ########################################################################### # 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]) ################################################# # 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 = 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 ########################################## # extract bone_rel_to_parent_mat anim data # bone scaling bone_scale = bone_rel_to_parent_mat.to_scale() # bone rotation (stored in radians, extrinsic XYZ Euler) 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_rel_to_parent_mat.to_translation() ########################################################## # store frame animation values of bone into bone_anim_data # scaling data 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][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][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 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 start of the first row for a bone # animation data (Scale X) contains the bone name if (j == 0): f.write("%s,Linear,Scale X:" % (bone.name)) elif(j == 1): f.write(",Linear,Scale Y:") elif(j == 2): f.write(",Linear,Scale Z:") elif(j == 3): f.write(",Linear,Rotation X:") elif(j == 4): f.write(",Linear,Rotation Y:") elif(j == 5): f.write(",Linear,Rotation Z:") elif(j == 6): f.write(",Linear,Translation X:") elif(j == 7): f.write(",Linear,Translation Y:") elif(j == 8): f.write(",Linear,Translation Z:") ################################### # write animation row data # will print values with 2 decimals ################################### for k in range(animation_length): # # get current animation value current_value = bone_anim_data[j][k] # 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 (k == (animation_length - 1)): f.write("\n") # store old animation value for the next loop if (current_value != ""): old_value = current_value # # # # exporter end 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_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' ) 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 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')