csv_anim_bck_export.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. '''
  2. CSV exporter for the BCK animation type
  3. CSV to be used with the j3d animation editor program
  4. '''
  5. import bpy, math, re
  6. from mathutils import Matrix
  7. from .my_functions import *
  8. # Notes (AFAIK):
  9. # - position/rotation/scaling values of a bone in an animation
  10. # must be the ones that are relative to its parent bone
  11. # - Extrinsic Euler XYZ system is the one being used for rotation values.
  12. # - all animations must start in Frame 0 (starting frame).
  13. ######################################
  14. # write_csv_bck (MAIN FUNCTION)
  15. # function to write a CSV file with
  16. # data for the BCK animation type
  17. # to be used with J3D Animation Editor
  18. ######################################
  19. def write_csv_bck(context, filepath, loop_mode, export_mode):
  20. # always needed
  21. scene = bpy.context.scene
  22. # loop mode variable declaration
  23. loop_number = 0
  24. if (loop_mode == "OPT_A"):
  25. loop_number = 0
  26. elif (loop_mode == "OPT_B"):
  27. loop_number = 1
  28. elif (loop_mode == "OPT_C"):
  29. loop_number = 2
  30. elif (loop_mode == "OPT_D"):
  31. loop_number = 3
  32. elif (loop_mode == "OPT_E"):
  33. loop_number = 4
  34. # if nothing is selected end the exporter
  35. if (scene.objects.active == None
  36. or
  37. scene.objects.active.type != 'ARMATURE'):
  38. error_string = "No Armature object selected. Select one and try again."
  39. print("\n### ERROR ###\n" + error_string + "\n### ERROR ###\n")
  40. show_message(error_string, "Error exporting collada file", 'ERROR')
  41. return {'FINISHED'}
  42. # get armature object
  43. armature = scene.objects.active
  44. print()
  45. print("###############")
  46. print("Armature found: %s" % armature.name)
  47. print()
  48. print("Creating CSV file (for BCK)...")
  49. # open file to write
  50. f = open(filepath, 'w', encoding='utf-8')
  51. print("Writing CSV header...")
  52. ###############################################
  53. # write the "header" of the CSV animation table
  54. ###############################################
  55. animation_length = scene.frame_end + 1
  56. f.write("%d,%d,%d,%s" % (loop_number, animation_length - 1, 0, ".bck"))
  57. f.write("\n")
  58. f.write("Bone Name,Tangent Interpolation,Component")
  59. for frame in range(animation_length):
  60. f.write(",Frame %d" % (frame))
  61. f.write("\n")
  62. ####################################################
  63. # get a detailed list of the keyframes used in the
  64. # animation and the bones related to those keyframes
  65. #
  66. # bone_kf_data will hold the bone and keyframe position as follows:
  67. #
  68. # bone name --> name of the animated bone
  69. # anim property --> Scale/Rotation/Translation
  70. # axis --> 0/1/2 --> X/Y/Z
  71. #
  72. # a bone name, anim property, axis, kf 1 pos, kf 2 pos, ...
  73. # another bone name, anim property, axis, kf 3 pos, kf 4 pos, ...
  74. # ...
  75. #
  76. # Note that each animation property can have different keyframe positions in time
  77. bone_kf_data = []
  78. for i in range(len(armature.animation_data.action.fcurves)):
  79. #
  80. # get curve
  81. curve = armature.animation_data.action.fcurves[i]
  82. # add new row for given curve in bone_kf_data
  83. bone_kf_data.append([])
  84. ################################################################
  85. # append bone name, animation property and axis related to curve
  86. # bone name
  87. bone_kf_data[i].append(re.search('\["(.+?)"\]', curve.data_path).group(1))
  88. # anim property
  89. if (curve.data_path.find("scale") + 1):
  90. bone_kf_data[i].append("scale")
  91. elif (curve.data_path.find("rotation_euler") + 1):
  92. bone_kf_data[i].append("rotation")
  93. elif (curve.data_path.find("location") + 1):
  94. bone_kf_data[i].append("translation")
  95. # axis
  96. if (curve.array_index == 0):
  97. bone_kf_data[i].append("x")
  98. elif (curve.array_index == 1):
  99. bone_kf_data[i].append("y")
  100. elif (curve.array_index == 2):
  101. bone_kf_data[i].append("z")
  102. # store keyframe data
  103. for j in range(len(curve.keyframe_points)):
  104. keyframe = curve.keyframe_points[j]
  105. bone_kf_data[i].append(int(keyframe.co[0])) # keyframe pos is an integer (frame)
  106. #
  107. # print bone_kf_data to terminal
  108. print()
  109. for row in bone_kf_data:
  110. print(row)
  111. ############################################################
  112. # get the armature bones that contain 2 or more keyframes
  113. # defined (read bone_kf_data) and store at the side of
  114. # each bone name the last keyframe position of its animation
  115. #
  116. # bone_last_kf_pos will contain data as follows:
  117. #
  118. # bone1 name, last bone1 keyframe position, bone2 name, last bone2 keyframe position, ...
  119. #
  120. bone_last_kf_pos = []
  121. for i in range(len(bone_kf_data)):
  122. #
  123. if (len(bone_last_kf_pos) != 0):
  124. # if the last bone name on bone_last_kf_pos is the same as the
  125. # one on bone_kf_data[i][0] go to the column element
  126. if (bone_last_kf_pos[len(bone_last_kf_pos) - 2] == bone_kf_data[i][0]):
  127. # check if the keyframe position of the bone is larger and store the larger value
  128. if (bone_last_kf_pos[len(bone_last_kf_pos) - 1] < bone_kf_data[i][len(bone_kf_data[i]) - 1]):
  129. bone_last_kf_pos[len(bone_last_kf_pos) - 1] = bone_kf_data[i][len(bone_kf_data[i]) - 1]
  130. continue
  131. # bone animation row has more than 1 keyframe on an anim property
  132. # append bone name and last keyframe position in time
  133. if (len(bone_kf_data[i]) > 4):
  134. bone_last_kf_pos.append(bone_kf_data[i][0])
  135. bone_last_kf_pos.append(bone_kf_data[i][len(bone_kf_data[i]) - 1])
  136. #
  137. # print bones_with_kf to terminal
  138. print()
  139. print(bone_last_kf_pos)
  140. print()
  141. ###################################################################
  142. # read animation data for one bone then dump the animation data for
  143. # said bone on the CSV file
  144. ###################################################################
  145. ########################
  146. # loop through each bone
  147. for i in range(len(armature.pose.bones)):
  148. # get bone
  149. bone = armature.pose.bones[i]
  150. # print bone going to be processed on terminal
  151. print("Processing animation for bone: %s" % (bone.name))
  152. # store the animation data scale/rotation/translation X/Y/Z on
  153. # bone_anim_data which will have 9 rows and each row will be the
  154. # length of the animation + 1 (including Frame 0 values)
  155. # row 1 --> Scale X
  156. # row 2 --> Scale Y
  157. # row 3 --> Scale Z
  158. # row 4 --> Rotation X
  159. # row 5 --> Rotation Y
  160. # row 6 --> Rotation Z
  161. # row 7 --> Translation X
  162. # row 8 --> Translation Y
  163. # row 9 --> Translation Z
  164. bone_anim_data = [[], [], [], [], [], [], [], [], []]
  165. ###############################################################
  166. # fill bone_anim_data so the rows have the animation length + 1
  167. for j in range(len(bone_anim_data)):
  168. for k in range(animation_length):
  169. bone_anim_data[j].append("")
  170. ###########################################################################
  171. # check if the bone has 2 or more keyframes (bone has an animation)
  172. # and store the animation length of that bone (read bone_last_kf_pos,
  173. # first frame counts on the animation length!, to use later)
  174. bone_has_anim = False
  175. anim_length_for_bone = 0 + 1
  176. for j in range(int(len(bone_last_kf_pos) / 2)):
  177. #
  178. if (bone_last_kf_pos[2 * j] == bone.name):
  179. bone_has_anim = True
  180. anim_length_for_bone = bone_last_kf_pos[(2 * j) + 1] + 1
  181. break
  182. #
  183. print("Bone has animation? ", end = "")
  184. print(bone_has_anim)
  185. print("Bone animation length: %d" % (anim_length_for_bone))
  186. ###################################################################
  187. # if export mode is "only keyframes" define current_bone_kf_data
  188. # and get from it all the bone keyframes in each animation property
  189. # keyframes on bone_kfs won't necessarily be on order
  190. # (do it on bones that have animation)
  191. current_bone_kf_data = []
  192. bone_kfs = []
  193. if (export_mode == "OPT_B" and bone_has_anim == True):
  194. #
  195. ##########################################################
  196. # store the rows of bone_kf_data in which the current bone
  197. # appears in current_bone_kf_data (to use later)
  198. current_bone_kf_data = []
  199. for j in range(len(bone_kf_data)):
  200. if (bone_kf_data[j][0] == bone.name):
  201. current_bone_kf_data.append(bone_kf_data[j])
  202. #################################################
  203. # read current_bone_kf_data to get all the unique
  204. # keyframe positions of the bone animation
  205. for j in range(len(current_bone_kf_data)):
  206. #
  207. # store the keyframes found on the first row
  208. # of current_bone_kf_data in bone_kfs
  209. if (j == 0):
  210. for k in range(len(current_bone_kf_data[0])):
  211. # make k equal to 3
  212. if (k < 3):
  213. continue
  214. bone_kfs.append(current_bone_kf_data[j][k])
  215. # other rows
  216. for k in range(len(current_bone_kf_data[j])):
  217. #
  218. if (k < 3): # make k equal to 3
  219. continue
  220. # loop through bone_kfs to check for new keyframe positions
  221. keyframe_exists = False
  222. for l in range(len(bone_kfs)):
  223. if (current_bone_kf_data[j][k] == bone_kfs[l]):
  224. keyframe_exists = True
  225. break
  226. if (keyframe_exists == False):
  227. bone_kfs.append(current_bone_kf_data[j][k])
  228. #
  229. #
  230. # ~ print(current_bone_kf_data)
  231. print("Bone's keyframes position:")
  232. print(bone_kfs)
  233. #
  234. print()
  235. # if bone_has_anim equals False only store its first frame animation values
  236. # if bone_has_anim equals True store (depending on the export mode) its
  237. # keyframes/all frame animation values (until the last keyframe)
  238. ########################################
  239. # loop through the animation of the bone
  240. # k is used to go through bone_kfs if
  241. # export mode is "only keyframes"
  242. k = 0
  243. for j in range(anim_length_for_bone):
  244. #
  245. ##########################################
  246. # set scene frame depending on export mode
  247. if (export_mode == "OPT_B" and bone_has_anim == True and j != 0):
  248. #
  249. # check k in case bone_kfs end is reached
  250. if (k == len(bone_kfs)):
  251. break
  252. frame = bone_kfs[k]
  253. scene.frame_set(frame)
  254. k = k + 1
  255. #
  256. else:
  257. #
  258. frame = j
  259. scene.frame_set(frame)
  260. #
  261. # first bone must be the outermost bone and therefore it
  262. # has no parent (batman moment, sorry batman u epik >:])
  263. if (i == 0):
  264. # -90 degree rotation matrix
  265. rot_mat = calc_rotation_matrix(math.radians(-90), 0, 0)
  266. # the first bone has to be rotated -90 degrees
  267. # on X to get correct animation values for it
  268. bone_rel_to_parent_mat = rot_mat * bone.matrix
  269. else: # for any other bone
  270. bone_rel_to_parent_mat = bone.parent.matrix.inverted() * bone.matrix
  271. ##########################################
  272. # extract bone_rel_to_parent_mat anim data
  273. # bone scaling
  274. bone_scale = bone_rel_to_parent_mat.to_scale()
  275. # bone rotation (stored in radians, extrinsic XYZ Euler)
  276. bone_rotation = bone_rel_to_parent_mat.to_euler("XYZ")
  277. # bone translation (multiplied by 100 because 1 GU is 100 meters)
  278. bone_translation = 100 * bone_rel_to_parent_mat.to_translation()
  279. ##########################################################
  280. # store frame animation values of bone into bone_anim_data
  281. # scaling data
  282. bone_anim_data[0][frame] = round(bone_scale[0], 2)
  283. bone_anim_data[1][frame] = round(bone_scale[1], 2)
  284. bone_anim_data[2][frame] = round(bone_scale[2], 2)
  285. # rotation data (must be in degrees!)
  286. bone_anim_data[3][frame] = round(math.degrees(bone_rotation[0]), 2)
  287. bone_anim_data[4][frame] = round(math.degrees(bone_rotation[1]), 2)
  288. bone_anim_data[5][frame] = round(math.degrees(bone_rotation[2]), 2)
  289. # position data
  290. bone_anim_data[6][frame] = round(bone_translation[0], 2)
  291. bone_anim_data[7][frame] = round(bone_translation[1], 2)
  292. bone_anim_data[8][frame] = round(bone_translation[2], 2)
  293. # ^ a lot of values can be repeated in each row.
  294. # When writing the animation data to the CSV is when
  295. # an optimization method will be done
  296. #
  297. # ~ for row in bone_anim_data:
  298. # ~ print(row)
  299. # ~ print()
  300. ####################################
  301. # write bone animation data into CSV
  302. # read bone_anim_data
  303. ####################################
  304. for j in range(9):
  305. #
  306. # first 3 rows are scaling data
  307. # the next 3 rows are rotation data
  308. # the final 3 rows are translation data
  309. # the start of the first row for a bone
  310. # animation data (Scale X) contains the bone name
  311. if (j == 0):
  312. f.write("%s,Linear,Scale X:" % (bone.name))
  313. elif(j == 1):
  314. f.write(",Linear,Scale Y:")
  315. elif(j == 2):
  316. f.write(",Linear,Scale Z:")
  317. elif(j == 3):
  318. f.write(",Linear,Rotation X:")
  319. elif(j == 4):
  320. f.write(",Linear,Rotation Y:")
  321. elif(j == 5):
  322. f.write(",Linear,Rotation Z:")
  323. elif(j == 6):
  324. f.write(",Linear,Translation X:")
  325. elif(j == 7):
  326. f.write(",Linear,Translation Y:")
  327. elif(j == 8):
  328. f.write(",Linear,Translation Z:")
  329. ###################################
  330. # write animation row data
  331. # will print values with 2 decimals
  332. ###################################
  333. for k in range(animation_length):
  334. #
  335. # get current animation value
  336. current_value = bone_anim_data[j][k]
  337. # write the first value from row
  338. if (k == 0):
  339. f.write(",%.2f" % (current_value))
  340. # compare old_value with current_value. if equal leave blank the
  341. # animation frame value otherwise write said value. This is done
  342. # to avoid repeating the same number each time (to save file size)
  343. elif (old_value == current_value or current_value == ""):
  344. f.write(",")
  345. else:
  346. f.write(",%.2f" % (current_value))
  347. # if the end of a row is reached write a newline char to the line
  348. if (k == (animation_length - 1)):
  349. f.write("\n")
  350. # store old animation value for the next loop
  351. if (current_value != ""):
  352. old_value = current_value
  353. #
  354. #
  355. #
  356. # exporter end
  357. return {'FINISHED'}
  358. #################################################
  359. # Stuff down is for the menu appending
  360. # of the exporter to work plus some setting stuff
  361. # comes from a Blender importer template
  362. #################################################
  363. from bpy_extras.io_utils import ExportHelper
  364. from bpy.props import StringProperty, BoolProperty, EnumProperty
  365. from bpy.types import Operator
  366. class Export_CSV_BCK(Operator, ExportHelper):
  367. #
  368. """Save a CSV file for BCK conversion (to use with J3D Animation Editor)"""
  369. bl_idname = "export_scene.csv_bck"
  370. bl_label = "Export CSV (for BCK)"
  371. filename_ext = ".csv"
  372. filter_glob = StringProperty(
  373. default="*.csv",
  374. options={'HIDDEN'},
  375. maxlen=255,
  376. )
  377. loop_mode = EnumProperty(
  378. name="Loop Mode",
  379. description="Choose the loop mode for the animation",
  380. items=( ('OPT_A', "Once", "Play the animation once and stop at the last frame"),
  381. ('OPT_B', "Once and Reset", "Play the animation once and stop at the first frame"),
  382. ('OPT_C', "Loop", "Loop the animation infinitely"),
  383. ('OPT_D', "Mirrored Once", "Play the animation forwards and then backwards once. Stops at the first frame"),
  384. ('OPT_E', "Mirrored Loop", "Play the animation forwards and then backwards infinitely")
  385. ), default='OPT_A'
  386. )
  387. export_mode = EnumProperty(
  388. name="Export Mode",
  389. description="Choose the method used to export the model animation data",
  390. items=( ('OPT_A', "All Frames", "Export animation values for each frame of the animation. Slow, higher CSV file size, more accurate animation"),
  391. ('OPT_B', "Only Keyframes", "Only export the animation keyframe values of each bone. Fast, lower CSV file size, least accurate animation"),
  392. ), default='OPT_A'
  393. )
  394. def execute(self, context):
  395. return write_csv_bck(context, self.filepath, self.loop_mode, self.export_mode)
  396. #
  397. # Only needed if you want to add into a dynamic menu
  398. def menu_export_csv_bck(self, context):
  399. self.layout.operator(Export_CSV_BCK.bl_idname, text="CSV Animation Table (for BCK) (.csv)")
  400. bpy.utils.register_class(Export_CSV_BCK)
  401. bpy.types.INFO_MT_file_export.append(menu_export_csv_bck)
  402. # test call
  403. bpy.ops.export_scene.csv_bck('INVOKE_DEFAULT')