summaryrefslogtreecommitdiff
path: root/csv_anim_bck_export.py
diff options
context:
space:
mode:
Diffstat (limited to 'csv_anim_bck_export.py')
-rw-r--r--csv_anim_bck_export.py523
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')