Untitled

mail@pastecode.io avatar
unknown
plain_text
a year ago
32 kB
5011
Indexable
Never
###
# Copyright 2018 Den_S/@DennisRBLX
# 
# 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:
# 
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#
# 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.
###
#
# Rbx Animations Blender Addon
# Written by Den_S/@DennisRBLX
# Refer to https://devforum.roblox.com/t/blender-rig-exporter-animation-importer/34729 for usage instructions
#
# For your information:
#   Armature is assumed to have the identity matrix(!!!)
#   When creating a rig, bones are first created in a way they were in the original rig data,
#     the resulting matrices are stored as base matrices.
#   Then, bone tails are moved to be in a more intuitive position (helps IK etc too)
#   This transformation is thus undone when exporting
#   Blender also uses a Z-up/-Y-forward coord system, so this results in more transformations
#   Transform <=> Original **world space** CFrame, should match the associate mesh base matrix, Transform1 <=> C1
#   The meshes are imported in a certain order. Mesh names are restored using attached metadata.
#   Rig data is also encoded in this metdata.
#
# Communication:
#   To blender: A bunch of extra meshes whose names encode metadata (they are numbered, the contents are together encoded in base32)
#   From blender: Base64-encoded string (after compression)
#

import bpy, math, re, json, bpy_extras
from itertools import chain
from mathutils import Vector, Matrix
import zlib
import base64
from bpy_extras.io_utils import ImportHelper
from bpy.props import *

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
identity_cf = [0,0,0,1,0,0,0,1,0,0,0,1] # identity CF components matrix
cf_round = False # round cframes before exporting? (reduce size)
cf_round_fac = 4 # round to how many decimals?

# y-up cf -> y-up mat
def cf_to_mat(cf):
    mat = Matrix.Translation((cf[0], cf[1], cf[2]))
    mat[0][0:3] = (cf[3], cf[4], cf[5])
    mat[1][0:3] = (cf[6], cf[7], cf[8])
    mat[2][0:3] = (cf[9], cf[10], cf[11])
    return mat

# y-up mat -> y-up cf
def mat_to_cf(mat):
    r_mat = [mat[0][3], mat[1][3], mat[2][3],
        mat[0][0], mat[0][1], mat[0][2],
        mat[1][0], mat[1][1], mat[1][2],
        mat[2][0], mat[2][1], mat[2][2]
    ]
    return r_mat

# links the passed object to the bone with the transformation equal to the current(!) transformation between the bone and object
def link_object_to_bone_rigid(obj, ao, bone):
    # remove existing
    for constraint in [c for c in obj.constraints if c.type == 'CHILD_OF']:
        obj.constraints.remove(constraint)

    # create new
    constraint = obj.constraints.new(type = 'CHILD_OF')
    constraint.target = ao
    constraint.subtarget = bone.name
    constraint.inverse_matrix = (ao.matrix_world @ bone.matrix).inverted()

# serializes the current bone state to a dict
def serialize_animation_state(ao):
    state = {}
    for bone in ao.pose.bones:
        if 'is_transformable' in bone.bone:
            # original matrices, straight from the import cfs
            # this is always the true baseline
            orig_mat = Matrix(bone.bone['transform'])
            orig_mat_tr1 = Matrix(bone.bone['transform1'])
            parent_orig_mat = Matrix(bone.parent.bone['transform'])
            parent_orig_mat_tr1 = Matrix(bone.parent.bone['transform1'])

            # get the bone neutral transform
            extr_transform = Matrix(bone.bone['nicetransform']).inverted()
            parent_extr_transform = Matrix(bone.parent.bone['nicetransform']).inverted()
            
            # z-up -> y-up transform matrix
            back_trans = transform_to_blender.inverted()

            # get the real bone transform
            cur_obj_transform = back_trans @ (bone.matrix @ extr_transform)
            parent_obj_transform = back_trans @ (bone.parent.matrix @ parent_extr_transform)
            
            # compute neutrals after applying C1/transform1
            orig_base_mat = back_trans @ (orig_mat @ orig_mat_tr1)
            parent_orig_base_mat = back_trans @ (parent_orig_mat @ parent_orig_mat_tr1)
            
            # compute y-up bone transform (transformation between C0 and C1)
            orig_transform = parent_orig_base_mat.inverted() @ orig_base_mat
            cur_transform = parent_obj_transform.inverted() @ cur_obj_transform
            bone_transform = orig_transform.inverted() @ cur_transform

            statel = mat_to_cf(bone_transform)
            if cf_round:
                statel = list(map(lambda x: round(x, cf_round_fac), statel)) # compresses result
            
            # flatten, compresses the resulting json too
            for i in range(len(statel)):
                if int(statel[i]) ==  statel[i]:
                    statel[i] = int(statel[i])
            
            # only store if not identity, compresses the resulting json
            if statel != identity_cf:
                state[bone.name] = statel
    
    return state

