2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,36 @@
if "bpy" not in locals():
import bpy
from . import library_manager # has to be first, to add the lib folder to the sys.path
from . import receiver
from . import animations
from . import animation_lists
from . import utils
from . import state_manager
from . import icon_manager
from . import recorder
from . import retargeting
from . import detection_manager
from . import detection_manager_v2
from . import custom_schemes_manager
from . import fbx_patcher
from . import login_manager
from . import live_data_manager
else:
import importlib
importlib.reload(library_manager)
importlib.reload(receiver)
importlib.reload(animations)
importlib.reload(animation_lists)
importlib.reload(utils)
importlib.reload(state_manager)
importlib.reload(icon_manager)
importlib.reload(recorder)
importlib.reload(retargeting)
importlib.reload(detection_manager)
importlib.reload(detection_manager_v2)
importlib.reload(custom_schemes_manager)
importlib.reload(fbx_patcher)
importlib.reload(login_manager)
importlib.reload(live_data_manager)
@@ -0,0 +1,278 @@
from mathutils import Quaternion
from collections import OrderedDict
from . import animations
# Face shapekeys
face_shapes = [
'eyeBlinkLeft',
'eyeLookDownLeft',
'eyeLookInLeft',
'eyeLookOutLeft',
'eyeLookUpLeft',
'eyeSquintLeft',
'eyeWideLeft',
'eyeBlinkRight',
'eyeLookDownRight',
'eyeLookInRight',
'eyeLookOutRight',
'eyeLookUpRight',
'eyeSquintRight',
'eyeWideRight',
'jawForward',
'jawLeft',
'jawRight',
'jawOpen',
'mouthClose',
'mouthFunnel',
'mouthPucker',
'mouthLeft',
'mouthRight',
'mouthSmileLeft',
'mouthSmileRight',
'mouthFrownLeft',
'mouthFrownRight',
'mouthDimpleLeft',
'mouthDimpleRight',
'mouthStretchLeft',
'mouthStretchRight',
'mouthRollLower',
'mouthRollUpper',
'mouthShrugLower',
'mouthShrugUpper',
'mouthPressLeft',
'mouthPressRight',
'mouthLowerDownLeft',
'mouthLowerDownRight',
'mouthUpperUpLeft',
'mouthUpperUpRight',
'browDownLeft',
'browDownRight',
'browInnerUp',
'browOuterUpLeft',
'browOuterUpRight',
'cheekPuff',
'cheekSquintLeft',
'cheekSquintRight',
'noseSneerLeft',
'noseSneerRight',
'tongueOut'
]
# Tpose from Studio live
actor_bones = OrderedDict()
actor_bones['hip'] = Quaternion((-1.0, 0.0, -0.0, 0.0))
actor_bones['spine'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
actor_bones['chest'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
actor_bones['neck'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
actor_bones['head'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
actor_bones['leftShoulder'] = Quaternion((-0.70711, 0.0, 0.0, 0.70711))
actor_bones['leftUpperArm'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
actor_bones['leftLowerArm'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
actor_bones['leftHand'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
actor_bones['rightShoulder'] = Quaternion((0.70711, 0.0, -0.0, 0.70711))
actor_bones['rightUpperArm'] = Quaternion((0.5, 0.5, -0.5, 0.5))
actor_bones['rightLowerArm'] = Quaternion((0.5, 0.5, -0.5, 0.5))
actor_bones['rightHand'] = Quaternion((0.5, 0.5, -0.5, 0.5))
actor_bones['leftUpLeg'] = Quaternion((0.70711, -0.0, 0.70711, -0.0))
actor_bones['leftLeg'] = Quaternion((0.70711, -0.0, 0.70711, 0.0))
actor_bones['leftFoot'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
actor_bones['leftToe'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
# actor_bones['leftToeEnd'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
actor_bones['rightUpLeg'] = Quaternion((0.70711, -0.0, -0.70711, 0.0))
actor_bones['rightLeg'] = Quaternion((0.70711, -0.0, -0.70711, 0.0))
actor_bones['rightFoot'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
actor_bones['rightToe'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
# actor_bones['rightToeEnd'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
# actor_bones['leftThumbProximal'] = Quaternion((-0.0923, -0.56098, -0.70106, 0.43046))
# actor_bones['leftThumbMedial'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
# actor_bones['leftThumbDistal'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
# # actor_bones['leftThumbTip'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
#
# actor_bones['leftIndexProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# actor_bones['leftIndexMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# actor_bones['leftIndexDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# # actor_bones['leftIndexTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
#
# actor_bones['leftMiddleProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# actor_bones['leftMiddleMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# actor_bones['leftMiddleDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# # actor_bones['leftMiddleTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
#
# actor_bones['leftRingProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# actor_bones['leftRingMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# actor_bones['leftRingDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# # actor_bones['leftRingTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
#
# actor_bones['leftLittleProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# actor_bones['leftLittleMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# actor_bones['leftLittleDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# # actor_bones['leftLittleTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
#
# actor_bones['rightThumbProximal'] = Quaternion((0.0923, 0.56099, -0.70106, 0.43046))
# actor_bones['rightThumbMedial'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
# actor_bones['rightThumbDistal'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
# # actor_bones['rightThumbTip'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
#
# actor_bones['rightIndexProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# actor_bones['rightIndexMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# actor_bones['rightIndexDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# # actor_bones['rightIndexTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
#
# actor_bones['rightMiddleProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# actor_bones['rightMiddleMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# actor_bones['rightMiddleDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# # actor_bones['rightMiddleTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
#
# actor_bones['rightRingProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# actor_bones['rightRingMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# actor_bones['rightRingDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# # actor_bones['rightRingTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
#
# actor_bones['rightLittleProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# actor_bones['rightLittleMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# actor_bones['rightLittleDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# # actor_bones['rightLittleTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones = OrderedDict()
glove_bones['hip'] = Quaternion((-1.0, 0.0, -0.0, 0.0))
glove_bones['spine'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
glove_bones['chest'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
glove_bones['neck'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
glove_bones['head'] = Quaternion((-0.0, -0.0, 0.0, -1.0))
glove_bones['leftShoulder'] = Quaternion((-0.70711, 0.0, 0.0, 0.70711))
glove_bones['leftUpperArm'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftLowerArm'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftHand'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['rightShoulder'] = Quaternion((0.70711, 0.0, -0.0, 0.70711))
glove_bones['rightUpperArm'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightLowerArm'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightHand'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['leftUpLeg'] = Quaternion((0.70711, -0.0, 0.70711, -0.0))
glove_bones['leftLeg'] = Quaternion((0.70711, -0.0, 0.70711, 0.0))
glove_bones['leftFoot'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
glove_bones['leftToe'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
# glove_bones['leftToeEnd'] = Quaternion((0.0, -0.0, 0.70711, -0.70711))
glove_bones['rightUpLeg'] = Quaternion((0.70711, -0.0, -0.70711, 0.0))
glove_bones['rightLeg'] = Quaternion((0.70711, -0.0, -0.70711, 0.0))
glove_bones['rightFoot'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
glove_bones['rightToe'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
# glove_bones['rightToeEnd'] = Quaternion((0.0, 0.0, -0.70711, 0.70711))
glove_bones['leftThumbProximal'] = Quaternion((-0.0923, -0.56098, -0.70106, 0.43046))
glove_bones['leftThumbMedial'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
glove_bones['leftThumbDistal'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
# glove_bones['leftThumbTip'] = Quaternion((-0.2706, -0.65328, -0.65328, 0.2706))
glove_bones['leftIndexProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftIndexMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftIndexDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# glove_bones['leftIndexTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftMiddleProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftMiddleMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftMiddleDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# glove_bones['leftMiddleTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftRingProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftRingMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftRingDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# glove_bones['leftRingTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftLittleProximal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftLittleMedial'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['leftLittleDistal'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
# glove_bones['leftLittleTip'] = Quaternion((-0.5, -0.5, -0.5, 0.5))
glove_bones['rightThumbProximal'] = Quaternion((0.0923, 0.56099, -0.70106, 0.43046))
glove_bones['rightThumbMedial'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
glove_bones['rightThumbDistal'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
# glove_bones['rightThumbTip'] = Quaternion((0.2706, 0.65328, -0.65328, 0.2706))
glove_bones['rightIndexProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightIndexMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightIndexDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# glove_bones['rightIndexTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightMiddleProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightMiddleMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightMiddleDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# glove_bones['rightMiddleTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightRingProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightRingMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightRingDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# glove_bones['rightRingTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightLittleProximal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightLittleMedial'] = Quaternion((0.5, 0.5, -0.5, 0.5))
glove_bones['rightLittleDistal'] = Quaternion((0.5, 0.5, -0.5, 0.5))
# glove_bones['rightLittleTip'] = Quaternion((0.5, 0.5, -0.5, 0.5))
def get_bones(with_gloves=True):
"""
Return all bones with their default positions. This represents the exact default position of the Studio reference avatar
:param with_gloves: Determines if the hand bones should be included or not
:return: dictionary with bone names and default positions
"""
# TODO: Remove redundant entries and create dicts at the start of the plugin
return glove_bones if with_gloves else actor_bones
# Creates the list of props and trackers for the objects panel
def get_props_trackers(self, context):
choices = [('None', '-None-', 'None')]
for prop in animations.live_data.props:
# 1. Will be returned by context.scene
# 2. Will be shown in lists
# 3. will be shown in the hover description (below description)
prop_name = animations.live_data.get_prop_name(prop)
choices.append((animations.live_data.get_prop_id(prop), prop_name, prop_name))
for tracker in animations.live_data.trackers:
tracker_name = animations.live_data.get_prop_name(tracker, is_tracker=True)
choices.append((animations.live_data.get_prop_id(tracker, is_tracker=True), tracker_name, tracker_name))
return choices
# Creates the list of faces for the objects panel
def get_faces(self, context):
choices = [('None', '-None-', 'None')]
for face in animations.live_data.faces:
# 1. Will be returned by context.scene
# 2. Will be shown in lists
# 3. will be shown in the hover description (below description)
face_id = animations.live_data.get_face_id(face)
choices.append((face_id, face_id, face_id))
return choices
# Creates the list of actors for the objects panel
def get_actors(self, context):
choices = [('None', '-None-', 'None')]
for actor in animations.live_data.actors:
# 1. Will be returned by context.scene
# 2. Will be shown in lists
# 3. will be shown in the hover description (below description)
actor_id = animations.live_data.get_actor_id(actor)
choices.append((actor_id, actor_id, actor_id))
return choices
@@ -0,0 +1,236 @@
import bpy
from mathutils import Quaternion, Matrix
from . import animation_lists, recorder
from .live_data_manager import LiveData
live_data: LiveData = LiveData()
def clear_animations():
live_data.clear_data()
def animate():
for obj in bpy.data.objects:
# Animate all trackers and props
if live_data.props or live_data.trackers:
animate_tracker_prop(obj)
# Animate all faces
if obj.type == 'MESH' and live_data.faces:
animate_face(obj)
# Animate all actors
elif obj.type == 'ARMATURE':
if live_data.actors:
animate_actor(obj)
def animate_tracker_prop(obj):
if not obj.rsl_animations_props_trackers or obj.rsl_animations_props_trackers == 'None':
return
# Get prop
prop = live_data.get_prop_by_obj(obj)
if not prop:
return
# Get the scene scaling
scene_scale = bpy.context.scene.rsl_scene_scaling
if obj.rsl_use_custom_scale:
scene_scale = obj.rsl_custom_scene_scale
# Set the transforms of the object
obj.rotation_mode = 'QUATERNION'
obj.location = pos_studio_to_blender(
prop['position']['x'] * scene_scale,
prop['position']['y'] * scene_scale,
prop['position']['z'] * scene_scale,
)
obj.rotation_quaternion = rot_studio_to_blender(
prop['rotation']['w'],
prop['rotation']['x'],
prop['rotation']['y'],
prop['rotation']['z'],
)
# Record data
if bpy.context.scene.rsl_recording:
recorder.record_object(live_data.timestamp, obj.name, obj.rotation_quaternion, obj.location)
def animate_face(obj):
if not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
return
if not obj.rsl_animations_faces or obj.rsl_animations_faces == 'None':
return
# Get the face live data
face = live_data.get_face_by_obj(obj)
if not face:
return
# Set each assigned shapekey to the value of it's according live data value
for shapekey_name in animation_lists.face_shapes:
# Get assigned shapekey
shapekey = obj.data.shape_keys.key_blocks.get(getattr(obj, 'rsl_face_' + shapekey_name))
if shapekey:
shapekey.slider_min = -1
shapekey.value = face[shapekey_name] / 100
if bpy.context.scene.rsl_recording:
# shapekey.keyframe_insert(data_path='value', group=obj.name)
recorder.record_face(live_data.timestamp, obj.name, shapekey_name, shapekey.value)
def animate_actor(obj):
# Return if no actor is assigned to this object
if not obj.rsl_animations_actors or obj.rsl_animations_actors == 'None':
return
# Get the actor data assigned to the object
actor = live_data.get_actor_by_obj(obj)
if not actor:
return
# Get current custom data from this object
# The models t-pose bone rotations and locations, which are set by the user, are stored inside this custom data
custom_data = obj.get('CUSTOM')
if not custom_data:
# print('NO CUSTOM DATA')
return
# Get tpose data from custom data
tpose_bones = custom_data.get('rsl_tpose_bones')
if not tpose_bones:
# print('NO TPOSE DATA')
return
# Go over every mapped bone and animate it
# bone_name: Name if the bone
# studio_reference_tpose_rot: Studios reference t-pose rotation (still in Studio space)
for bone_name, studio_reference_tpose_rot in animation_lists.get_bones(with_gloves=live_data.has_gloves(actor)).items():
# Gets the name of the bone assigned to this bone live data
bone_name_assigned = getattr(obj, 'rsl_actor_' + bone_name)
# Gets the assigned pose bone and it's tpose data set by the user
bone = obj.pose.bones.get(bone_name_assigned)
bone_data = obj.data.bones.get(bone_name_assigned)
bone_tpose_data = tpose_bones.get(bone_name_assigned)
try:
actor_bone_data = actor[bone_name] if live_data.version <= 2 else actor['body'][bone_name]
except KeyError:
print('Bone not found in live data:', bone_name)
continue
# Skip if there is no bone assigned to this live data or if there is no tpose data for this bone
if not bone or not bone_tpose_data:
continue
# Set the bones rotation mode to euler and disable inherit rotation
if bone.rotation_mode == 'QUATERNION':
bone.rotation_mode = 'XYZ'
bone_data.use_inherit_rotation = False
# The global rotation of the models t-pose, which was set by the user
bone_tpose_rot_global = Quaternion(bone_tpose_data['rotation_global'])
# The new pose in which the bone should be (still in Studio space)
studio_new_pose = Quaternion((
float(actor_bone_data['rotation']['w']),
float(actor_bone_data['rotation']['x']),
float(actor_bone_data['rotation']['y']),
float(actor_bone_data['rotation']['z']),
))
# Function to convert from Studio to Blender space
def rot_to_blender(rot):
return Quaternion((
rot.w,
rot.x,
-rot.y,
-rot.z,
)) @ Quaternion((0, 0, 0, 1))
mat_obj = obj.matrix_local.decompose()[1].to_matrix().to_4x4()
mat_default = Matrix((
(1, 0, 0, 0),
(0, 0, -1, 0),
(0, 1, 0, 0),
(0, 0, 0, 1)
))
rot_transform = (mat_default.inverted() @ mat_obj).to_quaternion()
def transform(rot):
return rot_transform @ rot
def transform_back(rot):
return rot_transform.inverted() @ rot
# Transform rotation matrix of tpose to target space
bone_tpose_rot_global = transform(bone_tpose_rot_global)
# Calculate bone offset from tpose and add it to live data rotation
rot_offset_ref = rot_to_blender(studio_reference_tpose_rot).inverted() @ bone_tpose_rot_global
final_rot = rot_to_blender(studio_new_pose) @ rot_offset_ref
# Transform rotation matrix back from target space
final_rot = transform_back(final_rot)
# Set new bone rotation
orig_loc, _, _ = bone.matrix.decompose()
orig_loc_mat = Matrix.Translation(orig_loc)
rotation_mat = final_rot.to_matrix().to_4x4()
# Set final bone matrix
bone.matrix = orig_loc_mat @ rotation_mat
# If hips bone, set its position
if bone_name == 'hip':
# Get correct space of hips location
axis = 0
multiplier = 1
if round(mat_obj[2][0], 0) == round(mat_obj[2][2], 0) == 0:
axis = 1
multiplier = mat_obj[2][1]
if round(mat_obj[2][0], 0) == round(mat_obj[2][1], 0) == 0:
axis = 2
multiplier = mat_obj[2][2]
# Get scale of studio model
studio_hip_height = actor.get('hipHeight') if live_data.version <= 2 else actor.get('dimensions').get('hipHeight')
if not studio_hip_height:
studio_hip_height = 1
tpose_hip_location_y = bone_tpose_data['location_object'][axis] * multiplier
location_new = pos_hips_studio_to_blender(
actor_bone_data['position']['x'] * tpose_hip_location_y / studio_hip_height,
actor_bone_data['position']['y'] * tpose_hip_location_y - tpose_hip_location_y * studio_hip_height,
actor_bone_data['position']['z'] * tpose_hip_location_y / studio_hip_height)
bone.location = location_new
# Record the data
if bpy.context.scene.rsl_recording:
recorder.record_bone(live_data.timestamp, obj.name, bone_name_assigned, bone.rotation_euler, location=bone.location if bone_name == 'hip' else None)
def animate_glove(obj):
pass
def pos_hips_studio_to_blender(x, y, z):
return -x, y, z
def pos_studio_to_blender(x, y, z):
return -x, -z, y
def rot_studio_to_blender(w, x, y, z):
return w, x, z, -y
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,55 @@
shape_list = {
'eyeBlinkLeft': [],
'eyeLookDownLeft': [],
'eyeLookInLeft': [],
'eyeLookOutLeft': [],
'eyeLookUpLeft': [],
'eyeSquintLeft': [],
'eyeWideLeft': [],
'eyeBlinkRight': [],
'eyeLookDownRight': [],
'eyeLookInRight': [],
'eyeLookOutRight': [],
'eyeLookUpRight': [],
'eyeSquintRight': [],
'eyeWideRight': [],
'jawForward': [],
'jawLeft': [],
'jawRight': [],
'jawOpen': [],
'mouthClose': [],
'mouthFunnel': [],
'mouthPucker': [],
'mouthLeft': [],
'mouthRight': [],
'mouthSmileLeft': [],
'mouthSmileRight': [],
'mouthFrownLeft': [],
'mouthFrownRight': [],
'mouthDimpleLeft': [],
'mouthDimpleRight': [],
'mouthStretchLeft': [],
'mouthStretchRight': [],
'mouthRollLower': [],
'mouthRollUpper': [],
'mouthShrugLower': [],
'mouthShrugUpper': [],
'mouthPressLeft': [],
'mouthPressRight': [],
'mouthLowerDownLeft': [],
'mouthLowerDownRight': [],
'mouthUpperUpLeft': [],
'mouthUpperUpRight': [],
'browDownLeft': [],
'browDownRight': [],
'browInnerUp': [],
'browOuterUpLeft': [],
'browOuterUpRight': [],
'cheekPuff': [],
'cheekSquintLeft': [],
'cheekSquintRight': [],
'noseSneerLeft': [],
'noseSneerRight': [],
'tongueOut': [],
}
@@ -0,0 +1,237 @@
import os
import bpy
import json
import pathlib
from . import retargeting
from . import detection_manager
main_dir = str(pathlib.Path(os.path.dirname(__file__)).parent.resolve())
resources_dir = os.path.join(main_dir, "resources")
custom_bones_dir = os.path.join(resources_dir, "custom_bones")
custom_bone_list_file = os.path.join(custom_bones_dir, "custom_bone_list.json")
def save_retargeting_to_list():
armature_target = retargeting.get_target_armature()
retargeting_dict = detection_manager.detect_retarget_bones()
for bone_item in bpy.context.scene.rsl_retargeting_bone_list:
if not bone_item.bone_name_source or not bone_item.bone_name_target:
continue
bone_name_key = bone_item.bone_name_key
bone_name_source = bone_item.bone_name_source.lower()
bone_name_target = bone_item.bone_name_target.lower()
bone_name_target_detected, bone_name_key_detected = retargeting_dict[bone_item.bone_name_source]
if bone_name_target_detected == bone_item.bone_name_target:
continue
if bone_name_key_detected and bone_name_key_detected != 'spine':
if not detection_manager.bone_detection_list_custom.get(bone_name_key_detected):
detection_manager.bone_detection_list_custom[bone_name_key_detected] = []
# TODO Idea: If a target bone got detected but was removed and left empty, add it to an ignore list. So if that exact match-up gets detected again, leave it empty
# If the detected target is in the custom bones list but it got changed, remove it from the list. If the new bone gets detected automatically now, don't add it to the custom list
if bone_name_target_detected.lower() in detection_manager.bone_detection_list_custom[bone_name_key_detected]:
if bone_name_key_detected.startswith('custom_bone_') and len(detection_manager.bone_detection_list_custom[bone_name_key_detected]) == 2:
detection_manager.bone_detection_list_custom.pop(bone_name_key_detected)
else:
detection_manager.bone_detection_list_custom[bone_name_key_detected].remove(bone_name_target_detected.lower())
# Update the bone detection list in order to correctly figure out if the new selected bone needs to be saved
detection_manager.bone_detection_list = detection_manager.combine_lists(detection_manager.bone_detection_list_unmodified, detection_manager.bone_detection_list_custom)
retargeting_dict = detection_manager.detect_retarget_bones()
bone_name_detected_new, _ = retargeting_dict[bone_item.bone_name_source]
if bone_name_detected_new.lower() == bone_name_target:
# print('No need to add new bone to save')
continue
# If the source bone got detected but the target bone got changed, save the target bone into the custom list
if bone_name_target not in detection_manager.bone_detection_list_custom[bone_name_key_detected]:
detection_manager.bone_detection_list_custom[bone_name_key_detected] = [bone_name_target] + detection_manager.bone_detection_list_custom[bone_name_key_detected]
continue
# If it is a completely new pair of bones or a spine bone, add it as a new bone to the list
detection_manager.bone_detection_list_custom['custom_bone_' + bone_name_source] = [bone_name_source, bone_name_target]
# Save the updated custom list locally and update
save_to_file_and_update()
def save_live_data_bone_to_list(bone_key, bone_name, bone_name_previous):
if not detection_manager.bone_detection_list_custom.get(bone_key):
detection_manager.bone_detection_list_custom[bone_key] = []
# If the previously detected bone name is in the custom bones list but it got changed, remove it from the list. If the new bone gets detected automatically now, don't add it to the custom list
if bone_name_previous.lower() in detection_manager.bone_detection_list_custom[bone_key]:
detection_manager.bone_detection_list_custom[bone_key].remove(bone_name_previous.lower())
# print('Removed:', bone_name_previous)
# Update the bone detection list in order to correctly figure out if the new selected bone needs to be saved
detection_manager.bone_detection_list = detection_manager.combine_lists(detection_manager.bone_detection_list_unmodified, detection_manager.bone_detection_list_custom)
bone_name_detected_new = detection_manager.detect_bone(bpy.context.active_object, bone_key)
if bone_name_detected_new == bone_name:
# print('No need to add new bone to save')
return
detection_manager.bone_detection_list_custom[bone_key] = [bone_name] + detection_manager.bone_detection_list_custom[bone_key]
def save_live_data_shape_to_list(shape_key, shape_name, shape_name_previous):
if not detection_manager.shape_detection_list_custom.get(shape_key):
detection_manager.shape_detection_list_custom[shape_key] = []
# If the previously detected shape name is in the custom shapes list but it got changed, remove it from the list. If the new shapekey gets detected automatically now, don't add it to the custom list
if shape_name_previous.lower() in detection_manager.shape_detection_list_custom[shape_key]:
detection_manager.shape_detection_list_custom[shape_key].remove(shape_name_previous.lower())
# print('Removed:', shape_name_previous)
# Update the shapekey detection list in order to correctly figure out if the new selected shapekey needs to be saved
detection_manager.shape_detection_list = detection_manager.combine_lists(detection_manager.shape_detection_list_unmodified, detection_manager.shape_detection_list_custom)
shape_name_detected_new = detection_manager.detect_shape(bpy.context.active_object, shape_key)
if shape_name_detected_new == shape_name:
# print('No need to add new bone to save')
return
detection_manager.shape_detection_list_custom[shape_key] = [shape_name] + detection_manager.shape_detection_list_custom[shape_key]
def save_to_file_and_update():
save_custom_to_file()
detection_manager.load_detection_lists()
def save_custom_to_file(file_path=custom_bone_list_file):
new_custom_list = clean_custom_list()
print('To File:', new_custom_list)
if not os.path.isdir(custom_bones_dir):
os.mkdir(custom_bones_dir)
with open(file_path, 'w', encoding="utf8") as outfile:
json.dump(new_custom_list, outfile, ensure_ascii=False, indent=4)
def load_custom_lists_from_file(file_path=custom_bone_list_file):
custom_bone_list = {}
try:
with open(file_path, encoding="utf8") as file:
custom_bone_list = json.load(file)
except FileNotFoundError:
print('Custom bone list not found.')
except json.decoder.JSONDecodeError:
print("Custom bone list is not a valid json file!")
if custom_bone_list.get('rokoko_custom_names') is None or custom_bone_list.get('version') is None or custom_bone_list.get('bones') is None or custom_bone_list.get('shapes') is None:
print("Custom name list file is not a valid name list file")
return {}, {}
custom_bone_list.pop('rokoko_custom_names')
custom_bone_list.pop('version')
return custom_bone_list.get('bones'), custom_bone_list.get('shapes')
def clean_custom_list():
new_custom_list = {
'rokoko_custom_names': True,
'version': 1,
'bones': {},
'shapes': {}
}
new_bone_list = {}
new_shape_list = {}
# Remove all empty fields and make all custom fields lowercase
for key, values in detection_manager.bone_detection_list_custom.items():
if not values:
continue
for i in range(len(values)):
values[i] = values[i].lower()
new_bone_list[key] = values
# Remove all empty fields and make all custom fields lowercase
for key, values in detection_manager.shape_detection_list_custom.items():
if not values:
continue
for i in range(len(values)):
values[i] = values[i].lower()
new_shape_list[key] = values
new_custom_list['bones'] = new_bone_list
new_custom_list['shapes'] = new_shape_list
return new_custom_list
def import_custom_list(directory, file_name):
file_path = os.path.join(directory, file_name)
new_custom_bone_list, new_custom_shape_list = load_custom_lists_from_file(file_path=file_path)
# Merge the new and old custom bone lists
for key, bones in detection_manager.bone_detection_list_custom.items():
if not new_custom_bone_list.get(key):
new_custom_bone_list[key] = []
for bone in new_custom_bone_list[key]:
if bone in bones:
bones.remove(bone)
new_custom_bone_list[key] += bones
# Merge the new and old custom shape lists
for key, shapes in detection_manager.shape_detection_list_custom.items():
if not new_custom_shape_list.get(key):
new_custom_shape_list[key] = []
for shape in new_custom_shape_list[key]:
if shape in shapes:
shapes.remove(shape)
new_custom_shape_list[key] += shapes
detection_manager.bone_detection_list_custom = new_custom_bone_list
detection_manager.shape_detection_list_custom = new_custom_shape_list
def export_custom_list2(directory):
file_path = os.path.join(directory, 'custom_bone_list.json')
i = 1
while os.path.isfile(file_path):
file_path = os.path.join(directory, 'custom_bone_list' + str(i) + '.json')
i += 1
save_custom_to_file(file_path=file_path)
return os.path.basename(file_path)
def export_custom_list(file_path):
if not detection_manager.bone_detection_list_custom and not detection_manager.shape_detection_list_custom:
return None
save_custom_to_file(file_path=file_path)
return os.path.basename(file_path)
def delete_custom_bone_list():
detection_manager.bone_detection_list_custom = {}
save_to_file_and_update()
def delete_custom_shape_list():
detection_manager.shape_detection_list_custom = {}
save_to_file_and_update()
@@ -0,0 +1,386 @@
import os
import bpy
import json
import pathlib
from . import retargeting
from .auto_detect_lists.bones import bone_list, ignore_rokoko_retargeting_bones
from .auto_detect_lists.shapes import shape_list
from .custom_schemes_manager import load_custom_lists_from_file
bone_detection_list = {}
bone_detection_list_unmodified = {}
bone_detection_list_custom = {}
shape_detection_list = {}
shape_detection_list_unmodified = {}
shape_detection_list_custom = {}
main_dir = str(pathlib.Path(os.path.dirname(__file__)).parent.resolve())
resources_dir = os.path.join(main_dir, "resources")
custom_bones_dir = os.path.join(resources_dir, "custom_bones")
custom_bone_list_file = os.path.join(custom_bones_dir, "custom_bone_list.json")
def load_detection_lists():
global bone_detection_list, bone_detection_list_unmodified, bone_detection_list_custom, shape_detection_list, shape_detection_list_unmodified, shape_detection_list_custom
# Create the lists from the internal naming lists
bone_detection_list_unmodified = setup_bone_list(bone_list)
shape_detection_list_unmodified = setup_shape_list()
# Load the custom naming lists from the file
bone_detection_list_custom, shape_detection_list_custom = load_custom_lists_from_file()
# Combine custom and internal lists
bone_detection_list = combine_lists(bone_detection_list_unmodified, bone_detection_list_custom)
shape_detection_list = combine_lists(shape_detection_list_unmodified, shape_detection_list_custom)
# Print the whole bone list
# print_bone_detection_list()
def setup_bone_list(raw_bone_list):
new_bone_list = {}
for bone_key, bone_values in raw_bone_list.items():
# Add the bones to the list if no side indicator is found
if 'left' not in bone_key:
new_bone_list[bone_key] = [bone_value.lower() for bone_value in bone_values]
if bone_key == 'spine':
new_bone_list['chest'] = [bone_value.lower() for bone_value in bone_values]
continue
# Add bones to the list that are two sided
bone_values_left = []
bone_values_right = []
for bone_name in bone_values:
bone_name = bone_name.lower()
if '\l' in bone_name:
for replacement in ['l', 'left', 'r', 'right']:
bone_name_new = bone_name.replace('\l', replacement)
# Debug if duplicates are found
if bone_name_new in bone_values_left or bone_name_new in bone_values_right:
print('Duplicate autodetect bone entry:', bone_name, bone_name_new)
continue
if 'l' in replacement:
bone_values_left.append(bone_name_new)
else:
bone_values_right.append(bone_name_new)
bone_key_left = bone_key
bone_key_right = bone_key.replace('left', 'right')
new_bone_list[bone_key_left] = bone_values_left
new_bone_list[bone_key_right] = bone_values_right
return new_bone_list
def setup_shape_list():
new_shape_list = {}
for shape_key, shape_names in shape_list.items():
new_shape_list[shape_key] = [shape_key.lower()] + [shape_name.lower() for shape_name in shape_names]
return new_shape_list
def combine_lists(internal_list, custom_list):
"""
Creates a combined list with the second list put in first but with the structure of the first list
"""
combined_list = {}
# Set dictionary structure
for key in internal_list.keys():
combined_list[key] = []
# Load in custom values into the dictionary
for key, values in custom_list.items():
combined_list[key] = []
for value in values:
combined_list[key].append(value.lower())
# Load in internal values
for key, values in internal_list.items():
for value in values:
combined_list[key].append(value)
return combined_list
def print_bone_detection_list():
print('BONES')
for key, values in bone_detection_list.items():
print(key, values)
print()
print('CUSTOM BONES')
for key, values in bone_detection_list_custom.items():
print(key, values)
print('--> ', bone_detection_list[key])
print()
# print('SHAPES')
# for key, values in shape_detection_list.items():
# print(key, values)
print('CUSTOM SHAPES')
for key, values in shape_detection_list_custom.items():
print(key, values)
print('--> ', shape_detection_list[key])
print()
print()
# def get_bone_list():
# return bone_detection_list
#
#
# def get_custom_bone_list():
# return bone_detection_list_custom
#
#
# def get_shape_list():
# return shape_detection_list
#
#
# def get_custom_shape_list():
# return shape_detection_list_custom
def standardize_bone_name(name):
# List of chars to replace if they are at the start of a bone name
starts_with = [
('_', ''),
('ValveBiped_', ''),
('Valvebiped_', ''),
('Bip1_', 'Bip_'),
('Bip01_', 'Bip_'),
('Bip001_', 'Bip_'),
('Character1_', ''),
('HLP_', ''),
('JD_', ''),
('JU_', ''),
('Armature|', ''),
('Bone_', ''),
('C_', ''),
('Cf_S_', ''),
('Cf_J_', ''),
('G_', ''),
('Joint_', ''),
('DEF_', ''),
('CC_Base_', ''),
]
# Standardize names
# Make all the underscores!
name = name.replace(' ', '_') \
.replace('-', '_') \
.replace('.', '_') \
.replace('____', '_') \
.replace('___', '_') \
.replace('__', '_') \
# Replace if name starts with specified chars
for replacement in starts_with:
if name.startswith(replacement[0]):
name = replacement[1] + name[len(replacement[0]):]
# Remove digits from the start
name_split = name.split('_')
if len(name_split) > 1 and name_split[0].isdigit():
name = name_split[1]
# Specific condition
name_split = name.split('"')
if len(name_split) > 3:
name = name_split[1]
# Another specific condition
if ':' in name:
for i, split in enumerate(name.split(':')):
if i == 0:
name = ''
else:
name += split
# Remove S0 from the end
if name[-2:] == 'S0':
name = name[:-2]
if name[-4:] == '_Jnt':
name = name[:-4]
return name.lower()
def detect_shape(obj, shape_name_key):
# Go through the target mesh and search for shapekey that fit the main shapekey
found_shape_name = ''
is_custom = False
for shapekey in obj.data.shape_keys.key_blocks:
if is_custom: # If a custom shapekey name was found, stop searching. it has priority
break
if shape_detection_list_custom.get(shape_name_key):
for shape_name_detected in shape_detection_list_custom[shape_name_key]:
if shape_name_detected == shapekey.name.lower():
found_shape_name = shapekey.name
is_custom = True
break
if found_shape_name and shape_name_key != 'chest': # If a shape_name was found, only continue looking for custom shapekey names, they have priority
continue
for shape_name_detected in shape_detection_list[shape_name_key]:
if shape_name_detected == shapekey.name.lower():
found_shape_name = shapekey.name
break
# If nothing was found, check if the shapekey names match exactly
if not found_shape_name and shape_name_key.lower() == shapekey.name.lower():
found_shape_name = shapekey.name
return found_shape_name
def detect_bone(obj, bone_name_key, bone_name_source=None):
# Go through the target armature and search for bones that fit the main source bone
found_bone_name = ''
is_custom = False
if not bone_name_source:
bone_name_source = bone_name_key
for bone in obj.pose.bones:
if is_custom: # If a custom bone name was found, stop searching. it has priority
break
if bone_detection_list_custom.get(bone_name_key):
for bone_name_detected in bone_detection_list_custom[bone_name_key]:
if bone_name_detected == bone.name.lower():
found_bone_name = bone.name
is_custom = True
break
if found_bone_name and bone_name_key != 'chest': # If a bone_name was found, only continue looking for custom bone names, they have priority
continue
for bone_name_detected in bone_detection_list[bone_name_key]:
if bone_name_detected == standardize_bone_name(bone.name):
found_bone_name = bone.name
break
# If nothing was found, check if the bone names match exactly
if not found_bone_name and bone_name_source.lower() == bone.name.lower():
found_bone_name = bone.name
return found_bone_name
def detect_retarget_bones() -> {str: (str, str)}:
"""
Detects all matching bones in the target and source armatures
:return: A dictionary with the source bone name as key and a tuple of the target bone name and their shared key name as value
"""
bone_list_animated = []
retargeting_dict = {}
armature_source = retargeting.get_source_armature()
armature_target = retargeting.get_target_armature()
# Get all source bones from the animation and add them to bone_list_animated
for fc in armature_source.animation_data.action.fcurves:
bone_name = fc.data_path.split('"')
if len(bone_name) == 3 and bone_name[1] not in bone_list_animated:
bone_list_animated.append(bone_name[1])
# Check if this animation is from Rokoko Studio. Ignore certain bones in that case
is_rokoko_animation = False
if 'newton' in bone_list_animated and 'RightFinger1Tip' in bone_list_animated and 'HeadVertex' in bone_list_animated and 'LeftFinger2Metacarpal' in bone_list_animated:
is_rokoko_animation = True
spines_source = []
spines_target = []
found_main_bones = []
# Then add all the bones to the retargeting dictionary
for bone_name in bone_list_animated:
if is_rokoko_animation and bone_name in ignore_rokoko_retargeting_bones:
continue
bone_item_source = bone_name
bone_item_target = ''
main_bone_name = ''
standardized_bone_name_source = standardize_bone_name(bone_name)
# Find the main bone name (bone name key) of the source bone
for bone_main, bone_values in bone_detection_list.items():
if bone_main == 'chest': # Ignore chest bones, these are only used for live data
continue
if bone_main in found_main_bones: # Only find main bones once, except for spines
continue
# If the source bone name is found in the bone detection list, add its main bone name to the list of found main bones
if bone_name.lower() in bone_values or standardized_bone_name_source in bone_values or standardized_bone_name_source == bone_main.lower():
main_bone_name = bone_main
if main_bone_name != 'spine': # Ignore the spine bones for now, so that it can add the custom spine bones first
found_main_bones.append(main_bone_name)
break
# Add the source bone and the main bone name to the retargeting dict with an empty targeting bone name
retargeting_dict[bone_item_source] = ("", main_bone_name)
# If no main bone name was found, continue
if not main_bone_name:
continue
# If it's a spine bone, add it to the list for later fixing
if main_bone_name == 'spine':
spines_source.append(bone_name)
continue
# If it's a custom spine/chest bone, add it to the spine list nonetheless
custom_main_bone = main_bone_name.startswith('custom_bone_')
if custom_main_bone and standardize_bone_name(main_bone_name.replace('custom_bone_', '')) in bone_detection_list['spine']:
spines_source.append(bone_name)
# Go through the target armature and search for bones that fit the main source bone
bone_item_target = detect_bone(armature_target, main_bone_name, bone_name_source=bone_item_source)
# Add the bone to the retargeting list again
retargeting_dict[bone_item_source] = (bone_item_target, main_bone_name)
# Add target spines to list for later fixing
for bone in armature_target.pose.bones:
bone_name_standardized = standardize_bone_name(bone.name)
if bone_name_standardized in bone_detection_list['spine']:
spines_target.append(bone.name)
# Fix spine auto detection
if spines_target and spines_source:
# print(spines_source)
spine_dict = {}
i = 0
for spine in reversed(spines_source):
i += 1
if i == len(spines_target):
break
spine_dict[spine] = spines_target[-i]
spine_dict[spines_source[0]] = spines_target[0]
# Fill in fixed spines into unfilled matches
for spine_source, spine_target in spine_dict.items():
for bone_source, bone_values in retargeting_dict.items():
bone_target, bone_key = bone_values
if bone_source == spine_source and not bone_target:
retargeting_dict[bone_source] = (spine_target, bone_key)
break
return retargeting_dict
@@ -0,0 +1,74 @@
import bpy
from .auto_detect_lists.bones import bone_list, ignore_rokoko_retargeting_bones
from .auto_detect_lists.shapes import shape_list
class DetectionManager:
def __init__(self, name_dict):
self.name_dict_original = name_dict
self.name_dict = {}
self._setup_dict()
def _setup_dict(self):
for key, values in self.name_dict_original.items():
self._add_names(key, values)
def _add_names(self, key, values):
raise NotImplementedError
def print_dict(self):
for key, values in self.name_dict.items():
print(f"{key}: {values}")
class BoneDetectionManager(DetectionManager):
def _add_names(self, key, values):
# Add names to the list without changes if the key is not sided
if "left" not in key:
self.name_dict[key] = [name.lower() for name in values]
if key == "spine":
self.name_dict["chest"] = self.name_dict[key].copy()
return
# Add names to the list with changes if the key is sided
names_left = []
names_right = []
for name in values:
name = name.lower()
if "\l" not in name:
print(f"Warning: {name} from {key} does not contain a '\\l' marker")
continue
for replacement in ['l', 'left', 'r', 'right']:
name_new = name.replace("\l", replacement)
if name_new in names_left or name_new in names_right:
print(f"Warning: {name_new} from {key} is already in the list")
continue
if "l" in replacement:
names_left.append(name_new)
else:
names_right.append(name_new)
self.name_dict[key] = names_left
self.name_dict[key.replace("left", "right")] = names_right
class ShapeDetectionManager(DetectionManager):
def _add_names(self, key, values):
self.name_dict[key] = [key] + [name.lower() for name in values]
# bones = BoneDetectionManager(bone_list)
# bones.print_dict()
# shapes = ShapeDetectionManager(shape_list)
# shapes.print_dict()
@@ -0,0 +1,212 @@
import bpy
import time
import addon_utils
from threading import Thread
if bpy.app.version < (2, 83, 17):
from io_scene_fbx import import_fbx
from io_scene_fbx.import_fbx import blen_read_animations_curves_iter, blen_read_object_transform_do
def start_fbx_patch_timer():
if bpy.app.version >= (2, 83, 17): # This patch is officially accepted in Blender 2.83.17, so don't patch it
return
# Asynchronously start the timer looking for the right time to patch the fbx importer
thread = Thread(target=time_fbx_patch, args=[])
thread.start()
def time_fbx_patch():
# Wait for Blender to finish loading up
found_scene = False
while not found_scene:
if hasattr(bpy.context, 'scene'):
found_scene = True
else:
time.sleep(0.5)
# Enable fbx if it isn't enabled yet
fbx_is_enabled = addon_utils.check('io_scene_fbx')[1]
if not fbx_is_enabled:
addon_utils.enable('io_scene_fbx')
# Patch fbx importer
patch_fbx_importer()
def patch_fbx_importer():
import_fbx.blen_read_animations_action_item = blen_read_animations_action_item_patched
# This is the modified Blender function that will replace the original one
def blen_read_animations_action_item_patched(action, item, cnodes, fps, anim_offset):
"""
'Bake' loc/rot/scale into the action,
taking any pre_ and post_ matrix into account to transform from fbx into blender space.
"""
from bpy.types import Object, PoseBone, ShapeKey, Material, Camera
from itertools import chain
fbx_curves = []
for curves, fbxprop in cnodes.values():
for (fbx_acdata, _blen_data), channel in curves.values():
fbx_curves.append((fbxprop, channel, fbx_acdata))
# Leave if no curves are attached (if a blender curve is attached to scale but without keys it defaults to 0).
if len(fbx_curves) == 0:
return
blen_curves = []
props = []
keyframes = {}
# Add each keyframe to the keyframe dict
def store_keyframe(fc, frame, value):
fc_key = (fc.data_path, fc.array_index)
if not keyframes.get(fc_key):
keyframes[fc_key] = []
keyframes[fc_key].append((frame, value))
if isinstance(item, Material):
grpname = item.name
props = [("diffuse_color", 3, grpname or "Diffuse Color")]
elif isinstance(item, ShapeKey):
props = [(item.path_from_id("value"), 1, "Key")]
elif isinstance(item, Camera):
props = [(item.path_from_id("lens"), 1, "Camera")]
else: # Object or PoseBone:
if item.is_bone:
bl_obj = item.bl_obj.pose.bones[item.bl_bone]
else:
bl_obj = item.bl_obj
# We want to create actions for objects, but for bones we 'reuse' armatures' actions!
grpname = item.bl_obj.name
# Since we might get other channels animated in the end, due to all FBX transform magic,
# we need to add curves for whole loc/rot/scale in any case.
props = [(bl_obj.path_from_id("location"), 3, grpname or "Location"),
None,
(bl_obj.path_from_id("scale"), 3, grpname or "Scale")]
rot_mode = bl_obj.rotation_mode
if rot_mode == 'QUATERNION':
props[1] = (bl_obj.path_from_id("rotation_quaternion"), 4, grpname or "Quaternion Rotation")
elif rot_mode == 'AXIS_ANGLE':
props[1] = (bl_obj.path_from_id("rotation_axis_angle"), 4, grpname or "Axis Angle Rotation")
else: # Euler
props[1] = (bl_obj.path_from_id("rotation_euler"), 3, grpname or "Euler Rotation")
blen_curves = [action.fcurves.new(prop, index=channel, action_group=grpname)
for prop, nbr_channels, grpname in props for channel in range(nbr_channels)]
if isinstance(item, Material):
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
value = [0,0,0]
for v, (fbxprop, channel, _fbx_acdata) in values:
assert(fbxprop == b'DiffuseColor')
assert(channel in {0, 1, 2})
value[channel] = v
for fc, v in zip(blen_curves, value):
store_keyframe(fc, frame, v)
elif isinstance(item, ShapeKey):
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
value = 0.0
for v, (fbxprop, channel, _fbx_acdata) in values:
assert(fbxprop == b'DeformPercent')
assert(channel == 0)
value = v / 100.0
for fc, v in zip(blen_curves, (value,)):
store_keyframe(fc, frame, v)
elif isinstance(item, Camera):
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
value = 0.0
for v, (fbxprop, channel, _fbx_acdata) in values:
assert(fbxprop == b'FocalLength')
assert(channel == 0)
value = v
for fc, v in zip(blen_curves, (value,)):
store_keyframe(fc, frame, v)
else: # Object or PoseBone:
if item.is_bone:
bl_obj = item.bl_obj.pose.bones[item.bl_bone]
else:
bl_obj = item.bl_obj
transform_data = item.fbx_transform_data
rot_eul_prev = bl_obj.rotation_euler.copy()
rot_quat_prev = bl_obj.rotation_quaternion.copy()
# Pre-compute inverted local rest matrix of the bone, if relevant.
restmat_inv = item.get_bind_matrix().inverted_safe() if item.is_bone else None
# Create a dict to store all keyframes in order to add them later all at once
keyframes = {}
for frame, values in blen_read_animations_curves_iter(fbx_curves, anim_offset, 0, fps):
for v, (fbxprop, channel, _fbx_acdata) in values:
if fbxprop == b'Lcl Translation':
transform_data.loc[channel] = v
elif fbxprop == b'Lcl Rotation':
transform_data.rot[channel] = v
elif fbxprop == b'Lcl Scaling':
transform_data.sca[channel] = v
mat, _, _ = blen_read_object_transform_do(transform_data)
# compensate for changes in the local matrix during processing
if item.anim_compensation_matrix:
mat = mat @ item.anim_compensation_matrix
# apply pre- and post matrix
# post-matrix will contain any correction for lights, camera and bone orientation
# pre-matrix will contain any correction for a parent's correction matrix or the global matrix
if item.pre_matrix:
mat = item.pre_matrix @ mat
if item.post_matrix:
mat = mat @ item.post_matrix
# And now, remove that rest pose matrix from current mat (also in parent space).
if restmat_inv:
mat = restmat_inv @ mat
# Now we have a virtual matrix of transform from AnimCurves, we can insert keyframes!
loc, rot, sca = mat.decompose()
if rot_mode == 'QUATERNION':
if rot_quat_prev.dot(rot) < 0.0:
rot = -rot
rot_quat_prev = rot
elif rot_mode == 'AXIS_ANGLE':
vec, ang = rot.to_axis_angle()
rot = ang, vec.x, vec.y, vec.z
else: # Euler
rot = rot.to_euler(rot_mode, rot_eul_prev)
rot_eul_prev = rot
# Add each keyframe and its value to the keyframe dict
for fc, value in zip(blen_curves, chain(loc, rot, sca)):
store_keyframe(fc, frame, value)
# Add all keyframe points to the fcurves at once and modify them after
for fc_key, key_values in keyframes.items():
data_path, index = fc_key
# Add all keyframe points at once
fcurve = action.fcurves.find(data_path=data_path, index=index)
num_keys = len(key_values)
fcurve.keyframe_points.add(num_keys)
# Apply values to each keyframe point
for kf_point, v in zip(fcurve.keyframe_points, key_values):
kf_point.co = v
kf_point.interpolation = 'LINEAR'
# Since we inserted our keyframes in 'FAST' mode, we have to update the fcurves now.
for fc in blen_curves:
fc.update()
@@ -0,0 +1,53 @@
import os
import pathlib
from enum import Enum
from bpy.utils import previews
icons = None
class Icons(Enum):
FACE = 'FACE'
SUIT = 'SUIT'
VP = 'VP'
PAIRED = 'PAIRED'
ROKOKO = 'ROKOKO'
START_RECORDING = 'RECORD'
STOP_RECORDING = 'STOP'
RESTART = 'RESTART'
CALIBRATE = 'CALIBRATE'
STUDIO_LIVE_LOGO = 'STUDIO_LIVE_LOGO'
def get_icon(self):
return icons.get(self.value).icon_id
def load_icons():
# Path to the icons folder
# The path is calculated relative to this py file inside the addon folder
main_dir = pathlib.Path(os.path.dirname(__file__)).parent.resolve()
resources_dir = os.path.join(str(main_dir), "resources")
icons_dir = os.path.join(resources_dir, "icons")
pcoll = previews.new()
# Load a preview thumbnail of a file and store in the previews collection
pcoll.load('FACE', os.path.join(icons_dir, 'icon-row-face-32.png'), 'IMAGE')
pcoll.load('SUIT', os.path.join(icons_dir, 'icon-row-suit-32.png'), 'IMAGE')
pcoll.load('VP', os.path.join(icons_dir, 'icon-vp-32.png'), 'IMAGE')
pcoll.load('PAIRED', os.path.join(icons_dir, 'icon-paired-32.png'), 'IMAGE')
pcoll.load('ROKOKO', os.path.join(icons_dir, 'icon-rokoko-32.png'), 'IMAGE')
pcoll.load('RECORD', os.path.join(icons_dir, 'icon-record-32.png'), 'IMAGE')
pcoll.load('RESTART', os.path.join(icons_dir, 'icon-restart-32.png'), 'IMAGE')
pcoll.load('STOP', os.path.join(icons_dir, 'icon-stop-white-32.png'), 'IMAGE')
pcoll.load('CALIBRATE', os.path.join(icons_dir, 'icon-straight-pose-32.png'), 'IMAGE')
pcoll.load('STUDIO_LIVE_LOGO', os.path.join(icons_dir, 'icon-studio-live-32.png'), 'IMAGE')
global icons
icons = pcoll
def unload_icons():
global icons
if icons:
previews.remove(icons)
@@ -0,0 +1,155 @@
import os
import bpy
import sys
import json
import shutil
import pkgutil
import pathlib
import platform
import ensurepip
import subprocess
class LibraryManager:
os_name = platform.system()
system_info = {
"operating_system": os_name,
}
pip_is_updated = False
def __init__(self, libs_main_dir: pathlib.Path):
self.libs_main_dir = libs_main_dir
self.libs_info_file = self.libs_main_dir / ".lib_info"
python_ver_str = "".join([str(ver) for ver in sys.version_info[:2]])
self.libs_dir = os.path.join(self.libs_main_dir, "python" + python_ver_str)
# Set python path on older Blender versions
try:
self.python = bpy.app.binary_path_python
except AttributeError:
self.python = sys.executable
self.check_libs_info()
self._prepare_libraries()
def _prepare_libraries(self):
# Create main library directory
if not os.path.isdir(self.libs_main_dir):
os.mkdir(self.libs_main_dir)
# Create python specific library directory
if not os.path.isdir(self.libs_dir):
os.mkdir(self.libs_dir)
# Add the library path to the modules, so they can be loaded from the plugin
if self.libs_dir not in sys.path:
sys.path.append(self.libs_dir)
def install_libraries(self, required):
missing_after_install = []
# Install missing libraries
missing = [mod for mod in required if not pkgutil.find_loader(mod)]
if missing:
# Ensure and update pip
self._update_pip()
# Install the missing libraries into the library path
print("Installing missing libraries:", missing)
try:
# command = [self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", *missing]
command = [self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", *missing]
subprocess.check_call(command, stdout=subprocess.DEVNULL)
except subprocess.CalledProcessError as e:
print("PIP Error:", e)
print("Installing libraries failed.")
if self.os_name != "Windows":
print("Retrying with sudo..")
# command = ["sudo", self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", *missing]
command = ["sudo", self.python, '-m', 'pip', 'install', f"--target={str(self.libs_dir)}", *missing]
subprocess.call(command, stdout=subprocess.DEVNULL)
finally:
# Reset console color, because it could still be colored after running pip
print('\033[39m')
# Check if all library installations were successful
missing_after_install = [mod for mod in required if not pkgutil.find_loader(mod)]
installed_libs = [lib for lib in missing if lib not in missing_after_install]
if missing_after_install:
print("WARNING: Could not install the following libraries:", missing_after_install)
if installed_libs:
print("Successfully installed missing libraries:", installed_libs)
# Create library info file after all libraries are installed to ensure everything is installed correctly
self.create_libs_info()
return missing_after_install
def check_libs_info(self):
if not os.path.isdir(self.libs_dir):
return
# If the library info file doesn't exist, delete the libs folder
if not os.path.isfile(self.libs_info_file):
print("Library info is missing, deleting library folder.")
shutil.rmtree(self.libs_main_dir)
return
# Read data from info file
current_data = self.system_info
with open(self.libs_info_file, 'r', encoding="utf8") as file:
loaded_data = json.load(file)
# Compare info and delete libs folder if it doesn't match
for key, val_current in current_data.items():
val_loaded = loaded_data.get(key)
if not val_loaded == val_current:
print("Current info:", current_data)
print("Loaded info: ", loaded_data)
print("Library info is not matching, deleting library folder.")
shutil.rmtree(self.libs_main_dir)
return
def create_libs_info(self):
# If the path doesn't exist or the info file already exists, don't create it
if not os.path.isdir(self.libs_dir) or os.path.isfile(self.libs_info_file):
return
# Write the current data to the info file
with open(self.libs_info_file, 'w', encoding="utf8") as file:
json.dump(self.system_info, file)
def _update_pip(self):
if self.pip_is_updated:
return
print("Ensuring pip")
try:
ensurepip.bootstrap()
except subprocess.CalledProcessError as e:
print("PIP Error:", e)
print("Ensuring pip failed.")
print("Updating pip")
try:
# subprocess.check_call([self.python, "-m", "pip", "install", "--upgrade", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", "pip"])
subprocess.check_call([self.python, "-m", "pip", "install", "--upgrade", "pip"])
except subprocess.CalledProcessError as e:
print("PIP Error:", e)
print("Updating pip failed.")
if self.os_name != "Windows":
print("Retrying with sudo..")
# subprocess.call(["sudo", self.python, "-m", "pip", "install", "--upgrade", "--index-url=http://pypi.python.org/simple/", "--trusted-host=pypi.python.org", "pip"])
subprocess.call(["sudo", self.python, "-m", "pip", "install", "--upgrade", "pip"])
finally:
# Reset console color, because it could still be colored after running pip
print('\033[39m')
self.pip_is_updated = True
# Setup library path in the Blender addons directory and start library manager
main_dir = pathlib.Path(os.path.dirname(__file__)).parent.parent
libs_dir = main_dir / "Rokoko Libraries"
lib_manager = LibraryManager(libs_dir)
@@ -0,0 +1,176 @@
import json
loaded_lz4 = False
unsupported_os = False
try:
from lz4 import frame
loaded_lz4 = True
except ModuleNotFoundError:
print("Error: LZ4 module didn't load. Unsupported OS or Python version!")
except ImportError:
print("Error: LZ4 module didn't load. Unsupported OS!")
unsupported_os = True
class LiveData:
data = None
version = 0
# JSON v2
timestamp = 0
props = []
trackers = []
faces = []
actors = []
# JSON v3
fps = 60
timestamp_prev = 0
timedelta_prev = 0
def init(self, data):
self.data = data
self._decode_data()
self.clear_data()
self._process_data()
def clear_data(self):
self.version = 0
# JSON v2
# self.timestamp = 0
self.props = []
self.trackers = []
self.faces = []
self.actors = []
# JSON v3
self.fps = 60
# self.timestamp_prev = 0
# self.timedelta_prev = 0
def _decode_data(self):
try:
self.data = frame.decompress(self.data)
except (RuntimeError, NameError):
pass
try:
self.data = json.loads(self.data)
except UnicodeDecodeError as e:
if loaded_lz4:
raise UnicodeDecodeError
# Raise an import error if the LZ4 module couldn't be loaded
raise ImportError("os" if unsupported_os else "")
if not self.data:
raise ValueError
def _process_data(self):
self.version = self.data.get('version')
ver_str = str(self.version).replace(".", ",")
if ',' in ver_str:
self.version = int(ver_str.split(',')[0])
# If the user selected JSON v2.5 in Studio 1, the version number is "3" but it contains the data from version 2
# This checks if this is the case and sets the version number accordingly
if self.version == 3 and self.data.get('trackers') is not None:
self.version = 2
if not self.version or self.version < 2:
raise TypeError
if self.version == 2:
self.timestamp = self.data['timestamp']
self.props = self.data['props']
self.trackers = self.data['trackers']
self.faces = self.data['faces']
self.actors = self.data['actors']
else:
self.fps = self.data['fps']
self.actors = self.data['scene']['actors']
self.props = self.data['scene']['props']
for actor in self.actors:
if actor['meta']["hasFace"]:
actor['face']['parentName'] = actor['name']
self.faces.append(actor['face'])
self._calc_timestamp()
def _calc_timestamp(self):
timestamp_new = self.data['scene']['timestamp']
delta = timestamp_new - self.timestamp_prev
if delta >= 0:
self.timestamp += delta
self.timestamp_prev = timestamp_new
self.timedelta_prev = delta
else:
self.timestamp += self.timedelta_prev
self.timestamp_prev = timestamp_new
def has_gloves(self, actor):
return self.version >= 3 and actor.get('meta') and actor.get('meta').get('hasGloves')
def supports_trackers(self):
return self.version <= 2
# Get data for and from the live data selection lists
def get_actor_by_obj(self, obj):
actors = [actor for actor in self.actors if actor['name'] == obj.rsl_animations_actors]
return actors[0] if actors else None
def get_actor_id(self, actor):
return actor['name']
def get_face_by_obj(self, obj):
face_id = 'faceId' if self.version <= 2 else 'parentName'
faces = [face for face in self.faces if face[face_id] == obj.rsl_animations_faces]
return faces[0] if faces else None
def get_face_id(self, face):
face_id = 'faceId' if self.version <= 2 else 'parentName'
return face[face_id]
def get_face_parent_id(self, face):
face_id = 'profileName' if self.version <= 2 else 'parentName'
return face[face_id]
def get_prop_by_obj(self, obj):
if self.version <= 2:
obj_id = obj.rsl_animations_props_trackers.split('|')
obj_type = obj_id[0]
obj_name = obj_id[1]
if obj_type == 'PR':
props = [prop for prop in self.props if prop['name'] == obj_name]
else:
props = [tracker for tracker in self.trackers if tracker['name'] == obj_name]
return props[0] if props else None
props = [prop for prop in self.props if prop['name'] == obj.rsl_animations_props_trackers]
return props[0] if props else None
def get_prop_id(self, prop, is_tracker=False):
if self.version <= 2:
return ('TR' if is_tracker else 'PR') + '|' + prop['name']
return prop['name']
def get_prop_name(self, prop, is_tracker=False):
if self.version <= 2:
return ('Tracker: ' if is_tracker else 'Prop: ') + prop['name']
return prop['name']
def get_prop_name_raw(self, prop, is_tracker=False):
return prop['name']
@@ -0,0 +1,520 @@
import os
import bpy
import ssl
import sys
import json
import pathlib
import asyncio
import logging
import datetime
import requests
import traceback
import webbrowser
from .. import updater
from .utils import ui_refresh_all, cancel_gen
from threading import Thread, Timer
from contextlib import suppress
from typing import AsyncGenerator
from urllib.parse import urlparse
# Import extra libraries
loaded_all_libs = False
try:
import boto3
from gql import Client, gql
from cryptography.fernet import Fernet
from gql.transport.appsync_websockets import AppSyncWebsocketsTransport
from gql.transport.appsync_auth import AppSyncApiKeyAuthentication
from gql.transport.websockets import log as websockets_logger
# Set logging levels
websockets_logger.setLevel(logging.CRITICAL)
logging.getLogger('boto').setLevel(logging.CRITICAL)
loaded_all_libs = True
except ImportError as e:
print(e)
# Disable SSL
ssl._create_default_https_context = ssl._create_unverified_context
class Login:
url = "https://rmp-gql-public.rokoko.com/graphql"
aws_url = "wss://a4rau2yngvb7hn3y6m37e3b53u.appsync-realtime-api.us-east-1.amazonaws.com/graphql"
api_key = "da2-pa7tlmpnvbcpdhe7l46q3eodvu"
login_url = "https://id.rokoko.com/?request_id="
timeout_duration = 60 # In seconds, how long the listener is waiting for the login event after opening the browser
def __init__(self):
self.request_id = None
self.session: Client
self.results: AsyncGenerator
self.timeout: Timer
def start(self):
user.logging_in = True
user.display_error = None
# Start the listener in a new thread so Blender can continue running
listener = Thread(target=self._start_async, args=[])
listener.start()
# Start the timeout thread which stops the listener after a few seconds if nothing happened
self.timeout = Timer(self.timeout_duration, self._timeout)
self.timeout.start()
def stop(self):
pass
def _start_async(self):
try:
# Get the request id from the server and run the listener
self._get_request_id()
asyncio.run(self._run_listener())
except Exception as e:
print(traceback.format_exc())
user.error("No internet connection..")
def _timeout(self):
# If the user no longer logging in, don't timeout
if not user.logging_in:
return
# Stop the login listener
print("Connection timeout, stopping listener..")
asyncio.run(cancel_gen(self.results))
# Stopping login and updating UI to show timeout error
user.error("Timeout, please try again.")
print("Stopped login listener")
def _get_request_id(self):
headers = {"x-api-key": self.api_key}
query = """
mutation {
createRequestToken(client_id: "blender") {
request_id
access_token
id_token
refresh_token
client_id
created_at
last_modified
email
username
given_name
family_name
ttl
}
}
"""
try:
request = requests.post(self.url, json={'query': query}, headers=headers)
except Exception as e:
user.logging_in = False
print("No connection to the server.")
return
if request.status_code != 200:
user.logging_in = False
print(f"Query failed to reach the server by returning code of {request.status_code}.")
return
data = request.json()
self.request_id = data.get("data").get("createRequestToken").get("request_id")
def _open_website(self):
webbrowser.open(self.login_url + self.request_id)
async def _run_listener(self):
# Extract host from aws_url and create auth
host = str(urlparse(self.aws_url).netloc)
auth = AppSyncApiKeyAuthentication(host=host, api_key=self.api_key)
transport = AppSyncWebsocketsTransport(url=self.aws_url, auth=auth, ssl=ssl._create_unverified_context())
async with Client(transport=transport) as session:
self.session = session
subscription = gql(
f"""
subscription {{
onTokenChange(request_id: "{self.request_id}") {{
request_id
access_token
id_token
refresh_token
client_id
created_at
last_modified
email
username
given_name
family_name
ttl
}}
}}
"""
)
print("Waiting for login event..")
# Subscribe to the login event
self.results = session.subscribe(subscription)
# Open the website to allow the user to login
self._open_website()
with suppress(asyncio.CancelledError):
# Wait for the login event
async for result in self.results:
# Check if the correct data was returned
data = result.get("onTokenChange")
if data:
if data.get("request_id") != self.request_id:
user.error("Error, please try again.")
print("Request ID not correct, please try again.")
break
print("Login successful, stopping listener..")
user.login(data)
user.login_cache.create_login_cache(data)
break
# If another event was returned (like maintenance), stop the login
user.error("Server error, please try again.")
print("Server error:", result)
break
# If the connection is closing by itself, cancel the timeout timer
self.timeout.cancel()
class LoginSilent:
region = 'us-east-1'
client_id = "39j3527cico5eicbtpjoc6627d"
def __init__(self):
logging.getLogger('boto').setLevel(logging.ERROR)
self.login()
def login(self):
# Start the listener in a new thread so Blender can continue running
thread = Thread(target=self._login_async, args=[])
thread.start()
def _login_async(self):
print("SILENT LOGIN")
if not user.refresh_token:
return
response = None
try:
sys.tracebacklimit = 0
client = boto3.client("cognito-idp", region_name=self.region)
response = client.initiate_auth(
ClientId=self.client_id,
AuthFlow='REFRESH_TOKEN',
AuthParameters={
'REFRESH_TOKEN': user.refresh_token
},
)
except Exception as e:
error_msg = str(e)
print("\nERROR:", error_msg, "\n")
if "NotAuthorizedException" in error_msg:
user.logout()
user.error("Logged out: Session expired")
finally:
del sys.tracebacklimit
# print("RESPONSE:", response)
if not response:
return
# Check response for challenge, logout if challenge detected
# See here for challenges:
# https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-idp.html#CognitoIdentityProvider.Client.initiate_auth
challenge_name = response.get("ChallengeName")
challenge_params = response.get("ChallengeParameters")
if challenge_name or challenge_params:
print("ERROR: Further account managing needed!")
user.logout()
user.error("Logged out:", challenge_name)
class User:
classes_logged_in = []
classes_logged_out = []
def __init__(self):
self.logging_in = False
self.login_cache = LoginCache()
self.logged_in = False
self.email = None
self.username = None # This is a unique id
self.access_token = None # Only gets used for the MixPanel API
self.refresh_token = None # Gets used to log in silently
self.display_email = False
self.display_error = None
self.login_time = None
self.version_str = "1.0.0"
self.classes_logged_in = []
self.classes_logged_out = []
def set_info(self, classes_logged_in, classes_logged_out, bl_info):
self.classes_logged_in = classes_logged_in
self.classes_logged_out = classes_logged_out
self.version_str = ".".join(map(str, bl_info.get("version")))
def auto_login(self):
# Check the login cache
data = self.login_cache.get_login_cache()
if not data:
return False
self.login(data, register_classes=True)
if self.logged_in:
LoginSilent()
return self.logged_in
def login(self, data, register_classes=True):
# Collect data
self.email = data.get("email")
self.username = data.get("username")
self.access_token = data.get("access_token")
self.refresh_token = data.get("refresh_token")
# Check data validity
self.logging_in = False
self.logged_in = self.email and self.username and self.refresh_token and self.access_token
if not self.logged_in:
print("ERROR: Not all fields are filled:", self.email, self.username, self.refresh_token, self.access_token)
self.error("Login failed, please try again")
return
self.display_error = None
self.login_time = datetime.datetime.utcnow().timestamp()
MixPanel.send_login_event()
if register_classes:
self.register_classes()
def logout(self):
if not self.logged_in:
return
MixPanel.send_logout_event()
self.logged_in = False
self.email = self.username = self.refresh_token = self.access_token = None
self.unregister_classes()
self.login_cache.delete_cache()
def quit(self):
MixPanel.send_logout_event()
def error(self, *msg):
# Update the UI if the user is still logging in or of the error message changes
update_ui = self.logging_in or msg != self.display_error
self.logging_in = False
self.display_error = msg
if update_ui and not self.logged_in:
ui_refresh_all()
def register_classes(self):
# Unregister logged out classes
for cls in reversed(self.classes_logged_out):
bpy.utils.unregister_class(cls)
# Register logged in classes
for cls in self.classes_logged_in:
bpy.utils.register_class(cls)
def unregister_classes(self):
# Unregister classes_logged_in
for cls in reversed(self.classes_logged_in):
bpy.utils.unregister_class(cls)
# Register classes_logged_out
for cls in self.classes_logged_out:
bpy.utils.register_class(cls)
class LoginCache:
main_dir = pathlib.Path(os.path.dirname(__file__)).parent.resolve()
resources_dir = os.path.join(main_dir, "resources")
cache_dir = os.path.join(resources_dir, "cache")
cache_file = os.path.join(cache_dir, ".cache")
key = 'p03Ab7CuvhUuwcbOU4nBAl_QkoaU8XxciKvHGb5Wfd0='
def __init__(self):
self.f = None
def create_login_cache(self, data):
if not self.f:
self.f = Fernet(self.key)
if not os.path.isdir(self.cache_dir):
os.mkdir(self.cache_dir)
data_str = json.dumps(data)
encoded_data = data_str.encode()
encrypted_data = self.f.encrypt(encoded_data)
with open(self.cache_file, 'wb') as file:
file.write(encrypted_data)
def get_login_cache(self):
if not self.f:
self.f = Fernet(self.key)
if not os.path.isfile(self.cache_file):
return None
with open(self.cache_file, 'rb') as file:
encrypted_data = file.read()
# Decrypt cache data and load it as json
encoded_data = self.f.decrypt(encrypted_data)
data_str = encoded_data.decode()
data = json.loads(data_str)
if not self.is_valid(data):
return None
return data
def delete_cache(self):
if os.path.isfile(self.cache_file):
os.remove(self.cache_file)
def is_valid(self, data):
if not data:
return False
# Check if the cache is too old
creation_date = data.get("created_at")
if not creation_date:
return False
duration_timestamp = int(datetime.datetime.now().timestamp()) - creation_date
duration = datetime.timedelta(seconds=duration_timestamp)
if duration.days > 90:
print("Cache too old, please login again")
self.delete_cache()
user.error("Login expired (90 days)")
return False
return True
class MixPanel:
# url = "https://rmp-team-gql.rokoko.com/graphql"
url = "https://rmp-gql-public.rokoko.com/graphql"
api_key = "da2-pa7tlmpnvbcpdhe7l46q3eodvu"
@staticmethod
def send_login_event():
if not user.username:
return
headers = {"x-api-key": MixPanel.api_key}
event_properties = {
"action": "login",
"blender_version": ".".join(map(str, bpy.app.version)),
"plugin_version": user.version_str,
}
event_properties = json.dumps(event_properties).replace("\"", "\\\"")
query = f"""
mutation {{
trackInMixpanel(input: {{
event_name: "session_start"
event_properties: "{event_properties}"
distinct_id: "{user.username}"
client_id: BLENDER
}}
)
}}
"""
try:
request = requests.post(MixPanel.url, json={'query': query}, headers=headers)
except Exception as e:
user.logging_in = False
print("No connection to the server.")
return
if request.status_code != 200:
user.logging_in = False
print(f"Query failed to reach the server by returning code of {request.status_code}.")
return
# data = request.json()
# print("MIXPANEL LOGIN RECEIVED DATA:", data)
@staticmethod
def send_logout_event():
if not user.username:
return
headers = {"x-api-key": MixPanel.api_key}
session_duration = 0
if user.login_time:
session_duration = datetime.datetime.utcnow().timestamp() - user.login_time
session_duration = round(session_duration, 2)
event_properties = {
"action": "logout",
"blender_version": ".".join(map(str, bpy.app.version)),
"plugin_version": user.version_str,
"session_duration": session_duration,
}
event_properties = json.dumps(event_properties).replace("\"", "\\\"")
query = f"""
mutation {{
trackInMixpanel(input: {{
event_name: "session_end"
event_properties: "{event_properties}"
distinct_id: "{user.username}"
client_id: BLENDER
}}
)
}}
"""
try:
request = requests.post(MixPanel.url, json={'query': query}, headers=headers)
except Exception as e:
user.logging_in = False
print("No connection to the server.")
return
if request.status_code != 200:
user.logging_in = False
print(f"Query failed to reach the server by returning code of {request.status_code}.")
return
user: User = User()
@@ -0,0 +1,161 @@
import bpy
import time
import socket
import traceback
from . import animations, utils
error_temp = ''
show_error = []
# Starts UPD server and handles data received from Rokoko Studio
class Receiver:
sock = None
# Redraw counters
i = -1 # Number of continuous received packets
i_np = 0 # Number of continuous no packets
# Error counters
error_temp = []
error_count = 0
def run(self):
data_raw = None
received = True
error = []
force_error = False
# Try to receive a packet
try:
data_raw, address = self.sock.recvfrom(81920) # Prev 65536
except BlockingIOError as e:
print('Blocking error:', e)
error = ['Receiving no data!']
except OSError as e:
print('Packet error:', e.strerror)
error = ['Packets too big!']
force_error = True
except AttributeError as e:
print('Socket error:', e)
error = ['Socket not running!']
force_error = True
# start_time = time.time()
# Process the packet
if data_raw:
# print('SIZE:', len(data_raw))
# print('DATA:', data_raw)
# Process animation data
error, force_error = self.process_data(data_raw)
# print(round((time.time() - start_time) * 1000, 4), 'ms')
self.handle_ui_updates(received)
self.handle_error(error, force_error)
def process_data(self, data_raw) -> ([str], bool):
"""
Processes the received data. If there was an error it returns a list of strings creating the error message
and if the error should be forced to show immediately instead of after a couple of packages
:param data_raw:
:return:
"""
try:
animations.live_data.init(data_raw)
except ValueError as e:
print('Packet contained no data')
print(e)
return ['Packets contain no data!'], False
except (UnicodeDecodeError, TypeError) as e:
print('Wrong live data format! Use JSON v2 or higher!')
print(e)
print(traceback.format_exc())
return ['Wrong data format!', 'Use JSON v2 or higher!'], True
except KeyError as e:
print('KeyError:', e)
return ['Incompatible JSON version!', 'Use the latest Studio', 'and plugin versions.'], True
except ImportError as e:
# This error occurs specifically when LZ4 isn't supported by the operating system
if "os" in e.msg:
print('LZ4 unsupported by OS!', 'Use "Json" in the', 'Custom panel in Studio.')
return ['LZ4 unsupported by OS!', 'Use "Json" in the', 'Custom panel in Studio.'], True
# This error occurs, when the LZ4 package could not be loaded while it was needed
print('LZ4 unsupported by OS or', 'Blender! Use "Json" in the', 'Custom panel in Studio.')
return ['LZ4 unsupported by OS or', 'Blender! Use "Json" in the', 'Custom panel in Studio.'], True
animations.animate()
return None, False
def handle_ui_updates(self, received):
# Update UI every 5 seconds when packets are received continuously
if received:
self.i += 1
self.i_np = 0
if self.i % (bpy.context.scene.rsl_receiver_fps * 5) == 0:
utils.ui_refresh_properties()
utils.ui_refresh_view_3d()
return
# If receiving a packet after one second of no packets, update UI with next packet
self.i_np += 1
if self.i_np == bpy.context.scene.rsl_receiver_fps:
self.i = -1
def handle_error(self, error, force_error):
global show_error
if not error:
self.error_count = 0
if not show_error:
return
self.error_temp = []
show_error = []
utils.ui_refresh_view_3d()
print('REFRESH')
return
if not self.error_temp:
self.error_temp = error
if force_error:
self.error_count = bpy.context.scene.rsl_receiver_fps - 1
return
if error == self.error_temp:
self.error_count += 1
else:
self.error_temp = error
if force_error:
self.error_count = bpy.context.scene.rsl_receiver_fps
else:
self.error_count = 0
if self.error_count == bpy.context.scene.rsl_receiver_fps:
show_error = self.error_temp
utils.ui_refresh_view_3d()
print('REFRESH')
def start(self, port):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.sock.setblocking(False)
self.sock.bind(('', port))
self.i = -1
self.i_np = 0
self.error_temp = []
self.error_count = 0
global show_error
show_error = False
print("Rokoko Studio Live started listening on port " + str(port))
def stop(self):
self.sock.close()
print("Rokoko Studio Live stopped listening")
@@ -0,0 +1,323 @@
import bpy
import copy
from math import radians, degrees
from collections import OrderedDict
recorded_data = {}
recorded_timestamps = OrderedDict()
def toggle_recording(self, context):
new_state = context.scene.rsl_recording
if new_state:
start_recorder(context)
else:
stop_recorder(context)
def start_recorder(context):
if recorded_data:
return
# Here can be stuff done when starting the recorder
pass
def stop_recorder(context):
if not recorded_data:
return
# Set animation settings
context.scene.render.fps = context.scene.rsl_receiver_fps
# Convert timestamps to keyframes to have a shared time axis
convert_timestamps_to_keyframes()
# Process each type of recorded data
for data_type, objects in recorded_data.items():
if not objects:
continue
if data_type == 'actors':
for obj_name, data in objects.items():
process_actor_recording(obj_name, data)
elif data_type == 'faces':
for obj_name, data in objects.items():
process_face_recording(obj_name, data)
elif data_type == 'objects':
for obj_name, data in objects.items():
process_object_recording(obj_name, data)
# Clear recorded data
recorded_data.clear()
recorded_timestamps.clear()
print('\nSuccessfully saved the recording!')
def process_actor_recording(obj_name, data):
armature = bpy.data.objects.get(obj_name)
if not armature:
print('Armature', obj_name, 'not found!')
return
# Create new action
action = bpy.data.actions.new(name='Anim Arm ' + obj_name)
action.use_fake_user = True
armature.animation_data_create().action = action
# Handle recorded data
data_paths = OrderedDict()
prev_rotations = {}
rotation_modifiers = {}
for item in data:
bone_name = item["bone_name"]
if item['location']:
data_path = 'pose.bones["%s"].location' % bone_name
if not data_paths.get(data_path):
data_paths[data_path] = []
data_paths[data_path].append((item['timestamp'], item['location']))
if item['rotation']:
rotation = item['rotation']
data_path = 'pose.bones["%s"].rotation_euler' % bone_name
if not data_paths.get(data_path):
data_paths[data_path] = []
# Load previous rotation from dict
prev_rot = prev_rotations.get(bone_name)
if not prev_rot:
prev_rot = rotation
# Fix each rotation axis separately
for i in [0, 1, 2]:
# Load rotation modifier of the bone rotation axis
# The rotation modifier is used to cut down on processing time. It gets set when a axis had been normalized
# and then is used to apply the same fix to subsequent rotations. This prevents having to normalize subsequent rotations
rotation_mod = rotation_modifiers.get((bone_name, i))
if not rotation_mod:
rotation_mod = 0
# Get axis rotation in degrees, since they are stored as radians. Also add the rotation modifier to the rotation
axis = degrees(rotation[i]) + rotation_mod
axis_prev = degrees(prev_rot[i])
# Normalize the rotation
axis_normalized, rotation_mod_new = normalize_rotation(axis, axis_prev)
# Save the normalized axis to the rotation and save the rotation modifier
rotation[i] = radians(axis_normalized)
rotation_modifiers[bone_name, i] = rotation_mod + rotation_mod_new
# Save the rotation to the data paths and save the current rotation as the previous rotation
data_paths[data_path].append((item['timestamp'], rotation))
prev_rotations[bone_name] = rotation
use_inherit_rotation = False
data_path = 'data.bones["%s"].use_inherit_rotation' % bone_name
if not data_paths.get(data_path):
data_paths[data_path] = []
data_paths[data_path].append((item['timestamp'], [use_inherit_rotation]))
# Go through each datapath (fcurve) and add all keyframes at once
for data_path, values_tmp in data_paths.items():
frame_count = len(values_tmp)
values_tmp = list(zip(*values_tmp)) # This unzips the list of tuples into two separate lists
timestamps = list(values_tmp[0])
values = list(values_tmp[1])
index_len = len(values[0])
for axis_i in range(index_len):
curve = action.fcurves.new(data_path=data_path, index=axis_i)
keyframe_points = curve.keyframe_points
keyframe_points.add(frame_count)
for frame_i in range(frame_count):
timestamp = timestamps[frame_i]
transform = values[frame_i][axis_i]
keyframe_points[frame_i].co = (
recorded_timestamps[timestamp],
transform)
keyframe_points[frame_i].interpolation = 'LINEAR'
def process_object_recording(obj_name, data):
obj = bpy.data.objects.get(obj_name)
if not obj:
print('Object', obj_name, 'not found!')
return
# Create new action
action = bpy.data.actions.new(name='Anim Obj ' + obj_name)
action.use_fake_user = True
obj.animation_data_create().action = action
# Handle recording data
data_paths = OrderedDict()
for item in data:
if item['location']:
data_path = 'location'
if not data_paths.get(data_path):
data_paths[data_path] = []
data_paths[data_path].append((item['timestamp'], item['location']))
if item['rotation']:
data_path = 'rotation_quaternion'
if not data_paths.get(data_path):
data_paths[data_path] = []
data_paths[data_path].append((item['timestamp'], item['rotation']))
for data_path, values in data_paths.items():
# print(data_path)
frame_count = len(values)
index_len = 3 if data_path.endswith('location') else 4
for axis_i in range(index_len):
curve = action.fcurves.new(data_path=data_path, index=axis_i)
keyframe_points = curve.keyframe_points
keyframe_points.add(frame_count)
for frame_i in range(frame_count):
timestamp = values[frame_i][0]
transform = values[frame_i][1]
keyframe_points[frame_i].co = (
recorded_timestamps[timestamp],
transform[axis_i])
keyframe_points[frame_i].interpolation = 'LINEAR'
def process_face_recording(obj_name, data):
mesh = bpy.data.objects.get(obj_name)
if not mesh:
print('Object', obj_name, 'not found!')
return
# Create new action
action = bpy.data.actions.new(name='Anim Face ' + obj_name)
action.use_fake_user = True
mesh.animation_data_create().action = action
# Handle recording data
data_paths = OrderedDict()
for item in data:
data_path = 'data.shape_keys.key_blocks["%s"].value' % item['shapekey_name']
if not data_paths.get(data_path):
data_paths[data_path] = []
data_paths[data_path].append((item['timestamp'], item['value']))
for data_path, values in data_paths.items():
# print(data_path)
frame_count = len(values)
curve = action.fcurves.new(data_path=data_path, index=0)
keyframe_points = curve.keyframe_points
keyframe_points.add(frame_count)
for frame_i in range(frame_count):
timestamp = values[frame_i][0]
shapekey_value = values[frame_i][1]
keyframe_points[frame_i].co = (
recorded_timestamps[timestamp],
shapekey_value)
keyframe_points[frame_i].interpolation = 'LINEAR'
def normalize_rotation(axis, axis_prev):
rotation_mod = 0
if abs(axis - axis_prev) > 180:
desired_axis = axis
if axis_prev > axis:
while abs(desired_axis - axis_prev) > 180 and axis_prev > axis:
print(axis_prev, axis, desired_axis)
desired_axis += 360
rotation_mod += 360
axis = desired_axis
else:
while abs(desired_axis - axis_prev) > 180 and axis_prev < axis:
print(axis_prev, axis, desired_axis)
desired_axis -= 360
rotation_mod -= 360
axis = desired_axis
return axis, rotation_mod
def convert_timestamps_to_keyframes():
timestamps = list(recorded_timestamps.keys())
def get_frame(frame_number):
return int(round((timestamps[frame_number] - timestamps[0]) * bpy.context.scene.rsl_receiver_fps, 0))
# Fix frame numbers that are incorrect because of rounding errors
for i, timestamp in enumerate(timestamps):
curr_frame = get_frame(i)
if 0 < i < len(timestamps) - 1:
prev_frame = get_frame(i - 1)
next_frame = get_frame(i + 1)
if prev_frame == curr_frame and next == curr_frame + 2:
curr_frame += 1
if next_frame == curr_frame and prev_frame == curr_frame - 2:
curr_frame -= 1
if i == len(timestamps) - 1:
prev_frame = get_frame(i - 1)
if prev_frame == curr_frame:
curr_frame += 1
recorded_timestamps[timestamp] = curr_frame
def record_bone(timestamp, arm_name, bone_name, rotation, location=None):
if not recorded_data.get('actors'):
recorded_data['actors'] = {}
if not recorded_data['actors'].get(arm_name):
recorded_data['actors'][arm_name] = []
data = {
'timestamp': timestamp,
'bone_name': bone_name,
'rotation': copy.deepcopy(rotation),
'location': copy.deepcopy(location)
}
recorded_data['actors'][arm_name].append(data)
recorded_timestamps[timestamp] = 0
def record_face(timestamp, mesh_name, shapekey_name, value):
if not recorded_data.get('faces'):
recorded_data['faces'] = {}
if not recorded_data['faces'].get(mesh_name):
recorded_data['faces'][mesh_name] = []
data = {
'timestamp': timestamp,
'shapekey_name': shapekey_name,
'value': copy.deepcopy(value)
}
recorded_data['faces'][mesh_name].append(data)
recorded_timestamps[timestamp] = 0
def record_object(timestamp, obj_name, rotation, location):
if not recorded_data.get('objects'):
recorded_data['objects'] = {}
if not recorded_data['objects'].get(obj_name):
recorded_data['objects'][obj_name] = []
data = {
'timestamp': timestamp,
'rotation': copy.deepcopy(rotation),
'location': copy.deepcopy(location)
}
recorded_data['objects'][obj_name].append(data)
recorded_timestamps[timestamp] = 0
@@ -0,0 +1,23 @@
import bpy
# This filters the objects shown to only include armatures and under certain conditions
def poll_source_armatures(self, obj):
return obj.type == 'ARMATURE' and obj.animation_data and obj.animation_data.action
def poll_target_armatures(self, obj):
return obj.type == 'ARMATURE' and obj != get_source_armature()
# If the retargeting armatures get changed, clear the bone list
def clear_bone_list(self, context):
context.scene.rsl_retargeting_bone_list.clear()
def get_source_armature():
return bpy.context.scene.rsl_retargeting_armature_source
def get_target_armature():
return bpy.context.scene.rsl_retargeting_armature_target
@@ -0,0 +1,293 @@
import bpy
import copy
from . import utils
from ..operators import receiver
objects = {}
faces = {}
armatures = {}
hidden_meshes = {}
def save_scene():
for obj in bpy.context.scene.objects:
save_object(obj)
if obj.type == 'MESH':
save_face(obj)
elif obj.type == 'ARMATURE':
save_armature(obj)
def load_scene():
for obj in bpy.context.scene.objects:
load_object(obj)
if obj.type == 'MESH':
load_face(obj)
elif obj.type == 'ARMATURE':
load_armature(obj)
hidden_meshes.clear()
# Object handler
def save_object(obj):
if obj.rsl_animations_props_trackers == 'None':
return
global objects
rotation_mode = obj.rotation_mode
obj.rotation_mode = 'QUATERNION'
objects[obj.name] = copy.deepcopy({
'location': obj.location,
'rotation': obj.rotation_quaternion,
'rotation_mode': rotation_mode,
# 'hidden': obj.hide_get()
})
def load_object(obj):
if not bpy.context.scene.rsl_reset_scene_on_stop:
return
global objects
obj_data = objects.get(obj.name)
if not obj_data:
return
obj.rotation_mode = 'QUATERNION'
obj.location = obj_data['location']
obj.rotation_quaternion = obj_data['rotation']
# obj.rotation_mode = obj_data['rotation_mode']
# obj.hide_set(obj_data['hidden'])
# Remove element from dictionary
objects.pop(obj.name)
# Face mesh handler
def save_face(obj):
if not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
return
if obj.rsl_animations_faces == 'None':
return
global faces
shapekeys = {}
for shapekey in obj.data.shape_keys.key_blocks:
shapekeys[shapekey.name] = shapekey.value
faces[obj.name] = copy.deepcopy(shapekeys)
if obj.name in hidden_meshes.keys():
unhide_mesh(obj, hidden_meshes[obj.name])
def load_face(obj):
if not bpy.context.scene.rsl_reset_scene_on_stop:
return
global faces
shapekey_data = faces.get(obj.name)
if not shapekey_data or not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
return
for shapekey_name, value in shapekey_data.items():
shapekey = obj.data.shape_keys.key_blocks.get(shapekey_name)
if not shapekey:
continue
shapekey.value = value
# Remove element from dictionary
faces.pop(obj.name)
# Hide this mesh if it is animated by an armature and it should be hidden
if bpy.context.scene.rsl_hide_mesh_during_play:
for mod in obj.modifiers:
if mod.type == 'ARMATURE':
armature = mod.object
if armatures.get(armature.name):
hide_mesh(obj, armature)
# Armature handler
def save_armature(obj):
global armatures
# Return if no actor and no glove is assigned to this armature
# if not obj.rsl_animations_actors or obj.rsl_animations_actors == 'None': # <-- This should work but for some reason it doesn't
if obj.rsl_animations_actors == 'None':
print('NO ASSIGNED DATA:', obj.rsl_animations_actors)
return
utils.set_active(obj)
bpy.ops.object.mode_set(mode='OBJECT')
bones = {}
for bone in obj.pose.bones:
# Fix rotation mode
if bone.rotation_mode == 'QUATERNION':
bone.rotation_mode = 'XYZ'
bones[bone.name] = {
'location': bone.location,
'rotation': bone.rotation_euler,
'rotation_mode': bone.rotation_mode,
'inherit_rotation': obj.data.bones.get(bone.name).use_inherit_rotation
}
armatures[obj.name] = copy.deepcopy(bones)
hide_meshes_on_play(obj)
def load_armature(obj):
unhide_meshes_on_stop(obj)
if not bpy.context.scene.rsl_reset_scene_on_stop:
return
global armatures
bone_data = armatures.get(obj.name)
if not bone_data:
return
for bone_name, bone_data in bone_data.items():
bone = obj.pose.bones.get(bone_name)
if not bone:
continue
location = bone_data['location']
rotation = bone_data['rotation']
rotation_mode = bone_data['rotation_mode']
inherit_rotation = bone_data['inherit_rotation']
# Fix rotation mode
if rotation_mode == 'QUATERNION':
rotation_mode = 'XYZ'
bone.location = location
bone.rotation_mode = rotation_mode
bone.rotation_euler = rotation
obj.data.bones.get(bone_name).use_inherit_rotation = inherit_rotation
# Remove element from dictionary
armatures.pop(obj.name)
def update_object(self, context):
if not receiver.receiver_enabled:
return
obj = context.object
new_state = obj.rsl_animations_props_trackers
if new_state != 'None':
if not objects.get(obj.name):
save_object(obj)
else:
load_object(obj)
def update_face(self, context):
if not receiver.receiver_enabled:
return
obj = context.object
new_state = obj.rsl_animations_faces
if new_state != 'None':
if not faces.get(obj.name):
save_face(obj)
else:
load_face(obj)
def update_actor(self, context):
if not receiver.receiver_enabled:
return
obj = context.object
new_state = obj.rsl_animations_actors
if new_state != 'None':
if not armatures.get(obj.name):
save_armature(obj)
else:
load_armature(obj)
def update_glove(self, context):
if not receiver.receiver_enabled:
return
obj = context.object
new_state = obj.rsl_animations_gloves
if new_state != 'None':
if not armatures.get(obj.name):
save_armature(obj)
else:
load_armature(obj)
def hide_meshes_on_play(armature):
if not bpy.context.scene.rsl_hide_mesh_during_play:
return
global faces
for mesh in bpy.context.scene.objects:
if mesh.type != 'MESH':
continue
hide_mesh(mesh, armature)
def unhide_meshes_on_stop(armature):
for mesh_name in copy.copy(hidden_meshes).keys():
mesh = bpy.context.scene.objects.get(mesh_name)
if not mesh:
continue
unhide_mesh(mesh, armature)
def update_hidden_meshes(self, context):
if not receiver.receiver_enabled:
return
new_state = context.scene.rsl_hide_mesh_during_play
for armature_name in armatures.keys():
armature = bpy.context.scene.objects.get(armature_name)
if not armature:
continue
if new_state:
hide_meshes_on_play(armature)
else:
unhide_meshes_on_stop(armature)
def hide_mesh(mesh, armature):
if faces.get(mesh.name):
return
for mod in mesh.modifiers:
if mod.type == 'ARMATURE' and mod.object == armature:
mesh.hide_set(True)
mod.object = None
hidden_meshes[mesh.name] = armature
def unhide_mesh(mesh, armature):
mesh.hide_set(False)
for mod in mesh.modifiers:
if mod.type == 'ARMATURE' and hidden_meshes[mesh.name] == armature:
mod.object = hidden_meshes[mesh.name]
hidden_meshes.pop(mesh.name)
@@ -0,0 +1,85 @@
import asyncio
import sys
import bpy
import math
from mathutils import Vector, Matrix
from contextlib import suppress
def ui_refresh_properties():
# Refreshes the properties panel
for windowManager in bpy.data.window_managers:
for window in windowManager.windows:
for area in window.screen.areas:
if area.type == 'PROPERTIES':
area.tag_redraw()
def ui_refresh_view_3d():
# Refreshes the view 3D panel
for windowManager in bpy.data.window_managers:
for window in windowManager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
def ui_refresh_all():
if not hasattr(bpy.data, "window_managers"):
return
# Refreshes all panels
for windowManager in bpy.data.window_managers:
for window in windowManager.windows:
for area in window.screen.areas:
area.tag_redraw()
def reprint(*x):
# This prints a message in the same console line continuously
sys.stdout.write("\r" + " ".join(x))
sys.stdout.flush()
def set_active(obj):
obj.select_set(True)
obj.hide_set(False)
bpy.context.view_layer.objects.active = obj
def mat3_to_vec_roll(mat):
vecmat = vec_roll_to_mat3(mat.col[1], 0)
vecmatinv = vecmat.inverted()
rollmat = vecmatinv @ mat
roll = math.atan2(rollmat[0][2], rollmat[2][2])
return roll
def vec_roll_to_mat3(vec, roll):
target = Vector((0, 0.1, 0))
nor = vec.normalized()
axis = target.cross(nor)
if axis.dot(axis) > 0.0000000001:
axis.normalize()
theta = target.angle(nor)
bMatrix = Matrix.Rotation(theta, 3, axis)
else:
updown = 1 if target.dot(nor) > 0 else -1
bMatrix = Matrix.Scale(updown, 3)
bMatrix[2][2] = 1.0
rMatrix = Matrix.Rotation(roll, 3, nor)
mat = rMatrix @ bMatrix
return mat
async def cancel_gen(agen):
"""
Stops an asynchronous generator from outside.
:param agen: The asynchronous generator
:return:
"""
task = asyncio.create_task(agen.__anext__())
task.cancel()
with suppress(Exception):
await task
await agen.aclose()