2025-12-01
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
if "bpy" not in locals():
|
||||
import bpy
|
||||
from . import receiver
|
||||
from . import detector
|
||||
from . import recorder
|
||||
from . import actor
|
||||
from . import command_api
|
||||
from . import info
|
||||
from . import retargeting
|
||||
from . import login
|
||||
else:
|
||||
import importlib
|
||||
|
||||
importlib.reload(receiver)
|
||||
importlib.reload(detector)
|
||||
importlib.reload(recorder)
|
||||
importlib.reload(actor)
|
||||
importlib.reload(command_api)
|
||||
importlib.reload(info)
|
||||
importlib.reload(retargeting)
|
||||
importlib.reload(login)
|
||||
@@ -0,0 +1,134 @@
|
||||
import bpy
|
||||
import copy
|
||||
|
||||
from . import receiver
|
||||
|
||||
|
||||
class InitTPose(bpy.types.Operator):
|
||||
bl_idname = "rsl.init_tpose"
|
||||
bl_label = "Set as T-Pose"
|
||||
bl_description = "Press this button if you have this armature in T-Pose"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
if obj.type != 'ARMATURE':
|
||||
self.report({'ERROR'}, 'This is not an armature!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Get current custom data
|
||||
custom_data = obj.get('CUSTOM')
|
||||
if not custom_data:
|
||||
custom_data = {}
|
||||
|
||||
bones = {}
|
||||
|
||||
# Save local and global space rotations and local and object space locations for each bone
|
||||
for bone in obj.pose.bones:
|
||||
# Save rotation mode
|
||||
rotation_mode = bone.rotation_mode
|
||||
|
||||
# Save bone pose data
|
||||
bone.rotation_mode = 'QUATERNION'
|
||||
bones[bone.name] = {
|
||||
'location_local': bone.location,
|
||||
'location_object': bone.matrix @ bone.location,
|
||||
'rotation_local': bone.rotation_quaternion,
|
||||
'rotation_global': bone.matrix.to_quaternion(),
|
||||
'inherit_rotation': obj.data.bones.get(bone.name).use_inherit_rotation,
|
||||
}
|
||||
|
||||
# Load rotation mode
|
||||
bone.rotation_mode = rotation_mode
|
||||
|
||||
# Save tpose data to custom data
|
||||
custom_data['rsl_tpose_bones'] = copy.deepcopy(bones)
|
||||
obj['CUSTOM'] = custom_data
|
||||
|
||||
self.report({'INFO'}, 'T-Pose successfully saved!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ResetTPose(bpy.types.Operator):
|
||||
bl_idname = "rsl.reset_tpose"
|
||||
bl_label = "Reset to T-Pose"
|
||||
bl_description = "Use this to reset the armature to it's T-Pose"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
if receiver.receiver_enabled:
|
||||
self.report({'ERROR'}, 'Receiver is currently running. Please stop it first.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
obj = context.object
|
||||
if obj.type != 'ARMATURE':
|
||||
self.report({'ERROR'}, 'This is not an armature!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Get current custom data
|
||||
custom_data = obj.get('CUSTOM')
|
||||
if not custom_data:
|
||||
self.report({'ERROR'}, 'Please set the T-Pose first.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Get tpose data from custom data
|
||||
tpose_bones = custom_data.get('rsl_tpose_bones')
|
||||
if not tpose_bones:
|
||||
self.report({'ERROR'}, 'Please set the T-Pose first.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Apply locations and rotations to bones
|
||||
for bone_name, data in tpose_bones.items():
|
||||
bone = obj.pose.bones.get(bone_name)
|
||||
if bone:
|
||||
# Save rotation mode
|
||||
rotation_mode = bone.rotation_mode
|
||||
if rotation_mode == 'QUATERNION':
|
||||
rotation_mode = 'XYZ'
|
||||
|
||||
bone.rotation_mode = 'QUATERNION'
|
||||
bone.rotation_quaternion = data['rotation_local']
|
||||
bone.location = data['location_local']
|
||||
obj.data.bones.get(bone_name).use_inherit_rotation = data['inherit_rotation']
|
||||
|
||||
# Load rotation mode
|
||||
bone.rotation_mode = rotation_mode
|
||||
|
||||
self.report({'INFO'}, 'T-Pose successfully restored!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class PrintCurrentPose(bpy.types.Operator):
|
||||
bl_idname = "rsl.print_current_pose"
|
||||
bl_label = "Print"
|
||||
bl_description = "Debugging. Prints world rotation of armature bones"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
if obj.type != 'ARMATURE':
|
||||
self.report({'ERROR'}, 'This is not an armature!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
for bone in obj.pose.bones:
|
||||
# Save rotation mode
|
||||
rotation_mode = bone.rotation_mode
|
||||
if rotation_mode == 'QUATERNION':
|
||||
rotation_mode = 'XYZ'
|
||||
|
||||
bone.rotation_mode = 'QUATERNION'
|
||||
rot = bone.matrix.to_euler().to_quaternion().copy()
|
||||
i = 5
|
||||
print('actor_bones[\'' + bone.name + '\'] = Quaternion(('
|
||||
+ str(round(rot[0], i)) + ', '
|
||||
+ str(round(rot[1], i)) + ', '
|
||||
+ str(round(rot[2], i)) + ', '
|
||||
+ str(round(rot[3], i))
|
||||
+ '))')
|
||||
|
||||
# Load rotation mode
|
||||
bone.rotation_mode = rotation_mode
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,146 @@
|
||||
|
||||
import bpy
|
||||
import requests
|
||||
import traceback
|
||||
|
||||
|
||||
class CommandTest(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_test'
|
||||
bl_label = 'Test Command API'
|
||||
bl_description = 'Testing'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = get_request('')
|
||||
except requests.exceptions.ConnectionError:
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Successfully tested!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class StartCalibration(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_start_calibration'
|
||||
bl_label = 'Start Calibration'
|
||||
bl_description = 'Starts calibration of a Smartsuit Pro'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = post_request('/calibrate')
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Calibration started successfully!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class Restart(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_restart'
|
||||
bl_label = 'Restart Smartsuits'
|
||||
bl_description = 'Restarts all Smartsuit Pro\'s'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = post_request('/restart')
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Smartsuits restarted successfully!!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class StartRecording(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_start_recording'
|
||||
bl_label = 'Start Recording'
|
||||
bl_description = 'Starts recording all connected Smartsuit Pro\'s'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = post_request('/recording/start')
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Recording started successfully!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class StopRecording(bpy.types.Operator):
|
||||
bl_idname = 'rsl.command_stop_recording'
|
||||
bl_label = 'Stop Recording'
|
||||
bl_description = 'Stops recording all connected Smartsuit Pro\'s'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
request = post_request('/recording/stop')
|
||||
except requests.exceptions.ConnectionError:
|
||||
print(traceback.format_exc())
|
||||
self.report({'ERROR'}, 'Could not connect to Rokoko Studio!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
data = request.json()
|
||||
if is_error(self, data):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, 'Recording stopped successfully!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def get_request(additions):
|
||||
scn = bpy.context.scene
|
||||
return requests.get(f'http://{scn.rsl_command_ip_address}:{scn.rsl_command_ip_port}/v1/{scn.rsl_command_api_key}' + additions)
|
||||
|
||||
|
||||
def post_request(additions, json=None):
|
||||
if json is None:
|
||||
json = {}
|
||||
scn = bpy.context.scene
|
||||
|
||||
url = f'http://{scn.rsl_command_ip_address}:{scn.rsl_command_ip_port}/v1/{scn.rsl_command_api_key}{additions}'
|
||||
print(url, json)
|
||||
request = requests.post(url, json=json)
|
||||
return request
|
||||
|
||||
|
||||
def is_error(self, data):
|
||||
print(data)
|
||||
if not data.get('response_code'):
|
||||
self.report({'ERROR'}, 'No response from Studio!')
|
||||
return True
|
||||
|
||||
if data.get('response_code') != 'OK':
|
||||
|
||||
if data.get('response_code') == 'INVALID_REQUEST':
|
||||
self.report({'ERROR'}, data.get('response_code') + '\n' + data.get('description') + ' Check your API key.')
|
||||
return True
|
||||
|
||||
self.report({'ERROR'}, data.get('response_code') + '\n' + data.get('description'))
|
||||
return True
|
||||
return False
|
||||
@@ -0,0 +1,227 @@
|
||||
import os
|
||||
import bpy
|
||||
import bpy_extras
|
||||
|
||||
from ..core import animation_lists
|
||||
from ..core import detection_manager
|
||||
from ..core import custom_schemes_manager
|
||||
|
||||
|
||||
class DetectFaceShapes(bpy.types.Operator):
|
||||
bl_idname = "rsl.detect_face_shapes"
|
||||
bl_label = "Auto Detect"
|
||||
bl_description = "Automatically detect face shape keys for supported naming schemes"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
if not hasattr(obj.data, 'shape_keys') or not hasattr(obj.data.shape_keys, 'key_blocks'):
|
||||
self.report({'ERROR'}, 'This mesh has no shapekeys!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
for shape_name_key in animation_lists.face_shapes:
|
||||
setattr(obj, 'rsl_face_' + shape_name_key, detection_manager.detect_shape(obj, shape_name_key))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class DetectActorBones(bpy.types.Operator):
|
||||
bl_idname = "rsl.detect_actor_bones"
|
||||
bl_label = "Auto Detect"
|
||||
bl_description = "Automatically detect actor bones for supported naming schemes"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
for bone_name_key in animation_lists.get_bones().keys():
|
||||
setattr(obj, 'rsl_actor_' + bone_name_key, detection_manager.detect_bone(obj, bone_name_key))
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveCustomShapes(bpy.types.Operator):
|
||||
bl_idname = "rsl.save_custom_shapes"
|
||||
bl_label = "Save Custom Shapes"
|
||||
bl_description = "This saves the currently selected shapekeys and they will then get automatically detected"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
# Go over all face shapekeys and see if the user changed the detected shapekey. If yes, save that new shapekey
|
||||
for shape_name_key in animation_lists.face_shapes:
|
||||
shape_name_selected = getattr(obj, 'rsl_face_' + shape_name_key)
|
||||
if not shape_name_selected:
|
||||
continue # TODO idea: maybe save these unselected choices as well
|
||||
|
||||
shape_name_detected = detection_manager.detect_shape(obj, shape_name_key)
|
||||
|
||||
if shape_name_detected == shape_name_selected: # This means that the user changed nothing, so don't save this
|
||||
continue
|
||||
|
||||
custom_schemes_manager.save_live_data_shape_to_list(shape_name_key, shape_name_selected, shape_name_detected)
|
||||
|
||||
# At the end save all custom shapes to the file
|
||||
custom_schemes_manager.save_to_file_and_update()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveCustomBones(bpy.types.Operator):
|
||||
bl_idname = "rsl.save_custom_bones"
|
||||
bl_label = "Save Custom Bones"
|
||||
bl_description = "This saves the currently selected bones and they will then get automatically detected"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
# Go over all actor bones and see if the user changed the detected bone. If yes, save that new bone
|
||||
for bone_name_key in animation_lists.get_bones().keys():
|
||||
bone_name_selected = getattr(obj, 'rsl_actor_' + bone_name_key)
|
||||
if not bone_name_selected:
|
||||
continue # TODO idea: maybe save these unselected choices as well
|
||||
|
||||
bone_name_detected = detection_manager.detect_bone(obj, bone_name_key)
|
||||
|
||||
if bone_name_detected == bone_name_selected: # This means that the user changed nothing, so don't save this
|
||||
continue
|
||||
|
||||
custom_schemes_manager.save_live_data_bone_to_list(bone_name_key, bone_name_selected, bone_name_detected)
|
||||
|
||||
# At the end save all custom bones to the file
|
||||
custom_schemes_manager.save_to_file_and_update()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class SaveCustomBonesRetargeting(bpy.types.Operator):
|
||||
bl_idname = "rsl.save_custom_bones_retargeting"
|
||||
bl_label = "Save Custom Bones"
|
||||
bl_description = "This saves the currently selected bones and they will then get automatically detected"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
# Save the bone list if the user changed anything
|
||||
custom_schemes_manager.save_retargeting_to_list()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ImportCustomBones(bpy.types.Operator, bpy_extras.io_utils.ImportHelper):
|
||||
bl_idname = "rsl.import_custom_schemes"
|
||||
bl_label = "Import Custom Scheme"
|
||||
bl_description = "Import a custom naming scheme"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
|
||||
directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
|
||||
filter_glob: bpy.props.StringProperty(default='*.json;', options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
import_count = 0
|
||||
if self.directory:
|
||||
for f in self.files:
|
||||
file_name = f.name
|
||||
if not file_name.endswith('.json'):
|
||||
continue
|
||||
custom_schemes_manager.import_custom_list(self.directory, file_name)
|
||||
import_count += 1
|
||||
|
||||
# If this operator is called with no directory but a filepath argument, import that
|
||||
elif self.filepath:
|
||||
custom_schemes_manager.import_custom_list(os.path.dirname(self.filepath), os.path.basename(self.filepath))
|
||||
import_count += 1
|
||||
|
||||
custom_schemes_manager.save_to_file_and_update()
|
||||
|
||||
if not import_count:
|
||||
self.report({'ERROR'}, 'No files were imported.')
|
||||
return {'FINISHED'}
|
||||
|
||||
self.report({'INFO'}, 'Successfully imported new naming schemes.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ExportCustomBones(bpy.types.Operator, bpy_extras.io_utils.ExportHelper):
|
||||
bl_idname = "rsl.export_custom_schemes"
|
||||
bl_label = "Export Custom Scheme"
|
||||
bl_description = "Export your custom naming schemes"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
filename_ext = ".json"
|
||||
filter_glob: bpy.props.StringProperty(default='*.json;', options={'HIDDEN'})
|
||||
|
||||
def execute(self, context):
|
||||
file_name = custom_schemes_manager.export_custom_list(self.filepath)
|
||||
|
||||
if not file_name:
|
||||
self.report({'ERROR'}, 'You don\'t have any custom naming schemes!')
|
||||
return {'FINISHED'}
|
||||
|
||||
self.report({'INFO'}, 'Exported custom naming schemes as "' + file_name + '".')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ClearCustomBones(bpy.types.Operator):
|
||||
bl_idname = "rsl.clear_custom_bones"
|
||||
bl_label = "Clear Custom Bones"
|
||||
bl_description = "Clear all custom bone naming schemes"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='You are about to delete all stored custom bone naming schemes.', icon='ERROR')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='Continue?', icon='BLANK1')
|
||||
|
||||
layout.separator()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def execute(self, context):
|
||||
custom_schemes_manager.delete_custom_bone_list()
|
||||
|
||||
self.report({'INFO'}, 'Cleared all custom bone naming schemes!')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ClearCustomShapes(bpy.types.Operator):
|
||||
bl_idname = "rsl.clear_custom_shapes"
|
||||
bl_label = "Clear Custom Shapekeys"
|
||||
bl_description = "Clear all custom shape naming schemes"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
layout.separator()
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='You are about to delete all stored custom shape naming schemes.', icon='ERROR')
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.scale_y = 0.5
|
||||
row.label(text='Continue?', icon='BLANK1')
|
||||
|
||||
layout.separator()
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_props_dialog(self, width=400)
|
||||
|
||||
def execute(self, context):
|
||||
custom_schemes_manager.delete_custom_shape_list()
|
||||
|
||||
self.report({'INFO'}, 'Cleared all custom shape naming schemes!')
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,63 @@
|
||||
import bpy
|
||||
import webbrowser
|
||||
|
||||
from ..core import login_manager as lm
|
||||
|
||||
|
||||
class LicenseButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.info_license'
|
||||
bl_label = 'License'
|
||||
bl_description = 'Opens the license in the browser'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open('https://github.com/RokokoElectronics/rokoko-studio-live-blender/blob/master/LICENSE.md')
|
||||
self.report({'INFO'}, 'Opened license.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RokokoButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.info_rokoko'
|
||||
bl_label = 'Website'
|
||||
bl_description = 'Opens the Rokoko website in the browser'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open('https://www.rokoko.com/en')
|
||||
self.report({'INFO'}, 'Opened Rokoko website.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class DocumentationButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.info_documentation'
|
||||
bl_label = 'Documentation'
|
||||
bl_description = 'Opens the documentation in the browser'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open('https://github.com/Rokoko/rokoko-studio-live-blender#readme')
|
||||
self.report({'INFO'}, 'Opened documentation.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ForumButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.info_forum'
|
||||
bl_label = 'Join our Forums'
|
||||
bl_description = 'Opens the Rokoko Blender forum in the browser'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
webbrowser.open('https://rokoko.freshdesk.com/support/discussions/forums/47000399880')
|
||||
self.report({'INFO'}, 'Opened forums.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ToggleRokokoIDButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.toggle_rokoko_id'
|
||||
bl_label = 'Toggle Rokoko ID'
|
||||
bl_description = 'Toggles the visibility of your Rokoko ID'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
lm.user.display_email = not lm.user.display_email
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,87 @@
|
||||
import traceback
|
||||
|
||||
import bpy
|
||||
import importlib
|
||||
from ..core import login_manager
|
||||
from ..core import library_manager
|
||||
from ..core import live_data_manager
|
||||
|
||||
|
||||
class LoginButton(bpy.types.Operator):
|
||||
bl_idname = "rsl.login_login"
|
||||
bl_label = "Sign in"
|
||||
bl_description = "Sign into your Rokoko account with your browser"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return not login_manager.user.logging_in
|
||||
|
||||
def execute(self, context):
|
||||
login = login_manager.Login()
|
||||
login.start()
|
||||
self.report({'INFO'}, 'Opened Rokoko ID website in your browser.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class LogoutButton(bpy.types.Operator):
|
||||
bl_idname = "rsl.login_logout"
|
||||
bl_label = "Sign out"
|
||||
bl_description = "Sign out of your Rokoko account"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
login_manager.user.logout()
|
||||
self.report({'INFO'}, 'Logout successful.')
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class InstallLibsButton(bpy.types.Operator):
|
||||
bl_idname = 'rsl.login_install_libs'
|
||||
bl_label = 'Install Required Libraries'
|
||||
bl_description = 'Installs the required libraries for this plugin to work'
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
# Install the libraries
|
||||
try:
|
||||
self.install_libs()
|
||||
except ImportError as e:
|
||||
self.report({'ERROR'}, str(e))
|
||||
return {'CANCELLED'}
|
||||
except Exception as e:
|
||||
trace = traceback.format_exc()
|
||||
error_str = f"Unable to install the libraries!" \
|
||||
f"\nTry running Blender as an admin and install the libraries again." \
|
||||
f"\n\nFull Error: \n\n{trace}"
|
||||
self.report({'ERROR'}, error_str)
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Save login manager data
|
||||
classes_logged_in = login_manager.user.classes_logged_in
|
||||
classes_logged_out = login_manager.user.classes_logged_out
|
||||
version_str = login_manager.user.version_str
|
||||
|
||||
# Reload files to load the libraries
|
||||
importlib.reload(login_manager)
|
||||
importlib.reload(live_data_manager)
|
||||
|
||||
# Load the login manager data
|
||||
login_manager.user.classes_logged_in = classes_logged_in
|
||||
login_manager.user.classes_logged_out = classes_logged_out
|
||||
login_manager.user.version_str = version_str
|
||||
|
||||
# Attempt to auto login the user
|
||||
login_manager.user.auto_login()
|
||||
|
||||
self.report({'INFO'}, 'Installed libraries successfully!')
|
||||
return {'FINISHED'}
|
||||
|
||||
def install_libs(self):
|
||||
missing = library_manager.lib_manager.install_libraries(["websockets", "gql", "cryptography", "boto3"])
|
||||
if missing:
|
||||
raise ImportError("The following libraries could not be installed: "
|
||||
"\n- " + " \n- ".join(missing) +
|
||||
" \n\nTry running Blender as an admin and install the libraries again."
|
||||
" \nSee console for more information.")
|
||||
library_manager.lib_manager.install_libraries(["lz4"])
|
||||
@@ -0,0 +1,100 @@
|
||||
import bpy
|
||||
import time
|
||||
from threading import Thread
|
||||
|
||||
from ..core import state_manager
|
||||
from ..core.receiver import Receiver
|
||||
from ..core.utils import ui_refresh_all
|
||||
from ..core.animations import clear_animations
|
||||
|
||||
timer = None
|
||||
receiver: Receiver = Receiver()
|
||||
receiver_enabled = False
|
||||
|
||||
|
||||
class ReceiverStart(bpy.types.Operator):
|
||||
bl_idname = "rsl.receiver_start"
|
||||
bl_label = "Start Receiver"
|
||||
bl_description = "Start receiving data from Rokoko Studio"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def modal(self, context, event):
|
||||
# If ECS or F8 is pressed, cancel
|
||||
if event.type == 'ESC' or event.type == 'F8' or not receiver_enabled:
|
||||
return self.cancel(context)
|
||||
|
||||
# This gets run every frame
|
||||
if event.type == 'TIMER':
|
||||
if bpy.context.screen.is_animation_playing:
|
||||
return self.cancel(context)
|
||||
|
||||
receiver.run()
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def execute(self, context):
|
||||
global receiver_enabled, receiver, timer
|
||||
|
||||
# Start the receiver
|
||||
try:
|
||||
receiver.start(context.scene.rsl_receiver_port)
|
||||
except OSError as e:
|
||||
print('Socket error:', e.strerror)
|
||||
self.report({'ERROR'}, 'This port is already in use!')
|
||||
return {'CANCELLED'}
|
||||
|
||||
receiver_enabled = True
|
||||
|
||||
# If animation is currently playing, stop it
|
||||
if context.screen.is_animation_playing:
|
||||
bpy.ops.screen.animation_play()
|
||||
|
||||
# Clear current live data
|
||||
clear_animations()
|
||||
|
||||
# Save the scene
|
||||
state_manager.save_scene()
|
||||
|
||||
# Register this classes modal operator in Blenders event handling system and execute it at the specified fps
|
||||
context.window_manager.modal_handler_add(self)
|
||||
timer = context.window_manager.event_timer_add(1 / context.scene.rsl_receiver_fps, window=bpy.context.window)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def cancel(self, context):
|
||||
ReceiverStart.force_disable()
|
||||
ui_refresh_all()
|
||||
return {'CANCELLED'}
|
||||
|
||||
@classmethod
|
||||
def force_disable(cls):
|
||||
global receiver_enabled, receiver, timer
|
||||
|
||||
receiver_enabled = False
|
||||
receiver.stop()
|
||||
|
||||
bpy.context.window_manager.event_timer_remove(timer)
|
||||
|
||||
# If the recording is still running, let it load the scene afterwards with a delay
|
||||
if bpy.context.scene.rsl_recording:
|
||||
bpy.context.scene.rsl_recording = False
|
||||
thread = Thread(target=load_scene_later, args=[])
|
||||
thread.start()
|
||||
else:
|
||||
state_manager.load_scene()
|
||||
|
||||
|
||||
def load_scene_later():
|
||||
time.sleep(0.04)
|
||||
state_manager.load_scene()
|
||||
|
||||
|
||||
class ReceiverStop(bpy.types.Operator):
|
||||
bl_idname = "rsl.receiver_stop"
|
||||
bl_label = "Stop Receiver"
|
||||
bl_description = "Stop receiving data from Rokoko Studio"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
global receiver_enabled
|
||||
receiver_enabled = False
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,32 @@
|
||||
import bpy
|
||||
|
||||
|
||||
class RecorderStart(bpy.types.Operator):
|
||||
bl_idname = "rsl.recorder_start"
|
||||
bl_label = "Start Recording"
|
||||
bl_description = "Start recording data from Rokoko Studio"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
if context.scene.rsl_recording:
|
||||
self.report({'ERROR'}, 'Already recording')
|
||||
return {'CANCELLED'}
|
||||
|
||||
context.scene.rsl_recording = True
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RecorderStop(bpy.types.Operator):
|
||||
bl_idname = "rsl.recorder_stop"
|
||||
bl_label = "Stop Recording"
|
||||
bl_description = "Stop recording data from Rokoko Studio" \
|
||||
"\nThe processing of the recording can take a couple minutes, depending on the length of the recording"
|
||||
bl_options = {'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
if not context.scene.rsl_recording:
|
||||
self.report({'ERROR'}, 'Not recording')
|
||||
return {'CANCELLED'}
|
||||
|
||||
context.scene.rsl_recording = False
|
||||
return {'FINISHED'}
|
||||
@@ -0,0 +1,536 @@
|
||||
import bpy
|
||||
import copy
|
||||
|
||||
from . import detector
|
||||
from ..core import utils
|
||||
from ..core.retargeting import get_source_armature, get_target_armature
|
||||
from ..core import detection_manager as detector
|
||||
from ..core import custom_schemes_manager
|
||||
from ..panels.retargeting import BoneListItem
|
||||
|
||||
RETARGET_ID = '_RSL_RETARGET'
|
||||
|
||||
|
||||
class BuildBoneList(bpy.types.Operator):
|
||||
bl_idname = "rsl.build_bone_list"
|
||||
bl_label = "Build Bone List"
|
||||
bl_description = "Builds the bone list from the animation and tries to automatically detect and match bones"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
armature_source = get_source_armature()
|
||||
armature_target = get_target_armature()
|
||||
|
||||
if not armature_source.animation_data or not armature_source.animation_data.action:
|
||||
self.report({'ERROR'}, 'No animation on the source armature found!'
|
||||
'\nSelect an armature with an animation as source.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if armature_source.name == armature_target.name:
|
||||
self.report({'ERROR'}, 'Source and target armature are the same!'
|
||||
'\nPlease select different armatures.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
retargeting_dict = detector.detect_retarget_bones()
|
||||
|
||||
# Clear the bone retargeting list
|
||||
context.scene.rsl_retargeting_bone_list.clear()
|
||||
|
||||
for bone_source, bone_values in retargeting_dict.items():
|
||||
bone_target, bone_key = bone_values
|
||||
|
||||
bone_item = context.scene.rsl_retargeting_bone_list.add()
|
||||
bone_item.bone_name_key = bone_key
|
||||
bone_item.bone_name_source = bone_source
|
||||
bone_item.bone_name_target = bone_target
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class AddBoneListItem(bpy.types.Operator):
|
||||
bl_idname = "rsl.add_bone_list_item"
|
||||
bl_label = "Add Bone List Item"
|
||||
bl_description = "Adds a customizable bone list item"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
bone_item = context.scene.rsl_retargeting_bone_list.add()
|
||||
bone_item.is_custom = True
|
||||
|
||||
context.scene.rsl_retargeting_bone_list_index = len(context.scene.rsl_retargeting_bone_list) - 1
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class ClearBoneList(bpy.types.Operator):
|
||||
bl_idname = "rsl.clear_bone_list"
|
||||
bl_label = "Clear Bone List"
|
||||
bl_description = "Clears the bone list so that you can manually fill in all bones"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
def execute(self, context):
|
||||
for bone_item in context.scene.rsl_retargeting_bone_list:
|
||||
bone_item.bone_name_target = ''
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class RetargetAnimation(bpy.types.Operator):
|
||||
bl_idname = "rsl.retarget_animation"
|
||||
bl_label = "Retarget Animation"
|
||||
bl_description = "Retargets the animation from the source armature to the target armature"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
retarget_bone_list: [BoneListItem] = []
|
||||
|
||||
def execute(self, context):
|
||||
armature_source = get_source_armature()
|
||||
armature_target = get_target_armature()
|
||||
|
||||
if not armature_source.animation_data or not armature_source.animation_data.action:
|
||||
self.report({'ERROR'}, 'No animation on the source armature found!'
|
||||
'\nSelect an armature with an animation as source.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if armature_source.name == armature_target.name:
|
||||
self.report({'ERROR'}, 'Source and target armature are the same!'
|
||||
'\nPlease select different armatures.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Build retargeting bone list
|
||||
self.retarget_bone_list.clear()
|
||||
for item in context.scene.rsl_retargeting_bone_list:
|
||||
if not item.bone_name_source or not item.bone_name_target \
|
||||
or not armature_source.pose.bones.get(item.bone_name_source) \
|
||||
or not armature_target.pose.bones.get(item.bone_name_target):
|
||||
continue
|
||||
self.retarget_bone_list.append(item)
|
||||
|
||||
# Find the root bones and cancel if none are found
|
||||
root_bones = self.find_root_bones(context, armature_source, armature_target)
|
||||
if not root_bones:
|
||||
self.report({'ERROR'}, 'No root bone found!'
|
||||
'\nCheck if the bones are mapped correctly or try rebuilding the bone list.')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Check for duplicate target bone entries
|
||||
seen = {}
|
||||
for item in self.retarget_bone_list:
|
||||
count = seen.get(item.bone_name_target)
|
||||
if not count:
|
||||
count = 0
|
||||
seen[item.bone_name_target] = count + 1
|
||||
duplicates = [key for key, value in seen.items() if value > 1]
|
||||
if duplicates:
|
||||
self.report({'ERROR'}, 'Duplicate target bone entries found! Please use each target bone only once:'
|
||||
f'\n{", ".join(duplicates)}')
|
||||
return {'CANCELLED'}
|
||||
|
||||
# Save the bone list if the user changed anything
|
||||
custom_schemes_manager.save_retargeting_to_list()
|
||||
|
||||
# Prepare armatures
|
||||
utils.set_active(armature_target)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
utils.set_active(armature_source)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Set armatures into pose mode
|
||||
armature_source.data.pose_position = 'POSE'
|
||||
armature_target.data.pose_position = 'POSE'
|
||||
|
||||
# Save and reset the current pose position of both armatures if rest position should be used
|
||||
pose_source, pose_target = {}, {}
|
||||
if bpy.context.scene.rsl_retargeting_use_pose == 'REST':
|
||||
pose_source = self.get_and_reset_pose_rotations(armature_source)
|
||||
pose_target = self.get_and_reset_pose_rotations(armature_target)
|
||||
|
||||
# Auto scaling
|
||||
source_scale = None
|
||||
if context.scene.rsl_retargeting_auto_scaling:
|
||||
# Clean source animation
|
||||
# TODO: This causes issues when all Hip bone data is on the armature itself
|
||||
self.clean_animation(armature_source)
|
||||
|
||||
# Scale the source armature to fit the target armature
|
||||
source_scale = copy.deepcopy(armature_source.scale)
|
||||
self.scale_armature(context, armature_source, armature_target, root_bones)
|
||||
|
||||
# Duplicate source armature to apply transforms to the animation
|
||||
armature_source_original = armature_source
|
||||
armature_source = self.copy_rest_pose(context, armature_source)
|
||||
|
||||
# Save transforms of target armature
|
||||
rotation_mode = armature_target.rotation_mode
|
||||
armature_target.rotation_mode = 'QUATERNION'
|
||||
rotation = copy.deepcopy(armature_target.rotation_quaternion)
|
||||
location = copy.deepcopy(armature_target.location)
|
||||
|
||||
# Apply transforms of the target armature
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_target)
|
||||
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
|
||||
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# Create a transformation dict of all bones of the target armature and unselect all bones
|
||||
bone_transforms = {}
|
||||
for bone in context.object.data.edit_bones:
|
||||
bone.select = False
|
||||
bone_transforms[bone.name] = armature_source.matrix_world.inverted() @ bone.head.copy(), \
|
||||
armature_source.matrix_world.inverted() @ bone.tail.copy(), \
|
||||
utils.mat3_to_vec_roll(armature_source.matrix_world.inverted().to_3x3() @ bone.matrix.to_3x3()) # Head loc, tail loc, bone roll
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_source)
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
|
||||
# Recreate bones from target armature in source armature
|
||||
for item in self.retarget_bone_list:
|
||||
bone_source = armature_source.data.edit_bones.get(item.bone_name_source)
|
||||
|
||||
# Recreate target bone
|
||||
bone_new = armature_source.data.edit_bones.new(item.bone_name_target + RETARGET_ID)
|
||||
bone_new.head, bone_new.tail, bone_new.roll = bone_transforms[item.bone_name_target]
|
||||
bone_new.parent = bone_source
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
# Add constraints to target armature and select the bones for animation
|
||||
for item in self.retarget_bone_list:
|
||||
bone_target = armature_target.pose.bones.get(item.bone_name_target)
|
||||
|
||||
# Add constraints
|
||||
constraint = bone_target.constraints.new('COPY_ROTATION')
|
||||
constraint.name += RETARGET_ID
|
||||
constraint.target = armature_source
|
||||
constraint.subtarget = item.bone_name_target + RETARGET_ID
|
||||
|
||||
if bone_target.name in root_bones:
|
||||
constraint = bone_target.constraints.new('COPY_LOCATION')
|
||||
constraint.name += RETARGET_ID
|
||||
constraint.target = armature_source
|
||||
constraint.subtarget = item.bone_name_source
|
||||
|
||||
# Select the bone for animation
|
||||
armature_target.data.bones.get(item.bone_name_target).select = True
|
||||
|
||||
# Bake the animation to the target armature
|
||||
self.bake_animation(armature_source, armature_target, root_bones)
|
||||
|
||||
# Delete the duplicate helper armature
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_source)
|
||||
bpy.data.actions.remove(armature_source.animation_data.action)
|
||||
bpy.ops.object.delete()
|
||||
|
||||
# Change armature source back to original
|
||||
armature_source = armature_source_original
|
||||
|
||||
# Change action name
|
||||
armature_target.animation_data.action.name = armature_source.animation_data.action.name + ' Retarget'
|
||||
|
||||
# Remove constraints from target armature
|
||||
for bone in armature_target.pose.bones:
|
||||
for constraint in bone.constraints:
|
||||
if RETARGET_ID in constraint.name:
|
||||
bone.constraints.remove(constraint)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_target)
|
||||
|
||||
# Reset target armature transforms to old state
|
||||
armature_target.rotation_quaternion = rotation
|
||||
armature_target.location = location
|
||||
|
||||
armature_target.rotation_quaternion.w = -armature_target.rotation_quaternion.w
|
||||
bpy.ops.object.transform_apply(location=False, rotation=True, scale=False)
|
||||
armature_target.rotation_quaternion = rotation
|
||||
armature_target.rotation_mode = rotation_mode
|
||||
|
||||
# Reset source armature scale
|
||||
if source_scale:
|
||||
armature_source.scale = source_scale
|
||||
|
||||
# Reset pose positions to old state
|
||||
# self.load_pose_rotations(armature_source, pose_source)
|
||||
# self.load_pose_rotations(armature_target, pose_target)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
self.report({'INFO'}, 'Retargeted animation.')
|
||||
return {'FINISHED'}
|
||||
|
||||
def find_root_bones(self, context, armature_source, armature_target):
|
||||
# Find all root bones
|
||||
root_bones = []
|
||||
for bone in armature_target.pose.bones:
|
||||
if not bone.parent:
|
||||
root_bones.append(bone)
|
||||
|
||||
# Find animated root bones
|
||||
root_bones_animated = []
|
||||
target_bones = [item.bone_name_target for item in self.retarget_bone_list]
|
||||
while root_bones:
|
||||
for bone in copy.copy(root_bones):
|
||||
root_bones.remove(bone)
|
||||
if bone.name in target_bones:
|
||||
root_bones_animated.append(bone.name)
|
||||
else:
|
||||
for bone_child in bone.children:
|
||||
root_bones.append(bone_child)
|
||||
return root_bones_animated
|
||||
|
||||
def clean_animation(self, armature_source):
|
||||
deletable_fcurves = ['location', 'rotation_euler', 'rotation_quaternion', 'scale']
|
||||
for fcurve in armature_source.animation_data.action.fcurves:
|
||||
if fcurve.data_path in deletable_fcurves:
|
||||
armature_source.animation_data.action.fcurves.remove(fcurve)
|
||||
|
||||
def get_and_reset_pose_rotations(self, armature):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Save rotations
|
||||
pose_rotations = {}
|
||||
for bone in armature.pose.bones:
|
||||
if bone.rotation_mode == 'QUATERNION':
|
||||
pose_rotations[bone.name] = copy.deepcopy(bone.rotation_quaternion)
|
||||
bone.rotation_quaternion = (1, 0, 0, 0)
|
||||
else:
|
||||
pose_rotations[bone.name] = copy.deepcopy(bone.rotation_euler)
|
||||
bone.rotation_euler = (0, 0, 0)
|
||||
|
||||
# Reset rotations
|
||||
# bpy.ops.pose.rot_clear()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
return pose_rotations
|
||||
|
||||
def load_pose_rotations(self, armature, pose_rotations):
|
||||
if not pose_rotations:
|
||||
return
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Load rotations
|
||||
for bone in armature.pose.bones:
|
||||
rot = pose_rotations.get(bone.name)
|
||||
if rot:
|
||||
if bone.rotation_mode == 'QUATERNION':
|
||||
bone.rotation_quaternion = rot
|
||||
else:
|
||||
bone.rotation_euler = rot
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
def scale_armature(self, context, armature_source, armature_target, root_bones):
|
||||
source_min = None
|
||||
source_min_root = None
|
||||
target_min = None
|
||||
target_min_root = None
|
||||
|
||||
for item in self.retarget_bone_list:
|
||||
bone_source = armature_source.pose.bones.get(item.bone_name_source)
|
||||
bone_target = armature_target.pose.bones.get(item.bone_name_target)
|
||||
|
||||
bone_source_z = (armature_source.matrix_world @ bone_source.head)[2]
|
||||
bone_target_z = (armature_target.matrix_world @ bone_target.head)[2]
|
||||
|
||||
if item.bone_name_target in root_bones:
|
||||
if source_min_root is None or source_min_root > bone_source_z:
|
||||
source_min_root = bone_source_z
|
||||
if target_min_root is None or target_min_root > bone_target_z:
|
||||
target_min_root = bone_target_z
|
||||
|
||||
if source_min is None or source_min > bone_source_z:
|
||||
source_min = bone_source_z
|
||||
if target_min is None or target_min > bone_target_z:
|
||||
target_min = bone_target_z
|
||||
|
||||
source_height = source_min_root - source_min
|
||||
target_height = target_min_root - target_min
|
||||
|
||||
if not source_height or not target_height:
|
||||
print('No scaling needed')
|
||||
return
|
||||
|
||||
scale_factor = target_height / source_height
|
||||
armature_source.scale *= scale_factor
|
||||
|
||||
def read_anim_start_end(self, armature):
|
||||
frame_start = None
|
||||
frame_end = None
|
||||
for fcurve in armature.animation_data.action.fcurves:
|
||||
for key in fcurve.keyframe_points:
|
||||
keyframe = key.co.x
|
||||
if frame_start is None:
|
||||
frame_start = keyframe
|
||||
if frame_end is None:
|
||||
frame_end = keyframe
|
||||
|
||||
if keyframe < frame_start:
|
||||
frame_start = keyframe
|
||||
if keyframe > frame_end:
|
||||
frame_end = keyframe
|
||||
|
||||
return frame_start, frame_end
|
||||
|
||||
def copy_rest_pose(self, context, armature_source):
|
||||
# make sure auto keyframe is disabled, leads to issues
|
||||
context.scene.tool_settings.use_keyframe_insert_auto = False
|
||||
|
||||
# ensure the source armature selection
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(armature_source)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
# Duplicate the source armature
|
||||
bpy.ops.object.duplicate_move(OBJECT_OT_duplicate={"linked": False, "mode": 'TRANSLATION'},
|
||||
TRANSFORM_OT_translate={"value": (0, 0, 0), "constraint_axis": (False, True, False), "mirror": False, "snap": False, "remove_on_cancel": False,
|
||||
"release_confirm": False})
|
||||
|
||||
# Set name of the copied source armature
|
||||
source_armature_copy = context.object
|
||||
source_armature_copy.name = armature_source.name + "_copy"
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
utils.set_active(source_armature_copy)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
|
||||
# Apply transforms of the new source armature. Unlink action temporarily to prevent warning in console
|
||||
action_tmp = source_armature_copy.animation_data.action
|
||||
source_armature_copy.animation_data.action = None
|
||||
bpy.ops.pose.armature_apply()
|
||||
source_armature_copy.animation_data.action = action_tmp
|
||||
|
||||
# Mimic the animation of the original source armature by adding constraints to the bones.
|
||||
# -> the new armature has the exact same animation but with applied transforms
|
||||
for bone in source_armature_copy.pose.bones:
|
||||
constraint = bone.constraints.new('COPY_TRANSFORMS')
|
||||
constraint.name = bone.name
|
||||
constraint.target = armature_source
|
||||
constraint.subtarget = bone.name
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
return source_armature_copy
|
||||
|
||||
def bake_animation(self, armature_source, armature_target, root_bones):
|
||||
frame_split = 25
|
||||
frame_start, frame_end = self.read_anim_start_end(armature_source)
|
||||
frame_start, frame_end = int(frame_start), int(frame_end)
|
||||
utils.set_active(armature_target)
|
||||
|
||||
actions_all = []
|
||||
|
||||
# Setup loading bar
|
||||
current_step = 0
|
||||
steps = int((frame_end - frame_start) / frame_split) + 1
|
||||
wm = bpy.context.window_manager
|
||||
wm.progress_begin(current_step, steps)
|
||||
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
# Bake the animation in parts because multiple short parts are processed much faster than one long animation
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
for frame in range(frame_start, frame_end + 2, frame_split):
|
||||
start = frame
|
||||
end = frame + frame_split - 1
|
||||
if end > frame_end:
|
||||
end = frame_end
|
||||
if start > end:
|
||||
continue
|
||||
|
||||
# Bake animation part
|
||||
bpy.ops.nla.bake(frame_start=start, frame_end=end, visual_keying=True, only_selected=True, use_current_action=False, bake_types={'POSE'})
|
||||
|
||||
# Rename animation part
|
||||
armature_target.animation_data.action.name = 'RSL_RETARGETING_' + str(frame)
|
||||
|
||||
actions_all.append(armature_target.animation_data.action)
|
||||
|
||||
current_step += 1
|
||||
if steps != current_step:
|
||||
wm.progress_update(current_step)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
if not actions_all:
|
||||
return
|
||||
|
||||
# Count all keys for all data_paths
|
||||
key_counts = {}
|
||||
for action in actions_all:
|
||||
for fcurve in action.fcurves:
|
||||
key = fcurve.data_path + str(fcurve.array_index)
|
||||
if not key_counts.get(key):
|
||||
key_counts[key] = 0
|
||||
key_counts[key] += len(fcurve.keyframe_points)
|
||||
|
||||
# Create new action
|
||||
action_final = bpy.data.actions.new(name='RSL_RETARGETING_FINAL')
|
||||
action_final.use_fake_user = True
|
||||
armature_target.animation_data_create().action = action_final
|
||||
|
||||
# Put all baked animations parts back together into one
|
||||
print_i = 0
|
||||
for fcurve in actions_all[0].fcurves:
|
||||
if fcurve.data_path.endswith('scale'):
|
||||
continue
|
||||
if fcurve.data_path.endswith('location'):
|
||||
bone_name = fcurve.data_path.split('"')
|
||||
if len(bone_name) != 3:
|
||||
continue
|
||||
if bone_name[1] not in root_bones:
|
||||
continue
|
||||
|
||||
curve_final = action_final.fcurves.new(data_path=fcurve.data_path, index=fcurve.array_index, action_group=fcurve.group.name)
|
||||
keyframe_points = curve_final.keyframe_points
|
||||
keyframe_points.add(key_counts[fcurve.data_path + str(fcurve.array_index)])
|
||||
|
||||
index = 0
|
||||
for action in actions_all:
|
||||
fcruve_to_add = action.fcurves.find(data_path=fcurve.data_path, index=fcurve.array_index)
|
||||
|
||||
for kp in fcruve_to_add.keyframe_points:
|
||||
keyframe_points[index].co.x = kp.co.x
|
||||
keyframe_points[index].co.y = kp.co.y
|
||||
keyframe_points[index].interpolation = 'LINEAR'
|
||||
index += 1
|
||||
|
||||
print_i += 1
|
||||
|
||||
# Clean up animation. Delete all keyframes the use the same value as the previous and next one
|
||||
for fcurve in action_final.fcurves:
|
||||
if len(fcurve.keyframe_points) <= 2:
|
||||
continue
|
||||
|
||||
kp_pre_pre = fcurve.keyframe_points[0]
|
||||
kp_pre = fcurve.keyframe_points[1]
|
||||
|
||||
kp_to_delete = []
|
||||
for kp in fcurve.keyframe_points[2:]:
|
||||
if round(kp_pre_pre.co.y, 5) == round(kp_pre.co.y, 5) == round(kp.co.y, 5):
|
||||
kp_to_delete.append(kp_pre)
|
||||
kp_pre_pre = kp_pre
|
||||
kp_pre = kp
|
||||
|
||||
for kp in reversed(kp_to_delete):
|
||||
fcurve.keyframe_points.remove(kp)
|
||||
|
||||
# Delete all baked animation parts, only the combined one is needed
|
||||
for action in actions_all:
|
||||
bpy.data.actions.remove(action)
|
||||
|
||||
print('Retargeting Time:', round(time.time() - start_time, 2), 'seconds')
|
||||
wm.progress_end()
|
||||
|
||||
# Set the action slot sub action
|
||||
if hasattr(armature_target.animation_data, "action_slot"):
|
||||
armature_target.animation_data.action_slot = armature_target.animation_data.action_suitable_slots[0]
|
||||
Reference in New Issue
Block a user