# removes all IK stuff from a bone
def remove_ik_config(ao, tail_bone):
    to_clear = []
    for constraint in [c for c in tail_bone.constraints if c.type == 'IK']:
        if constraint.target and constraint.subtarget:
            to_clear.append((constraint.target, constraint.subtarget))
        if constraint.pole_target and constraint.pole_subtarget:
            to_clear.append((constraint.pole_target, constraint.pole_subtarget))
            
        tail_bone.constraints.remove(constraint)
    
    bpy.ops.object.mode_set(mode='EDIT')
    
    for util_bone in to_clear:
        util_bone[0].data.edit_bones.remove(util_bone[0].data.edit_bones[util_bone[1]])
    
    bpy.ops.object.mode_set(mode='POSE')

# created IK bones and constraints for a given chain
def create_ik_config(ao, tail_bone, chain_count, create_pose_bone, lock_tail):
    lock_tail = False # not implemented
    
    bpy.ops.object.mode_set(mode='EDIT')
    
    amt = ao.data
    ik_target_bone = tail_bone if not lock_tail else tail_bone.parent
    
    ik_target_bone_name = ik_target_bone.name
    ik_name = "{}-IKTarget".format(ik_target_bone_name)
    ik_name_pole = "{}-IKPole".format(ik_target_bone_name)
    
    ik_bone = amt.edit_bones.new(ik_name)
    ik_bone.head = ik_target_bone.tail
    ik_bone.tail = (Matrix.Translation(ik_bone.head) @ ik_target_bone.matrix.to_3x3().to_4x4()) @ Vector((0, 0, -.5))
    ik_bone.bbone_x *= 1.5
    ik_bone.bbone_z *= 1.5
    
    ik_pole = None
    if create_pose_bone:
        pos_low = tail_bone.tail
        pos_high = tail_bone.parent_recursive[chain_count-2].head
        pos_avg = (pos_low + pos_high) * .5
        dist = (pos_low - pos_high).length
        
        basal_bone = tail_bone
        for i in range(1, chain_count):
            if basal_bone.parent:
                basal_bone = basal_bone.parent
        
        basal_mat = basal_bone.bone.matrix_local

        ik_pole = amt.edit_bones.new(ik_name_pole)
        ik_pole.head = basal_mat @ Vector((0, 0, dist * -.25))
        ik_pole.tail = basal_mat @ Vector((0, 0, dist * -.25 - .3))
        ik_pole.bbone_x *= .5
        ik_pole.bbone_z *= .5

    bpy.ops.object.mode_set(mode='POSE')
    
    pose_bone = ao.pose.bones[ik_target_bone_name]
    constraint = pose_bone.constraints.new(type = 'IK')
    constraint.target = ao
    constraint.subtarget = ik_name
    if create_pose_bone:
        constraint.pole_target = ao
        constraint.pole_subtarget = ik_name_pole
        constraint.pole_angle = math.pi * -.5
    constraint.chain_count = chain_count

