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