123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645 |
- '''
- CSV importer for the BCK animation type
- CSVs that come from the j3d animation editor program
- animations for now will be imported with linear interpolation
- seems to me that the majority of the animations just use linear
- as the interpolation method between keyframes of animation rows
- '''
- # Notes (AFAIK):
- #
- # - reference link --> https://wiki.cloudmodding.com/tww/BCK
- # - In all the Mario CSV BCKs I've seen from J3D Animation
- # Editor the "smooth" interpolation type isn't used at all
- # for animation rows that contain more than the first frame value
- # it could be that JAE just converts the animation to linear
- # timing for easier processing but I am not certain
- import bpy, math, re
- from mathutils import Matrix
- from .my_functions import *
- #################################################
- # read_csv_bck function (MAIN FUNCTION)
- # read CSV file of BCK from J3D Animation Editor
- # used to apply BCK animation to model on Blender
- #################################################
- def read_csv_bck(context, filepath, import_type):
- #
- # this thing is always needed for stuff
- scene = bpy.context.scene
-
- # 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()
-
- # deselect everything (to be sure nothing weird happens)
- bpy.ops.object.select_all(action='DESELECT')
- scene.objects.active = None
-
- # re-select the armature object only
- armature.select = True
- scene.objects.active = armature
-
- # open file to read
- f = open(filepath, 'r', encoding='utf-8')
-
- # check the file if it is from a BCK file and check the
- # number of bones, if the file is not from a BCK
- # or the bone count differs from the armature
- # bone count end the importer
-
- # first line has the animation type
- line = f.readline()
-
- if not (line.find("bck") + 1):
- error_string = "CSV is not for the BCK animation type. Check the file and try again."
- print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n")
- show_message(error_string, "Error importing CSV file data", 'ERROR')
- return {'FINISHED'}
-
- # count the bones of the animation on the CSV
- # store each time the "Scale X" is found in a line
- bone_count = 0
- while True:
-
- line = f.readline()
-
- # count the bones
- if (line.find("Scale X") + 1):
- bone_count = bone_count + 1
-
- # if an empty line or a line with a newline character
- # is reached end of file must've been reached
- if (line == "" or line == '\n'):
- break
-
- # check bone count
- if (bone_count != len(armature.data.bones)):
- error_string = "Number of bones on the Armature is different than the number of animated bones on the CSV. Check the CSV file and try again."
- print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n")
- show_message(error_string, "Error importing CSV file data", 'ERROR')
- return {'FINISHED'}
-
- ###############
- # file is valid
- ###############
-
- # start extracting animation data
-
- ###########################################################################
- ###########################################################################
- # import type "ignore rest pose"
- # in here I don't need to do any calculation of the data read from the CSV
- # the armature in which this animation is to be applied must've been
- # imported before in the "ignore rest pose" mode (i.e. the child bones are
- # in the origin of the reference system defined by the parent's bone head).
- # This is like this because BCK CSVs contain the data of how a child bone
- # moves with respect to its parent.
- ###########################################################################
- ###########################################################################
- if (import_type == True):
- #
- # read the CSV from the start again
- # to import the animation values
-
- f = open(filepath, 'r', encoding='utf-8')
-
- line = f.readline()
-
- # first line of the CSV
- csv_first_line = line.split(',')
- animation_length = int(csv_first_line[1])
-
- # get the animation last frame and set it in blender
- bpy.data.scenes["Scene"].frame_end = int(csv_first_line[1])
-
- # the next line comes with the notable keyframes used in the animation
- line = f.readline()
-
- # get all keyframe numbers
- csv_keyframe_numbers = re.findall('[0-9]+', line)
- csv_keyframe_numbers = [int(string) for string in csv_keyframe_numbers]
-
- ###############################################################
- # in the next loop starts the bone animation data table reading
- # loop through every bone
- ###############################################################
- for bone_number in range(bone_count):
-
- # get pose bone
- pose_bone = armature.pose.bones[bone_number]
- print("Reading/Writing animation for bone: %s" % (pose_bone.name))
-
- # set bone rotation mode to Extrinsic Euler XYZ
- pose_bone.rotation_mode = 'XYZ'
-
- # bone_anim_data will hold the bone keyframe animation
- # data for a single bone from the CSV file as follows
-
- # 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 = [[], [], [], [], [], [], [], [], []]
-
- ###########################
- # read bone animation table
- ###########################
- for i in range(9):
- #
- line = f.readline()
- row_data = line.split(',')
-
- # if row_data's length is not equal to the length of
- # csv_keyframe_numbers + 3 fill it with empty strings to match said length
- while (len(row_data) != (len(csv_keyframe_numbers) + 3)):
- row_data.append("")
-
- ############################################################
- # copy the keyframe values from row_data into bone_anim_data
- ############################################################
- for j in range(len(row_data)):
- #
- # first 3 elements of row_data are skipped
- # example: Joint 0, Scale X:, Linear, ...
- if (j < 3):
- continue
-
- # if there is no animation value for the bone on the
- # current keyframe data append None to said position
- # otherwise append the value
- if (row_data[j] == "" or row_data[j] == '\n'):
- bone_anim_data[i].append(None)
- else:
- bone_anim_data[i].append(float(row_data[j]))
- #
- #
-
- ####################################
- # write bone animation table to bone
- # only go through the keyframes
- # pointed to in csv_keyframe_numbers
- ####################################
- for k in range(len(csv_keyframe_numbers)):
- #
- # set frame on Blender
- scene.frame_set(csv_keyframe_numbers[k])
-
- # apply scale values X-Y-Z
- if (bone_anim_data[0][k] != None):
- pose_bone.scale[0] = bone_anim_data[0][k]
- pose_bone.keyframe_insert(data_path = 'scale', index = 0)
- if (bone_anim_data[1][k] != None):
- pose_bone.scale[1] = bone_anim_data[1][k]
- pose_bone.keyframe_insert(data_path = 'scale', index = 1)
- if (bone_anim_data[2][k] != None):
- pose_bone.scale[2] = bone_anim_data[2][k]
- pose_bone.keyframe_insert(data_path = 'scale', index = 2)
-
- # apply rotation values (converted to radians)
- if (bone_anim_data[3][k] != None):
- pose_bone.rotation_euler[0] = math.radians(bone_anim_data[3][k])
- pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 0)
- if (bone_anim_data[4][k] != None):
- pose_bone.rotation_euler[1] = math.radians(bone_anim_data[4][k])
- pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 1)
- if (bone_anim_data[5][k] != None):
- pose_bone.rotation_euler[2] = math.radians(bone_anim_data[5][k])
- pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 2)
-
- # apply translation values (divided by 100 because 1 GU is 100 meters)
- if (bone_anim_data[6][k] != None):
- pose_bone.location[0] = bone_anim_data[6][k] / 100
- pose_bone.keyframe_insert(data_path = 'location', index = 0)
- if (bone_anim_data[7][k] != None):
- pose_bone.location[1] = bone_anim_data[7][k] / 100
- pose_bone.keyframe_insert(data_path = 'location', index = 1)
- if (bone_anim_data[8][k] != None):
- pose_bone.location[2] = bone_anim_data[8][k] / 100
- pose_bone.keyframe_insert(data_path = 'location', index = 2)
- #
- #
- #
-
- ###########################################################################
- ###########################################################################
- # import type "rest pose"
- # in here I need to calculate the animation values for the reference system
- # defined by the bone's rest pose as the BCK CSVs store animation data of a
- # bone with respect to its parent. Conversion from a reference system to
- # another can be done easily with matrix multiplication but the rotation
- # data must be preprocessed first (angles that are outside the -180/+180
- # degree range are lost in the conversion process)
- ###########################################################################
- ###########################################################################
- else:
- #
- # read the CSV from the start again
- # to import the animation values
-
- f = open(filepath, 'r', encoding='utf-8')
-
- line = f.readline()
-
- # first line of the CSV
- csv_first_line = line.split(',')
- animation_length = int(csv_first_line[1])
-
- # get the animation last frame and set it in blender
- bpy.data.scenes["Scene"].frame_end = int(csv_first_line[1])
-
- # the next line comes with the notable keyframes used in the animation
- line = f.readline()
-
- # get all keyframe numbers
- csv_keyframe_numbers = re.findall('[0-9]+', line)
- csv_keyframe_numbers = [int(string) for string in csv_keyframe_numbers]
-
- ###############################################################
- # in the next loop starts the bone animation data table reading
- # loop through every bone
- ###############################################################
- for bone_number in range(bone_count):
-
- # get pose.bone and data.bone
- pose_bone = armature.pose.bones[bone_number]
- data_bone = armature.data.bones[bone_number]
- print("Reading/Writing animation for bone: %s" % (pose_bone.name))
-
- # set bone rotation mode to Extrinsic Euler XYZ
- pose_bone.rotation_mode = 'XYZ'
-
- # bone_anim_data will hold the bone frame animation (all frames)
- # data for a single bone from the CSV file as follows
-
- # 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 = [[], [], [], [], [], [], [], [], []]
-
- ###########################
- # read bone animation table
- ###########################
- for i in range(9):
- #
- line = f.readline()
- row_data = line.split(',')
-
- # if row_data's length is not equal to the length of
- # csv_keyframe_numbers + 3 fill it with empty strings to match said length
- while (len(row_data) != (len(csv_keyframe_numbers) + 3)):
- row_data.append("")
-
- ############################################################
- # copy the keyframe values from row_data into bone_anim_data
- # and make bone_anim_data hold all frame values for the bone
- ############################################################
-
- # k will be used to go through csv_keyframe_numbers
- # and also through row_data as its length is
- # (len(csv_keyframe_numbers) + 3)
- j = 0
- for k in range(animation_length + 1):
- #
- # Note: first 3 elements of row_data are skipped
- # example: Joint 0, Scale X:, Linear, ...
-
- # if the animation frame isn't a keyframe append None to
- # bone_anim_data[i] and go to the next frame
- if (k != csv_keyframe_numbers[j]):
- bone_anim_data[i].append(None)
- continue
-
- # if it is a keyframe advance to the next keyframe
- # on csv_keyframe_numbers
-
- # if there is no animation value for the bone on the
- # current keyframe data append None to said position
- # otherwise append the value
- if (row_data[j + 3] == "" or row_data[j + 3] == '\n'):
- bone_anim_data[i].append(None)
- else:
- bone_anim_data[i].append(float(row_data[j + 3]))
-
- # advance in row_data
- j = j + 1
- #
- #
-
- ########################################################
- # modify bone_anim_data rotation animation values
- # so that the angles on it are between -180/+180
- # to avoid visual animation data loss
- # convert_rot_anim_to_180() will be an external function
- # the process of converting the animation rotation on all
- # axises into the -180/180 degree range can create new
- # keyframes to the aniamtion and they will be appended
- # at the end of csv_keyframe_numbers so that those
- # keyframes are processed after the main ones are done
- ########################################################
-
- convert_rot_anim_to_180(bone_anim_data[3], csv_keyframe_numbers)
- convert_rot_anim_to_180(bone_anim_data[4], csv_keyframe_numbers)
- convert_rot_anim_to_180(bone_anim_data[5], csv_keyframe_numbers)
-
- ####################################
- # write bone animation table to bone
- # only go through the keyframes
- # pointed to in csv_keyframe_numbers
- ####################################
-
- # k is used to go through csv_keyframe_numbers
- # kf_num is used to go through bone_anim_data
- for k in range(len(csv_keyframe_numbers)):
- #
- # get keyframe number
- kf_num = csv_keyframe_numbers[k]
- # set keyframe on Blender
- scene.frame_set(kf_num)
-
- # - get animation data
- # - convert animation to rest pose reference system
-
- # find all 9 animation properties with linear interpolation if needed
- # (for now) the final goal is to only keep the keyframes from the BCK without
- # having to assign a keyframe for each frame of the animation in Blender
-
- # anim_temp will be used to hold the 9 animation
- # property data as it is read/calculated
-
- anim_temp = []
-
- ########################################################
- # find scale values in ignore rest pose reference system
- ########################################################
-
- #########
- # Scale X
- if (bone_anim_data[0][kf_num] != None):
- anim_temp.append(bone_anim_data[0][kf_num])
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[0], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear"))
-
- #########
- # Scale Y
- if (bone_anim_data[1][kf_num] != None):
- anim_temp.append(bone_anim_data[1][kf_num])
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[1], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear"))
-
- #########
- # Scale Z
- if (bone_anim_data[2][kf_num] != None):
- anim_temp.append(bone_anim_data[2][kf_num])
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[2], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear"))
-
- ########################################################################
- # find rotation values in ignore rest pose reference system (in radians)
- ########################################################################
-
- ############
- # Rotation X
- if (bone_anim_data[3][kf_num] != None):
- anim_temp.append(math.radians(bone_anim_data[3][kf_num]))
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[3], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(math.radians(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear")))
-
- ############
- # Rotation Y
- if (bone_anim_data[4][kf_num] != None):
- anim_temp.append(math.radians(bone_anim_data[4][kf_num]))
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[4], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(math.radians(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear")))
-
- ############
- # Rotation Z
- if (bone_anim_data[5][kf_num] != None):
- anim_temp.append(math.radians(bone_anim_data[5][kf_num]))
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[5], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(math.radians(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear")))
-
- ##############################################################
- # find translation values in ignore rest pose reference system
- # divided by 100 because 1 GU is 100 meters
- ##############################################################
-
- ###############
- # Translation X
- if (bone_anim_data[6][kf_num] != None):
- anim_temp.append(bone_anim_data[6][kf_num] / 100)
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[6], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear") / 100)
-
- ###############
- # Translation Y
- if (bone_anim_data[7][kf_num] != None):
- anim_temp.append(bone_anim_data[7][kf_num] / 100)
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[7], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear") / 100)
-
- ###############
- # Translation Z
- if (bone_anim_data[8][kf_num] != None):
- anim_temp.append(bone_anim_data[8][kf_num] / 100)
- else:
- # find left and right values (store in temp array)
- a = find_left_right(bone_anim_data[8], kf_num)
- # interpolate ^ [l_val_pos, l_val, r_val_pos, r_val]
- anim_temp.append(interpolate(a[0], a[1], a[2], a[3], kf_num, None, "linear") / 100)
-
- ###################################################
- # create the animation matrix related to the ignore
- # rest pose (irp) transformation matrix (T * R * S)
- ###################################################
-
- anim_irp_mat = calc_translation_matrix(anim_temp[6], anim_temp[7], anim_temp[8]) * calc_rotation_matrix(anim_temp[3], anim_temp[4], anim_temp[5]) * calc_scale_matrix(anim_temp[0], anim_temp[1], anim_temp[2])
-
- #########################################################
- # calculate the animation matrix related to the rest pose
- # rest pose matrix of the bone is in the data_bone
- # the first bone has no parent
- #########################################################
-
- if (bone_number == 0):
- anim_rp_mat = anim_irp_mat
- else:
- anim_rp_mat = (data_bone.parent.matrix_local.inverted() * data_bone.matrix_local).inverted() * anim_irp_mat
-
- #####################################
- # extract calculated animation values
- #####################################
-
- # Scaling
- bone_scale = anim_rp_mat.to_scale()
- # Rotation (Extrinsic Euler XYZ)
- bone_rotation = anim_rp_mat.to_euler('XYZ')
- # Translation
- bone_translation = anim_rp_mat.to_translation()
-
- ####################################################
- # apply the respective keyframe values to the bone
- # if any of the values on scale/rotation/translation
- # is different than None create a keyframe for all
- # XYZ axises on said scale/rotation/translation
- # animation property. Will keep the individual
- # keyframe assignment just in case it is needed in
- # a future program logic update
- ####################################################
-
- ########################
- # apply scale values XYZ
- if (bone_anim_data[0][kf_num] != None
- or
- bone_anim_data[1][kf_num] != None
- or
- bone_anim_data[2][kf_num] != None):
- pose_bone.scale[0] = bone_scale[0]
- pose_bone.keyframe_insert(data_path = 'scale', index = 0)
- pose_bone.scale[1] = bone_scale[1]
- pose_bone.keyframe_insert(data_path = 'scale', index = 1)
- pose_bone.scale[2] = bone_scale[2]
- pose_bone.keyframe_insert(data_path = 'scale', index = 2)
-
- ###########################
- # apply rotation values XYZ
- if (bone_anim_data[3][kf_num] != None
- or
- bone_anim_data[4][kf_num] != None
- or
- bone_anim_data[5][kf_num] != None):
- pose_bone.rotation_euler[0] = bone_rotation[0]
- pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 0)
- pose_bone.rotation_euler[1] = bone_rotation[1]
- pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 1)
- pose_bone.rotation_euler[2] = bone_rotation[2]
- pose_bone.keyframe_insert(data_path = 'rotation_euler', index = 2)
-
- ##############################
- # apply translation values XYZ
- if (bone_anim_data[6][kf_num] != None
- or
- bone_anim_data[7][kf_num] != None
- or
- bone_anim_data[8][kf_num] != None):
- pose_bone.location[0] = bone_translation[0]
- pose_bone.keyframe_insert(data_path = 'location', index = 0)
- pose_bone.location[1] = bone_translation[1]
- pose_bone.keyframe_insert(data_path = 'location', index = 1)
- pose_bone.location[2] = bone_translation[2]
- pose_bone.keyframe_insert(data_path = 'location', index = 2)
- #
- #
- #
-
- ######################################
- # make all interpolation curves linear
- # loop through each curve and through
- # each of the curve's keyframe
- curves = bpy.context.active_object.animation_data.action.fcurves
- for curve in curves:
- for keyframe in curve.keyframe_points:
- keyframe.interpolation = 'LINEAR'
-
- # importer end
- return {'FINISHED'}
- #
- #################################################
- # Stuff down is for the menu appending
- # of the importer 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 Import_CSV_BCK(Operator, ExportHelper):
- #
- """Import a CSV file from J3D Animation Editor of the BCK animation type. Armature to which the animation must be applied must be the only armature in scene and must be the correct one for the animation"""
- bl_idname = "import_scene.csv_bck"
- bl_label = "Import CSV of BCK (from J3D Anim Editor)"
- filename_ext = ".csv"
- filter_glob = StringProperty(
- default="*.csv",
- options={'HIDDEN'},
- maxlen=255,
- )
-
- import_type = BoolProperty( name = "Ignore Rest Pose",
- description = "Ignore all bone's rest poses and apply animations as SMG does. Modifies the bone's original rest pose.",
- default = False,
- )
-
- def execute(self, context):
- return read_csv_bck(context, self.filepath, self.import_type)
- #
- def menu_import_csv_bck(self, context):
- self.layout.operator(Import_CSV_BCK.bl_idname, text="CSV of BCK (from J3D Animation Editor) (.csv)")
- bpy.utils.register_class(Import_CSV_BCK)
- bpy.types.INFO_MT_file_import.append(menu_import_csv_bck)
- # test call
- bpy.ops.import_scene.csv_bck('INVOKE_DEFAULT')
|