# loads a (child) rig bone
def load_rigbone(ao, rigging_type, rigsubdef, parent_bone):
    amt = ao.data
    bone = amt.edit_bones.new(rigsubdef['jname'])
    
    mat = cf_to_mat(rigsubdef['transform'])
    bone["transform"] = mat
    bone_dir = (transform_to_blender @ mat).to_3x3().to_4x4() @ Vector((0, 0, 1))
    
    if 'jointtransform0' not in rigsubdef:
        # Rig root
        bone.head = (transform_to_blender @ mat).to_translation()
        bone.tail = (transform_to_blender @ mat) @ Vector((0, .01, 0))
        bone["transform0"] = Matrix()
        bone["transform1"] = Matrix()
        bone['nicetransform'] = Matrix()
        bone.align_roll(bone_dir)
        bone.hide_select = True
        pre_mat = bone.matrix
        o_trans = transform_to_blender @ mat
    else:
        mat0 = cf_to_mat(rigsubdef['jointtransform0'])
        mat1 = cf_to_mat(rigsubdef['jointtransform1'])
        bone["transform0"] = mat0
        bone["transform1"] = mat1
        bone["is_transformable"] = True
        
        bone.parent = parent_bone
        o_trans = transform_to_blender @ (mat @ mat1)
        bone.head = o_trans.to_translation()
        real_tail = o_trans @ Vector((0, .25, 0))
        
        neutral_pos = (transform_to_blender @ mat).to_translation()
        bone.tail = real_tail
        bone.align_roll(bone_dir)
        
        # store neutral matrix
        pre_mat = bone.matrix
        
        if rigging_type != 'RAW': # If so, apply some transform
            if len(rigsubdef['children']) == 1:
                nextmat = cf_to_mat(rigsubdef['children'][0]['transform'])
                nextmat1 = cf_to_mat(rigsubdef['children'][0]['jointtransform1'])
                next_joint_pos = (transform_to_blender @ (nextmat @ nextmat1)).to_translation()
                    
                if rigging_type == 'CONNECT': # Instantly connect
                    bone.tail = next_joint_pos
                else:
                    axis = 'y'
                    if rigging_type == 'LOCAL_AXIS_EXTEND': # Allow non-Y too
                        invtrf = pre_mat.inverted() * next_joint_pos
                        bestdist = abs(invtrf.y)
                        for paxis in ['x', 'z']:
                            dist = abs(getattr(invtrf, paxis))
                            if dist > bestdist:
                                bestdist = dist
                                axis = paxis
                    
                    next_connect_to_parent = True
                    
                    ppd_nr_dir = real_tail - bone.head
                    ppd_nr_dir.normalize()
                    proj = ppd_nr_dir.dot(next_joint_pos - bone.head)
                    vis_world_root = ppd_nr_dir * proj
                    bone.tail = bone.head + vis_world_root
                
            else:
                bone.tail = bone.head + (bone.head - neutral_pos) * -2
        
            if (bone.tail - bone.head).length < .01:
                # just reset, no "nice" config can be found
                bone.tail = real_tail
                bone.align_roll(bone_dir)
    
    # fix roll
    bone.align_roll(bone_dir)
    
    post_mat = bone.matrix
    
    # this value stores the transform between the "proper" matrix and the "nice" matrix where bones are oriented in a more friendly way
    bone['nicetransform'] = o_trans.inverted() @ post_mat

    # link objects to bone
    for aux in rigsubdef['aux']:
        if aux and aux in bpy.data.objects:
            obj = bpy.data.objects[aux]
            link_object_to_bone_rigid(obj, ao, bone)
    
    # handle child bones
    for child in rigsubdef['children']:
        load_rigbone(ao, rigging_type, child, bone)

# renames parts to whatever the metadata defines, mostly just for user-friendlyness (not required)
def autoname_parts(partnames, basename):
    indexmatcher = re.compile(basename + '(\d+)1(\.\d+)?', re.IGNORECASE)
    for object in bpy.data.objects:
        match = indexmatcher.match(object.name.lower())
        if match:
            index = int(match.group(1))
            object.name = partnames[index - 1]

# removes existing rig if it exists, then builds a new one using the stored metadata
def create_rig(rigging_type):
    bpy.ops.object.mode_set(mode='OBJECT')
    if '__Rig' in bpy.data.objects:
        bpy.data.objects['__Rig'].select_set(True)
        bpy.ops.object.delete()
        
    meta_loaded = json.loads(bpy.data.objects['__RigMeta']['RigMeta'])
    
    bpy.ops.object.add(type='ARMATURE', enter_editmode=True, location=(0,0,0))
    ao = bpy.context.object
    ao.show_in_front = True
    ao.name = '__Rig'
    amt = ao.data
    amt.name = '__RigArm'
    amt.show_axes = True
    amt.show_names = True
    
    bpy.ops.object.mode_set(mode='EDIT')
    load_rigbone(ao, rigging_type, meta_loaded['rig'], None)
    
    bpy.ops.object.mode_set(mode='OBJECT')


