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