Cautioned

RBX_Animation_Importer | Cautioned's Fork

Dec 14th, 2023 (edited)
78,399
8
Never
18
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 42.81 KB | Source Code | 11 3
  1. ###
  2. # Copyright 2024 Den_S/@DennisRBLX / Cautioned's Fork v1.96 - @Cautioned_co Roblox: CAUTLONED - LAST UPDATED: 7/16/2024
  3. #
  4. # PLEASE USE NEW BLENDER IMPORT/EXPORT STUDIO PLUGIN BY ME: https://create.roblox.com/store/asset/16708835782/Blender-Animations-ultimate-edition
  5. #
  6. # 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:
  7. #
  8. # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  9. #
  10. # 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.
  11. ###
  12. #
  13. # Rbx Animations Blender Addon
  14. # Written by Den_S/@DennisRBLX, Updated by Cautioned/@Cautloned
  15. # Refer to https://devforum.roblox.com/t/blender-rig-exporter-animation-importer/34729 for usage instructions
  16. #
  17. # For your information:
  18. #   Armature is assumed to have the identity matrix(!!!)
  19. #   When creating a rig, bones are first created in a way they were in the original rig data,
  20. #     the resulting matrices are stored as base matrices.
  21. #   Then, bone tails are moved to be in a more intuitive position (helps IK etc too)
  22. #   This transformation is thus undone when exporting
  23. #   Blender also uses a Z-up/-Y-forward coord system, so this results in more transformations
  24. #   Transform <=> Original **world space** CFrame, should match the associate mesh base matrix, Transform1 <=> C1
  25. #   The meshes are imported in a certain order. Mesh names are restored using attached metadata.
  26. #   Rig data is also encoded in this metdata.
  27. #
  28. # Communication:
  29. #   To blender: A bunch of extra meshes whose names encode metadata (they are numbered, the contents are together encoded in base32)
  30. #   From blender: Base64-encoded string (after compression)
  31. #
  32.  
  33. bl_info = {
  34.     "name": "Roblox Animations Importer/Exporter",
  35.     "description": "Plugin for importing roblox rigs and exporting animations.",
  36.     "author": "Den_S/@DennisRBLX, Updated by Cautioned/@Cautloned",
  37.     "version": (1, 9, 6),
  38.     "blender": (4, 0, 0),  # Recommended Blender version for this add-on
  39.     "location": "View3D > Toolbar"
  40. }
  41.  
  42.  
  43. version = 1.96
  44.  
  45. import bpy, math, re, json, bpy_extras, os
  46. from itertools import chain
  47. from mathutils import Vector, Matrix
  48. import zlib
  49. import base64
  50. from bpy_extras.io_utils import ImportHelper, ExportHelper
  51. from bpy.props import *
  52. import mathutils
  53. import urllib.request
  54. from bpy.types import Operator
  55.  
  56. blender_version = bpy.app.version
  57.  
  58. transform_to_blender = bpy_extras.io_utils.axis_conversion(
  59.     from_forward="Z", from_up="Y", to_forward="-Y", to_up="Z"
  60. ).to_4x4()  # transformation matrix from Y-up to Z-up
  61. identity_cf = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]  # identity CF components matrix
  62. cf_round = False  # round cframes before exporting? (reduce size)
  63. cf_round_fac = 4  # round to how many decimals?
  64.  
  65.  
  66. def armature_items(self, context):
  67.     items = []
  68.     for obj in bpy.data.objects:
  69.         if obj.type == "ARMATURE":
  70.             items.append((obj.name, obj.name, ""))
  71.     return items
  72.  
  73.  
  74. bpy.types.Scene.rbx_anim_armature = bpy.props.EnumProperty(
  75.     items=armature_items, name="Armature", description="Select an armature"
  76. )
  77.  
  78. bpy.types.Scene.ignore_unchanged_keyframes = bpy.props.BoolProperty(
  79.     name="Optimized Bake [Experimental]",
  80.     description="Ignore keyframes that are the same as the previous keyframe when baking (WIP). Also optimizes Constant Animations. (This is experimental, please report any issues)",
  81. )
  82.  
  83.  
  84. # utilities
  85. # y-up cf -> y-up mat
  86. def cf_to_mat(cf):
  87.     mat = Matrix.Translation((cf[0], cf[1], cf[2]))
  88.     mat[0][0:3] = (cf[3], cf[4], cf[5])
  89.     mat[1][0:3] = (cf[6], cf[7], cf[8])
  90.     mat[2][0:3] = (cf[9], cf[10], cf[11])
  91.     return mat
  92.  
  93.  
  94. # y-up mat -> y-up cf
  95. def mat_to_cf(mat):
  96.     r_mat = [
  97.         mat[0][3],
  98.         mat[1][3],
  99.         mat[2][3],
  100.         mat[0][0],
  101.         mat[0][1],
  102.         mat[0][2],
  103.         mat[1][0],
  104.         mat[1][1],
  105.         mat[1][2],
  106.         mat[2][0],
  107.         mat[2][1],
  108.         mat[2][2],
  109.     ]
  110.     return r_mat
  111.  
  112.  
  113. # links the passed object to the bone with the transformation equal to the current(!) transformation between the bone and object
  114. def link_object_to_bone_rigid(obj, ao, bone):
  115.     # remove existing
  116.     for constraint in [c for c in obj.constraints if c.type == "CHILD_OF"]:
  117.         obj.constraints.remove(constraint)
  118.  
  119.     # create new
  120.     constraint = obj.constraints.new(type="CHILD_OF")
  121.     constraint.target = ao
  122.     constraint.subtarget = bone.name
  123.     constraint.inverse_matrix = (ao.matrix_world @ bone.matrix).inverted()
  124.  
  125.  
  126. # serializes the current bone state to a dict
  127. def serialize_animation_state(ao):
  128.     state = {}
  129.     for bone in ao.pose.bones:
  130.         if "is_transformable" in bone.bone:
  131.             orig_mat = Matrix(bone.bone["transform"])
  132.             orig_mat_tr1 = Matrix(bone.bone["transform1"])
  133.             extr_transform = Matrix(bone.bone["nicetransform"]).inverted()
  134.             back_trans = transform_to_blender.inverted()
  135.             cur_obj_transform = back_trans @ (bone.matrix @ extr_transform)
  136.  
  137.             # Check if the bone has a parent and if the parent has a 'bone' attribute
  138.             if (
  139.                 bone.parent
  140.                 and hasattr(bone.parent, "bone")
  141.                 and "transform" in bone.parent.bone
  142.             ):
  143.                 parent_orig_mat = Matrix(bone.parent.bone["transform"])
  144.                 parent_orig_mat_tr1 = Matrix(bone.parent.bone["transform1"])
  145.                 parent_extr_transform = Matrix(
  146.                     bone.parent.bone["nicetransform"]
  147.                 ).inverted()
  148.                 parent_obj_transform = back_trans @ (
  149.                     bone.parent.matrix @ parent_extr_transform
  150.                 )
  151.  
  152.                 orig_base_mat = back_trans @ (orig_mat @ orig_mat_tr1)
  153.                 parent_orig_base_mat = back_trans @ (
  154.                     parent_orig_mat @ parent_orig_mat_tr1
  155.                 )
  156.  
  157.                 orig_transform = parent_orig_base_mat.inverted() @ orig_base_mat
  158.                 cur_transform = parent_obj_transform.inverted() @ cur_obj_transform
  159.                 bone_transform = orig_transform.inverted() @ cur_transform
  160.             else:
  161.                 # Handle the case where the bone has no parent or the parent has no 'bone' attribute
  162.                 bone_transform = cur_obj_transform
  163.  
  164.             statel = mat_to_cf(bone_transform)
  165.             if cf_round:
  166.                 statel = list(map(lambda x: round(x, cf_round_fac), statel))
  167.  
  168.             for i in range(len(statel)):
  169.                 if int(statel[i]) == statel[i]:
  170.                     statel[i] = int(statel[i])
  171.  
  172.             if statel != identity_cf:
  173.                 state[bone.name] = statel
  174.  
  175.     return state
  176.  
  177.  
  178. # removes all IK stuff from a bone
  179. def remove_ik_config(ao, tail_bone):
  180.     to_clear = []
  181.     for constraint in [c for c in tail_bone.constraints if c.type == "IK"]:
  182.         if constraint.target and constraint.subtarget:
  183.             to_clear.append((constraint.target, constraint.subtarget))
  184.         if constraint.pole_target and constraint.pole_subtarget:
  185.             to_clear.append((constraint.pole_target, constraint.pole_subtarget))
  186.  
  187.         tail_bone.constraints.remove(constraint)
  188.  
  189.     bpy.ops.object.mode_set(mode="EDIT")
  190.  
  191.     for util_bone in to_clear:
  192.         util_bone[0].data.edit_bones.remove(util_bone[0].data.edit_bones[util_bone[1]])
  193.  
  194.     bpy.ops.object.mode_set(mode="POSE")
  195.  
  196. # created IK bones and constraints for a given chain
  197. def create_ik_config(ao, tail_bone, chain_count, create_pose_bone, lock_tail):
  198.     lock_tail = False # not implemented
  199.    
  200.     bpy.ops.object.mode_set(mode='EDIT')
  201.    
  202.     amt = ao.data
  203.     ik_target_bone = tail_bone if not lock_tail else tail_bone.parent
  204.    
  205.     ik_target_bone_name = ik_target_bone.name
  206.     ik_name = "{}-IKTarget".format(ik_target_bone_name)
  207.     ik_name_pole = "{}-IKPole".format(ik_target_bone_name)
  208.    
  209.     ik_bone = amt.edit_bones.new(ik_name)
  210.     ik_bone.head = ik_target_bone.tail
  211.     ik_bone.tail = (Matrix.Translation(ik_bone.head) @ ik_target_bone.matrix.to_3x3().to_4x4()) @ Vector((0, 0, -.5))
  212.     ik_bone.bbone_x *= 1.5
  213.     ik_bone.bbone_z *= 1.5
  214.    
  215.     ik_pole = None
  216.     if create_pose_bone:
  217.         pos_low = tail_bone.tail
  218.         pos_high = tail_bone.parent_recursive[chain_count-2].head
  219.         pos_avg = (pos_low + pos_high) * .5
  220.         dist = (pos_low - pos_high).length
  221.        
  222.         basal_bone = tail_bone
  223.         for i in range(1, chain_count):
  224.             if basal_bone.parent:
  225.                 basal_bone = basal_bone.parent
  226.        
  227.         basal_mat = basal_bone.bone.matrix_local
  228.  
  229.         ik_pole = amt.edit_bones.new(ik_name_pole)
  230.         ik_pole.head = basal_mat @ Vector((0, 0, dist * -.25))
  231.         ik_pole.tail = basal_mat @ Vector((0, 0, dist * -.25 - .3))
  232.         ik_pole.bbone_x *= .5
  233.         ik_pole.bbone_z *= .5
  234.  
  235.     bpy.ops.object.mode_set(mode='POSE')
  236.    
  237.     pose_bone = ao.pose.bones[ik_target_bone_name]
  238.     constraint = pose_bone.constraints.new(type = 'IK')
  239.     constraint.target = ao
  240.     constraint.subtarget = ik_name
  241.     if create_pose_bone:
  242.         constraint.pole_target = ao
  243.         constraint.pole_subtarget = ik_name_pole
  244.         constraint.pole_angle = math.pi * -.5
  245.     constraint.chain_count = chain_count
  246.  
  247.  
  248. # loads a (child) rig bone
  249. def load_rigbone(ao, rigging_type, rigsubdef, parent_bone):
  250.     amt = ao.data
  251.     bone = amt.edit_bones.new(rigsubdef["jname"])
  252.  
  253.     mat = cf_to_mat(rigsubdef["transform"])
  254.     bone["transform"] = mat
  255.     bone_dir = (transform_to_blender @ mat).to_3x3().to_4x4() @ Vector((0, 0, 1))
  256.  
  257.     if "jointtransform0" not in rigsubdef:
  258.         # Rig root
  259.         bone.head = (transform_to_blender @ mat).to_translation()
  260.         bone.tail = (transform_to_blender @ mat) @ Vector((0, 0.01, 0))
  261.         bone["transform0"] = Matrix()
  262.         bone["transform1"] = Matrix()
  263.         bone["nicetransform"] = Matrix()
  264.         bone.align_roll(bone_dir)
  265.         bone.hide_select = True
  266.         pre_mat = bone.matrix
  267.         o_trans = transform_to_blender @ mat
  268.     else:
  269.         mat0 = cf_to_mat(rigsubdef["jointtransform0"])
  270.         mat1 = cf_to_mat(rigsubdef["jointtransform1"])
  271.         bone["transform0"] = mat0
  272.         bone["transform1"] = mat1
  273.         bone["is_transformable"] = True
  274.  
  275.         bone.parent = parent_bone
  276.         o_trans = transform_to_blender @ (mat @ mat1)
  277.         bone.head = o_trans.to_translation()
  278.         real_tail = o_trans @ Vector((0, 0.25, 0))
  279.  
  280.         neutral_pos = (transform_to_blender @ mat).to_translation()
  281.         bone.tail = real_tail
  282.         bone.align_roll(bone_dir)
  283.  
  284.         # store neutral matrix
  285.         pre_mat = bone.matrix
  286.  
  287.         if rigging_type != "RAW":  # If so, apply some transform
  288.             if len(rigsubdef["children"]) == 1:
  289.                 nextmat = cf_to_mat(rigsubdef["children"][0]["transform"])
  290.                 nextmat1 = cf_to_mat(rigsubdef["children"][0]["jointtransform1"])
  291.                 next_joint_pos = (
  292.                     transform_to_blender @ (nextmat @ nextmat1)
  293.                 ).to_translation()
  294.  
  295.                 if rigging_type == "CONNECT":  # Instantly connect
  296.                     bone.tail = next_joint_pos
  297.                 else:
  298.                     axis = "y"
  299.                     if rigging_type == "LOCAL_AXIS_EXTEND":  # Allow non-Y too
  300.                         invtrf = pre_mat.inverted() * next_joint_pos
  301.                         bestdist = abs(invtrf.y)
  302.                         for paxis in ["x", "z"]:
  303.                             dist = abs(getattr(invtrf, paxis))
  304.                             if dist > bestdist:
  305.                                 bestdist = dist
  306.                                 axis = paxis
  307.  
  308.                     next_connect_to_parent = True
  309.  
  310.                     ppd_nr_dir = real_tail - bone.head
  311.                     ppd_nr_dir.normalize()
  312.                     proj = ppd_nr_dir.dot(next_joint_pos - bone.head)
  313.                     vis_world_root = ppd_nr_dir * proj
  314.                     bone.tail = bone.head + vis_world_root
  315.  
  316.             else:
  317.                 bone.tail = bone.head + (bone.head - neutral_pos) * -2
  318.  
  319.             if (bone.tail - bone.head).length < 0.01:
  320.                 # just reset, no "nice" config can be found
  321.                 bone.tail = real_tail
  322.                 bone.align_roll(bone_dir)
  323.  
  324.     # fix roll
  325.     bone.align_roll(bone_dir)
  326.  
  327.     post_mat = bone.matrix
  328.  
  329.     # this value stores the transform between the "proper" matrix and the "nice" matrix where bones are oriented in a more friendly way
  330.     bone["nicetransform"] = o_trans.inverted() @ post_mat
  331.  
  332.     # link objects to bone
  333.     for aux in rigsubdef["aux"]:
  334.         if aux and aux in bpy.data.objects:
  335.             obj = bpy.data.objects[aux]
  336.             link_object_to_bone_rigid(obj, ao, bone)
  337.  
  338.     # handle child bones
  339.     for child in rigsubdef["children"]:
  340.         load_rigbone(ao, rigging_type, child, bone)
  341.  
  342.  
  343. # renames parts to whatever the metadata defines, mostly just for user-friendlyness (not required)
  344. def autoname_parts(partnames, basename):
  345.     indexmatcher = re.compile(basename + "(\d+)1(\.\d+)?", re.IGNORECASE)
  346.     for object in bpy.data.objects:
  347.         match = indexmatcher.match(object.name.lower())
  348.         if match:
  349.             index = int(match.group(1))
  350.             object.name = partnames[index - 1]
  351.  
  352.  
  353. def get_unique_name(base_name):
  354.     existing_names = {obj.name for obj in bpy.data.objects}
  355.     if base_name not in existing_names:
  356.         return base_name
  357.    
  358.     counter = 1
  359.     new_name = f"{base_name}.{counter:03d}"
  360.     while new_name in existing_names:
  361.         counter += 1
  362.         new_name = f"{base_name}.{counter:03d}"
  363.     return new_name
  364.  
  365. def create_rig(rigging_type):
  366.     bpy.ops.object.mode_set(mode="OBJECT")
  367.  
  368.     meta_loaded = json.loads(bpy.data.objects["__RigMeta"]["RigMeta"])
  369.  
  370.     bpy.ops.object.add(type="ARMATURE", enter_editmode=True, location=(0, 0, 0))
  371.     ao = bpy.context.object
  372.     ao.show_in_front = True
  373.  
  374.     # Set a unique name for the armature
  375.     ao.name = get_unique_name("Armature")
  376.     amt = ao.data
  377.     amt.name = get_unique_name("__RigArm")
  378.     amt.show_axes = True
  379.     amt.show_names = True
  380.  
  381.     bpy.ops.object.mode_set(mode="EDIT")
  382.     load_rigbone(ao, rigging_type, meta_loaded["rig"], None)
  383.  
  384.     bpy.ops.object.mode_set(mode="OBJECT")
  385.  
  386. def set_scene_fps(desired_fps):
  387.     scene = bpy.context.scene
  388.     scene.render.fps = int(desired_fps)
  389.     scene.render.fps_base = 1.0  # Ensure fps_base is set to 1.0 for consistency
  390.  
  391. def get_scene_fps():
  392.     scene = bpy.context.scene
  393.     return scene.render.fps / scene.render.fps_base
  394.  
  395.  
  396. def serialize():
  397.     armature_name = bpy.context.scene.rbx_anim_armature
  398.     ao = bpy.data.objects[armature_name]
  399.     ctx = bpy.context
  400.     bake_jump = ctx.scene.frame_step
  401.     ignore_unchanged = ctx.scene.ignore_unchanged_keyframes
  402.  
  403.     collected = []
  404.     frames = ctx.scene.frame_end + 1 - ctx.scene.frame_start
  405.     cur_frame = ctx.scene.frame_current
  406.     prev_state = None
  407.     last_change_frame = None
  408.  
  409.     desired_fps = get_scene_fps()  # Capture the desired FPS
  410.  
  411.     for i in range(ctx.scene.frame_start, ctx.scene.frame_end + 1, bake_jump):
  412.         ctx.scene.frame_set(i)
  413.         bpy.context.evaluated_depsgraph_get().update()
  414.  
  415.         state = serialize_animation_state(ao)
  416.  
  417.         # Check if state has changed since the last keyframe
  418.         state_changed = prev_state is None or state != prev_state
  419.  
  420.         if ignore_unchanged:
  421.             if state_changed or i == ctx.scene.frame_start or i == ctx.scene.frame_end:
  422.                 # For constant easing, insert a keyframe just before the change
  423.                 if last_change_frame is not None and last_change_frame != i - bake_jump:
  424.                     collected.append(
  425.                         {
  426.                             "t": ((i - bake_jump) - ctx.scene.frame_start)
  427.                             / desired_fps,  # Use desired FPS
  428.                             "kf": prev_state,
  429.                         }
  430.                     )
  431.                 collected.append(
  432.                     {
  433.                         "t": (i - ctx.scene.frame_start) / desired_fps,  # Use desired FPS
  434.                         "kf": state,
  435.                     }
  436.                 )
  437.                 last_change_frame = i
  438.         else:
  439.             # When ignore_unchanged is False, add keyframes at every step
  440.             collected.append(
  441.                 {"t": (i - ctx.scene.frame_start) / desired_fps,  # Use desired FPS
  442.                  "kf": state}
  443.             )
  444.  
  445.         prev_state = state
  446.  
  447.     ctx.scene.frame_set(cur_frame)
  448.  
  449.     result = {"t": (frames - 1) / desired_fps, "kfs": collected}  # Use desired FPS
  450.  
  451.     return result
  452.  
  453.  
  454.  
  455.  
  456. def copy_anim_state_bone(target, source, bone):
  457.     # get transform mat of the bone in the source ao
  458.     bpy.context.view_layer.objects.active = source
  459.     t_mat = source.pose.bones[bone.name].matrix
  460.  
  461.     bpy.context.view_layer.objects.active = target
  462.  
  463.     # root bone transform is ignored, this is carried to child bones (keeps HRP static)
  464.     if bone.parent:
  465.         # apply transform w.r.t. the current parent bone transform
  466.         r_mat = bone.bone.matrix_local
  467.         p_mat = bone.parent.matrix
  468.         p_r_mat = bone.parent.bone.matrix_local
  469.         bone.matrix_basis = (p_r_mat.inverted() @ r_mat).inverted() @ (
  470.             p_mat.inverted() @ t_mat
  471.         )
  472.  
  473.     # update properties (hacky :p)
  474.     bpy.ops.anim.keyframe_insert()
  475.     bpy.context.scene.frame_set(bpy.context.scene.frame_current)
  476.  
  477.     # now apply on children (which use the parents transform)
  478.     for ch in bone.children:
  479.         copy_anim_state_bone(target, source, ch)
  480.  
  481.  
  482. def copy_anim_state(target, source):
  483.     # to pose mode
  484.     bpy.context.view_layer.objects.active = source
  485.     bpy.ops.object.mode_set(mode="POSE")
  486.  
  487.     bpy.context.view_layer.objects.active = target
  488.     bpy.ops.object.mode_set(mode="POSE")
  489.  
  490.     root = target.pose.bones["HumanoidRootPart"]
  491.  
  492.     for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end + 1):
  493.         bpy.context.scene.frame_set(i)
  494.         copy_anim_state_bone(target, source, root)
  495.         bpy.ops.anim.keyframe_insert()
  496.  
  497.  
  498. def prepare_for_kf_map():
  499.     # clear anim data from target rig
  500.     armature_name = bpy.context.scene.rbx_anim_armature
  501.     bpy.data.objects[armature_name].animation_data_clear()
  502.  
  503.     # select all pose bones in the target rig (simply generate kfs for everything)
  504.     bpy.context.view_layer.objects.active = bpy.data.objects[armature_name]
  505.     bpy.ops.object.mode_set(mode="POSE")
  506.     for bone in bpy.data.objects[armature_name].pose.bones:
  507.         bone.bone.select = not not bone.parent
  508.  
  509.  
  510. def get_mapping_error_bones(target, source):
  511.     return [
  512.         bone.name
  513.         for bone in target.data.bones
  514.         if bone.name not in [bone2.name for bone2 in source.data.bones]
  515.     ]
  516.  
  517.  
  518. # apply ao transforms to the root PoseBone
  519. # + clear ao animation tracks (root only, not Pose anim data) + reset ao transform to identity
  520. def apply_ao_transform(ao):
  521.     bpy.context.view_layer.objects.active = ao
  522.     bpy.ops.object.mode_set(mode="POSE")
  523.  
  524.     # select only root bones
  525.     for bone in ao.pose.bones:
  526.         bone.bone.select = not bone.parent
  527.  
  528.     for root in [bone for bone in ao.pose.bones if not bone.parent]:
  529.         # collect initial root matrices (if they do not exist yet, this will prevent interpolation from keyframes that are being set in the next loop)
  530.         root_matrix_at = {}
  531.         for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end + 1):
  532.             bpy.context.scene.frame_set(i)
  533.             root_matrix_at[i] = root.matrix.copy()
  534.  
  535.         # apply world space transform to root bone
  536.         for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end + 1):
  537.             bpy.context.scene.frame_set(i)
  538.             root.matrix = ao.matrix_world @ root_matrix_at[i]
  539.             bpy.ops.anim.keyframe_insert()
  540.  
  541.     # clear non-pose fcurves
  542.     fcurves = ao.animation_data.action.fcurves
  543.     for c in [c for c in fcurves if not c.data_path.startswith("pose")]:
  544.         fcurves.remove(c)
  545.  
  546.     # reset ao transform
  547.     ao.matrix_basis = Matrix.Identity(4)
  548.     bpy.context.evaluated_depsgraph_get().update()
  549.  
  550.  
  551. ## UI/OPERATOR STUFF ##
  552.  
  553.  
  554. class OBJECT_OT_ImportModel(bpy.types.Operator, ImportHelper):
  555.     bl_label = "Import rig data (.obj)"
  556.     bl_idname = "object.rbxanims_importmodel"
  557.     bl_description = "Import rig data (.obj)"
  558.  
  559.     filename_ext = ".obj"
  560.     filter_glob: bpy.props.StringProperty(default="*.obj", options={'HIDDEN'})
  561.     filepath: bpy.props.StringProperty(name="File Path", maxlen=1024, default="")
  562.  
  563.     def execute(self, context):
  564.         # Do not clear objects
  565.         if blender_version >= (4, 0, 0):
  566.             bpy.ops.wm.obj_import(filepath=self.properties.filepath, use_split_groups=True)
  567.         else:
  568.             bpy.ops.import_scene.obj(filepath=self.properties.filepath, use_split_groups=True)
  569.        
  570.         # Extract meta...
  571.         encodedmeta = ''
  572.         partial = {}
  573.         for obj in bpy.data.objects:
  574.             match = re.search(r'^Meta(\d+)q1(.*?)q1\d*(\.\d+)?$', obj.name)
  575.             if match:
  576.                 partial[int(match.group(1))] = match.group(2)
  577.            
  578.             obj.select_set(not not match)
  579.         bpy.ops.object.delete() # delete meta objects
  580.        
  581.         for i in range(1, len(partial) + 1):
  582.             encodedmeta += partial[i]
  583.         encodedmeta = encodedmeta.replace('0', '=')
  584.         meta = base64.b32decode(encodedmeta, True).decode('utf-8')
  585.        
  586.         # Store meta in an empty
  587.         bpy.ops.object.add(type='EMPTY', location=(0, 0, 0))
  588.         ob = bpy.context.object
  589.         ob.name = '__RigMeta'
  590.         ob['RigMeta'] = meta
  591.        
  592.         meta_loaded = json.loads(meta)
  593.         autoname_parts(meta_loaded['parts'], meta_loaded['rigName'])
  594.        
  595.         return {'FINISHED'}    
  596.  
  597.     def invoke(self, context, event):
  598.         context.window_manager.fileselect_add(self)
  599.         return {'RUNNING_MODAL'}
  600.    
  601.  
  602.  
  603. class OBJECT_OT_GenRig(bpy.types.Operator):
  604.     bl_label = "Generate rig"
  605.     bl_idname = "object.rbxanims_genrig"
  606.     bl_description = "Generate rig"
  607.  
  608.     pr_rigging_type: bpy.props.EnumProperty(
  609.         items=[
  610.             ("RAW", "Nodes only", ""),
  611.             ("LOCAL_AXIS_EXTEND", "Local axis aligned bones", ""),
  612.             ("LOCAL_YAXIS_EXTEND", "Local Y-axis aligned bones", ""),
  613.             ("CONNECT", "Connect", ""),
  614.         ],
  615.         name="Rigging type",
  616.     )
  617.  
  618.     @classmethod
  619.     def poll(cls, context):
  620.         meta_obj = bpy.data.objects.get("__RigMeta")
  621.         return meta_obj and "RigMeta" in meta_obj
  622.  
  623.     def execute(self, context):
  624.         create_rig(self.pr_rigging_type)
  625.         self.report({"INFO"}, "Rig rebuilt.")
  626.         return {"FINISHED"}
  627.  
  628.     def invoke(self, context, event):
  629.         self.pr_rigging_type = "LOCAL_YAXIS_EXTEND"
  630.  
  631.         wm = context.window_manager
  632.         return wm.invoke_props_dialog(self)
  633.  
  634.  
  635. class OBJECT_OT_GenIK(bpy.types.Operator):
  636.     bl_label = "Generate IK"
  637.     bl_idname = "object.rbxanims_genik"
  638.     bl_description = "Generate IK"
  639.    
  640.     pr_chain_count: bpy.props.IntProperty(name="Chain count (0 = to root)", min=0, default=1)
  641.     pr_create_pose_bone: bpy.props.BoolProperty(name="Create pose bone", default=False)
  642.     pr_lock_tail_bone: bpy.props.BoolProperty(name="Lock final bone orientation", default=False)
  643.    
  644.     @classmethod
  645.     def poll(cls, context):
  646.         obj = context.active_object
  647.         return obj and obj.mode == 'POSE' and obj.type == 'ARMATURE' and any(b.bone.select for b in obj.pose.bones)
  648.  
  649.     def execute(self, context):
  650.         obj = context.active_object
  651.         selected_bones = [b for b in obj.pose.bones if b.bone.select]
  652.        
  653.         for bone in selected_bones:
  654.             create_ik_config(obj, bone, self.pr_chain_count, self.pr_create_pose_bone, self.pr_lock_tail_bone)
  655.  
  656.         return {'FINISHED'}
  657.  
  658.     def invoke(self, context, event):
  659.         obj = context.active_object
  660.         selected_bones = [b for b in obj.pose.bones if b.bone.select]
  661.  
  662.         if not selected_bones:
  663.             self.report({'WARNING'}, "No bones selected")
  664.             return {'CANCELLED'}
  665.        
  666.         rec_chain_len = 1
  667.         no_loop_mech = set()
  668.         bone = selected_bones[0].bone
  669.         while bone and bone.parent and len(bone.parent.children) == 1 and bone not in no_loop_mech:
  670.             rec_chain_len += 1
  671.             no_loop_mech.add(bone)
  672.             bone = bone.parent
  673.        
  674.         self.pr_chain_count = rec_chain_len
  675.  
  676.         wm = context.window_manager
  677.         return wm.invoke_props_dialog(self)
  678.  
  679. class OBJECT_OT_RemoveIK(bpy.types.Operator):
  680.     bl_label = "Remove IK"
  681.     bl_idname = "object.rbxanims_removeik"
  682.     bl_description = "Remove IK"
  683.  
  684.     @classmethod
  685.     def poll(cls, context):
  686.         obj = context.active_object
  687.         return obj and obj.mode == 'POSE' and any(b.bone.select for b in obj.pose.bones)
  688.  
  689.     def execute(self, context):
  690.         obj = context.active_object
  691.         selected_bones = [b for b in obj.pose.bones if b.bone.select]
  692.        
  693.         for bone in selected_bones:
  694.             remove_ik_config(obj, bone)
  695.            
  696.         return {'FINISHED'}
  697.  
  698.  
  699. class OBJECT_OT_ImportFbxAnimation(bpy.types.Operator, ImportHelper):
  700.     bl_label = "Import animation data (.fbx)"
  701.     bl_idname = "object.rbxanims_importfbxanimation"
  702.     bl_description = "Import animation data (.fbx) --- FBX file should contain an armature, which will be mapped onto the generated rig by bone names."
  703.  
  704.     filename_ext = ".fbx"
  705.     filter_glob: bpy.props.StringProperty(default="*.fbx", options={'HIDDEN'})
  706.     filepath: bpy.props.StringProperty(name="File Path", maxlen=1024, default="")
  707.    
  708.     @classmethod
  709.     def poll(cls, context):
  710.         armature_name = bpy.context.scene.rbx_anim_armature
  711.         return bpy.data.objects.get(armature_name)
  712.  
  713.     def execute(self, context):
  714.         armature_name = bpy.context.scene.rbx_anim_armature
  715.         # check active keying set
  716.         if not bpy.context.scene.keying_sets.active:
  717.             self.report({'ERROR'}, 'There is no active keying set, this is required.')
  718.             return {'FINISHED'}
  719.        
  720.         # import and keep track of what is imported
  721.         objnames_before_import = [x.name for x in bpy.data.objects]
  722.         bpy.ops.import_scene.fbx(filepath=self.properties.filepath)
  723.         objnames_imported = [x.name for x in bpy.data.objects if x.name not in objnames_before_import]
  724.        
  725.         def clear_imported():
  726.             bpy.ops.object.mode_set(mode='OBJECT')
  727.             for obj in bpy.data.objects:
  728.                 obj.select_set(obj.name in objnames_imported)
  729.             bpy.ops.object.delete()
  730.        
  731.         # check that there's only 1 armature
  732.         armatures_imported = [x for x in bpy.data.objects if x.type == 'ARMATURE' and x.name in objnames_imported]
  733.         if len(armatures_imported) != 1:
  734.             self.report({'ERROR'}, 'Imported file contains {:d} armatures, expected 1.'.format(len(armatures_imported)))
  735.             clear_imported()
  736.             return {'FINISHED'}
  737.        
  738.         ao_imp = armatures_imported[0]
  739.        
  740.         err_mappings = get_mapping_error_bones(bpy.data.objects[armature_name], ao_imp)
  741.         if len(err_mappings) > 0:
  742.             self.report({'ERROR'}, 'Cannot map rig, the following bones are missing from the source rig: {}.'.format(', '.join(err_mappings)))
  743.             clear_imported()
  744.             return {'FINISHED'}
  745.        
  746.         print(dir(bpy.context.scene))
  747.         bpy.context.view_layer.objects.active = ao_imp
  748.        
  749.         # check that the ao contains anim data
  750.         if not ao_imp.animation_data or not ao_imp.animation_data.action or not ao_imp.animation_data.action.fcurves:
  751.             self.report({'ERROR'}, 'Imported armature contains no animation data.')
  752.             clear_imported()
  753.             return {'FINISHED'}
  754.        
  755.         # get keyframes + boundary timestamps
  756.         fcurves = ao_imp.animation_data.action.fcurves
  757.         kp_frames = []
  758.         for key in fcurves:
  759.             kp_frames += [kp.co.x for kp in key.keyframe_points]
  760.         if len(kp_frames) <= 0:
  761.             self.report({'ERROR'}, 'Imported armature contains no keyframes.')
  762.             clear_imported()
  763.             return {'FINISHED'}
  764.        
  765.         # set frame range
  766.         bpy.context.scene.frame_start = math.floor(min(kp_frames))
  767.         bpy.context.scene.frame_end = math.ceil(max(kp_frames))
  768.        
  769.         # for the imported rig, apply ao transforms
  770.         apply_ao_transform(ao_imp)
  771.        
  772.         prepare_for_kf_map()
  773.  
  774.         armature_name = bpy.context.scene.rbx_anim_armature
  775.         try:
  776.             armature = bpy.data.objects[armature_name]
  777.         except KeyError:
  778.             self.report({'ERROR'}, f"No object named '{armature_name}' found. Please ensure the correct rig is selected.")
  779.             return {'FINISHED'}
  780.  
  781.         if armature.animation_data is None:
  782.             self.report({'ERROR'}, f"The object '{armature_name}' has no animation data. Please ensure the correct rig is selected.")
  783.             return {'FINISHED'}
  784.  
  785.        
  786.         # actually copy state
  787.         copy_anim_state(bpy.data.objects[armature_name], ao_imp)
  788.        
  789.         clear_imported()
  790.         return {'FINISHED'}    
  791.  
  792.     def invoke(self, context, event):
  793.         context.window_manager.fileselect_add(self)
  794.         return {'RUNNING_MODAL'}    
  795.  
  796.  
  797. class OBJECT_OT_ApplyTransform(bpy.types.Operator):
  798.     bl_label = "Apply armature object transform to the root bone for each keyframe"
  799.     bl_idname = "object.rbxanims_applytransform"
  800.     bl_description = "Apply armature object transform to the root bone for each keyframe -- Must set a proper frame range first!"
  801.  
  802.     @classmethod
  803.     def poll(cls, context):
  804.         armature_name = bpy.context.scene.rbx_anim_armature
  805.         grig = bpy.data.objects.get(armature_name)
  806.         return (
  807.             grig
  808.             and bpy.context.active_object
  809.             and bpy.context.active_object.animation_data
  810.         )
  811.  
  812.     def execute(self, context):
  813.         if not bpy.context.scene.keying_sets.active:
  814.             self.report({"ERROR"}, "There is no active keying set, this is required.")
  815.             return {"FINISHED"}
  816.  
  817.         apply_ao_transform(bpy.context.view_layer.objects.active)
  818.  
  819.         return {"FINISHED"}
  820.  
  821.  
  822. class OBJECT_OT_MapKeyframes(bpy.types.Operator):
  823.     bl_label = "Map keyframes by bone name"
  824.     bl_idname = "object.rbxanims_mapkeyframes"
  825.     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!"
  826.  
  827.     @classmethod
  828.     def poll(cls, context):
  829.         armature_name = bpy.context.scene.rbx_anim_armature
  830.         grig = bpy.data.objects.get(armature_name)
  831.         return grig and bpy.context.active_object and bpy.context.active_object != grig
  832.  
  833.     def execute(self, context):
  834.         armature_name = bpy.context.scene.rbx_anim_armature
  835.         if not bpy.context.scene.keying_sets.active:
  836.             self.report({"ERROR"}, "There is no active keying set, this is required.")
  837.             return {"FINISHED"}
  838.  
  839.         ao_imp = bpy.context.view_layer.objects.active
  840.  
  841.         err_mappings = get_mapping_error_bones(bpy.data.objects[armature_name], ao_imp)
  842.         if len(err_mappings) > 0:
  843.             self.report(
  844.                 {"ERROR"},
  845.                 "Cannot map rig, the following bones are missing from the source rig: {}.".format(
  846.                     ", ".join(err_mappings)
  847.                 ),
  848.             )
  849.             return {"FINISHED"}
  850.  
  851.         prepare_for_kf_map()
  852.  
  853.         copy_anim_state(bpy.data.objects[armature_name], ao_imp)
  854.  
  855.         return {"FINISHED"}
  856.  
  857.  
  858. class OBJECT_OT_Bake(bpy.types.Operator):
  859.     bl_label = "Bake"
  860.     bl_idname = "object.rbxanims_bake"
  861.     bl_description = "Bake animation for export"
  862.  
  863.     def execute(self, context):
  864.         desired_fps = get_scene_fps()  # Capture the desired FPS
  865.         set_scene_fps(desired_fps)  # Ensure the FPS is set correctly
  866.  
  867.         serialized = serialize()
  868.         encoded = json.dumps(serialized, separators=(",", ":"))
  869.         bpy.context.window_manager.clipboard = (
  870.             base64.b64encode(zlib.compress(encoded.encode(), 9))
  871.         ).decode("utf-8")
  872.         duration = serialized["t"]
  873.         num_keyframes = len(serialized["kfs"])
  874.         self.report(
  875.             {"INFO"},
  876.             "Baked animation data exported to the system clipboard ({:d} keyframes, {:.2f} seconds, {} FPS).".format(
  877.                 num_keyframes, duration, desired_fps
  878.             ),
  879.         )
  880.         return {"FINISHED"}
  881.  
  882. class OBJECT_OT_Bake_File(Operator, ExportHelper):
  883.     bl_label = "Bake to File"
  884.     bl_idname = "object.rbxanims_bake_file"
  885.     bl_description = "Bake animation for export"
  886.  
  887.     # ExportHelper mixin class uses this
  888.     filename_ext = ".rbxanim"
  889.  
  890.     filter_glob: StringProperty(
  891.         default="*.rbxanim",
  892.         options={'HIDDEN'},
  893.         maxlen=255,  # Max internal buffer length, longer would be clamped.
  894.     )
  895.  
  896.     def execute(self, context):
  897.         desired_fps = get_scene_fps()  # Capture the desired FPS
  898.         set_scene_fps(desired_fps)  # Ensure the FPS is set correctly
  899.  
  900.         serialized = serialize()  # Ensure you have a serialize function defined
  901.         encoded = json.dumps(serialized, separators=(",", ":"))
  902.         compressed_data = base64.b64encode(zlib.compress(encoded.encode(), 9)).decode("utf-8")
  903.  
  904.         # Save to file using the provided file path
  905.         filepath = self.filepath
  906.         with open(filepath, 'w') as file:
  907.             file.write(compressed_data)
  908.  
  909.         duration = serialized["t"]
  910.         num_keyframes = len(serialized["kfs"])
  911.         self.report(
  912.             {"INFO"},
  913.             f"Baked animation data exported to {filepath} ({num_keyframes} keyframes, {duration:.2f} seconds, {desired_fps} FPS)."
  914.         )
  915.         return {"FINISHED"}
  916.  
  917.    
  918. class OBJECT_OT_AutoConstraint(bpy.types.Operator):
  919.     bl_label = "Auto Constraint Parts"
  920.     bl_idname = "object.rbxanims_autoconstraint"
  921.     bl_description = "Automatically constrain parts/meshes with the same name as the bones in the armature. Rename your parts to match the bone names, then this will attach them to the rig."
  922.  
  923.     @classmethod
  924.     def poll(cls, context):
  925.         return context.scene.rbx_anim_armature in bpy.data.objects
  926.  
  927.     def execute(self, context):
  928.         armature_name = context.scene.rbx_anim_armature
  929.         armature = bpy.data.objects[armature_name]
  930.         bone_name_map = {bone.name.lower(): bone.name for bone in armature.data.bones}  # Create a mapping of lowercase to actual bone names
  931.         matched_parts = []
  932.  
  933.         # Create or get a collection for auto-constrained parts
  934.         collection_name = f"{armature_name}_Parts"
  935.         if collection_name not in bpy.data.collections:
  936.             new_collection = bpy.data.collections.new(collection_name)
  937.             bpy.context.scene.collection.children.link(new_collection)
  938.         else:
  939.             new_collection = bpy.data.collections[collection_name]
  940.  
  941.         # Ensure only objects in the main scene collection or relevant collection are processed
  942.         for obj in bpy.data.objects:
  943.             if obj.type == 'MESH' and obj.name.lower() in bone_name_map:  # Check if the lowercase name of the object is in the bone name map
  944.                 # Skip objects in other _Parts collections
  945.                 if any(col.name.endswith('_Parts') and col.name != collection_name for col in obj.users_collection):
  946.                     continue
  947.  
  948.                 # Check for existing constraints and clear if they belong to another armature
  949.                 for constraint in obj.constraints:
  950.                     if constraint.type == 'CHILD_OF' and constraint.target != armature:
  951.                         obj.constraints.remove(constraint)
  952.  
  953.                 # Move the object to the new collection if not already in it
  954.                 if new_collection not in obj.users_collection:
  955.                     for collection in obj.users_collection:
  956.                         collection.objects.unlink(obj)
  957.                     new_collection.objects.link(obj)
  958.  
  959.                 # Find the correct bone name
  960.                 bone_name = bone_name_map[obj.name.lower()]
  961.  
  962.                 # Add constraint if not already constrained to the correct bone
  963.                 existing_constraint = next((c for c in obj.constraints if c.type == 'CHILD_OF' and c.target == armature and c.subtarget == bone_name), None)
  964.                 if not existing_constraint:
  965.                     constraint = obj.constraints.new(type='CHILD_OF')
  966.                     constraint.target = armature
  967.                     constraint.subtarget = bone_name
  968.                 matched_parts.append(obj.name)
  969.  
  970.         if not matched_parts:
  971.             self.report({'INFO'}, f'No matching parts found for armature {armature_name}')
  972.         else:
  973.             self.report({'INFO'}, f'Constraints added to parts: {", ".join(matched_parts)}')
  974.  
  975.         return {'FINISHED'}
  976.  
  977.  
  978.  
  979.  
  980.  
  981.  
  982.  
  983. class UpdateOperator(bpy.types.Operator):
  984.     bl_idname = "my_plugin.update"
  985.     bl_label = "Check for Updates"
  986.     bl_description = "Check for any New Updates"
  987.  
  988.     @classmethod
  989.     def check_for_updates(cls, self):
  990.         # Replace with your Pastebin link
  991.         url = "https://pastebin.com/raw/DhTbba6C"
  992.  
  993.         try:
  994.             response = urllib.request.urlopen(url)
  995.             new_code = response.read().decode()
  996.  
  997.             # Extract the version number from the new code
  998.             match = re.search(r"version = (\d+\.\d+)", new_code)
  999.             if match:
  1000.                 new_version = float(match.group(1))
  1001.                 if new_version > version:
  1002.                     self.report(
  1003.                         {"INFO"},
  1004.                         "Update Available ⚠️: v"
  1005.                         + str(new_version)
  1006.                         + " https://pastebin.com/raw/DhTbba6C",
  1007.                     )
  1008.                 else:
  1009.                     self.report({"INFO"}, "No Updates Available 🙁")
  1010.             else:
  1011.                 self.report({"ERROR"}, "Failed to check for updates.🔌")
  1012.  
  1013.         except Exception as e:
  1014.             self.report({"ERROR"}, str(e))
  1015.  
  1016.     def execute(self, context):
  1017.         self.check_for_updates(self)
  1018.         return {"FINISHED"}
  1019.  
  1020.  
  1021. def load_handler(dummy):
  1022.     UpdateOperator.check_for_updates()
  1023.  
  1024.  
  1025. class OBJECT_PT_RbxAnimations(bpy.types.Panel):
  1026.     bl_label = "Rbx Animations"
  1027.     bl_idname = "OBJECT_PT_RbxAnimations"
  1028.     bl_category = "Rbx Animations"
  1029.     bl_space_type = "VIEW_3D"
  1030.     bl_region_type = "UI"
  1031.  
  1032.     @classmethod
  1033.     def poll(cls, context):
  1034.         # return bpy.data.objects.get("__RigMeta")
  1035.         return True
  1036.  
  1037.     def draw(self, context):
  1038.         layout = self.layout
  1039.         layout.use_property_split = True
  1040.  
  1041.         rig_meta_exists = "__RigMeta" in bpy.data.objects
  1042.         armatures_exist = any(obj for obj in bpy.data.objects if obj.type == 'ARMATURE')
  1043.  
  1044.         # Check if "__RigMeta" exists
  1045.         if not rig_meta_exists or not armatures_exist:
  1046.             # Display warning message if "__RigMeta" or armatures are missing
  1047.             row = layout.row()
  1048.  
  1049.             if not rig_meta_exists and not armatures_exist:
  1050.                 row.label(text="No armatures or unbuilt rigs found in scene! Please import or append a rig.", icon="ERROR")
  1051.                 return  # Stop drawing the rest of the UI if essential data is missing
  1052.            
  1053.         layout = self.layout
  1054.         layout.use_property_split = True
  1055.         obj = context.object
  1056.         layout.prop(context.scene, "rbx_anim_armature", text="Select A Rig")
  1057.         layout.label(text="Rigging:")
  1058.         layout.operator("object.rbxanims_genrig", text="Rebuild rig", icon = "ARMATURE_DATA")
  1059.         layout.operator("object.rbxanims_autoconstraint", text="Constraint Matching Parts", icon = "CONSTRAINT")
  1060.         layout.label(text="Quick inverse kinematics:")
  1061.         layout.operator("object.rbxanims_genik", text="Create IK constraints")
  1062.         layout.operator("object.rbxanims_removeik", text="Remove IK constraints")
  1063.         layout.label(text="Animation import:")
  1064.         layout.operator(
  1065.             "object.rbxanims_unbake", text="Import animation from clipboard"
  1066.         )
  1067.         layout.operator("object.rbxanims_importfbxanimation", text="Import FBX")
  1068.         layout.operator(
  1069.             "object.rbxanims_mapkeyframes", text="Map keyframes by bone name"
  1070.         )
  1071.         layout.operator(
  1072.             "object.rbxanims_applytransform", text="Apply armature transform"
  1073.         )
  1074.         layout.label(text="Export Settings:")
  1075.         layout.prop(
  1076.             context.scene,
  1077.             "ignore_unchanged_keyframes",
  1078.             text="Optimized Bake [Experimental]",
  1079.             icon="ACTION",
  1080.         )
  1081.         layout.label(text="Exports:")
  1082.         layout.operator(
  1083.             "object.rbxanims_bake", text="Export animation", icon="RENDER_ANIMATION"
  1084.         )
  1085.         layout.operator(
  1086.             "object.rbxanims_bake_file", text="Export animation to file", icon="EXPORT"
  1087.         )
  1088.         layout.operator(
  1089.             "my_plugin.update", text=UpdateOperator.bl_label, icon="FILE_REFRESH"
  1090.         )
  1091.        
  1092.  
  1093.  
  1094. def draw_func(self, context):
  1095.     layout = self.layout
  1096.     layout.operator(UpdateOperator.bl_idname)
  1097.  
  1098.  
  1099. def file_import_extend(self, context):
  1100.     self.layout.operator(
  1101.         "object.rbxanims_importmodel", text="[Rbx Animations] Rig import (.obj)"
  1102.     )
  1103.  
  1104.  
  1105.  
  1106.  
  1107. module_classes = [
  1108.     OBJECT_OT_ImportModel,
  1109.     OBJECT_OT_GenRig,
  1110.     OBJECT_OT_AutoConstraint,
  1111.     OBJECT_OT_GenIK,
  1112.     OBJECT_OT_RemoveIK,
  1113.     OBJECT_OT_ImportFbxAnimation,
  1114.     OBJECT_OT_ApplyTransform,
  1115.     OBJECT_OT_MapKeyframes,
  1116.     OBJECT_OT_Bake,
  1117.     OBJECT_OT_Bake_File,
  1118.     OBJECT_PT_RbxAnimations,
  1119.     UpdateOperator,
  1120. ]
  1121.  
  1122. register_classes, unregister_classes = bpy.utils.register_classes_factory(
  1123.     module_classes
  1124. )
  1125.  
  1126.  
  1127. def register():
  1128.     register_classes()
  1129.     bpy.types.TOPBAR_MT_file_import.append(file_import_extend)
  1130.  
  1131.  
  1132.  
  1133. def unregister():
  1134.     unregister_classes()
  1135.     bpy.types.TOPBAR_MT_file_import.remove(file_import_extend)
  1136.  
  1137.  
  1138. if __name__ == "__main__":
  1139.     register()
  1140.  