# export the entire animation to the clipboard (serialized), returns animation time
def serialize():
    ao = bpy.data.objects['__Rig']
    ctx = bpy.context
    bake_jump = ctx.scene.frame_step
    
    collected = []
    frames = ctx.scene.frame_end+1 - ctx.scene.frame_start
    cur_frame = ctx.scene.frame_current
    for i in range(ctx.scene.frame_start, ctx.scene.frame_end+1, bake_jump):
        ctx.scene.frame_set(i)
        bpy.context.evaluated_depsgraph_get().update()
    
        state = serialize_animation_state(ao)
        collected.append({'t': (i - ctx.scene.frame_start) / ctx.scene.render.fps, 'kf': state})
    
    ctx.scene.frame_set(cur_frame)
    
    result = {
        't': (frames-1) / ctx.scene.render.fps,
        'kfs': collected
    }
    
    return result

def copy_anim_state_bone(target, source, bone):
    # get transform mat of the bone in the source ao
    bpy.context.view_layer.objects.active = source
    t_mat = source.pose.bones[bone.name].matrix

    bpy.context.view_layer.objects.active = target
        
    # root bone transform is ignored, this is carried to child bones (keeps HRP static)
    if bone.parent:
        # apply transform w.r.t. the current parent bone transform
        r_mat = bone.bone.matrix_local
        p_mat = bone.parent.matrix
        p_r_mat = bone.parent.bone.matrix_local
        bone.matrix_basis = (p_r_mat.inverted() @ r_mat).inverted() @ (p_mat.inverted() @ t_mat)

    # update properties (hacky :p)
    bpy.ops.anim.keyframe_insert()
    bpy.context.scene.frame_set(bpy.context.scene.frame_current)

    # now apply on children (which use the parents transform)
    for ch in bone.children:
        copy_anim_state_bone(target, source, ch)
    
def copy_anim_state(target, source):
    # to pose mode
    bpy.context.view_layer.objects.active = source
    bpy.ops.object.mode_set(mode='POSE')

    bpy.context.view_layer.objects.active = target
    bpy.ops.object.mode_set(mode='POSE')

    root = target.pose.bones['HumanoidRootPart']
 
    for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1):
        bpy.context.scene.frame_set(i)
        copy_anim_state_bone(target, source, root)
        bpy.ops.anim.keyframe_insert()

def prepare_for_kf_map():
    # clear anim data from target rig
    bpy.data.objects['__Rig'].animation_data_clear()
    
    # select all pose bones in the target rig (simply generate kfs for everything)
    bpy.context.view_layer.objects.active = bpy.data.objects['__Rig']
    bpy.ops.object.mode_set(mode='POSE')
    for bone in bpy.data.objects['__Rig'].pose.bones:
        bone.bone.select = not not bone.parent

def get_mapping_error_bones(target, source):
    return [bone.name for bone in target.data.bones if bone.name not in [bone2.name for bone2 in source.data.bones]]
    
# apply ao transforms to the root PoseBone
# + clear ao animation tracks (root only, not Pose anim data) + reset ao transform to identity
def apply_ao_transform(ao):
    bpy.context.view_layer.objects.active = ao
    bpy.ops.object.mode_set(mode='POSE')
    
    # select only root bones
    for bone in ao.pose.bones:
        bone.bone.select = not bone.parent
    
    for root in [bone for bone in ao.pose.bones if not bone.parent]:
        # collect initial root matrices (if they do not exist yet, this will prevent interpolation from keyframes that are being set in the next loop)
        root_matrix_at = {}
        for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1):
            bpy.context.scene.frame_set(i)
            root_matrix_at[i] = root.matrix.copy()
        
        # apply world space transform to root bone
        for i in range(bpy.context.scene.frame_start, bpy.context.scene.frame_end+1):
            bpy.context.scene.frame_set(i)
            root.matrix = ao.matrix_world @ root_matrix_at[i]
            bpy.ops.anim.keyframe_insert()

    # clear non-pose fcurves
    fcurves = ao.animation_data.action.fcurves
    for c in [c for c in fcurves if not c.data_path.startswith('pose')]:
        fcurves.remove(c)
        
    # reset ao transform
    ao.matrix_basis = Matrix.Identity(4)
    bpy.context.evaluated_depsgraph_get().update()

## UI/OPERATOR STUFF ##

