2025-12-01
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user