kent911t

2.8 Blender Animation

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