class OBJECT_OT_ImportModel(bpy.types.Operator, ImportHelper):
    bl_label = "Import rig data (.obj)"
    bl_idname = "object.rbxanims_importmodel"
    bl_description = "Import rig data (.obj)"

    filename_ext = ".obj"
    filter_glob: bpy.props.StringProperty(default="*.obj", options={'HIDDEN'})
    filepath: bpy.props.StringProperty(name="File Path", maxlen=1024, default="")
 
    def execute(self, context):
        # clear objects first
        for obj in bpy.data.objects:
            obj.select_set(obj.type == 'MESH' or obj.type == 'ARMATURE' or obj.name.startswith('__RigMeta'))
        bpy.ops.object.delete()
        
        bpy.ops.import_scene.obj(filepath=self.properties.filepath, use_split_groups=True)
        
        # Extract meta...
        encodedmeta = ''
        partial = {}
        for obj in bpy.data.objects:
            match = re.search(r'^Meta(\d+)q1(.*?)q1\d*(\.\d+)?$', obj.name)
            if match:
                partial[int(match.group(1))] = match.group(2)
            
            obj.select_set(not not match)
        bpy.ops.object.delete() # delete meta objects
        
        for i in range(1, len(partial)+1):
            encodedmeta += partial[i]
        encodedmeta = encodedmeta.replace('0', '=')
        meta = base64.b32decode(encodedmeta, True).decode('utf-8')
        
        # store meta in an empty
        bpy.ops.object.add(type='EMPTY', location=(0,0,0))
        ob = bpy.context.object
        ob.name = '__RigMeta'
        ob['RigMeta'] = meta
        
        meta_loaded = json.loads(meta)
        autoname_parts(meta_loaded['parts'], meta_loaded['rigName'])
        
        return {'FINISHED'}    
 
    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}    

class OBJECT_OT_GenRig(bpy.types.Operator):
    bl_label = "Generate rig"
    bl_idname = "object.rbxanims_genrig"
    bl_description = "Generate rig"

    pr_rigging_type: bpy.props.EnumProperty(items=[
        ('RAW', 'Nodes only', ''), 
        ('LOCAL_AXIS_EXTEND', 'Local axis aligned bones', ''),
        ('LOCAL_YAXIS_EXTEND', 'Local Y-axis aligned bones', ''),
        ('CONNECT', 'Connect', '')
    ], name="Rigging type");
 
    @classmethod
    def poll(cls, context):
        meta_obj = bpy.data.objects.get('__RigMeta')
        return meta_obj and 'RigMeta' in meta_obj
 
    def execute(self, context):
        create_rig(self.pr_rigging_type)
        self.report({'INFO'}, "Rig rebuilt.")
        return {'FINISHED'}
    
    def invoke(self, context, event):
        self.pr_rigging_type = 'LOCAL_YAXIS_EXTEND'
        
        wm = context.window_manager
        return wm.invoke_props_dialog(self)

class OBJECT_OT_GenIK(bpy.types.Operator):
    bl_label = "Generate IK"
    bl_idname = "object.rbxanims_genik"
    bl_description = "Generate IK"
    
    pr_chain_count: bpy.props.IntProperty(name = "Chain count (0 = to root)", min=0)
    pr_create_pose_bone: bpy.props.BoolProperty(name = "Create pose bone")
    pr_lock_tail_bone: bpy.props.BoolProperty(name = "Lock final bone orientation")
    
    @classmethod
    def poll(cls, context):
        premise = context.active_object and context.active_object.mode == 'POSE'
        premise = premise and context.active_object and context.active_object.type == 'ARMATURE'
        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

    def execute(self, context):
        
        to_apply = [b for b in context.active_object.pose.bones if b.bone.select]
        
        for bone in to_apply:
            create_ik_config(context.active_object, bone, self.pr_chain_count, self.pr_create_pose_bone, self.pr_lock_tail_bone)

        return {'FINISHED'}

    def invoke(self, context, event):
        to_apply = [b for b in context.active_object.pose.bones if b.bone.select]
        if len(to_apply) == 0:
            return {'FINISHED'}
        
        rec_chain_len = 1
        no_loop_mech = set()
        itr = to_apply[0].bone
        while itr and itr.parent and len(itr.parent.children) == 1 and itr not in no_loop_mech:
            rec_chain_len += 1
            no_loop_mech.add(itr)
            itr = itr.parent
        
        self.pr_chain_count = rec_chain_len
        self.pr_create_pose_bone = False
        self.pr_lock_tail_bone = False
        
        wm = context.window_manager
        return wm.invoke_props_dialog(self)
    
