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