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.py314
1 files changed, 314 insertions, 0 deletions
diff --git a/csv_anim_bck_export.py b/csv_anim_bck_export.py
new file mode 100644
index 0000000..a1eeaa3
--- /dev/null
+++ b/csv_anim_bck_export.py
@@ -0,0 +1,314 @@
+'''
+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')