LawMixer

c

Feb 29th, 2024
13
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 31.07 KB | None | 0 0
  1. ###
  2. # Copyright 2018 Den_S/@DennisRBLX
  3. #
  4. # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  5. #
  6. # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  7. #
  8. # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  9. ###
  10. #
  11. # Rbx Animations Blender Addon
  12. # Written by Den_S/@DennisRBLX
  13. # Refer to https://devforum.roblox.com/t/blender-rig-exporter-animation-importer/34729 for usage instructions
  14. #
  15. # For your information:
  16. # Armature is assumed to have the identity matrix(!!!)
  17. # When creating a rig, bones are first created in a way they were in the original rig data,
  18. # the resulting matrices are stored as base matrices.
  19. # Then, bone tails are moved to be in a more intuitive position (helps IK etc too)
  20. # This transformation is thus undone when exporting
  21. # Blender also uses a Z-up/-Y-forward coord system, so this results in more transformations
  22. # Transform <=> Original **world space** CFrame, should match the associate mesh base matrix, Transform1 <=> C1
  23. # The meshes are imported in a certain order. Mesh names are restored using attached metadata.
  24. # Rig data is also encoded in this metdata.
  25. #
  26. # Communication:
  27. # To blender: A bunch of extra meshes whose names encode metadata (they are numbered, the contents are together encoded in base32)
  28. # From blender: Base64-encoded string (after compression)
  29. #
  30.  
  31. import bpy, math, re, json, bpy_extras
  32. from itertools import chain
  33. from mathutils import Vector, Matrix
  34. import zlib
  35. import base64
  36. from bpy_extras.io_utils import ImportHelper
  37. from bpy.props import *
  38.  
  39. transform_to_blender = bpy_extras.io_utils.axis_conversion(from_forward='Z', from_up='Y', to_forward='-Y', to_up='Z').to_4x4() # transformation matrix from Y-up to Z-up
  40. identity_cf = [0,0,0,1,0,0,0,1,0,0,0,1] # identity CF components matrix
  41. cf_round = False # round cframes before exporting? (reduce size)
  42. cf_round_fac = 4 # round to how many decimals?
  43.  
  44. # y-up cf -> y-up mat
  45. def cf_to_mat(cf):
  46. mat = Matrix.Translation((cf[0], cf[1], cf[2]))
  47. mat[0][0:3] = (cf[3], cf[4], cf[5])
  48. mat[1][0:3] = (cf[6], cf[7], cf[8])
  49. mat[2][0:3] = (cf[9], cf[10], cf[11])
  50. return mat
  51.  
  52. # y-up mat -> y-up cf
  53. def mat_to_cf(mat):
  54. r_mat = [mat[0][3], mat[1][3], mat[2][3],
  55. mat[0][0], mat[0][1], mat[0][2],
  56. mat[1][0], mat[1][1], mat[1][2],
  57. mat[2][0], mat[2][1], mat[2][2]
  58. ]
  59. return r_mat
  60.  
  61. # links the passed object to the bone with the transformation equal to the current(!) transformation between the bone and object
  62. def link_object_to_bone_rigid(obj, ao, bone):
  63. # remove existing
  64. for constraint in [c for c in obj.constraints if c.type == 'CHILD_OF']:
  65. obj.constraints.remove(constraint)
  66.  
  67. # create new
  68. constraint = obj.constraints.new(type = 'CHILD_OF')
  69. constraint.target = ao
  70. constraint.subtarget = bone.name
  71. constraint.inverse_matrix = (ao.matrix_world @ bone.matrix).inverted()
  72.  
  73. # serializes the current bone state to a dict
  74. def serialize_animation_state(ao):
  75. state = {}
  76. for bone in ao.pose.bones:
  77. if 'is_transformable' in bone.bone:
  78. # original matrices, straight from the import cfs
  79. # this is always the true baseline
  80. orig_mat = Matrix(bone.bone['transform'])
  81. orig_mat_tr1 = Matrix(bone.bone['transform1'])
  82. parent_orig_mat = Matrix(bone.parent.bone['transform'])
  83. parent_orig_mat_tr1 = Matrix(bone.parent.bone['transform1'])
  84.  
  85. # get the bone neutral transform
  86. extr_transform = Matrix(bone.bone['nicetransform']).inverted()
  87. parent_extr_transform = Matrix(bone.parent.bone['nicetransform']).inverted()
  88.  
  89. # z-up -> y-up transform matrix
  90. back_trans = transform_to_blender.inverted()
  91.  
  92. # get the real bone transform
  93. cur_obj_transform = back_trans @ (bone.matrix @ extr_transform)
  94. parent_obj_transform = back_trans @ (bone.parent.matrix @ parent_extr_transform)
  95.  
  96. # compute neutrals after applying C1/transform1
  97. orig_base_mat = back_trans @ (orig_mat @ orig_mat_tr1)
  98. parent_orig_base_mat = back_trans @ (parent_orig_mat @ parent_orig_mat_tr1)
  99.  
  100. # compute y-up bone transform (transformation between C0 and C1)
  101. orig_transform = parent_orig_base_mat.inverted() @ orig_base_mat
  102. cur_transform = parent_obj_transform.inverted() @ cur_obj_transform
  103. bone_transform = orig_transform.inverted() @ cur_transform
  104.  
  105. statel = mat_to_cf(bone_transform)
  106. if cf_round:
  107. statel = list(map(lambda x: round(x, cf_round_fac), statel)) # compresses result
  108.  
  109. # flatten, compresses the resulting json too
  110. for i in range(len(statel)):
  111. if int(statel[i]) == statel[i]:
  112. statel[i] = int(statel[i])
  113.  
  114. # only store if not identity, compresses the resulting json
  115. if statel != identity_cf:
  116. state[bone.name] = statel
  117.  
  118. return state
  119.  
  120. # removes all IK stuff from a bone
  121. def remove_ik_config(ao, tail_bone):
  122. to_clear = []
  123. for constraint in [c for c in tail_bone.constraints if c.type == 'IK']:
  124. if constraint.target and constraint.subtarget:
  125. to_clear.append((constraint.target, constraint.subtarget))
  126. if constraint.pole_target and constraint.pole_subtarget:
  127. to_clear.append((constraint.pole_target, constraint.pole_subtarget))
  128.  
  129. tail_bone.constraints.remove(constraint)
  130.  
  131. bpy.ops.object.mode_set(mode='EDIT')
  132.  
  133. for util_bone in to_clear:
  134. util_bone[0].data.edit_bones.remove(util_bone[0].data.edit_bones[util_bone[1]])
  135.  
  136. bpy.ops.object.mode_set(mode='POSE')
  137.  
  138. # created IK bones and constraints for a given chain
  139. def create_ik_config(ao, tail_bone, chain_count, create_pose_bone, lock_tail):
  140. lock_tail = False # not implemented
  141.  
  142. bpy.ops.object.mode_set(mode='EDIT')
  143.  
  144. amt = ao.data
  145. ik_target_bone = tail_bone if not lock_tail else tail_bone.parent
  146.  
  147. ik_target_bone_name = ik_target_bone.name
  148. ik_name = "{}-IKTarget".format(ik_target_bone_name)
  149. ik_name_pole = "{}-IKPole".format(ik_target_bone_name)
  150.  
  151. ik_bone = amt.edit_bones.new(ik_name)
  152. ik_bone.head = ik_target_bone.tail
  153. ik_bone.tail = (Matrix.Translation(ik_bone.head) @ ik_target_bone.matrix.to_3x3().to_4x4()) @ Vector((0, 0, -.5))
  154. ik_bone.bbone_x *= 1.5
  155. ik_bone.bbone_z *= 1.5
  156.  
  157. ik_pole = None
  158. if create_pose_bone:
  159. pos_low = tail_bone.tail
  160. pos_high = tail_bone.parent_recursive[chain_count-2].head
  161. pos_avg = (pos_low + pos_high) * .5
  162. dist = (pos_low - pos_high).length
  163.  
  164. basal_bone = tail_bone
  165. for i in range(1, chain_count):
  166. if basal_bone.parent:
  167. basal_bone = basal_bone.parent
  168.  
  169. basal_mat = basal_bone.bone.matrix_local
  170.  
  171. ik_pole = amt.edit_bones.new(ik_name_pole)
  172. ik_pole.head = basal_mat @ Vector((0, 0, dist * -.25))
  173. ik_pole.tail = basal_mat @ Vector((0, 0, dist * -.25 - .3))
  174. ik_pole.bbone_x *= .5
  175. ik_pole.bbone_z *= .5
  176.  
  177. bpy.ops.object.mode_set(mode='POSE')
  178.  
  179. pose_bone = ao.pose.bones[ik_target_bone_name]
  180. constraint = pose_bone.constraints.new(type = 'IK')
  181. constraint.target = ao
  182. constraint.subtarget = ik_name
  183. if create_pose_bone:
  184. constraint.pole_target = ao
  185. constraint.pole_subtarget = ik_name_pole
  186. constraint.pole_angle = math.pi * -.5
  187. constraint.chain_count = chain_count
  188.  
  189. # loads a (child) rig bone
  190. def load_rigbone(ao, rigging_type, rigsubdef, parent_bone):
  191. amt = ao.data
  192. bone = amt.edit_bones.new(rigsubdef['jname'])
  193.  
  194. mat = cf_to_mat(rigsubdef['transform'])
  195. bone["transform"] = mat
  196. bone_dir = (transform_to_blender @ mat).to_3x3().to_4x4() @ Vector((0, 0, 1))
  197.  
  198. if 'jointtransform0' not in rigsubdef:
  199. # Rig root
  200. bone.head = (transform_to_blender @ mat).to_translation()
  201. bone.tail = (transform_to_blender @ mat) @ Vector((0, .01, 0))
  202. bone["transform0"] = Matrix()
  203. bone["transform1"] = Matrix()
  204. bone['nicetransform'] = Matrix()
  205. bone.align_roll(bone_dir)
  206. bone.hide_select = True
  207. pre_mat = bone.matrix
  208. o_trans = transform_to_blender @ mat
  209. else:
  210. mat0 = cf_to_mat(rigsubdef['jointtransform0'])
  211. mat1 = cf_to_mat(rigsubdef['jointtransform1'])
  212. bone["transform0"] = mat0
  213. bone["transform1"] = mat1
  214. bone["is_transformable"] = True
  215.  
  216. bone.parent = parent_bone
  217. o_trans = transform_to_blender @ (mat @ mat1)
  218. bone.head = o_trans.to_translation()
  219. real_tail = o_trans @ Vector((0, .25, 0))
  220.  
  221. neutral_pos = (transform_to_blender @ mat).to_translation()
  222. bone.tail = real_tail
  223. bone.align_roll(bone_dir)
  224.  
  225. # store neutral matrix
  226. pre_mat = bone.matrix
  227.  
  228. if rigging_type != 'RAW': # If so, apply some transform
  229. if len(rigsubdef['children']) == 1:
  230. nextmat = cf_to_mat(rigsubdef['children'][0]['transform'])
  231. nextmat1 = cf_to_mat(rigsubdef['children'][0]['jointtransform1'])
  232. next_joint_pos = (transform_to_blender @ (nextmat @ nextmat1)).to_translation()
  233.  
  234. if rigging_type == 'CONNECT': # Instantly connect
  235. bone.tail = next_joint_pos
  236. else:
  237. axis = 'y'
  238. if rigging_type == 'LOCAL_AXIS_EXTEND': # Allow non-Y too
  239. invtrf = pre_mat.inverted() * next_joint_pos
  240. bestdist = abs(invtrf.y)
  241. for paxis in ['x', 'z']:
  242. dist = abs(getattr(invtrf, paxis))
  243. if dist > bestdist:
  244. bestdist = dist
  245. axis = paxis
  246.  
  247. next_connect_to_parent = True
  248.  
  249. ppd_nr_dir = real_tail - bone.head
  250. ppd_nr_dir.normalize()
  251. proj = ppd_nr_dir.dot(next_joint_pos - bone.head)
  252. vis_world_root = ppd_nr_dir * proj
  253. bone.tail = bone.head + vis_world_root
  254.  
  255. else:
  256. bone.tail = bone.head + (bone.head - neutral_pos) * -2
  257.  
  258. if (bone.tail - bone.head).length < .01:
  259. # just reset, no "nice" config can be found
  260. bone.tail = real_tail
  261. bone.align_roll(bone_dir)
  262.  
  263. # fix roll
  264. bone.align_roll(bone_dir)
  265.  
  266. post_mat = bone.matrix
  267.  
  268. # this value stores the transform between the "proper" matrix and the "nice" matrix where bones are oriented in a more friendly way
  269. bone['nicetransform'] = o_trans.inverted() @ post_mat
  270.  
  271. # link objects to bone
  272. for aux in rigsubdef['aux']:
  273. if aux and aux in bpy.data.objects:
  274. obj = bpy.data.objects[aux]
  275. link_object_to_bone_rigid(obj, ao, bone)
  276.  
  277. # handle child bones
  278. for child in rigsubdef['children']:
  279. load_rigbone(ao, rigging_type, child, bone)
  280.  
  281. # renames parts to whatever the metadata defines, mostly just for user-friendlyness (not required)
  282. def autoname_parts(partnames, basename):
  283. indexmatcher = re.compile(basename + '(\d+)1(\.\d+)?', re.IGNORECASE)
  284. for object in bpy.data.objects:
  285. match = indexmatcher.match(object.name.lower())
  286. if match:
  287. index = int(match.group(1))
  288. object.name = partnames[index - 1]
  289.  
  290. # removes existing rig if it exists, then builds a new one using the stored metadata
  291. def create_rig(rigging_type):
  292. bpy.ops.object.mode_set(mode='OBJECT')
  293. if '__Rig' in bpy.data.objects:
  294. bpy.data.objects['__Rig'].select_set(True)
  295. bpy.ops.object.delete()
  296.  
  297. meta_loaded = json.loads(bpy.data.objects['__RigMeta']['RigMeta'])
  298.  
  299. bpy.ops.object.add(type='ARMATURE', enter_editmode=True, location=(0,0,0))
  300. ao = bpy.context.object
  301. ao.show_in_front = True
  302. ao.name = '__Rig'
  303. amt = ao.data
  304. amt.name = '__RigArm'
  305. amt.show_axes = True
  306. amt.show_names = True
  307.  
  308. bpy.ops.object.mode_set(mode='EDIT')
  309. load_rigbone(ao, rigging_type, meta_loaded['rig'], None)
  310.  
  311. bpy.ops.object.mode_set(mode='OBJECT')
  312.  
  313.  
  314. # export the entire animation to the clipboard (serialized), returns animation time
  315. def serialize():
  316. ao = bpy.data.objects['__Rig']
  317. ctx = bpy.context
  318. bake_jump = ctx.scene.frame_step
  319.  
  320. collected = []
  321. frames = ctx.scene.frame_end+1 - ctx.scene.frame_start
  322. cur_frame = ctx.scene.frame_current
  323. for i in range(ctx.scene.frame_start, ctx.scene.frame_end+1, bake_jump):
  324. ctx.scene.frame_set(i)
  325. bpy.context.evaluated_depsgraph_get().update()
  326.  
  327. state = serialize_animation_state(ao)
  328. collected.append({'t': (i - ctx.scene.frame_start) / ctx.scene.render.fps, 'kf': state})
  329.  
  330. ctx.scene.frame_set(cur_frame)
  331.  
  332. result = {
  333. 't': (frames-1) / ctx.scene.render.fps,
  334. 'kfs': collected
  335. }
  336.  
  337. return result
  338.  
  339. def copy_anim_state_bone(target, source, bone):
  340. # get transform mat of the bone in the source ao
  341. bpy.context.view_layer.objects.active = source
  342. t_mat = source.pose.bones[bone.name].matrix
  343.  
  344. bpy.context.view_layer.objects.active = target
  345.  
  346. # root bone transform is ignored, this is carried to child bones (keeps HRP static)
  347. if bone.parent:
  348. # apply transform w.r.t. the current parent bone transform
  349. r_mat = bone.bone.matrix_local
  350. p_mat = bone.parent.matrix
  351. p_r_mat = bone.parent.bone.matrix_local
  352. bone.matrix_basis = (p_r_mat.inverted() @ r_mat).inverted() @ (p_mat.inverted() @ t_mat)
  353.  
  354. # update properties (hacky :p)
  355. bpy.ops.anim.keyframe_insert()
  356. bpy.context.scene.frame_set(bpy.context.scene.frame_current)
  357.  
  358. # now apply on children (which use the parents transform)
  359. for ch in bone.children:
  360. copy_anim_state_bone(target, source, ch)
  361.  
  362. def copy_anim_state(target, source):
  363. # to pose mode
  364. bpy.context.view_layer.objects.active = source
  365. bpy.ops.object.mode_set(mode='POSE')
  366.  
  367. bpy.context.view_layer.objects.active = target
  368. bpy.ops.object.mode_set(mode='POSE')
  369.  
  370. root = target.pose.bones['HumanoidRootPart']
  371.  
  372. for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1):
  373. bpy.context.scene.frame_set(i)
  374. copy_anim_state_bone(target, source, root)
  375. bpy.ops.anim.keyframe_insert()
  376.  
  377. def prepare_for_kf_map():
  378. # clear anim data from target rig
  379. bpy.data.objects['__Rig'].animation_data_clear()
  380.  
  381. # select all pose bones in the target rig (simply generate kfs for everything)
  382. bpy.context.view_layer.objects.active = bpy.data.objects['__Rig']
  383. bpy.ops.object.mode_set(mode='POSE')
  384. for bone in bpy.data.objects['__Rig'].pose.bones:
  385. bone.bone.select = not not bone.parent
  386.  
  387. def get_mapping_error_bones(target, source):
  388. return [bone.name for bone in target.data.bones if bone.name not in [bone2.name for bone2 in source.data.bones]]
  389.  
  390. # apply ao transforms to the root PoseBone
  391. # + clear ao animation tracks (root only, not Pose anim data) + reset ao transform to identity
  392. def apply_ao_transform(ao):
  393. bpy.context.view_layer.objects.active = ao
  394. bpy.ops.object.mode_set(mode='POSE')
  395.  
  396. # select only root bones
  397. for bone in ao.pose.bones:
  398. bone.bone.select = not bone.parent
  399.  
  400. for root in [bone for bone in ao.pose.bones if not bone.parent]:
  401. # collect initial root matrices (if they do not exist yet, this will prevent interpolation from keyframes that are being set in the next loop)
  402. root_matrix_at = {}
  403. for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1):
  404. bpy.context.scene.frame_set(i)
  405. root_matrix_at[i] = root.matrix.copy()
  406.  
  407. # apply world space transform to root bone
  408. for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1):
  409. bpy.context.scene.frame_set(i)
  410. root.matrix = ao.matrix_world @ root_matrix_at[i]
  411. bpy.ops.anim.keyframe_insert()
  412.  
  413. # clear non-pose fcurves
  414. fcurves = ao.animation_data.action.fcurves
  415. for c in [c for c in fcurves if not c.data_path.startswith('pose')]:
  416. fcurves.remove(c)
  417.  
  418. # reset ao transform
  419. ao.matrix_basis = Matrix.Identity(4)
  420. bpy.context.evaluated_depsgraph_get().update()
  421.  
  422. ## UI/OPERATOR STUFF ##
  423.  
  424. class OBJECT_OT_ImportModel(bpy.types.Operator, ImportHelper):
  425. bl_label = "Import rig data (.obj)"
  426. bl_idname = "object.rbxanims_importmodel"
  427. bl_description = "Import rig data (.obj)"
  428.  
  429. filename_ext = ".obj"
  430. filter_glob: bpy.props.StringProperty(default="*.obj", options={'HIDDEN'})
  431. filepath: bpy.props.StringProperty(name="File Path", maxlen=1024, default="")
  432.  
  433. def execute(self, context):
  434. # clear objects first
  435. for obj in bpy.data.objects:
  436. obj.select_set(obj.type == 'MESH' or obj.type == 'ARMATURE' or obj.name.startswith('__RigMeta'))
  437. bpy.ops.object.delete()
  438.  
  439. bpy.ops.wm.obj_import.obj(filepath=self.properties.filepath, use_split_groups=True)
  440.  
  441. # Extract meta...
  442. encodedmeta = ''
  443. partial = {}
  444. for obj in bpy.data.objects:
  445. match = re.search(r'^Meta(\d+)q1(.*?)q1\d*(\.\d+)?$', obj.name)
  446. if match:
  447. partial[int(match.group(1))] = match.group(2)
  448.  
  449. obj.select_set(not not match)
  450. bpy.ops.object.delete() # delete meta objects
  451.  
  452. for i in range(1, len(partial)+1):
  453. encodedmeta += partial[i]
  454. encodedmeta = encodedmeta.replace('0', '=')
  455. meta = base64.b32decode(encodedmeta, True).decode('utf-8')
  456.  
  457. # store meta in an empty
  458. bpy.ops.object.add(type='EMPTY', location=(0,0,0))
  459. ob = bpy.context.object
  460. ob.name = '__RigMeta'
  461. ob['RigMeta'] = meta
  462.  
  463. meta_loaded = json.loads(meta)
  464. autoname_parts(meta_loaded['parts'], meta_loaded['rigName'])
  465.  
  466. return {'FINISHED'}
  467.  
  468. def invoke(self, context, event):
  469. context.window_manager.fileselect_add(self)
  470. return {'RUNNING_MODAL'}
  471.  
  472. class OBJECT_OT_GenRig(bpy.types.Operator):
  473. bl_label = "Generate rig"
  474. bl_idname = "object.rbxanims_genrig"
  475. bl_description = "Generate rig"
  476.  
  477. pr_rigging_type: bpy.props.EnumProperty(items=[
  478. ('RAW', 'Nodes only', ''),
  479. ('LOCAL_AXIS_EXTEND', 'Local axis aligned bones', ''),
  480. ('LOCAL_YAXIS_EXTEND', 'Local Y-axis aligned bones', ''),
  481. ('CONNECT', 'Connect', '')
  482. ], name="Rigging type");
  483.  
  484. @classmethod
  485. def poll(cls, context):
  486. meta_obj = bpy.data.objects.get('__RigMeta')
  487. return meta_obj and 'RigMeta' in meta_obj
  488.  
  489. def execute(self, context):
  490. create_rig(self.pr_rigging_type)
  491. self.report({'INFO'}, "Rig rebuilt.")
  492. return {'FINISHED'}
  493.  
  494. def invoke(self, context, event):
  495. self.pr_rigging_type = 'LOCAL_YAXIS_EXTEND'
  496.  
  497. wm = context.window_manager
  498. return wm.invoke_props_dialog(self)
  499.  
  500. class OBJECT_OT_GenIK(bpy.types.Operator):
  501. bl_label = "Generate IK"
  502. bl_idname = "object.rbxanims_genik"
  503. bl_description = "Generate IK"
  504.  
  505. pr_chain_count: bpy.props.IntProperty(name = "Chain count (0 = to root)", min=0)
  506. pr_create_pose_bone: bpy.props.BoolProperty(name = "Create pose bone")
  507. pr_lock_tail_bone: bpy.props.BoolProperty(name = "Lock final bone orientation")
  508.  
  509. @classmethod
  510. def poll(cls, context):
  511. premise = context.active_object and context.active_object.mode == 'POSE'
  512. premise = premise and context.active_object and context.active_object.type == 'ARMATURE'
  513. return context.active_object and context.active_object.mode == 'POSE' and len([x for x in context.active_object.pose.bones if x.bone.select]) > 0
  514.  
  515. def execute(self, context):
  516.  
  517. to_apply = [b for b in context.active_object.pose.bones if b.bone.select]
  518.  
  519. for bone in to_apply:
  520. create_ik_config(context.active_object, bone, self.pr_chain_count, self.pr_create_pose_bone, self.pr_lock_tail_bone)
  521.  
  522. return {'FINISHED'}
  523.  
  524. def invoke(self, context, event):
  525. to_apply = [b for b in context.active_object.pose.bones if b.bone.select]
  526. if len(to_apply) == 0:
  527. return {'FINISHED'}
  528.  
  529. rec_chain_len = 1
  530. no_loop_mech = set()
  531. itr = to_apply[0].bone
  532. while itr and itr.parent and len(itr.parent.children) == 1 and itr not in no_loop_mech:
  533. rec_chain_len += 1
  534. no_loop_mech.add(itr)
  535. itr = itr.parent
  536.  
  537. self.pr_chain_count = rec_chain_len
  538. self.pr_create_pose_bone = False
  539. self.pr_lock_tail_bone = False
  540.  
  541. wm = context.window_manager
  542. return wm.invoke_props_dialog(self)
  543.  
  544. class OBJECT_OT_RemoveIK(bpy.types.Operator):
  545. bl_label = "Remove IK"
  546. bl_idname = "object.rbxanims_removeik"
  547. bl_description = "Remove IK"
  548.  
  549. @classmethod
  550. def poll(cls, context):
  551. premise = context.active_object and context.active_object.mode == 'POSE'
  552. premise = premise and context.active_object
  553. return context.active_object and context.active_object.mode == 'POSE' and len([x for x in context.active_object.pose.bones if x.bone.select]) > 0
  554.  
  555. def execute(self, context):
  556. to_apply = [b for b in context.active_object.pose.bones if b.bone.select]
  557.  
  558. for bone in to_apply:
  559. remove_ik_config(context.active_object, bone)
  560.  
  561. return {'FINISHED'}
  562.  
  563. class OBJECT_OT_ImportFbxAnimation(bpy.types.Operator, ImportHelper):
  564. bl_label = "Import animation data (.fbx)"
  565. bl_idname = "object.rbxanims_importfbxanimation"
  566. bl_description = "Import animation data (.fbx) --- FBX file should contain an armature, which will be mapped onto the generated rig by bone names."
  567.  
  568. filename_ext = ".fbx"
  569. filter_glob: bpy.props.StringProperty(default="*.fbx", options={'HIDDEN'})
  570. filepath: bpy.props.StringProperty(name="File Path", maxlen=1024, default="")
  571.  
  572. @classmethod
  573. def poll(cls, context):
  574. return bpy.data.objects.get('__Rig')
  575.  
  576. def execute(self, context):
  577. # check active keying set
  578. if not bpy.context.scene.keying_sets.active:
  579. self.report({'ERROR'}, 'There is no active keying set, this is required.')
  580. return {'FINISHED'}
  581.  
  582. # import and keep track of what is imported
  583. objnames_before_import = [x.name for x in bpy.data.objects]
  584. bpy.ops.import_scene.fbx(filepath=self.properties.filepath)
  585. objnames_imported = [x.name for x in bpy.data.objects if x.name not in objnames_before_import]
  586.  
  587. def clear_imported():
  588. bpy.ops.object.mode_set(mode='OBJECT')
  589. for obj in bpy.data.objects:
  590. obj.select_set(obj.name in objnames_imported)
  591. bpy.ops.object.delete()
  592.  
  593. # check that there's only 1 armature
  594. armatures_imported = [x for x in bpy.data.objects if x.type == 'ARMATURE' and x.name in objnames_imported]
  595. if len(armatures_imported) != 1:
  596. self.report({'ERROR'}, 'Imported file contains {:d} armatures, expected 1.'.format(len(armatures_imported)))
  597. clear_imported()
  598. return {'FINISHED'}
  599.  
  600. ao_imp = armatures_imported[0]
  601.  
  602. err_mappings = get_mapping_error_bones(bpy.data.objects['__Rig'], ao_imp)
  603. if len(err_mappings) > 0:
  604. self.report({'ERROR'}, 'Cannot map rig, the following bones are missing from the source rig: {}.'.format(', '.join(err_mappings)))
  605. clear_imported()
  606. return {'FINISHED'}
  607.  
  608. print(dir(bpy.context.scene))
  609. bpy.context.view_layer.objects.active = ao_imp
  610.  
  611. # check that the ao contains anim data
  612. if not ao_imp.animation_data or not ao_imp.animation_data.action or not ao_imp.animation_data.action.fcurves:
  613. self.report({'ERROR'}, 'Imported armature contains no animation data.')
  614. clear_imported()
  615. return {'FINISHED'}
  616.  
  617. # get keyframes + boundary timestamps
  618. fcurves = ao_imp.animation_data.action.fcurves
  619. kp_frames = []
  620. for key in fcurves:
  621. kp_frames += [kp.co.x for kp in key.keyframe_points]
  622. if len(kp_frames) <= 0:
  623. self.report({'ERROR'}, 'Imported armature contains no keyframes.')
  624. clear_imported()
  625. return {'FINISHED'}
  626.  
  627. # set frame range
  628. bpy.context.scene.frame_start = math.floor(min(kp_frames))
  629. bpy.context.scene.frame_end = math.ceil(max(kp_frames))
  630.  
  631. # for the imported rig, apply ao transforms
  632. apply_ao_transform(ao_imp)
  633.  
  634. prepare_for_kf_map()
  635.  
  636. # actually copy state
  637. copy_anim_state(bpy.data.objects['__Rig'], ao_imp)
  638.  
  639. clear_imported()
  640. return {'FINISHED'}
  641.  
  642. def invoke(self, context, event):
  643. context.window_manager.fileselect_add(self)
  644. return {'RUNNING_MODAL'}
  645.  
  646. class OBJECT_OT_ApplyTransform(bpy.types.Operator):
  647. bl_label = "Apply armature object transform to the root bone for each keyframe"
  648. bl_idname = "object.rbxanims_applytransform"
  649. bl_description = "Apply armature object transform to the root bone for each keyframe -- Must set a proper frame range first!"
  650.  
  651. @classmethod
  652. def poll(cls, context):
  653. grig = bpy.data.objects.get('__Rig')
  654. return grig and bpy.context.active_object and bpy.context.active_object.animation_data
  655.  
  656. def execute(self, context):
  657. if not bpy.context.scene.keying_sets.active:
  658. self.report({'ERROR'}, 'There is no active keying set, this is required.')
  659. return {'FINISHED'}
  660.  
  661. apply_ao_transform(bpy.context.view_layer.objects.active)
  662.  
  663. return {'FINISHED'}
  664.  
  665. class OBJECT_OT_MapKeyframes(bpy.types.Operator):
  666. bl_label = "Map keyframes by bone name"
  667. bl_idname = "object.rbxanims_mapkeyframes"
  668. bl_description = "Map keyframes by bone name --- From a selected armature, maps data (using a new keyframe per frame) onto the generated rig by name. Set frame ranges first!"
  669.  
  670. @classmethod
  671. def poll(cls, context):
  672. grig = bpy.data.objects.get('__Rig')
  673. return grig and bpy.context.active_object and bpy.context.active_object != grig
  674.  
  675. def execute(self, context):
  676. if not bpy.context.scene.keying_sets.active:
  677. self.report({'ERROR'}, 'There is no active keying set, this is required.')
  678. return {'FINISHED'}
  679.  
  680. ao_imp = bpy.context.scene.objects.active
  681.  
  682. err_mappings = get_mapping_error_bones(bpy.data.objects['__Rig'], ao_imp)
  683. if len(err_mappings) > 0:
  684. self.report({'ERROR'}, 'Cannot map rig, the following bones are missing from the source rig: {}.'.format(', '.join(err_mappings)))
  685. return {'FINISHED'}
  686.  
  687. prepare_for_kf_map()
  688.  
  689. copy_anim_state(bpy.data.objects['__Rig'], ao_imp)
  690.  
  691. return {'FINISHED'}
  692.  
  693. class OBJECT_OT_Bake(bpy.types.Operator):
  694. bl_label = "Bake"
  695. bl_idname = "object.rbxanims_bake"
  696. bl_description = "Bake animation for export"
  697.  
  698. def execute(self, context):
  699. serialized = serialize()
  700. encoded = json.dumps(serialized, separators=(',',':'))
  701. bpy.context.window_manager.clipboard = (base64.b64encode(zlib.compress(encoded.encode(), 9))).decode('utf-8')
  702. self.report({'INFO'}, 'Baked animation data exported to the system clipboard ({:d} keyframes, {:.2f} seconds).'.format(len(serialized['kfs']), serialized['t']))
  703. return {'FINISHED'}
  704.  
  705. class OBJECT_PT_RbxAnimations(bpy.types.Panel):
  706. bl_label = "Rbx Animations"
  707. bl_idname = "OBJECT_PT_RbxAnimations"
  708. bl_category = "Rbx Animations"
  709. bl_space_type = 'VIEW_3D'
  710. bl_region_type = 'UI'
  711.  
  712. @classmethod
  713. def poll(cls, context):
  714. return bpy.data.objects.get('__RigMeta')
  715.  
  716. def draw(self, context):
  717. layout = self.layout
  718. layout.use_property_split = True
  719. obj = context.object
  720.  
  721. layout.label(text="Rigging:")
  722. layout.operator("object.rbxanims_genrig", text="Rebuild rig")
  723. layout.label(text="Quick inverse kinematics:")
  724. layout.operator("object.rbxanims_genik", text="Create IK constraints")
  725. layout.operator("object.rbxanims_removeik", text="Remove IK constraints")
  726. layout.label(text="Animation import:")
  727. layout.operator("object.rbxanims_importfbxanimation", text="Import FBX")
  728. layout.operator("object.rbxanims_mapkeyframes", text="Map keyframes by bone name")
  729. layout.operator("object.rbxanims_applytransform", text="Apply armature transform")
  730. layout.label(text="Export:")
  731. layout.operator("object.rbxanims_bake", text="Export animation", icon='RENDER_ANIMATION')
  732.  
  733. def file_import_extend(self, context):
  734. self.layout.operator("object.rbxanims_importmodel", text="[Rbx Animations] Rig import (.obj)")
  735.  
  736. bl_info = {"name": "Rbx Animations", "category": "Animation", "blender": (2, 80, 0)}
  737.  
  738. module_classes = [
  739. OBJECT_OT_ImportModel,
  740. OBJECT_OT_GenRig,
  741. OBJECT_OT_GenIK,
  742. OBJECT_OT_RemoveIK,
  743. OBJECT_OT_ImportFbxAnimation,
  744. OBJECT_OT_ApplyTransform,
  745. OBJECT_OT_MapKeyframes,
  746. OBJECT_OT_Bake,
  747. OBJECT_PT_RbxAnimations,
  748. ]
  749.  
  750. register_classes, unregister_classes = bpy.utils.register_classes_factory(module_classes)
  751.  
  752. def register():
  753. register_classes()
  754. bpy.types.TOPBAR_MT_file_import.append(file_import_extend)
  755.  
  756. def unregister():
  757. unregister_classes()
  758. bpy.types.TOPBAR_MT_file_import.remove(file_import_extend)
  759.  
  760. if __name__ == "__main__":
  761. register()
Add Comment
Please, Sign In to add comment