Comments
  • jackiechan12374
    352 days
    # text 0.03 KB | 0 0
    1. how do you add it to blender?
    • Sxmxk24
      342 days
      # text 0.21 KB | 0 0
      1. click the download button and put the .py file in a folder and name it whatever, then open blender click edit, preferences, add ons, install, then find the folder and double click the .py file. hope this helps!
      • ImranPlays
        318 days
        # text 0.03 KB | 0 0
        1. i still dont understand by that
        2.  
        • molecularseal
          286 days
          # text 0.68 KB | 2 0
          1. 1. after downloading blender, click the 'download' button at the top of the page.
          2. 2. open file explorer, open 'downloads' to see the .py file you downloaded from here.
          3. 3. create a new and empty folder. drag the .py file into that folder
          4. 4. open blender, click on 'edit' then 'preferences..'
          5. 5. click on 'add-ons', the arrow at the top-right, then 'install from disk'
          6. 6. your files should pop up, click on 'downloads' (or whatever section you moved the .py folder to), then double click on the folder that contains the .py file. click on the .py file itself then "install from disk"
          7. 7. you can exit preferences and if you open it again "Roblox Animations Importer/Exporter" should be there
  • nugget47
    339 days
    # text 0.01 KB | 0 0
    1. doesnt work
    2.  
  • TomCodes247
    323 days
    # text 0.03 KB | 1 0
    1. where's the download button
  • siroxy0199
    320 days
    # text 0.03 KB | 2 0
    1. where is the download button
    2.  
  • Wildcat1223
    303 days
    # text 0.07 KB | 0 0
    1. there is a err on blender when I try to use the rebuild rig option
    2.  
  • benilolze
    68 days
    # text 0.04 KB | 0 0
    1. how do i import my animation tho?.
    2.  
  • ThatAnimeGuyOnPB
    42 days (edited)
    # text 0.35 KB | 1 0
    1. I get this error when trying to rebuild the rig:
    2. '''
    3. Python: Traceback (most recent call last):
    4. File "C:\Program Files\Blender Foundation\Blender 4.1\4.1\python\Lib\json\decoder.py", line 355, in raw_decode
    5. raise JSONDecodeError("Expecting value", s, err.value) from None
    6. json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
    7. '''
  • Lubaz
    38 days
    # text 0.04 KB | 0 0
    1. Does it work on any blender version?
    2.  
Add Comment
Please, Sign In to add comment