1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
|
'''
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')
|