''' 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')