class OBJECT_OT_RemoveIK(bpy.types.Operator):
    bl_label = "Remove IK"
    bl_idname = "object.rbxanims_removeik"
    bl_description = "Remove IK"

    @classmethod
    def poll(cls, context):
        premise = context.active_object and context.active_object.mode == 'POSE'
        premise = premise and context.active_object
        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

    def execute(self, context):
        to_apply = [b for b in context.active_object.pose.bones if b.bone.select]
        
        for bone in to_apply:
            remove_ik_config(context.active_object, bone)
            
        return {'FINISHED'}

class OBJECT_OT_ImportFbxAnimation(bpy.types.Operator, ImportHelper):
    bl_label = "Import animation data (.fbx)"
    bl_idname = "object.rbxanims_importfbxanimation"
    bl_description = "Import animation data (.fbx) --- FBX file should contain an armature, which will be mapped onto the generated rig by bone names."

    filename_ext = ".fbx"
    filter_glob: bpy.props.StringProperty(default="*.fbx", options={'HIDDEN'})
    filepath: bpy.props.StringProperty(name="File Path", maxlen=1024, default="")
    
    @classmethod
    def poll(cls, context):
        return bpy.data.objects.get('__Rig')
 
    def execute(self, context):
        # check active keying set
        if not bpy.context.scene.keying_sets.active:
            self.report({'ERROR'}, 'There is no active keying set, this is required.')
            return {'FINISHED'}
        
        # import and keep track of what is imported
        objnames_before_import = [x.name for x in bpy.data.objects]
        bpy.ops.import_scene.fbx(filepath=self.properties.filepath)
        objnames_imported = [x.name for x in bpy.data.objects if x.name not in objnames_before_import]
        
        def clear_imported():
            bpy.ops.object.mode_set(mode='OBJECT')
            for obj in bpy.data.objects:
                obj.select_set(obj.name in objnames_imported)
            bpy.ops.object.delete()
        
        # check that there's only 1 armature
        armatures_imported = [x for x in bpy.data.objects if x.type == 'ARMATURE' and x.name in objnames_imported]
        if len(armatures_imported) != 1:
            self.report({'ERROR'}, 'Imported file contains {:d} armatures, expected 1.'.format(len(armatures_imported)))
            clear_imported()
            return {'FINISHED'}
        
        ao_imp = armatures_imported[0]
        
        err_mappings = get_mapping_error_bones(bpy.data.objects['__Rig'], ao_imp)
        if len(err_mappings) > 0:
            self.report({'ERROR'}, 'Cannot map rig, the following bones are missing from the source rig: {}.'.format(', '.join(err_mappings)))
            clear_imported()
            return {'FINISHED'}
        
        print(dir(bpy.context.scene))
        bpy.context.view_layer.objects.active = ao_imp
        
        # check that the ao contains anim data
        if not ao_imp.animation_data or not ao_imp.animation_data.action or not ao_imp.animation_data.action.fcurves:
            self.report({'ERROR'}, 'Imported armature contains no animation data.')
            clear_imported()
            return {'FINISHED'}
        
        # get keyframes + boundary timestamps
        fcurves = ao_imp.animation_data.action.fcurves
        kp_frames = []
        for key in fcurves:
            kp_frames += [kp.co.x for kp in key.keyframe_points]
        if len(kp_frames) <= 0:
            self.report({'ERROR'}, 'Imported armature contains no keyframes.')
            clear_imported()
            return {'FINISHED'}
        
        # set frame range
        bpy.context.scene.frame_start = math.floor(min(kp_frames))
        bpy.context.scene.frame_end = math.ceil(max(kp_frames))
        
        # for the imported rig, apply ao transforms
        apply_ao_transform(ao_imp)
        
        prepare_for_kf_map()
        
        # actually copy state
        copy_anim_state(bpy.data.objects['__Rig'], ao_imp)
        
        clear_imported()
        return {'FINISHED'}    
 
    def invoke(self, context, event):
        context.window_manager.fileselect_add(self)
        return {'RUNNING_MODAL'}    

