Blenderscript Py
Blenderscript Py
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?
# 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()
statel = mat_to_cf(bone_transform)
if cf_round:
statel = list(map(lambda x: round(x, cf_round_fac), statel)) #
compresses result
return state
tail_bone.constraints.remove(constraint)
bpy.ops.object.mode_set(mode='EDIT')
util_bone[0].data.edit_bones.remove(util_bone[0].data.edit_bones[util_bone[1]])
bpy.ops.object.mode_set(mode='POSE')
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
mat = cf_to_mat(rigsubdef['transform'])
bone["transform"] = mat
bone_dir = (transform_to_blender @ mat).to_3x3().to_4x4() @ Vector((0, 0, 1))
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))
next_connect_to_parent = True
else:
bone.tail = bone.head + (bone.head - neutral_pos) * -2
# 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
# renames parts to whatever the metadata defines, mostly just for user-friendlyness
(not required)
def autoname_parts(partnames, basename):
indexmatcher = re.compile(basename + '(\d+)(\.\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]
# 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.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
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)
bpy.context.view_layer.objects.active = target
bpy.ops.object.mode_set(mode='POSE')
root = target.pose.bones['HumanoidRootPart']
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
# reset ao transform
ao.matrix_basis = Matrix.Identity(4)
bpy.context.evaluated_depsgraph_get().update()
## UI/OPERATOR STUFF ##
filename_ext = ".obj"
filter_glob: bpy.props.StringProperty(default="*.obj", options={'HIDDEN'})
filepath: bpy.props.StringProperty(name="File Path", maxlen=1024, default="")
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)
meta_loaded = json.loads(meta)
autoname_parts(meta_loaded['parts'], meta_loaded['rigName'])
return {'FINISHED'}
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
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"
@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
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
return {'FINISHED'}
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 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()
ao_imp = armatures_imported[0]
print(dir(bpy.context.scene))
bpy.context.view_layer.objects.active = ao_imp
prepare_for_kf_map()
clear_imported()
return {'FINISHED'}
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
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
ao_imp = bpy.context.scene.objects.active
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"
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')
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')
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()