class OBJECT_OT_ApplyTransform(bpy.types.Operator):
    bl_label = "Apply armature object transform to the root bone for each keyframe"
    bl_idname = "object.rbxanims_applytransform"
    bl_description = "Apply armature object transform to the root bone for each keyframe -- Must set a proper frame range first!"

    @classmethod
    def poll(cls, context):
        grig = bpy.data.objects.get('__Rig')
        return grig and bpy.context.active_object and bpy.context.active_object.animation_data
 
    def execute(self, context):
        if not bpy.context.scene.keying_sets.active:
            self.report({'ERROR'}, 'There is no active keying set, this is required.')
            return {'FINISHED'}
        
        apply_ao_transform(bpy.context.view_layer.objects.active)

        return {'FINISHED'} 

class OBJECT_OT_MapKeyframes(bpy.types.Operator):
    bl_label = "Map keyframes by bone name"
    bl_idname = "object.rbxanims_mapkeyframes"
    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!"

    @classmethod
    def poll(cls, context):
        grig = bpy.data.objects.get('__Rig')
        return grig and bpy.context.active_object and bpy.context.active_object != grig
 
    def execute(self, context):
        if not bpy.context.scene.keying_sets.active:
            self.report({'ERROR'}, 'There is no active keying set, this is required.')
            return {'FINISHED'}
        
        ao_imp = bpy.context.scene.objects.active
        
        err_mappings = get_mapping_error_bones(bpy.data.objects['__Rig'], ao_imp)
        if len(err_mappings) > 0:
            self.report({'ERROR'}, 'Cannot map rig, the following bones are missing from the source rig: {}.'.format(', '.join(err_mappings)))
            return {'FINISHED'}
        
        prepare_for_kf_map()
        
        copy_anim_state(bpy.data.objects['__Rig'], ao_imp)

        return {'FINISHED'} 

class OBJECT_OT_Bake(bpy.types.Operator):
    bl_label = "Bake"
    bl_idname = "object.rbxanims_bake"
    bl_description = "Bake animation for export"
 
    def execute(self, context):
        serialized = serialize()
        encoded = json.dumps(serialized, separators=(',',':'))
        bpy.context.window_manager.clipboard = (base64.b64encode(zlib.compress(encoded.encode(), 9))).decode('utf-8')
        self.report({'INFO'}, 'Baked animation data exported to the system clipboard ({:d} keyframes, {:.2f} seconds).'.format(len(serialized['kfs']), serialized['t']))
        return {'FINISHED'}

class OBJECT_PT_RbxAnimations(bpy.types.Panel):
    bl_label = "Rbx Animations"
    bl_idname = "OBJECT_PT_RbxAnimations"
    bl_category = "Rbx Animations"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'

    @classmethod
    def poll(cls, context):
        return bpy.data.objects.get('__RigMeta')

    def draw(self, context):
        layout = self.layout
        layout.use_property_split = True
        obj = context.object

        layout.label(text="Rigging:")
        layout.operator("object.rbxanims_genrig", text="Rebuild rig")
        layout.label(text="Quick inverse kinematics:")
        layout.operator("object.rbxanims_genik", text="Create IK constraints")
        layout.operator("object.rbxanims_removeik", text="Remove IK constraints")
        layout.label(text="Animation import:")
        layout.operator("object.rbxanims_importfbxanimation", text="Import FBX")
        layout.operator("object.rbxanims_mapkeyframes", text="Map keyframes by bone name")
        layout.operator("object.rbxanims_applytransform", text="Apply armature transform")
        layout.label(text="Export:")
        layout.operator("object.rbxanims_bake", text="Export animation", icon='RENDER_ANIMATION')

def file_import_extend(self, context):
    self.layout.operator("object.rbxanims_importmodel", text="[Rbx Animations] Rig import (.obj)")

bl_info = {"name": "Rbx Animations", "category": "Animation", "blender": (2, 80, 0)}

module_classes = [
    OBJECT_OT_ImportModel,
    OBJECT_OT_GenRig,
    OBJECT_OT_GenIK,
    OBJECT_OT_RemoveIK,
    OBJECT_OT_ImportFbxAnimation,
    OBJECT_OT_ApplyTransform,
    OBJECT_OT_MapKeyframes,
    OBJECT_OT_Bake,
    OBJECT_PT_RbxAnimations,
]

register_classes, unregister_classes = bpy.utils.register_classes_factory(module_classes)

def register():
    register_classes()
    bpy.types.TOPBAR_MT_file_import.append(file_import_extend)

def unregister():
    unregister_classes()
    bpy.types.TOPBAR_MT_file_import.remove(file_import_extend)
    
if __name__ == "__main__":
    register()