Files
blender-portable-repo/scripts/addons/cc_blender_tools-2_4_0/link.py
T
2026-03-17 15:34:28 -06:00

4991 lines
205 KiB
Python

import bpy #, bpy_extras
from bpy.app.handlers import persistent
#import bpy_extras.view3d_utils as v3d
from enum import IntEnum
import atexit
import os, socket, time, select, struct, json, copy, shutil, tempfile
#import subprocess
from mathutils import Vector, Quaternion, Matrix, Color, Euler
from . import (rlx, importer, exporter, facerig, bones, geom, colorspace,
world, rigging, rigutils, drivers, modifiers,
cc, jsonutils, utils, vars)
from typing import Tuple, List
import textwrap
BLENDER_PORT = 9333
UNITY_PORT = 9334
HANDSHAKE_TIMEOUT_S = 60
KEEPALIVE_TIMEOUT_S = 300
PING_INTERVAL_S = 120
TIMER_INTERVAL = 1/30
MAX_CHUNK_SIZE = 32768
SERVER_ONLY = False
CLIENT_ONLY = True
CHARACTER_TEMPLATE: list = None
MAX_RECEIVE = 30
USE_PING = False
USE_KEEPALIVE = False
SOCKET_TIMEOUT = 5.0
INCLUDE_POSE_MESHES = False
class OpCodes(IntEnum):
NONE = 0
HELLO = 1
PING = 2
STOP = 10
DISCONNECT = 11
DEBUG = 15
NOTIFY = 50
INVALID = 55
SAVE = 60
FILE = 75
FPS = 80
MORPH = 90
MORPH_UPDATE = 91
REPLACE_MESH = 95
MATERIALS = 96
CHARACTER = 100
CHARACTER_UPDATE = 101
PROP = 102
STAGING = 104
LIGHTS_UPDATE = 105
CAMERA = 106
CAMERA_UPDATE = 107
UPDATE_REPLACE = 108
RIGIFY = 110
TEMPLATE = 200
POSE = 210
POSE_FRAME = 211
SEQUENCE = 220
SEQUENCE_FRAME = 221
SEQUENCE_END = 222
SEQUENCE_ACK = 223
LIGHTING = 230
CAMERA_SYNC = 231
FRAME_SYNC = 232
MOTION = 240
REQUEST = 250
CONFIRM = 251
VISEME_NAME_MAP = {
"None": "None",
"Open": "V_Open",
"Explosive": "V_Explosive",
"Upper Dental": "V_Dental_Lip",
"Tight O": "V_Tight_O",
"Pucker": "V_Tight",
"Wide": "V_Wide",
"Affricate": "V_Affricate",
"Lips Parted": "V_Lip_Open",
"Tongue Up": "V_Tongue_up",
"Tongue Raised": "V_Tongue_Raise",
"Tongue Out": "V_Tongue_Out",
"Tongue Narrow": "V_Tongue_Narrow",
"Tongue Lower": "V_Tongue_Lower",
"Tongue Curl-U": "V_Tongue_Curl_U",
"Tongue Curl-D": "V_Tongue_Curl_D",
"EE": "EE",
"Er": "Er",
"Ih": "IH",
"Ah": "Ah",
"Oh": "Oh",
"W.OO": "W_OO",
"S.Z": "S_Z",
"Ch.J": "Ch_J",
"F.V": "F_V",
"Th": "TH",
"T.L.D": "T_L_D_N",
"B.M.P": "B_M_P",
"K.G": "K_G_H_NG",
"N.NG": "AE",
"R": "R",
}
class LinkActor():
name: str = "Name"
chr_cache = None
object: bpy.types.Object = None
bones: list = None
meshes: list = None
id_tree: dict = None
id_map: dict = None
skin_meshes: dict = None
rig_bones: list = None
expressions: list = None
visemes: list = None
morphs: list = None
cache: dict = None
alias: list = None
shape_keys: dict = None
ik_store: dict = None
rigify_ik_fk: float = 0.0
action_store_id: str = ""
def __init__(self, obj_or_chr_cache):
if type(obj_or_chr_cache) is bpy.types.Object:
self.object = obj_or_chr_cache
self.name = obj_or_chr_cache.name
else:
self.object = None
self.chr_cache = obj_or_chr_cache
self.name = obj_or_chr_cache.character_name
self.bones = []
self.meshes = []
self.id_tree = None
self.id_map = None
self.skin_meshes = None
self.rig_bones = []
self.expressions = []
self.visemes = []
self.morphs = []
self.cache = None
self.alias = []
self.shape_keys = {}
return
def get_chr_cache(self):
return self.chr_cache
def get_import_modes(self) -> Tuple[str, str]:
"""return (action_mode, frame_mode)\n
action_mode = {"NEW", "REPLACE", "BLEND"}\n
frame_mode = {"START", "CURRENT", "MATCH"}
"""
chr_cache = self.get_chr_cache()
if chr_cache:
return chr_cache.action_options.get_action_mode(), chr_cache.action_options.get_frame_mode()
else:
props = vars.props()
action_mode = props.action_options.get_action_mode()
frame_mode = props.action_options.get_frame_mode()
return action_mode, frame_mode
def get_link_id(self):
if self.object:
return utils.get_rl_link_id(self.object)
elif self.chr_cache:
return self.chr_cache.get_link_id()
return None
def get_armature(self):
if self.chr_cache:
return self.chr_cache.get_armature()
return None
def select(self):
if self.chr_cache:
self.chr_cache.select_all()
elif self.object:
utils.try_select_object(self.object)
def get_type(self):
"""AVATAR|PROP|LIGHT|CAMERA|NONE"""
if self.chr_cache:
return self.chr_cache_type(self.chr_cache)
elif self.object:
return self.object.type
return "NONE"
def add_alias(self, link_id):
chr_cache = self.get_chr_cache()
if chr_cache:
actor_link_id = chr_cache.link_id
if not actor_link_id:
utils.log_info(f"Assigning actor link_id: {chr_cache.character_name}: {link_id}")
chr_cache.set_link_id(link_id)
return
if link_id not in self.alias and actor_link_id != link_id:
utils.log_info(f"Assigning actor alias: {chr_cache.character_name}: {link_id}")
self.alias.append(link_id)
return
@staticmethod
def get_start_frame(actor: 'LinkActor', ccic_frame, blender_current):
props = vars.props()
chr_cache = actor.get_chr_cache() if actor else None
if chr_cache:
return chr_cache.action_options.get_start_frame(ccic_frame, blender_current)
else:
return props.action_options.get_start_frame(ccic_frame, blender_current)
@staticmethod
def get_sequence_frame(actor: 'LinkActor', ccic_frame, ccic_frame_start, blender_current):
props = vars.props()
chr_cache = actor.get_chr_cache() if actor else None
if chr_cache:
return chr_cache.action_options.get_sequence_frame(ccic_frame, ccic_frame_start, blender_current)
else:
return props.action_options.get_sequence_frame(ccic_frame, ccic_frame_start, blender_current)
@staticmethod
def find_actor(link_id, search_name=None, search_type=None, context_chr_cache=None):
props = vars.props()
prefs = vars.prefs()
link_data = get_link_data()
utils.log_detail(f"Looking for LinkActor: {search_name} {link_id} {search_type}")
actor: LinkActor = None
for obj in bpy.data.objects:
if obj.type == "LIGHT" or obj.type == "CAMERA":
if not search_type or (obj.type == search_type):
obj_link_id = utils.get_rl_link_id(obj)
if obj_link_id is not None and link_id == obj_link_id:
actor = LinkActor(obj)
utils.log_detail(f"Staging (Light/Camera) found by link_id: {actor.name} / {link_id}")
return actor
chr_cache = props.find_character_by_link_id(link_id)
if chr_cache:
if not search_type or LinkActor.chr_cache_type(chr_cache) == search_type:
actor = LinkActor(chr_cache)
utils.log_detail(f"Chr found by link_id: {actor.name} / {link_id}")
return actor
utils.log_detail(f"Chr not found by link_id")
# try to find the character by name if the link id finds nothing
# character id's change after every reload in iClone/CC4 so these can change.
if search_name:
chr_cache = props.find_character_by_name(search_name)
if chr_cache:
if not search_type or LinkActor.chr_cache_type(chr_cache) == search_type:
utils.log_detail(f"Chr found by name: {chr_cache.character_name} / {chr_cache.link_id} -> {link_id}")
actor = LinkActor(chr_cache)
actor.add_alias(link_id)
return actor
utils.log_detail(f"Chr not found by name")
# finally if matching to any avatar, trying to find an avatar and there is only
# one avatar in the scene, use that one avatar, otherwise use the selected avatar
if False and link_data and link_data.is_cc() and prefs.datalink_match_any_avatar and search_type == "AVATAR":
chr_cache = None
if len(props.get_avatars()) == 1:
chr_cache = props.get_first_avatar()
else:
if not context_chr_cache:
context_chr_cache = props.get_context_character_cache()
if context_chr_cache and context_chr_cache.is_avatar():
chr_cache = context_chr_cache
if chr_cache:
utils.log_detail(f"Falling back to first Chr Avatar: {chr_cache.character_name} / {chr_cache.link_id} -> {link_id}")
actor = LinkActor(chr_cache)
actor.add_alias(link_id)
return actor
utils.log_info(f"LinkActor not found: {search_name} {link_id} {search_type}")
return actor
@staticmethod
def chr_cache_type(chr_cache):
if chr_cache:
return chr_cache.cache_type()
return "NONE"
def get_mesh_objects(self):
objects = None
chr_cache = self.get_chr_cache()
if chr_cache:
objects = chr_cache.get_all_objects(include_armature=False,
include_children=True,
of_type="MESH")
return objects
def object_has_sequence_shape_keys(self, obj):
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
for expression_name in self.expressions:
if expression_name in obj.data.shape_keys.key_blocks:
return True
for viseme_name in self.visemes:
if viseme_name in obj.data.shape_keys.key_blocks:
return True
return False
def collect_shape_keys(self):
self.shape_keys = {}
objects: list = self.get_mesh_objects()
# sort objects by reverse shape_key count (this should put the body mesh first)
objects.sort(key=utils.key_count, reverse=True)
# collect dictionary of shape keys and their primary key block
for obj in objects:
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
for key in obj.data.shape_keys.key_blocks:
if key.name not in self.shape_keys:
self.shape_keys[key.name] = key
def get_sequence_objects(self):
objects = []
non_sequence_objects = []
chr_cache = self.get_chr_cache()
if chr_cache:
all_objects = chr_cache.get_all_objects(include_armature=False,
include_children=True,
of_type="MESH")
for obj in all_objects:
if self.object_has_sequence_shape_keys(obj):
objects.append(obj)
else:
non_sequence_objects.append(obj)
return objects, non_sequence_objects
def set_template(self, actor_data: dict):
self.set_id_tree(actor_data.get("bones"),
actor_data.get("ids"),
actor_data.get("id_tree"))
self.expressions = actor_data.get("expressions")
self.visemes = self.remap_visemes(actor_data.get("visemes"))
self.morphs = actor_data.get("morphs")
skin_meshes = {}
if vars.DEV:
if self.get_type() == "AVATAR" or self.get_type() == "PROP":
utils.log_detail(f"Actor: {self.name}")
utils.log_detail(f"Bones: {self.bones}")
utils.log_detail(f"{json.dumps(self.id_tree, indent=4)}")
utils.log_detail(f"{json.dumps(self.id_map, indent=4)}")
for id, id_def in self.id_map.items():
if id_def["mesh"]:
obj: bpy.types.Object = bpy.data.objects[id_def["name"]]
skin_meshes[id] = [obj, Vector((0,0,0)), Quaternion((1,0,0,0)), Vector((1,1,1))]
self.skin_meshes = skin_meshes
def set_id_tree(self, bones, ids, id_tree):
arm = self.get_armature()
if self.is_rigified():
arm = None
if bones and ids and id_tree:
self.bones = bones
self.ids = ids
self.id_tree, self.id_map = cc.convert_id_tree(arm, id_tree)
self.meshes = [ id_def["name"] for id_def in self.id_map.values() if id_def["mesh"] ]
cc.confirm_bone_order(bones, ids, self.id_map)
else:
self.bones = None
self.ids = None
self.id_tree = None
self.id_map = None
def get_bone_id(self, bone_name):
if self.id_map:
for id, id_def in self.id_map.items():
if id_def["name"] == bone_name:
return id
if "_BoneRoot" in bone_name and "_BoneRoot" in id_def["name"]:
return id
return -1
def remap_visemes(self, visemes):
exported_visemes = []
for viseme_name in visemes:
if viseme_name in VISEME_NAME_MAP:
exported_visemes.append(VISEME_NAME_MAP[viseme_name])
return exported_visemes
def clear_template(self):
self.bones = None
def set_cache(self, cache):
self.cache = cache
def clear_cache(self):
self.cache = None
def update_name(self, new_name):
self.name = new_name
chr_cache = self.get_chr_cache()
if chr_cache:
chr_cache.character_name = new_name
def update_link_id(self, new_link_id):
chr_cache = self.get_chr_cache()
if chr_cache:
utils.log_info(f"Assigning new link_id: {chr_cache.character_name}: {new_link_id}")
chr_cache.set_link_id(new_link_id)
def ready(self, require_cache=True):
if require_cache and not self.cache:
return False
return (self.chr_cache and self.get_armature()) or self.object
def is_rigified(self):
chr_cache = self.get_chr_cache()
if chr_cache:
return chr_cache.rigified
return False
def has_key(self):
chr_cache = self.get_chr_cache()
if chr_cache:
return chr_cache.get_import_has_key()
return False
def can_go_cc(self):
chr_cache = self.get_chr_cache()
if chr_cache:
return chr_cache.can_go_cc()
return False
def can_go_ic(self):
chr_cache = self.get_chr_cache()
if chr_cache:
return chr_cache.can_go_ic()
return False
class LinkData():
actors: list = []
# Sequence/Pose Props
sequence_current_frame: int = 0
sequence_start_frame: int = 0
sequence_end_frame: int = 0
sequence_actors: list = None
sequence_type: str = None
scene_current_frame: int = 0
#
sequence_action_mode: str = ""
sequence_frame_mode: str = ""
#
preview_shape_keys: bool = True
preview_skip_frames: bool = False
# remote props
remote_app: str = None
remote_version: str = None
remote_path: str = None
remote_exe: str = None
#
ack_rate: float = 0.0
ack_time: float = 0.0
#
motion_prefix: str = ""
use_fake_user: bool = False
set_keyframes: bool = True
#
link_fps: int = 0
def __init__(self):
return
def reset(self):
self.actors = []
self.sequence_actors = None
self.sequence_type = None
def is_cc(self):
if self.remote_app == "Character Creator":
return True
else:
return False
def find_sequence_actor(self, link_id) -> LinkActor:
for actor in self.sequence_actors:
if actor.get_link_id() == link_id:
return actor
for actor in self.sequence_actors:
if link_id in actor.alias:
return actor
return None
def set_action_settings(self, prefix: str, fake_user, set_keyframes):
self.motion_prefix = prefix.strip()
self.use_fake_user = fake_user
self.set_keyframes = set_keyframes
LINK_DATA = LinkData()
def get_link_data():
global LINK_DATA
return LINK_DATA
def encode_from_json(json_data) -> bytearray:
json_string = json.dumps(json_data)
json_bytes = bytearray(json_string, "utf-8")
return json_bytes
def decode_to_json(data) -> dict:
text = data.decode("utf-8")
json_data = json.loads(text)
return json_data
def pack_string(s) -> bytearray:
buffer = bytearray()
buffer += struct.pack("!I", len(s))
buffer += bytes(s, encoding="utf-8")
return buffer
def unpack_string(buffer, offset=0):
length = struct.unpack_from("!I", buffer, offset)[0]
offset += 4
string: bytearray = buffer[offset:offset+length]
offset += length
return offset, string.decode(encoding="utf-8")
def get_datalink_temp_local_folder():
prefs = vars.prefs()
link_props = vars.link_props()
# prefs.temp_folder is the user definable temp folder
# link_props.temp_folder is the temp folder that was used to generate the current link_props.temp_files folder
# if prefs.temp_folder is changed, generate a new link_props.temp_files and store the new temp_folder
if prefs.temp_folder != link_props.temp_folder:
link_props.temp_files = ""
if not link_props.temp_files:
parent_dir = prefs.temp_folder if prefs.temp_folder else None
link_props.temp_files = tempfile.mkdtemp(dir=parent_dir)
link_props.temp_folder = prefs.temp_folder
return link_props.temp_files
def get_local_data_path():
prefs = vars.prefs()
link_props = vars.link_props()
local_path = utils.local_path()
blend_file_name = utils.blend_file_name()
data_path = ""
# if blend file is saved and has a local path, always use this as the parent folder to save local files
if local_path and blend_file_name:
data_path = local_path
# otherwise, if not saved yet, determine a temp folder location
else:
# if connected locally and we have CC/iClone's datalink path, use that for our local files
if (LINK_SERVICE and
LINK_SERVICE.is_local() and
LINK_SERVICE.remote_path and
not link_props.temp_files):
link_props.temp_files = tempfile.mkdtemp(dir=LINK_SERVICE.remote_path)
data_path = link_props.temp_files
# otherwise generate a temp folder in either the system temp files or in the user temp folder
# or regenerate a new one if the user temp folder has changed
elif not link_props.temp_files or prefs.temp_folder != link_props.temp_folder:
data_path = get_datalink_temp_local_folder()
else:
data_path = link_props.temp_files
return data_path
def get_remote_tar_file_path(remote_id):
data_path = get_local_data_path()
remote_import_path = utils.make_sub_folder(data_path, "imports")
remote_file_path = os.path.join(remote_import_path, f"{remote_id}.tar")
return remote_file_path
def get_unpacked_tar_file_folder(remote_id):
data_path = get_local_data_path()
remote_import_path = utils.make_sub_folder(data_path, "imports")
remote_files_folder = os.path.join(data_path, "imports", remote_id)
return remote_files_folder
def find_rig_pivot_bone(rig, parent):
bone: bpy.types.PoseBone
for bone in rig.pose.bones:
if bone.name.startswith("CC_Base_Pivot"):
if bones.is_target_bone_name(bone.parent.name, parent):
return bone.name
return None
def BFA(f):
"""Blender Frame Adjust:
Convert Blender frame index (starting at frame 1)
to CC/iC frame index (starting at frame 0)
"""
return max(0, f - 1)
def RLFA(f):
"""Reallusion Frame Adjust:
Convert Reallusion frame index (starting at frame 0)
to Blender frame index (starting at frame 1)
"""
return f + 1
def make_datalink_import_rig(actor: LinkActor, objects: list):
"""Creates or re-uses and existing datalink pose rig for the character.
This uses a pre-generated character template (list of bones in the character)
sent from CC/iC to avoid encoding the bone names into the pose data stream."""
if not actor:
utils.log_error("make_datalink_import_rig - Invalid Actor:")
return None
if not actor.get_chr_cache():
utils.log_error(f"make_datalink_import_rig - Invalid Actor cache: {actor.name}")
return None
# get character armature
chr_rig = actor.get_armature()
if not chr_rig:
utils.log_error(f"make_datalink_import_rig - Invalid Actor armature: {actor.name}")
return None
RV = utils.store_render_visibility_state(chr_rig)
utils.unhide(chr_rig)
chr_cache = actor.get_chr_cache()
is_prop = actor.get_type() == "PROP"
if utils.object_exists_is_armature(chr_cache.rig_datalink_rig):
actor.rig_bones = actor.bones.copy()
utils.hide(chr_cache.rig_datalink_rig)
#utils.log_info(f"Using existing datalink transfer rig: {chr_cache.rig_datalink_rig.name}")
# add child proxy objects
for obj in chr_cache.rig_datalink_rig.children:
if utils.object_exists_is_mesh(obj):
objects.append(obj)
utils.restore_render_visibility_state(RV)
return chr_cache.rig_datalink_rig
rig_name = f"{chr_cache.character_name}_Link_Rig"
utils.log_info(f"Creating datalink transfer rig: {rig_name}")
# create pose armature
datalink_rig = utils.get_armature(rig_name)
if not datalink_rig:
datalink_rig = utils.create_reuse_armature(rig_name)
chr_rig = chr_cache.get_armature()
chr_collections = utils.get_object_scene_collections(chr_rig)
utils.move_object_to_scene_collections(datalink_rig, chr_collections)
edit_bone: bpy.types.EditBone
arm: bpy.types.Armature = datalink_rig.data
rig_bones = []
if utils.edit_mode_to(datalink_rig):
while len(datalink_rig.data.edit_bones) > 0:
datalink_rig.data.edit_bones.remove(datalink_rig.data.edit_bones[0])
for i, id in enumerate(actor.ids):
if id in actor.id_map:
id_def = actor.id_map[id]
if not id_def["mesh"]:
edit_bone = arm.edit_bones.new(id_def["name"])
rig_bones.append(id_def["name"])
edit_bone.head = Vector((0,0,0))
edit_bone.tail = Vector((0,1,0))
edit_bone.align_roll(Vector((0,0,1)))
edit_bone.length = 0.1
utils.object_mode_to(datalink_rig)
datalink_rig.show_in_front = False
datalink_rig.data.display_type = "STICK"
# constrain character armature if not rigified
if not chr_cache.rigified:
for i, rig_bone_name in enumerate(rig_bones):
sk_bone_name = actor.bones[i]
chr_bone_name = bones.find_target_bone_name(chr_rig, rig_bone_name)
if chr_bone_name:
bones.add_copy_location_constraint(datalink_rig, chr_rig, rig_bone_name, chr_bone_name)
bones.add_copy_rotation_constraint(datalink_rig, chr_rig, rig_bone_name, chr_bone_name)
bones.add_copy_scale_constraint(datalink_rig, chr_rig, rig_bone_name, chr_bone_name)
else:
utils.log_warn(f"Could not find target bone for: {rig_bone_name} in character rig!")
utils.safe_set_action(datalink_rig, None)
utils.object_mode_to(datalink_rig)
utils.hide(datalink_rig)
chr_cache.rig_datalink_rig = datalink_rig
if chr_cache.rigified:
# a rigified character must retarget the link rig, but...
# the link rig doesn't have a valid bind pose, so the retargeting rig
# can't use it as a source rig for the roll axes on the ORG bones,
# so we use the original ones for the character type (option to_original_rig)
# (data on the original bones is added the ORG bones during rigify process)
rigging.adv_retarget_remove_pair(None, chr_cache)
if not chr_cache.rig_retarget_rig:
rigging.adv_retarget_pair_rigs(None, chr_cache,
source_rig=datalink_rig,
source_action=None,
to_original_rig=True,
objects=objects,
shape_keys=actor.expressions)
utils.restore_render_visibility_state(RV)
return datalink_rig
def remove_datalink_import_rig(actor: LinkActor, apply_constraints=False):
if actor:
chr_cache = actor.get_chr_cache()
chr_rig = actor.get_armature()
RV = utils.store_render_visibility_state(chr_rig)
utils.unhide(chr_rig)
if apply_constraints and chr_rig:
if utils.set_active_object(chr_rig):
if utils.pose_mode_to(chr_rig):
action, slot = utils.safe_get_action_slot(chr_rig)
utils.safe_set_action(chr_rig, None)
bpy.ops.pose.visual_transform_apply()
pose = bones.copy_pose(chr_rig)
utils.safe_set_action(chr_rig, action, slot=slot)
if utils.object_exists_is_armature(chr_cache.rig_datalink_rig):
if chr_cache.rigified:
rigging.adv_retarget_remove_pair(None, chr_cache)
if actor.ik_store:
rigutils.set_rigify_ik_fk_influence(chr_rig, actor.ik_store["ik_fk"])
rigutils.restore_ik_stretch(actor.ik_store)
else:
# remove all contraints on the character rig
if utils.object_exists(chr_rig):
if utils.object_mode_to(chr_rig):
for pose_bone in chr_rig.pose.bones:
bones.clear_constraints(chr_rig, pose_bone.name)
utils.delete_armature_object(chr_cache.rig_datalink_rig)
chr_cache.rig_datalink_rig = None
if apply_constraints and chr_rig:
if utils.set_active_object(chr_rig):
if utils.pose_mode_to(chr_rig):
bones.paste_pose(chr_rig, pose)
#rigging.reset_shape_keys(chr_cache)
utils.restore_render_visibility_state(RV)
utils.object_mode_to(chr_rig)
def set_actor_expression_weight(objects, expression_name, weight):
global LINK_DATA
if objects:
obj: bpy.types.Object
for obj in objects:
if expression_name in obj.data.shape_keys.key_blocks:
if obj.data.shape_keys.key_blocks[expression_name].value != weight:
obj.data.shape_keys.key_blocks[expression_name].value = weight
def set_actor_viseme_weight(objects, viseme_name, weight):
global LINK_DATA
if objects and LINK_DATA.preview_shape_keys:
for obj in objects:
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
if viseme_name in obj.data.shape_keys.key_blocks:
if obj.data.shape_keys.key_blocks[viseme_name].value != weight:
obj.data.shape_keys.key_blocks[viseme_name].value = weight
def ensure_current_frame(current_frame):
if bpy.context.scene.frame_current != current_frame:
bpy.context.scene.frame_current = current_frame
return current_frame
def next_frame(current_frame=None):
if current_frame is None:
current_frame = bpy.context.scene.frame_current
fps = bpy.context.scene.render.fps
end_frame = bpy.context.scene.frame_end
current_frame = min(end_frame, current_frame + 1)
bpy.context.scene.frame_current = current_frame
return current_frame
def prev_frame(current_frame=None):
if current_frame is None:
current_frame = bpy.context.scene.frame_current
fps = bpy.context.scene.render.fps
start_frame = bpy.context.scene.frame_start
current_frame = max(start_frame, current_frame - 1)
bpy.context.scene.frame_current = current_frame
return current_frame
def create_rotation_fcurves_cache(obj, count):
if obj.rotation_mode == "QUATERNION":
indices = 4
defaults = [1,0,0,0]
elif obj.rotation_mode == "AXIS_ANGLE":
indices = 4
defaults = [0,0,1,0]
else: # transform_object.rotation_mode in [ "XYZ", "XZY", "YXZ", "YZX", "ZXY", "ZYX" ]:
indices = 3
defaults = [0,0,0]
return create_fcurves_cache(count, indices, defaults, cache_type=obj.rotation_mode)
def create_fcurves_cache(count, indices, defaults, cache_type="VALUE"):
curves = []
cache = {
"count": count,
"indices": indices,
"curves": curves,
"type": cache_type,
}
for i in range(0, indices):
d = defaults[i]
cache_data = [d]*(count*2)
curves.append(cache_data)
return cache
def get_datalink_rig_action(rig, motion_id=None, slotted=False):
if not motion_id:
motion_id = "DataLink"
rig_id = rigutils.get_rig_id(rig)
action_name = rigutils.make_armature_action_name(rig_id, motion_id, LINK_DATA.motion_prefix, slotted=slotted)
action = bpy.data.actions.new(action_name)
slot, channel = rigutils.add_action_ob_slot_channelbag(action, rig)
utils.safe_set_action(rig, action, slot=slot)
action.use_fake_user = LINK_DATA.use_fake_user
return action
# TODO Not used
def get_datalink_obj_actions(obj, motion_id=None):
prefs = vars.prefs()
if not motion_id:
motion_id = "DataLink"
name = obj.name
T = utils.get_slot_type_for(obj.data)
ob_name = rigutils.generate_action_name(name, "O", "", motion_id, LINK_DATA.motion_prefix)
data_name = rigutils.generate_action_name(name, T[0], "", motion_id, LINK_DATA.motion_prefix)
if ob_name in bpy.data.actions:
ob_action = bpy.data.actions[ob_name]
else:
ob_action = bpy.data.actions.new(ob_name)
utils.safe_set_action(obj, ob_action)
ob_action.use_fake_user = LINK_DATA.use_fake_user
data_action = ob_action
if not prefs.use_action_slots():
if data_name in bpy.data.actions:
data_action = bpy.data.actions[data_name]
else:
data_action = bpy.data.actions.new(data_name)
utils.safe_set_action(obj.data, data_action)
data_action.use_fake_user = LINK_DATA.use_fake_user
return ob_action, data_action
def prep_pose_actor(actor: LinkActor, start_frame, end_frame):
"""Prepares the character rig for keyframing poses from the pose data stream."""
props = vars.props()
prefs = vars.prefs()
motion_id = "Pose" if LINK_DATA.sequence_type == "POSE" else "Sequence"
chr_cache = actor.get_chr_cache()
action_mode, frame_mode = actor.get_import_modes()
if actor and actor.get_type() == "LIGHT":
# create keyframe cache for light animation sequences
if LINK_DATA.set_keyframes:
rlx.prep_rlx_actions(actor, motion_id,
timestamp=False,
motion_prefix=LINK_DATA.motion_prefix)
count = end_frame - start_frame + 1
transform_cache = {}
light_cache = {}
actor_cache = {
"object": actor.object,
"transform": transform_cache,
"light": light_cache,
"start": start_frame,
"end": end_frame,
}
transform_cache["loc"] = create_fcurves_cache(count, 3, [0,0,0])
transform_cache["rot"] = create_rotation_fcurves_cache(actor.object, count)
transform_cache["sca"] = create_fcurves_cache(count, 3, [1,1,1])
light_cache["color"] = create_fcurves_cache(count, 3, [1,1,1])
light_cache["energy"] = create_fcurves_cache(count, 1, [1])
light_cache["cutoff_distance"] = create_fcurves_cache(count, 1, [9])
light_cache["spot_blend"] = create_fcurves_cache(count, 1, [1])
light_cache["spot_size"] = create_fcurves_cache(count, 1, [1])
actor.set_cache(actor_cache)
else:
# when not setting keyframes remove all actions from the light
# and let the DataLink set the pose and light settings directly
utils.safe_set_action(actor.object, None)
utils.safe_set_action(actor.object.data, None)
elif actor and actor.get_type() == "CAMERA":
# create keyframe cache for camera animation sequences
if LINK_DATA.set_keyframes:
rlx.prep_rlx_actions(actor, motion_id,
timestamp=False,
motion_prefix=LINK_DATA.motion_prefix)
count = end_frame - start_frame + 1
transform_cache = {}
camera_cache = {}
actor_cache = {
"object": actor.object,
"transform": transform_cache,
"camera": camera_cache,
"start": start_frame,
"end": end_frame,
}
transform_cache["loc"] = create_fcurves_cache(count, 3, [0,0,0])
transform_cache["rot"] = create_rotation_fcurves_cache(actor.object, count)
transform_cache["sca"] = create_fcurves_cache(count, 3, [1,1,1])
camera_cache["lens"] = create_fcurves_cache(count, 1, [50])
camera_cache["dof"] = create_fcurves_cache(count, 1, [1])
camera_cache["focus_distance"] = create_fcurves_cache(count, 1, [1])
camera_cache["f_stop"] = create_fcurves_cache(count, 1, [2.8])
actor.set_cache(actor_cache)
actor.action_store_id = props.store_actions(actor.object)
else:
# when not setting keyframes remove all actions from the camera
# and let the DataLink set the pose and light settings directly
utils.safe_set_action(actor.object, None)
utils.safe_set_action(actor.object.data, None)
elif actor and actor.get_chr_cache():
# create keyframe cache for avatar or prop animation sequences
rig = actor.get_armature()
# store current actions for replacing or mixing
actor.action_store_id = props.store_actions(rig)
if not rig:
utils.log_error(f"Actor: {actor.name} invalid rig!")
return
objects, none_objects = actor.get_sequence_objects()
if rig:
rig_id = rigutils.get_rig_id(rig)
rl_arm_id = utils.get_rl_object_id(rig)
use_slotted = prefs.use_action_slots()
utils.log_info(f"Preparing Character Rig: {actor.name} {rig_id} / {len(actor.bones)} bones")
if LINK_DATA.set_keyframes:
# generate new action set data
# always import into a new action set, then decide what to do with it after writing the action
motion_id = rigutils.get_unique_set_motion_id(rig_id, motion_id, LINK_DATA.motion_prefix, slotted=use_slotted)
set_id, set_generation = rigutils.generate_motion_set(rig, motion_id, LINK_DATA.motion_prefix)
# rig action
action = get_datalink_rig_action(rig, motion_id, slotted=use_slotted)
rigutils.add_motion_set_data(action, set_id, set_generation, arm_id=rl_arm_id, slotted=use_slotted)
utils.log_info(f"Preparing rig action: {action.name}")
# shape key actions
num_expressions = len(actor.expressions)
num_visemes = len(actor.visemes)
if objects:
for obj in objects:
obj_id = rigutils.get_obj_id(obj)
if use_slotted:
slot, channel = rigutils.add_action_key_slot_channelbag(action, obj)
utils.safe_set_action(obj.data.shape_keys, action, slot=slot)
else:
action_name = rigutils.make_key_action_name(rig_id, motion_id, obj_id, LINK_DATA.motion_prefix)
utils.log_info(f"Preparing shape key action: {action_name} / {num_expressions}+{num_visemes} shape keys")
action = bpy.data.actions.new(action_name)
slot, channel = rigutils.add_action_key_slot_channelbag(action, obj)
rigutils.add_motion_set_data(action, set_id, set_generation, obj_id=obj_id)
utils.safe_set_action(obj.data.shape_keys, action, slot=slot)
action.use_fake_user = LINK_DATA.use_fake_user
# remove actions from non sequence objects
for obj in none_objects:
utils.safe_set_action(obj.data.shape_keys, None)
else:
# when not setting keyframes remove all actions from the rig
# and let the DataLink set the pose and shape keys directly
utils.safe_set_action(rig, None)
for obj in objects:
utils.safe_set_action(obj.data.shape_keys, None)
if chr_cache.rigified:
# disable IK stretch, set rig to FK during transfer
actor.ik_store = rigutils.disable_ik_stretch(rig)
actor.ik_store["ik_fk"] = rigutils.get_rigify_ik_fk_influence(rig)
rigutils.set_rigify_ik_fk_influence(rig, 1.0)
ccic240_arrangement = facerig.is_ccic_240_rig(rig)
BAKE_BONE_GROUPS = ["FK", "IK", "Special", "Root", "Face"] #not Tweak and Extra
BAKE_BONE_COLLECTIONS = ["Face",
"Torso", "Torso (Tweak)",
"Fingers", "Fingers (Detail)",
"Arm.L (IK)", "Arm.L (FK)", "Arm.L (Tweak)",
"Leg.L (IK)", "Leg.L (FK)", "Leg.L (Tweak)",
"Arm.R (IK)", "Arm.R (FK)", "Arm.R (Tweak)",
"Leg.R (IK)", "Leg.R (FK)", "Leg.R (Tweak)",
"Root"]
if ccic240_arrangement:
BAKE_BONE_COLLECTIONS += ["Face (Primary)"]
SHOW_BONE_COLLECTIONS = BAKE_BONE_COLLECTIONS.copy()
else:
BAKE_BONE_COLLECTIONS += ["Face (Expressions)"]
SHOW_BONE_COLLECTIONS = [ "Face (UI)" ] + BAKE_BONE_COLLECTIONS
# These bones may need to have their pose reset as they are damped tracked in the rig:
# - adv pair rigs now resets all pose bones.
BAKE_BONE_EXCLUSIONS = [
"thigh_ik.L", "thigh_ik.R", "thigh_parent.L", "thigh_parent.R",
"upper_arm_ik.L", "upper_arm_ik.R", "upper_arm_parent.L", "upper_arm_parent.R"
]
BAKE_BONE_LAYERS = [0,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,22,28]
SHOW_BONE_LAYERS = [ 23 ]
SHOW_BONE_LAYERS.extend(BAKE_BONE_LAYERS)
#if rigutils.is_face_rig(rig) and not ccic240_arrangement:
# SHOW_BONE_COLLECTIONS.remove("Face")
# SHOW_BONE_LAYERS.remove(0)
if utils.object_mode_to(rig):
bone: bpy.types.Bone
pose_bone: bpy.types.PoseBone
bones.make_bones_visible(rig, collections=SHOW_BONE_COLLECTIONS, layers=SHOW_BONE_LAYERS)
for pose_bone in rig.pose.bones:
bones.select_bone(rig, pose_bone, False)
bone = pose_bone.bone
if bones.is_bone_in_collections(rig, bone, BAKE_BONE_COLLECTIONS,
BAKE_BONE_GROUPS):
if bone.name not in BAKE_BONE_EXCLUSIONS:
bone.hide = False
if bones.can_unlock(pose_bone):
bone.hide_select = False
bones.select_bone(rig, bone, True)
else:
if utils.object_mode_to(rig):
bone: bpy.types.Bone
pose_bone: bpy.types.PoseBone
for pose_bone in rig.pose.bones:
bone = pose_bone.bone
bone.hide = False
if bones.can_unlock(pose_bone):
bone.hide_select = False
bones.select_bone(rig, bone, True)
# create keyframe cache for animation sequences
if LINK_DATA.set_keyframes:
count = end_frame - start_frame + 1
bone_cache = {}
expression_cache = {}
viseme_cache = {}
morph_cache = {}
actor_cache = {
"rig": rig,
"bones": bone_cache,
"expressions": expression_cache,
"visemes": viseme_cache,
"morphs": morph_cache,
"start": start_frame,
"end": end_frame,
}
for pose_bone in rig.pose.bones:
bone_name = pose_bone.name
if bones.get_bone_selected(rig, pose_bone):
loc_cache = create_fcurves_cache(count, 3, [0,0,0])
sca_cache = create_fcurves_cache(count, 3, [1,1,1])
rot_cache = create_rotation_fcurves_cache(pose_bone, count)
bone_cache[bone_name] = {
"loc": loc_cache,
"sca": sca_cache,
"rot": rot_cache,
}
for expression_name in actor.expressions:
expression_cache[expression_name] = create_fcurves_cache(count, 1, [0])
for viseme_name in actor.visemes:
viseme_cache[viseme_name] = create_fcurves_cache(count, 1, [0])
for morph_name in actor.morphs:
pass
actor.set_cache(actor_cache)
def set_frame_range(start, end, preview=False):
if preview:
bpy.context.scene.use_preview_range = True
bpy.context.scene.frame_preview_start = start
bpy.context.scene.frame_preview_end = end
else:
bpy.context.scene.frame_start = start
bpy.context.scene.frame_end = end
def set_frame(frame):
bpy.context.scene.frame_current = frame
bpy.context.view_layer.update()
def key_frame_pose_visual():
area = [a for a in bpy.context.screen.areas if a.type=="VIEW_3D"][0]
with bpy.context.temp_override(area=area):
bpy.ops.anim.keyframe_insert_menu(type='BUILTIN_KSI_VisualLocRot')
def store_cache_curves_frame(cache, prop, frame, start, value):
T = type(value)
index = (frame - start) * 2
if T is Quaternion:
cache_type = cache[prop]["type"]
if cache_type == "QUATERNION":
l = len(value)
for i in range(0, l):
curve = cache[prop]["curves"][i]
curve[index] = frame
curve[index + 1] = value[i]
elif cache_type == "AXIS_ANGLE":
# convert quaternion to angle axis
v,a = value.to_axis_angle()
l = len(v)
for i in range(0, l):
curve = cache[prop]["curves"][i]
curve[index] = frame
curve[index + 1] = v[i]
curve = cache[prop]["curves"][3]
curve[index] = frame
curve[index + 1] = a
else:
euler = value.to_euler(cache_type)
l = len(euler)
for i in range(0, l):
curve = cache[prop]["curves"][i]
curve[index] = frame
curve[index + 1] = euler[i]
elif T is Vector or T is Color or T is tuple or T is list:
l = len(value)
for i in range(0, l):
curve = cache[prop]["curves"][i]
curve[index] = frame
curve[index + 1] = value[i]
else:
curve = cache[prop]["curves"][0]
curve[index] = frame
curve[index + 1] = value
def store_bone_cache_keyframes(actor: LinkActor, frame, start):
"""Needs to be called after all constraints have been set and all bones in the pose positioned"""
if not actor.cache:
utils.log_error(f"No actor cache: {actor.name}")
return
rig = actor.get_armature()
bone_cache = actor.cache["bones"]
for bone_name in bone_cache:
pose_bone: bpy.types.PoseBone = rig.pose.bones[bone_name]
L: Matrix # local space matrix we want
NL: Matrix # non-local space matrix we want (if not using local location or inherit rotation)
M: Matrix = pose_bone.matrix # object space matrix of the pose bone after contraints and drivers
R: Matrix = pose_bone.bone.matrix_local # bone rest pose matrix
RI: Matrix = R.inverted() # bone rest pose matrix inverted
if pose_bone.parent:
PI: Matrix = pose_bone.parent.matrix.inverted() # parent object space matrix inverted (after contraints and drivers)
PR: Matrix = pose_bone.parent.bone.matrix_local # parent rest pose matrix
L = RI @ (PR @ (PI @ M))
NL = PI @ M
else:
L = RI @ M
NL = M
if not pose_bone.bone.use_local_location:
loc = NL.to_translation()
else:
loc = L.to_translation()
sca = L.to_scale()
if not pose_bone.bone.use_inherit_rotation:
rot = NL.to_quaternion()
else:
rot = L.to_quaternion()
store_cache_curves_frame(bone_cache[bone_name], "loc", frame, start, loc)
store_cache_curves_frame(bone_cache[bone_name], "rot", frame, start, rot)
store_cache_curves_frame(bone_cache[bone_name], "sca", frame, start, sca)
def store_shape_key_cache_keyframes(actor: LinkActor, frame, start, expression_weights, viseme_weights, morph_weights):
if not actor.cache:
utils.log_error(f"No actor cache: {actor.name}")
return
expression_cache = actor.cache["expressions"]
for i, expression_name in enumerate(expression_cache):
store_cache_curves_frame(actor.cache["expressions"], expression_name, frame, start, expression_weights[i])
viseme_cache = actor.cache["visemes"]
for i, viseme_name in enumerate(viseme_cache):
store_cache_curves_frame(actor.cache["visemes"], viseme_name, frame, start, viseme_weights[i])
def store_light_cache_keyframes(actor: LinkActor, frame, start):
if not actor.cache:
utils.log_error(f"No actor cache: {actor.name}")
return
light: bpy.types.Object = actor.object
data: bpy.types.SpotLight = light.data
transform_cache = actor.cache["transform"]
light_cache = actor.cache["light"]
M: Matrix = light.matrix_local
loc = M.to_translation()
rot = M.to_quaternion()
sca = M.to_scale()
store_cache_curves_frame(transform_cache, "loc", frame, start, loc)
store_cache_curves_frame(transform_cache, "rot", frame, start, rot)
store_cache_curves_frame(transform_cache, "sca", frame, start, sca)
store_cache_curves_frame(light_cache, "color", frame, start, data.color)
store_cache_curves_frame(light_cache, "energy", frame, start, data.energy)
store_cache_curves_frame(light_cache, "cutoff_distance", frame, start, data.cutoff_distance)
if light.type == "SPOT":
store_cache_curves_frame(light_cache, "spot_blend", frame, start, data.spot_blend)
store_cache_curves_frame(light_cache, "spot_size", frame, start, data.spot_size)
def store_camera_cache_keyframes(actor: LinkActor, frame, start):
if not actor.cache:
utils.log_error(f"No actor cache: {actor.name}")
return
camera: bpy.types.Object = actor.object
data: bpy.types.Camera = camera.data
transform_cache = actor.cache["transform"]
camera_cache = actor.cache["camera"]
M: Matrix = camera.matrix_local
loc = M.to_translation()
rot = M.to_quaternion()
sca = M.to_scale()
store_cache_curves_frame(transform_cache, "loc", frame, start, loc)
store_cache_curves_frame(transform_cache, "rot", frame, start, rot)
store_cache_curves_frame(transform_cache, "sca", frame, start, sca)
store_cache_curves_frame(camera_cache, "lens", frame, start, data.lens)
store_cache_curves_frame(camera_cache, "dof", frame, start, 1.0 if data.dof.use_dof else 0.0)
store_cache_curves_frame(camera_cache, "focus_distance", frame, start, data.dof.focus_distance)
store_cache_curves_frame(camera_cache, "f_stop", frame, start, data.dof.aperture_fstop)
def write_action_rotation_cache_curve(action: bpy.types.Action, cache, prop, obj, num_frames, group_name=None, slot=None, slot_type=None, reduce=False):
cache_type = cache[prop]["type"]
data_path = None
if cache_type == "QUATERNION":
data_path = obj.path_from_id("rotation_quaternion")
if not group_name:
group_name = "Rotation Quaternion"
elif cache_type == "AXIS_ANGLE":
data_path = obj.path_from_id("rotation_axis_angle")
if not group_name:
group_name = "Rotation Axis-Angle"
else:
data_path = obj.path_from_id("rotation_euler")
if not group_name:
group_name = "Rotation Euler"
write_action_cache_curve(action, cache, prop, data_path, num_frames, group_name, slot=slot, slot_type=slot_type, reduce=reduce)
def write_action_cache_curve(action: bpy.types.Action, cache, prop, data_path, num_frames, group_name, slot=None, slot_type=None, reduce=False):
if not LINK_DATA.set_keyframes: return
prop_cache = cache[prop]
num_curves = len(prop_cache["curves"])
channel = utils.get_action_channelbag(action, slot=slot, slot_type=slot_type)
if channel:
fcurve: bpy.types.FCurve = None
if group_name not in channel.groups:
channel.groups.new(group_name)
for i in range(0, num_curves):
cache_curve = prop_cache["curves"][i]
fcurve = channel.fcurves.new(data_path, index=i)
set_count = num_frames * 2
if set_count < len(cache_curve):
# if setting fewer frames than are in the cache (sequence was stopped early)
cache_data = cache_curve[:set_count]
else:
cache_data = cache_curve
if reduce:
reduced = rlx.reduce_cache(cache_data, "LINEAR")
num_reduced = int(len(reduced) / 2)
fcurve.keyframe_points.add(num_reduced)
fcurve.keyframe_points.foreach_set('co', reduced)
else:
fcurve.keyframe_points.add(num_frames)
fcurve.keyframe_points.foreach_set('co', cache_curve)
rigutils.reset_fcurve_interpolation(fcurve)
def write_sequence_actions(actor: LinkActor, num_frames, start_frame):
props = vars.props()
if actor.cache:
if actor.get_type() == "PROP" or actor.get_type() == "AVATAR":
rig = actor.cache["rig"]
rig_action, rig_slot = utils.safe_get_action_slot(rig)
objects, none_objects = actor.get_sequence_objects()
if rig_action:
# it should already be clear
#utils.clear_action(rig_action, "OBJECT", rig_action.name)
bone_cache = actor.cache["bones"]
for bone_name in bone_cache:
pose_bone: bpy.types.PoseBone = rig.pose.bones[bone_name]
write_action_cache_curve(rig_action, bone_cache[bone_name], "loc",
pose_bone.path_from_id("location"), num_frames, bone_name,
slot=rig_slot)
write_action_rotation_cache_curve(rig_action, bone_cache[bone_name], "rot",
pose_bone, num_frames, group_name=bone_name,
slot=rig_slot)
write_action_cache_curve(rig_action, bone_cache[bone_name], "sca",
pose_bone.path_from_id("scale"), num_frames, bone_name,
slot=rig_slot)
# re-apply action to fix slot
utils.safe_set_action(rig, rig_action, slot=rig_slot)
expression_cache = actor.cache["expressions"]
viseme_cache = actor.cache["visemes"]
key_actions = []
for obj in objects:
key_action, key_slot = utils.safe_get_action_slot(obj.data.shape_keys)
if key_action:
# also should be clear already
#utils.clear_action(obj_action, "KEY", obj_action.name)
for expression_name in expression_cache:
if expression_name in obj.data.shape_keys.key_blocks:
key = obj.data.shape_keys.key_blocks[expression_name]
write_action_cache_curve(key_action, expression_cache, expression_name,
key.path_from_id("value"), num_frames, "Expression",
slot=key_slot)
for viseme_name in viseme_cache:
if viseme_name in obj.data.shape_keys.key_blocks:
key = obj.data.shape_keys.key_blocks[viseme_name]
write_action_cache_curve(key_action, viseme_cache, viseme_name,
key.path_from_id("value"), num_frames, "Viseme",
slot=key_slot)
#utils.safe_set_action(obj.data.shape_keys, key_action, slot=key_slot) # re-apply action to fix slot
key_actions.append(key_action)
# remove actions from non sequence objects
for obj in none_objects:
utils.safe_set_action(obj.data.shape_keys, None)
if rig_action:
action_mode, frame_mode = actor.get_import_modes()
rigutils.load_motion_set(rig, rig_action)
rigutils.finalize_motion_import(rig, rig_action, actor.action_store_id, action_mode)
elif actor.get_type() == "LIGHT":
light = actor.object
ob_action = utils.safe_get_action(light)
light_action = utils.safe_get_action(light.data)
ob_slot = utils.get_action_slot(ob_action, "OBJECT")
light_slot = utils.get_action_slot(light_action, "LIGHT")
write_action_cache_curve(ob_action, actor.cache["transform"], "loc", "location", num_frames, "Location", slot=ob_slot, reduce=True)
write_action_rotation_cache_curve(ob_action, actor.cache["transform"], "rot", light, num_frames, slot=ob_slot, reduce=True)
write_action_cache_curve(ob_action, actor.cache["transform"], "sca", "scale", num_frames, "Scale", slot=ob_slot, reduce=True)
write_action_cache_curve(light_action, actor.cache["light"], "color", "color", num_frames, "Light", slot=light_slot, reduce=True)
write_action_cache_curve(light_action, actor.cache["light"], "energy", "energy", num_frames, "Light", slot=light_slot, reduce=True)
write_action_cache_curve(light_action, actor.cache["light"], "cutoff_distance", "cutoff_distance", num_frames, "Light", slot=light_slot, reduce=True)
if light.type == "SPOT":
write_action_cache_curve(light_action, actor.cache["light"], "spot_blend", "spot_blend", num_frames, "Spotlight", slot=light_slot, reduce=True)
write_action_cache_curve(light_action, actor.cache["light"], "spot_size", "spot_size", num_frames, "Spotlight", slot=light_slot, reduce=True)
# re-apply actions to fix slot
utils.safe_set_action(light, ob_action, slot=ob_slot)
utils.safe_set_action(light.data, light_action, slot=light_slot)
action_mode, frame_mode = actor.get_import_modes()
rigutils.finalize_rlx_import(light, [ob_action, light_action], actor.action_store_id, action_mode)
elif actor.get_type() == "CAMERA":
camera = actor.object
ob_action = utils.safe_get_action(camera)
cam_action = utils.safe_get_action(camera.data)
ob_slot = utils.get_action_slot(ob_action, "OBJECT")
cam_slot = utils.get_action_slot(cam_action, "CAMERA")
write_action_cache_curve(ob_action, actor.cache["transform"], "loc", "location", num_frames, "Location", slot=ob_slot, reduce=True)
write_action_rotation_cache_curve(ob_action, actor.cache["transform"], "rot", camera, num_frames, slot=ob_slot, reduce=True)
write_action_cache_curve(ob_action, actor.cache["transform"], "sca", "scale", num_frames, "Scale", slot=ob_slot, reduce=True)
write_action_cache_curve(cam_action, actor.cache["camera"], "lens", "lens", num_frames, "Light", slot=cam_slot, reduce=True)
write_action_cache_curve(cam_action, actor.cache["camera"], "dof", "dof.use_dof", num_frames, "Light", slot=cam_slot, reduce=True)
write_action_cache_curve(cam_action, actor.cache["camera"], "focus_distance", "dof.focus_distance", num_frames, "Light", slot=cam_slot, reduce=True)
write_action_cache_curve(cam_action, actor.cache["camera"], "f_stop", "dof.aperture_f_stop", num_frames, "Light", slot=cam_slot, reduce=True)
# re-apply actions to fix slot
utils.safe_set_action(camera, ob_action, slot=ob_slot)
utils.safe_set_action(camera.data, cam_action, slot=cam_slot)
action_mode, frame_mode = actor.get_import_modes()
rigutils.finalize_rlx_import(camera, [ob_action, cam_action], actor.action_store_id, action_mode)
actor.clear_cache()
class Signal():
callbacks: list = None
def __init__(self):
self.callbacks = []
def connect(self, func):
self.callbacks.append(func)
def disconnect(self, func=None):
if func:
self.callbacks.remove(func)
else:
self.callbacks.clear()
def emit(self, *args):
for func in self.callbacks:
func(*args)
@atexit.register
def shutdown():
link_service = get_link_service()
if link_service:
link_service.shutdown()
class LinkService():
timer = None
server_sock: socket.socket = None
client_sock: socket.socket = None
server_sockets = []
client_sockets = []
empty_sockets = []
client_ip: str = "127.0.0.1"
client_port: int = BLENDER_PORT
is_listening: bool = False
is_connected: bool = False
is_connecting: bool = False
ping_timer: float = 0
keepalive_timer: float = 0
time: float = 0
is_data: bool = False
is_sequence: bool = False
is_import: bool = False
loop_rate: float = 0.0
loop_count: int = 0
sequence_send_count: int = 5
sequence_send_rate: float = 5.0
# Signals
listening = Signal()
connecting = Signal()
connected = Signal()
lost_connection = Signal()
server_stopped = Signal()
client_stopped = Signal()
received = Signal()
accepted = Signal()
sent = Signal()
changed = Signal()
sequence = Signal()
# local props
local_app: str = None
local_version: str = None
local_path: str = None
# remote props
remote_app: str = None
remote_version: str = None
remote_path: str = None
remote_exe: str = None
plugin_version: str = None
link_data: LinkData = None
remote_is_local: bool = True
def __init__(self):
global LINK_DATA
self.link_data = LINK_DATA
def __enter__(self):
return self
def __exit__(self, exception_type, excetpion_value, exception_traceback):
self.service_stop()
def compatible_plugin(self, plugin_version):
if f"v{plugin_version}" == vars.VERSION_STRING:
return True
if plugin_version in vars.PLUGIN_COMPATIBLE:
return True
return False
def is_cc(self):
return self.remote_app == "Character Creator"
def is_iclone(self):
return self.remote_app == "iClone"
def start_server(self):
if not self.server_sock:
try:
self.keepalive_timer = HANDSHAKE_TIMEOUT_S
self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_sock.settimeout(SOCKET_TIMEOUT)
self.server_sock.bind(('', BLENDER_PORT))
self.server_sock.listen(5)
#self.server_sock.setblocking(False)
self.server_sockets = [self.server_sock]
self.is_listening = True
utils.log_info(f"Listening on TCP *:{BLENDER_PORT}")
self.listening.emit()
self.changed.emit()
except Exception as e:
utils.log_error(f"Unable to start server on TCP *:{BLENDER_PORT}", e)
self.server_sock = None
self.server_sockets = []
self.is_listening = False
def stop_server(self):
try:
if self.server_sock:
utils.log_info(f"Closing Server Socket")
try:
# no shutdown for server sockets, just close.
self.server_sock.close()
except Exception as e:
utils.log_error(f"Closing Server Socket failed!", e)
self.is_listening = False
self.server_sock = None
self.server_sockets = []
self.server_stopped.emit()
self.changed.emit()
except Exception as e:
utils.log_error("Stop Server error!", e)
self.is_listening = False
self.server_sock = None
self.server_sockets = []
def start_timer(self):
self.time = time.time()
if not self.timer:
if not bpy.app.timers.is_registered(self.loop):
bpy.app.timers.register(self.loop, first_interval=TIMER_INTERVAL)
self.timer = True
utils.log_info(f"Service timer started")
def stop_timer(self):
if self.timer:
try:
if bpy.app.timers.is_registered(self.loop):
bpy.app.timers.unregister(self.loop)
except Exception as e:
utils.log_error("Stop Timer error!", e)
self.timer = False
utils.log_info(f"Service timer stopped")
def try_start_client(self, host, port):
link_props = vars.link_props()
if not self.client_sock:
utils.log_info(f"Attempting to connect")
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(SOCKET_TIMEOUT)
sock.connect((host, port))
#sock.setblocking(False)
self.is_connected = False
if link_props:
link_props.connected = False
self.is_connecting = True
self.client_sock = sock
self.client_sockets = [sock]
self.client_ip = host
self.client_port = port
self.keepalive_timer = KEEPALIVE_TIMEOUT_S
self.ping_timer = PING_INTERVAL_S
self.remote_is_local = True if self.client_ip == "127.0.0.1" else False
utils.log_info(f"connecting with data link server on {host}:{port}")
self.send_hello()
self.connecting.emit()
self.changed.emit()
return True
except Exception as e:
utils.log_error(f"Client socket connect failed!", e)
self.client_sock = None
self.client_sockets = []
self.is_connected = False
self.is_connecting = False
if link_props:
link_props.connected = False
return False
else:
utils.log_info(f"Client already connected!")
return True
def send_hello(self):
prefs = vars.prefs()
self.local_app = "Blender"
self.local_version = bpy.app.version_string
self.local_path = get_local_data_path()
json_data = {
"Application": self.local_app,
"Version": self.local_version,
"Path": self.local_path,
"Addon": vars.VERSION_STRING[1:],
"Local": self.remote_is_local,
"FPS": bpy.context.scene.render.fps,
}
self.link_data.link_fps = bpy.context.scene.render.fps
utils.log_info(f"Send Hello: {self.local_path}")
self.send(OpCodes.HELLO, encode_from_json(json_data))
def stop_client(self):
link_props = vars.link_props()
try:
if self.client_sock:
utils.log_info(f"Closing Client Socket")
try:
self.client_sock.shutdown(socket.SHUT_RDWR)
self.client_sock.close()
except Exception as e:
utils.log_error("Closing Client Socket failed!", e)
self.is_connected = False
self.is_connecting = False
if link_props:
link_props.connected = False
self.client_sock = None
self.client_sockets = []
if self.listening:
self.keepalive_timer = HANDSHAKE_TIMEOUT_S
self.client_stopped.emit()
self.changed.emit()
except Exception as e:
utils.log_error("Stop Client error!", e)
self.is_connected = False
self.is_connecting = False
self.client_sock = None
self.client_sockets = []
if link_props:
link_props.connected = False
def has_client_sock(self):
if self.client_sock and (self.is_connected or self.is_connecting):
return True
else:
return False
def recv(self):
prefs = vars.prefs()
self.is_data = False
self.is_import = False
if self.has_client_sock():
try:
r,w,x = select.select(self.client_sockets, self.empty_sockets, self.empty_sockets, 0)
except Exception as e:
utils.log_error("Client socket recv:select failed!", e)
self.client_lost()
return
count = 0
while r:
op_code = None
try:
header = self.client_sock.recv(8)
if header == 0:
utils.log_always("Socket closed by client")
self.client_lost()
return
except Exception as e:
utils.log_error("Client socket recv:recv header failed!", e)
self.client_lost()
return
if header and len(header) == 8:
op_code, size = struct.unpack("!II", header)
data = None
if size > 0:
data = bytearray()
while size > 0:
chunk_size = min(size, MAX_CHUNK_SIZE)
try:
chunk = self.client_sock.recv(chunk_size)
except Exception as e:
utils.log_error("Client socket recv:recv chunk failed!", e)
self.client_lost()
return
data.extend(chunk)
size -= len(chunk)
if op_code == OpCodes.FILE:
remote_id = data.decode(encoding="utf-8")
chunk = self.client_sock.recv(4)
size = struct.unpack("!I", chunk)[0]
tar_file_path = get_remote_tar_file_path(remote_id)
with open(tar_file_path, 'wb') as file:
while size > 0:
chunk_size = min(size, MAX_CHUNK_SIZE)
try:
chunk = self.client_sock.recv(chunk_size)
file.write(chunk)
except Exception as e:
utils.log_error("Client socket recv:recv file chunk failed!", e)
self.client_lost()
return
size -= len(chunk)
self.parse(op_code, data)
self.received.emit(op_code, data)
count += 1
self.is_data = False
# parse may have received a disconnect notice
if not self.has_client_sock():
return
# if preview frame sync update every frame in sequence
if op_code == OpCodes.SEQUENCE_FRAME and prefs.datalink_frame_sync:
self.is_data = True
return
# if not key framing, update every frame
if not LINK_DATA.set_keyframes:
self.is_data = True
return
if (op_code == OpCodes.CHARACTER or
op_code == OpCodes.PROP or
op_code == OpCodes.STAGING or
op_code == OpCodes.CAMERA):
# give imports time to process, otherwise bad things happen
self.is_data = False
self.is_import = True
return
try:
r,w,x = select.select(self.client_sockets, self.empty_sockets, self.empty_sockets, 0)
except Exception as e:
utils.log_error("Client socket recv:select (reselect) failed!", e)
self.client_lost()
return
if r:
self.is_data = True
if count >= MAX_RECEIVE or op_code == OpCodes.NOTIFY or op_code == OpCodes.INVALID:
return
def accept(self):
link_props = vars.link_props()
if self.server_sock and self.is_listening:
r,w,x = select.select(self.server_sockets, self.empty_sockets, self.empty_sockets, 0)
while r:
try:
sock, address = self.server_sock.accept()
except Exception as e:
utils.log_error("Server socket accept failed!", e)
self.service_lost()
return
self.client_sock = sock
self.client_sockets = [sock]
self.client_ip = address[0]
self.client_port = address[1]
self.is_connected = False
self.is_connecting = True
link_props.connected = False
self.keepalive_timer = KEEPALIVE_TIMEOUT_S
self.ping_timer = PING_INTERVAL_S
utils.log_info(f"Incoming connection received from: {address[0]}:{address[1]}")
self.send_hello()
self.accepted.emit(self.client_ip, self.client_port)
self.changed.emit()
r,w,x = select.select(self.server_sockets, self.empty_sockets, self.empty_sockets, 0)
def parse(self, op_code, data):
props = vars.props()
link_props = vars.link_props()
self.keepalive_timer = KEEPALIVE_TIMEOUT_S
if op_code == OpCodes.HELLO:
utils.log_info(f"Hello Received")
if data:
json_data = decode_to_json(data)
self.remote_app = json_data["Application"]
self.remote_version = json_data["Version"]
self.remote_path = json_data["Path"]
self.remote_exe = json_data["Exe"]
self.plugin_version = json_data.get("Plugin", "")
self.link_data.remote_app = self.remote_app
self.link_data.remote_version = self.remote_version
self.link_data.remote_path = self.remote_path
self.link_data.remote_exe = self.remote_exe
if self.compatible_plugin(self.plugin_version):
self.service_initialize()
link_props.remote_app = self.remote_app
link_props.remote_version = f"{self.remote_version[0]}.{self.remote_version[1]}.{self.remote_version[2]}"
link_props.remote_path = self.remote_path
link_props.remote_exe = self.remote_exe
utils.log_always(f"Connected to: {self.remote_app} {self.remote_version} / {self.plugin_version}")
utils.log_always(f"Using file path: {self.remote_path}")
utils.log_always(f"Using exe path: {self.remote_exe}")
else:
self.service_disconnect()
messages = ["CC/iC Plug-in and Blender Add-on versions do not match!",
f"Blender add-on version: {vars.VERSION_STRING}",
f"CC/iC plug-in version: {self.plugin_version}",
f"*Compatible plug-in versions: {vars.PLUGIN_COMPATIBLE}"]
utils.message_box_multi("Version Error", icon="ERROR", messages=messages)
elif op_code == OpCodes.PING:
utils.log_info(f"Ping Received")
elif op_code == OpCodes.STOP:
utils.log_info(f"Termination Received")
self.service_stop()
elif op_code == OpCodes.DISCONNECT:
utils.log_info(f"Disconnection Received")
self.service_recv_disconnected()
elif op_code == OpCodes.NOTIFY:
self.receive_notify(data)
elif op_code == OpCodes.INVALID:
self.receive_invalid(data)
elif op_code == OpCodes.DEBUG:
self.receive_debug(data)
##
#
elif op_code == OpCodes.FPS:
self.receive_fps(data)
elif op_code == OpCodes.SAVE:
self.receive_save(data)
elif op_code == OpCodes.FILE:
self.receive_remote_file(data)
elif op_code == OpCodes.TEMPLATE:
self.receive_actor_templates(data)
elif op_code == OpCodes.POSE:
self.receive_pose(data)
elif op_code == OpCodes.POSE_FRAME:
self.receive_pose_frame(data)
elif op_code == OpCodes.MORPH:
self.receive_morph(data)
elif op_code == OpCodes.MORPH_UPDATE:
self.receive_morph(data, update=True)
elif op_code == OpCodes.CHARACTER:
self.receive_actor_import(data)
elif op_code == OpCodes.PROP:
self.receive_actor_import(data)
elif op_code == OpCodes.STAGING:
self.receive_rlx_import(data)
elif op_code == OpCodes.CAMERA:
self.receive_camera_fbx_import(data)
elif op_code == OpCodes.MOTION:
self.receive_motion_import(data)
elif op_code == OpCodes.CHARACTER_UPDATE:
self.receive_actor_update(data)
elif op_code == OpCodes.UPDATE_REPLACE:
self.receive_update_replace(data)
elif op_code == OpCodes.RIGIFY:
self.receive_rigify_request(data)
elif op_code == OpCodes.SEQUENCE:
self.receive_sequence(data)
elif op_code == OpCodes.SEQUENCE_FRAME:
self.receive_sequence_frame(data)
elif op_code == OpCodes.SEQUENCE_END:
self.receive_sequence_end(data)
elif op_code == OpCodes.SEQUENCE_ACK:
self.receive_sequence_ack(data)
elif op_code == OpCodes.LIGHTING:
self.receive_lighting_sync(data)
elif op_code == OpCodes.CAMERA_SYNC:
self.receive_camera_sync(data)
elif op_code == OpCodes.FRAME_SYNC:
self.receive_frame_sync(data)
elif op_code == OpCodes.REQUEST:
self.receive_request(data)
elif op_code == OpCodes.CONFIRM:
self.receive_confirm(data)
def service_start(self, host, port):
if not self.is_listening:
self.start_timer()
if SERVER_ONLY:
self.start_server()
else:
if not self.try_start_client(host, port):
if not CLIENT_ONLY:
self.start_server()
def service_initialize(self):
link_props = vars.link_props()
if self.is_connecting:
self.is_connecting = False
self.is_connected = True
link_props.connected = True
self.on_connected()
self.connected.emit()
self.changed.emit()
def shutdown(self):
self.send(OpCodes.DISCONNECT)
def service_disconnect(self):
try:
self.send(OpCodes.DISCONNECT)
except Exception as e:
utils.log_error("Service Disconnect error: Send", e)
try:
self.stop_timer()
except Exception as e:
utils.log_error("Service Disconnect error: Stop Timer", e)
try:
self.stop_client()
except Exception as e:
utils.log_error("Service Disconnect error: Stop Client", e)
try:
self.stop_server()
except Exception as e:
utils.log_error("Service Disconnect error: Stop Server", e)
def service_recv_disconnected(self):
try:
if CLIENT_ONLY:
self.stop_timer()
except Exception as e:
utils.log_error("Service Recv Disconnected error: Stop Timer", e)
try:
self.stop_client()
except Exception as e:
utils.log_error("Service Recv Disconnected error: Stop Client", e)
def service_stop(self):
self.send(OpCodes.STOP)
self.stop_timer()
self.stop_client()
self.stop_server()
def service_lost(self):
self.lost_connection.emit()
self.stop_timer()
self.stop_client()
self.stop_server()
def client_lost(self):
self.lost_connection.emit()
if CLIENT_ONLY:
self.stop_timer()
self.stop_client()
def is_remote(self):
return not self.remote_is_local
def is_local(self):
return self.remote_is_local
def check_service(self):
global LINK_SERVICE
global LINK_DATA
if not LINK_SERVICE or not LINK_DATA:
utils.log_info("DataLink service data lost. Due to script reload?")
utils.log_info("Connection is maintained but actor data has been reset.")
LINK_SERVICE = self
LINK_DATA = self.link_data
LINK_DATA.reset()
return True
def check_paths(self):
local_path = get_local_data_path()
if local_path != self.local_path:
self.local_path = local_path
self.send_hello()
def loop(self):
try:
current_time = time.time()
delta_time = current_time - self.time
self.time = current_time
if delta_time > 0:
rate = 1.0 / delta_time
self.loop_rate = self.loop_rate * 0.75 + rate * 0.25
#if self.loop_count % 100 == 0:
# utils.log_detail(f"LinkServer loop timer rate: {self.loop_rate}")
self.loop_count += 1
self.check_paths()
if not self.check_service():
return None
if not self.timer:
return None
if self.is_connected:
self.ping_timer -= delta_time
self.keepalive_timer -= delta_time
if USE_PING and self.ping_timer <= 0:
self.send(OpCodes.PING)
if USE_KEEPALIVE and self.keepalive_timer <= 0:
utils.log_info("lost connection!")
self.service_stop()
return None
self.check_fps()
elif self.is_listening:
self.keepalive_timer -= delta_time
if USE_KEEPALIVE and self.keepalive_timer <= 0:
utils.log_info("no connection within time limit!")
self.service_stop()
return None
# accept incoming connections
self.accept()
# receive client data
self.recv()
# run anything in sequence
for i in range(0, self.sequence_send_count):
self.sequence.emit()
if self.is_import:
return 0.5
else:
interval = 0.0 if (self.is_data or self.is_sequence) else TIMER_INTERVAL
return interval
except Exception as e:
utils.log_error("LinkService timer loop crash!", e)
return TIMER_INTERVAL
def send(self, op_code, binary_data = None):
try:
if self.client_sock and (self.is_connected or self.is_connecting):
data_length = len(binary_data) if binary_data else 0
header = struct.pack("!II", op_code, data_length)
data = bytearray()
data.extend(header)
if binary_data:
data.extend(binary_data)
try:
self.client_sock.sendall(data)
except Exception as e:
utils.log_error("Client socket sendall failed!")
self.client_lost()
return
self.ping_timer = PING_INTERVAL_S
self.sent.emit()
except Exception as e:
utils.log_error("LinkService send failed!", e)
def send_file(self, tar_id, tar_file):
try:
utils.log_info(f"Sending Remote files: {tar_file}")
if self.client_sock and (self.is_connected or self.is_connecting):
file_size = os.path.getsize(tar_file)
id_data = pack_string(tar_id)
data = bytearray()
data.extend(struct.pack("!I", OpCodes.FILE))
data.extend(id_data)
data.extend(struct.pack("!I", file_size))
self.client_sock.send(data)
remaining_size = file_size
with open(tar_file, 'rb') as file:
while remaining_size > 0:
chunk_size = min(MAX_CHUNK_SIZE, remaining_size)
byte_array = bytearray(file.read(chunk_size))
remaining_size -= MAX_CHUNK_SIZE
self.client_sock.send(byte_array)
self.ping_timer = PING_INTERVAL_S
self.sent.emit()
except Exception as e:
utils.log_error("LinkService send failed!", e)
def start_sequence(self, func=None):
self.is_sequence = True
self.sequence_send_count = 5
self.sequence_send_rate = 5.0
if func:
self.sequence.connect(func)
else:
self.sequence.disconnect()
def stop_sequence(self):
self.is_sequence = False
self.sequence.disconnect()
def update_sequence(self, count, delta_frames):
if count is None:
self.sequence_send_rate = 5.0
self.sequence_send_count = 5
else:
self.sequence_send_rate = count
self.sequence_send_count = count
if self.loop_count % 30 == 0:
utils.log_info(f"send_count: {self.sequence_send_count} delta_frames: {delta_frames}")
def on_connected(self):
self.send_notify("Connected")
def send_notify(self, message):
notify_json = { "message": message }
self.send(OpCodes.NOTIFY, encode_from_json(notify_json))
def send_invalid(self, message):
notify_json = { "message": message }
self.send(OpCodes.INVALID, encode_from_json(notify_json))
def receive_notify(self, data):
notify_json = decode_to_json(data)
update_link_status(notify_json["message"])
def receive_invalid(self, data):
invalid_json = decode_to_json(data)
update_link_status(invalid_json["message"])
self.abort_sequence()
def receive_save(self, data):
if bpy.data.filepath:
utils.log_info("Saving Mainfile")
bpy.ops.wm.save_mainfile()
def receive_remote_file(self, data: bytearray):
remote_id = data.decode(encoding="utf-8")
tar_file_path = get_remote_tar_file_path(remote_id)
parent_path = os.path.dirname(tar_file_path)
unpack_folder = utils.make_sub_folder(parent_path, remote_id)
utils.log_info(f"Receive Remote Files: {remote_id} / {unpack_folder}")
shutil.unpack_archive(tar_file_path, unpack_folder, "tar")
os.remove(tar_file_path)
#utils.show_system_file_browser(unpack_folder)
def receive_debug(self, data):
debug_json = None
if data:
debug_json = decode_to_json(data)
debug(debug_json)
def get_key_path(self, model_path, key_ext):
dir, file = os.path.split(model_path)
name, ext = os.path.splitext(file)
key_path = os.path.normpath(os.path.join(dir, name + key_ext))
return key_path
def get_export_folder(self, folder_name, reuse=False):
remote_path = self.remote_path
local_path = self.local_path
if not local_path:
local_path = get_local_data_path()
if local_path:
export_folder = utils.make_sub_folder(local_path, "exports")
else:
export_folder = utils.make_sub_folder(remote_path, "exports")
character_export_folder = utils.get_unique_folder_path(export_folder, folder_name, create=True, reuse=reuse)
return character_export_folder
def get_export_path(self, folder_name, file_name, reuse_folder=False, reuse_file=False):
character_export_folder = self.get_export_folder(folder_name, reuse=reuse_folder)
export_path = utils.get_unique_file_path(character_export_folder, file_name, reuse=reuse_file)
return export_path
def send_remote_files(self, export_folder):
link_service: LinkService = LINK_SERVICE
remote_id = ""
if link_service.is_remote():
parent_folder = os.path.dirname(export_folder)
remote_id = str(time.time_ns())
cwd = os.getcwd()
tar_file_name = remote_id
os.chdir(parent_folder)
utils.log_info(f"Packing Remote files: {tar_file_name}")
update_link_status("Packing Remote files")
shutil.make_archive(tar_file_name, "tar", export_folder)
os.chdir(cwd)
tar_file_path = os.path.join(parent_folder, f"{tar_file_name}.tar")
if os.path.exists(tar_file_path):
update_link_status("Sending Remote files")
link_service.send_file(remote_id, tar_file_path)
update_link_status("Files Sent")
if os.path.exists(tar_file_path):
utils.log_info(f"Cleaning up remote export package: {tar_file_path}")
os.remove(tar_file_path)
if os.path.exists(export_folder):
utils.log_info(f"Cleaning up remote export folder: {export_folder}")
shutil.rmtree(export_folder)
return remote_id
def get_actor_from_object(self, obj):
global LINK_DATA
props = vars.props()
chr_cache = props.get_character_cache(obj, None)
if chr_cache:
actor = LinkActor(chr_cache)
return actor
return None
def get_selected_actors(self):
global LINK_DATA
props = vars.props()
selected_objects = bpy.context.selected_objects
avatars = props.get_avatars()
actors = []
cache_actors = []
# if nothing selected then use the first available Avatar
if not selected_objects and len(avatars) == 1:
cache_actors.append(avatars[0])
else:
for obj in selected_objects:
chr_cache = props.get_character_cache(obj, None)
if chr_cache and chr_cache not in cache_actors:
cache_actors.append(chr_cache)
for chr_cache in cache_actors:
actor = LinkActor(chr_cache)
actors.append(actor)
for obj in selected_objects:
if obj.type == "LIGHT" or obj.type == "CAMERA" and utils.get_rl_link_id(obj):
actor = LinkActor(obj)
actors.append(actor)
return actors
def get_actor_mesh_selection(self):
selection = {}
for obj in bpy.context.selected_objects:
if obj.type == "MESH" or obj.type == "ARMATURE":
actor = self.get_actor_from_object(obj)
chr_cache = actor.get_chr_cache()
selection.setdefault(chr_cache, {"meshes": [], "armatures": []})
if obj.type == "MESH":
selection[chr_cache]["meshes"].append(obj)
elif obj.type == "ARMATURE":
selection[chr_cache]["armatures"].append(obj)
return selection
def get_active_actor(self):
global LINK_DATA
props = vars.props()
active_object = utils.get_active_object()
if active_object:
chr_cache = props.get_character_cache(active_object, None)
if chr_cache:
actor = LinkActor(chr_cache)
return actor
return None
def send_actor(self):
actors = self.get_selected_actors()
state = utils.store_mode_selection_state()
utils.clear_selected_objects()
actor: LinkActor
utils.log_info(f"Sending LinkActors: {([a.name for a in actors])}")
count = 0
for actor in actors:
if actor.get_type() != "PROP" and actor.get_type() != "AVATAR": continue
if self.is_cc() and not actor.can_go_cc(): continue
if self.is_iclone() and not actor.can_go_ic(): continue
self.send_notify(f"Blender Exporting: {actor.name}...")
# Determine export path
export_folder = self.get_export_folder(actor.name)
export_file = actor.name + ".fbx"
export_path = os.path.join(export_folder, export_file)
if not export_path: continue
# Export Actor Fbx
self.send_notify(f"Exporting: {actor.name}")
is_remote = LINK_SERVICE.is_remote()
if actor.get_type() == "PROP":
bpy.ops.cc3.exporter(param="EXPORT_CC3", link_id_override=actor.get_link_id(), filepath=export_path, include_textures=is_remote)
elif actor.get_type() == "AVATAR":
bpy.ops.cc3.exporter(param="EXPORT_CC3", link_id_override=actor.get_link_id(), filepath=export_path, include_textures=is_remote)
# Send Remote Files First
remote_id = self.send_remote_files(export_folder)
# Send Actor
update_link_status(f"Sending: {actor.name}")
export_data = encode_from_json({
"path": export_path,
"remote_id": remote_id,
"name": actor.name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
})
if is_remote or os.path.exists(export_path):
self.send(OpCodes.CHARACTER, export_data)
update_link_status(f"Sent: {actor.name}")
count += 1
utils.restore_mode_selection_state(state)
return count
def send_morph(self):
actor: LinkActor = self.get_active_actor()
if actor:
self.send_notify(f"Blender Exporting: {actor.name}...")
# Determine export path
export_folder = self.get_export_folder("Morphs", reuse=True)
export_file = actor.name + "_morph.obj"
export_path = os.path.join(export_folder, export_file)
key_path = self.get_key_path(export_path, ".ObjKey")
if not export_path:
return
# Export Morph Obj
self.send_notify(f"Exporting: {actor.name}")
is_remote = LINK_SERVICE.is_remote()
state = utils.store_mode_selection_state()
bpy.ops.cc3.exporter(param="EXPORT_CC3", filepath=export_path)
# Send Remote Files First
remote_id = self.send_remote_files(export_folder)
# Send Morph
update_link_status(f"Sending: {actor.name}")
export_data = encode_from_json({
"path": export_path,
"remote_id": remote_id,
"key_path": key_path,
"name": actor.name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
"morph_name": "Test Morph",
"morph_path": "Some/Path",
})
utils.restore_mode_selection_state(state)
if is_remote or os.path.exists(export_path):
self.send(OpCodes.MORPH, export_data)
update_link_status(f"Sent: {actor.name}")
return True
return False
def obj_export(self, file_path, use_selection=False, use_animation=False, global_scale=100,
use_vertex_colors=False, use_vertex_groups=False, apply_modifiers=True,
keep_vertex_order=False, use_materials=False):
if utils.B330():
bpy.ops.wm.obj_export(filepath=file_path,
global_scale=global_scale,
export_selected_objects=use_selection,
export_animation=use_animation,
export_materials=use_materials,
export_colors=use_vertex_colors,
export_vertex_groups=use_vertex_groups,
export_uv=True,
export_normals=True,
apply_modifiers=apply_modifiers)
else:
bpy.ops.export_scene.obj(filepath=file_path,
global_scale=global_scale,
use_selection=use_selection,
use_materials=use_materials,
use_animation=use_animation,
use_vertex_groups=use_vertex_groups,
use_mesh_modifiers=apply_modifiers,
use_uvs=True,
use_normals=True,
keep_vertex_order=keep_vertex_order)
def send_replace_mesh(self):
state = utils.store_mode_selection_state()
objects = utils.get_selected_meshes()
# important that character is in the exact same pose on both sides,
# so make sure the character is on the same frame in the animation.
self.send_frame_sync()
count = 0
for obj in objects:
if obj.type == "MESH":
actor = self.get_actor_from_object(obj)
if actor:
obj_cache = actor.get_chr_cache().get_object_cache(obj)
object_name = obj.name
mesh_name = obj.data.name
if obj_cache:
object_name = obj_cache.source_name
mesh_name = obj_cache.source_name
export_path = self.get_export_path("Meshes", f"{obj.name}_mesh.obj",
reuse_folder=True, reuse_file=True)
utils.set_active_object(obj, deselect_all=True)
self.obj_export(export_path, use_selection=True, use_vertex_colors=False)
export_data = encode_from_json({
"path": export_path,
"actor_name": actor.name,
"object_name": object_name,
"mesh_name": mesh_name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
})
self.send(OpCodes.REPLACE_MESH, export_data)
update_link_status(f"Sent Mesh: {actor.name}")
count += 1
utils.restore_mode_selection_state(state)
return count
def export_object_material_data(self, context, actor: LinkActor, objects):
prefs = vars.prefs()
obj: bpy.types.Object
chr_cache = actor.get_chr_cache()
if chr_cache:
if prefs.datalink_send_mode == "ACTIVE":
materials = []
for obj in objects:
idx = obj.active_material_index
if len(obj.material_slots) > idx:
mat = obj.material_slots[idx].material
if mat:
materials.append(mat)
else:
materials = None
export_path = self.get_export_path("Materials", f"{actor.name}.json",
reuse_folder=True, reuse_file=True)
export_dir, json_file = os.path.split(export_path)
json_data = chr_cache.get_json_data()
if not json_data:
json_data = jsonutils.generate_character_base_json_data(actor.name)
exporter.set_character_generation(json_data, chr_cache, actor.name)
exporter.prep_export(context, chr_cache, actor.name, objects, json_data,
chr_cache.get_import_dir(), export_dir,
False, False, False, False, True,
materials=materials, sync=True, force_bake=True)
jsonutils.write_json(json_data, export_path)
export_data = encode_from_json({
"path": export_path,
"actor_name": actor.name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
})
self.send(OpCodes.MATERIALS, export_data)
def send_material_update(self, context):
state = utils.store_mode_selection_state()
selection = self.get_actor_mesh_selection()
count = 0
for chr_cache in selection:
actor = LinkActor(chr_cache)
meshes = selection[chr_cache]["meshes"]
armatures = selection[chr_cache]["armatures"]
if armatures:
# export material info for whole character
all_meshes = actor.get_mesh_objects()
self.export_object_material_data(context, actor, all_meshes)
count += 1
elif meshes:
# export material info just for selected meshes
self.export_object_material_data(context, actor, meshes)
count += 1
utils.restore_mode_selection_state(state)
return count
def encode_actor_templates(self, actors: list):
pose_bone: bpy.types.PoseBone
actor_data = []
character_template = {
"count": len(actors),
"actors": actor_data,
}
actor: LinkActor
for actor in actors:
if actor.get_type() in ["PROP", "AVATAR"]:
chr_cache = actor.get_chr_cache()
bones = []
meshes = []
bone_ids = []
mesh_ids = []
if chr_cache.rigified:
rig = actor.get_armature()
# disable IK stretch
actor.ik_store = rigutils.disable_ik_stretch(rig)
# add the export retarget rig
if utils.object_exists_is_armature(chr_cache.rig_export_rig):
export_rig = chr_cache.rig_export_rig
else:
export_rig = rigging.adv_export_pair_rigs(chr_cache, link_target=True)[0]
# get all the exportable deformation bones
if rigutils.select_rig(export_rig):
for pose_bone in export_rig.pose.bones:
if (pose_bone.name != "root" and
not pose_bone.name.startswith("DEF-") and
not pose_bone.name.startswith("NDP-") and
not pose_bone.name.startswith("NDC-")):
bones.append(pose_bone.name)
bone_id = actor.get_bone_id(pose_bone.name)
bone_ids.append(bone_id)
driver_mode = "BONE"
for i, id in enumerate(bone_ids):
if id == -1:
utils.log_info(f"Unidentified bone: {bones[i]}")
else:
# get all the bones
rig: bpy.types.Object = chr_cache.get_armature()
if rigutils.select_rig(rig):
for pose_bone in rig.pose.bones:
bones.append(pose_bone.name)
bone_id = actor.get_bone_id(pose_bone.name)
bone_ids.append(bone_id)
if drivers.has_facial_shape_key_bone_drivers(chr_cache) or rigutils.is_face_rig(rig):
driver_mode = "EXPRESSION"
else:
driver_mode = "BONE"
meshes = []
for mesh_name in actor.meshes:
if mesh_name in bpy.data.objects:
mesh_obj = bpy.data.objects[mesh_name]
if utils.object_exists_is_mesh(mesh_obj):
meshes.append(mesh_name)
mesh_id = actor.get_bone_id(mesh_name)
mesh_ids.append(mesh_id)
actor.collect_shape_keys()
shapes = [key for key in actor.shape_keys]
actor.bones = bones
actor.meshes = meshes
actor_data.append({
"name": actor.name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
"bones": bones,
"bone_ids": bone_ids,
"meshes": meshes, # meshes derived from the template send in confirm
"mesh_ids": mesh_ids,
"shapes": shapes,
"drivers": driver_mode,
})
else:
# lights and cameras just have root transforms to animate
# and fixed properties
actor_data.append({
"name": actor.name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
})
return encode_from_json(character_template)
def encode_request_data(self, actors, request_type):
actors_data = []
data = {
"type": request_type,
"actors": actors_data,
}
actor: LinkActor
for actor in actors:
actors_data.append({
"name": actor.name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
})
return encode_from_json(data)
def encode_pose_data(self, actors):
fps = bpy.context.scene.render.fps
start_frame = BFA(bpy.context.scene.frame_start)
end_frame = BFA(bpy.context.scene.frame_end)
start_time = start_frame / fps
end_time = end_frame / fps
frame = BFA(bpy.context.scene.frame_current)
time = frame / fps
actors_data = []
data = {
"fps": fps,
"start_time": start_time,
"end_time": end_time,
"start_frame": start_frame,
"end_frame": end_frame,
"time": time,
"frame": frame,
"actors": actors_data,
}
actor: LinkActor
for actor in actors:
actors_data.append({
"name": actor.name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
})
return encode_from_json(data)
def encode_pose_frame_data(self, actors: list):
pose_bone: bpy.types.PoseBone
data = bytearray()
data += struct.pack("!II", len(actors), BFA(bpy.context.scene.frame_current))
actor: LinkActor
for actor in actors:
actor_type = actor.get_type()
data += pack_string(actor.name)
data += pack_string(actor.get_type())
data += pack_string(actor.get_link_id())
if actor_type == "PROP" or actor_type == "AVATAR":
chr_cache = actor.get_chr_cache()
if chr_cache.rigified:
# add the import retarget rig
if utils.object_exists_is_armature(chr_cache.rig_export_rig):
export_rig = chr_cache.rig_export_rig
else:
export_rig = rigging.adv_export_pair_rigs(chr_cache, link_target=True)[0]
M: Matrix = export_rig.matrix_world
# pack object transform
T: Matrix = M
t = T.to_translation() * 100
r = T.to_quaternion()
s = T.to_scale()
data += struct.pack("!ffffffffff", t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z)
# pack all the bone data for the exportable deformation bones
data += struct.pack("!I", len(actor.bones))
if utils.object_mode_to(export_rig):
for bone_name in actor.bones:
pose_bone = export_rig.pose.bones[bone_name]
T: Matrix = M @ pose_bone.matrix
t = T.to_translation() * 100
r = T.to_quaternion()
s = T.to_scale()
data += struct.pack("!ffffffffff", t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z)
else:
rig: bpy.types.Object = chr_cache.get_armature()
M: Matrix = rig.matrix_world
# pack object transform
T: Matrix = M
t = T.to_translation() * 100
r = T.to_quaternion()
s = T.to_scale()
data += struct.pack("!ffffffffff", t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z)
# pack all the bone data
data += struct.pack("!I", len(rig.pose.bones))
if utils.object_mode_to(rig):
pose_bone: bpy.types.PoseBone
for pose_bone in rig.pose.bones:
T: Matrix = M @ pose_bone.matrix
t = T.to_translation()
r = T.to_quaternion()
s = T.to_scale()
data += struct.pack("!ffffffffff", t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z)
# pack mesh transforms (actor.meshes is sanitized by encode_actor_templates)
if INCLUDE_POSE_MESHES:
data += struct.pack("!I", len(actor.meshes))
if utils.object_mode_to(rig):
mesh_obj: bpy.types.Object
for mesh_name in actor.meshes:
mesh_obj = bpy.data.objects[mesh_name]
T: Matrix = mesh_obj.matrix_world
t = T.to_translation()
r = T.to_quaternion()
s = T.to_scale()
data += struct.pack("!ffffffffff", t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z)
# pack shape_keys
data += struct.pack("!I", len(actor.shape_keys))
for shape_key, key in actor.shape_keys.items():
data += struct.pack("!f", key.value)
elif actor_type == "LIGHT":
M: Matrix = actor.object.matrix_world
T: Matrix = M
t = T.to_translation() * 100
r = T.to_quaternion()
s = T.to_scale()
data += struct.pack("!ffffffffff", t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z)
light: bpy.types.SpotLight = actor.object.data
# pack animateable light data
data += struct.pack("!?fffffff",
light.energy > 0.0001,
light.color[0],
light.color[1],
light.color[2],
light.energy,
light.cutoff_distance * 100,
light.spot_size if light.type == "SPOT" else 0.0,
light.spot_blend if light.type == "SPOT" else 0.0)
elif actor_type == "CAMERA":
M: Matrix = actor.object.matrix_world
T: Matrix = M
t = T.to_translation() * 100
r = T.to_quaternion()
s = T.to_scale()
data += struct.pack("!ffffffffff", t.x, t.y, t.z, r.x, r.y, r.z, r.w, s.x, s.y, s.z)
camera: bpy.types.Camera = actor.object.data
# pack animateable camera data
data += struct.pack("!f?ff",
camera.lens,
camera.dof.use_dof,
camera.dof.focus_distance * 100,
camera.dof.aperture_fstop)
return data
def encode_sequence_data(self, actors, aborted=False):
fps = bpy.context.scene.render.fps
start_frame = BFA(bpy.context.scene.frame_start)
end_frame = BFA(bpy.context.scene.frame_end)
start_time = start_frame / fps
end_time = end_frame / fps
frame = BFA(bpy.context.scene.frame_current)
time = frame / fps
actors_data = []
data = {
"fps": fps,
"start_time": start_time,
"end_time": end_time,
"start_frame": start_frame,
"end_frame": end_frame,
"time": time,
"frame": frame,
"actors": actors_data,
"aborted": aborted,
}
actor: LinkActor
for actor in actors:
actors_data.append({
"name": actor.name,
"type": actor.get_type(),
"link_id": actor.get_link_id(),
})
return encode_from_json(data)
def restore_actor_rigs(self, actors: LinkActor):
"""Restores any disabled IK stretch settings after export"""
for actor in actors:
chr_cache = actor.get_chr_cache()
if chr_cache:
arm = chr_cache.get_armature()
if arm and chr_cache.rigified:
if actor.ik_store:
rigutils.restore_ik_stretch(actor.ik_store)
# remove the export rigs
utils.delete_armature_object(chr_cache.rig_export_rig)
def send_request(self, request_type):
global LINK_DATA
# get actors
actors = self.get_selected_actors()
if actors:
mode_selection = utils.store_mode_selection_state()
update_link_status(f"Sending Request")
self.send_notify(f"Request")
# send request
pose_data = self.encode_request_data(actors, request_type)
self.send(OpCodes.REQUEST, pose_data)
# store the actors
LINK_DATA.sequence_actors = actors
LINK_DATA.sequence_type = request_type
# restore
utils.restore_mode_selection_state(mode_selection)
def send_pose_request(self):
self.send_request("POSE")
def send_sequence_request(self):
self.send_request("SEQUENCE")
def receive_request(self, data):
update_link_status(f"Receiving Request ...")
json_data = decode_to_json(data)
request_type = json_data["type"]
actors_data = json_data["actors"]
json_data["FPS"] = bpy.context.scene.render.fps
for actor_data in actors_data:
name = actor_data["name"]
link_id = actor_data["link_id"]
character_type = actor_data["type"]
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
actor_data["confirm"] = actor is not None
utils.log_info(f"Actor: {name} " + ("Confirmed!" if actor_data["confirm"] else "Missing!"))
if actor:
if actor.get_link_id() != link_id:
actor_data["update_link_id"] = actor.get_link_id()
if actor.name != name:
actor_data["update_name"] = actor.name
self.send(OpCodes.CONFIRM, encode_from_json(json_data))
def receive_confirm(self, data):
global LINK_DATA
json_data = decode_to_json(data)
request_type = json_data["type"]
actors_data = json_data["actors"]
for actor_data in actors_data:
link_id = actor_data.get("link_id")
name = actor_data.get("name")
character_type = actor_data.get("type")
new_link_id = actor_data.get("new_link_id")
new_name = actor_data.get("new_name")
actor = LINK_DATA.find_sequence_actor(link_id)
if actor:
if new_link_id:
actor.update_link_id(new_link_id)
if new_name:
actor.update_name(new_name)
actor.set_id_tree(actor_data.get("bones"),
actor_data.get("ids"),
actor_data.get("id_tree"))
if request_type == "POSE":
self.send_pose()
elif request_type == "SEQUENCE":
self.send_sequence()
return
def send_pose(self):
global LINK_DATA
# get actors
if not LINK_DATA.sequence_actors:
LINK_DATA.sequence_actors = self.get_selected_actors()
actors = LINK_DATA.sequence_actors
if actors:
mode_selection = utils.store_mode_selection_state()
update_link_status(f"Sending Current Pose Set")
self.send_notify(f"Pose Set")
# send pose info
pose_data = self.encode_pose_data(actors)
self.send(OpCodes.POSE, pose_data)
# send template data first
template_data = self.encode_actor_templates(actors)
self.send(OpCodes.TEMPLATE, template_data)
# store the actors
LINK_DATA.sequence_actors = actors
LINK_DATA.sequence_type = "POSE"
# force recalculate all transforms
bpy.context.view_layer.update()
# send pose data
pose_frame_data = self.encode_pose_frame_data(actors)
self.send(OpCodes.POSE_FRAME, pose_frame_data)
# clear the actors
self.restore_actor_rigs(LINK_DATA.sequence_actors)
LINK_DATA.sequence_actors = None
LINK_DATA.sequence_type = None
# restore
utils.restore_mode_selection_state(mode_selection)
def send_animation(self):
return
def abort_sequence(self):
global LINK_DATA
if self.is_sequence:
# as the next frame was never sent, go back 1 frame
LINK_DATA.sequence_current_frame = prev_frame(LINK_DATA.sequence_current_frame)
update_link_status(f"Sequence Aborted: {LINK_DATA.sequence_current_frame}")
self.stop_sequence()
self.send_sequence_end(aborted=True)
return True
return False
def send_sequence(self):
global LINK_DATA
# get actors
if not LINK_DATA.sequence_actors:
LINK_DATA.sequence_actors = self.get_selected_actors()
actors = LINK_DATA.sequence_actors
if actors:
update_link_status(f"Sending Animation Sequence")
self.send_notify(f"Animation Sequence")
# reset animation to start
bpy.context.scene.frame_current = bpy.context.scene.frame_start
LINK_DATA.sequence_current_frame = bpy.context.scene.frame_current
# send animation meta data
sequence_data = self.encode_sequence_data(actors)
self.send(OpCodes.SEQUENCE, sequence_data)
# send template data first
template_data = self.encode_actor_templates(actors)
self.send(OpCodes.TEMPLATE, template_data)
# store the actors
LINK_DATA.sequence_actors = actors
LINK_DATA.sequence_type = "SEQUENCE"
# start the sending sequence
self.start_sequence(self.send_sequence_frame)
def send_sequence_frame(self):
global LINK_DATA
# set/fetch the current frame in the sequence
current_frame = ensure_current_frame(LINK_DATA.sequence_current_frame)
update_link_status(f"Sequence Frame: {current_frame}")
# force recalculate all transforms
bpy.context.view_layer.update()
# send current sequence frame pose
pose_data = self.encode_pose_frame_data(LINK_DATA.sequence_actors)
self.send(OpCodes.SEQUENCE_FRAME, pose_data)
# check for end
if current_frame >= bpy.context.scene.frame_end:
self.stop_sequence()
self.send_sequence_end()
return
# advance to next frame now
LINK_DATA.sequence_current_frame = next_frame(current_frame)
def send_sequence_end(self, aborted=False):
sequence_data = self.encode_sequence_data(LINK_DATA.sequence_actors, aborted=aborted)
self.send(OpCodes.SEQUENCE_END, sequence_data)
# clear the actors
self.restore_actor_rigs(LINK_DATA.sequence_actors)
LINK_DATA.sequence_actors = None
LINK_DATA.sequence_type = None
def send_sequence_ack(self, frame):
global LINK_DATA
# encode sequence ack
data = encode_from_json({
"frame": BFA(frame),
"rate": self.loop_rate,
})
# send sequence ack
self.send(OpCodes.SEQUENCE_ACK, data)
def decode_pose_frame_header(self, pose_data):
count, frame = struct.unpack_from("!II", pose_data)
frame = RLFA(frame)
LINK_DATA.sequence_current_frame = frame
return frame
def decode_pose_frame_data(self, pose_data):
global LINK_DATA
prefs = vars.prefs()
offset = 0
count, frame = struct.unpack_from("!II", pose_data, offset)
frame = RLFA(frame)
opt_frame = LinkActor.get_sequence_frame(None, frame, LINK_DATA.sequence_start_frame, LINK_DATA.scene_current_frame)
opt_start_frame = LinkActor.get_sequence_frame(None, LINK_DATA.sequence_start_frame, LINK_DATA.sequence_start_frame, LINK_DATA.scene_current_frame)
if LINK_DATA.set_keyframes:
ensure_current_frame(opt_frame)
LINK_DATA.sequence_current_frame = frame
offset = 8
actors = []
for i in range(0, count):
offset, name = unpack_string(pose_data, offset)
offset, character_type = unpack_string(pose_data, offset)
offset, link_id = unpack_string(pose_data, offset)
actor = LINK_DATA.find_sequence_actor(link_id)
actor_ready = False
if actor:
objects, none_objects = actor.get_sequence_objects()
rig: bpy.types.Object = actor.get_armature()
actor_ready = actor.ready(require_cache=LINK_DATA.set_keyframes)
if actor_ready:
actors.append(actor)
else:
utils.log_error(f"Actor not ready: {name}/ {link_id}")
is_prop = actor.get_type() == "PROP"
else:
utils.log_error(f"Could not find actor: {name}/ {link_id}")
objects = []
rig = None
is_prop = False
# unpack rig transform
tx,ty,tz,rx,ry,rz,rw,sx,sy,sz = struct.unpack_from("!ffffffffff", pose_data, offset)
loc = Vector((tx, ty, tz)) * 0.01
rot = Quaternion((rw, rx, ry, rz))
sca = Vector((sx, sy, sz))
offset += 40
if rig:
rig.location = Vector((0, 0, 0))
rot_mode = rig.rotation_mode
utils.set_transform_rotation(rig, Quaternion((1, 0, 0, 0)))
if actor.get_chr_cache().rigified:
rig.scale = Vector((1, 1, 1))
else:
rig.scale = Vector((0.01, 0.01, 0.01))
rig.rotation_mode = rot_mode
if character_type == "PROP" or character_type == "AVATAR":
datalink_rig = make_datalink_import_rig(actor, objects) if actor_ready else None
# unpack bone transforms
num_bones = struct.unpack_from("!I", pose_data, offset)[0]
offset += 4
# unpack the binary transform data directly into the datalink rig pose bones
for i in range(0, num_bones):
tx,ty,tz,rx,ry,rz,rw,sx,sy,sz = struct.unpack_from("!ffffffffff", pose_data, offset)
offset += 40
if actor and datalink_rig:
id = actor.ids[i]
if id in actor.id_map:
id_def = actor.id_map[id]
if id_def["mesh"]:
obj = actor.skin_meshes[id][0]
actor.skin_meshes[id][1] = Vector((tx, ty, tz)) * 0.01
actor.skin_meshes[id][2] = Quaternion((rw, rx, ry, rz))
actor.skin_meshes[id][3] = Vector((utils.sign(sx), utils.sign(sy), utils.sign(sz))) * rig.scale
else:
bone_name = id_def["name"]
pose_bone: bpy.types.PoseBone = datalink_rig.pose.bones[bone_name]
loc = Vector((tx, ty, tz)) * 0.01
rot = Quaternion((rw, rx, ry, rz))
sca = Vector((utils.sign(sx), utils.sign(sy), utils.sign(sz))) * rig.scale
pose_bone.location = loc
utils.set_transform_rotation(pose_bone, rot)
pose_bone.scale = sca
# unpack the expression shape keys into the mesh objects
num_weights = struct.unpack_from("!I", pose_data, offset)[0]
offset += 4
expression_weights = [0] * num_weights
for i in range(0, num_weights):
weight = struct.unpack_from("!f", pose_data, offset)[0]
offset += 4
if actor and objects and (prefs.datalink_preview_shape_keys or not LINK_DATA.set_keyframes):
expression_name = actor.expressions[i]
set_actor_expression_weight(objects, expression_name, weight)
expression_weights[i] = weight
# unpack the viseme shape keys into the mesh objects
num_weights = struct.unpack_from("!I", pose_data, offset)[0]
offset += 4
viseme_weights = [0] * num_weights
for i in range(0, num_weights):
weight = struct.unpack_from("!f", pose_data, offset)[0]
offset += 4
if actor and objects and (prefs.datalink_preview_shape_keys or not LINK_DATA.set_keyframes):
viseme_name = actor.visemes[i]
set_actor_viseme_weight(objects, viseme_name, weight)
viseme_weights[i] = weight
# TODO: morph weights
morph_weights = []
# store shape keys in the cache
if LINK_DATA.set_keyframes and actor_ready:
store_shape_key_cache_keyframes(actor, opt_frame, opt_start_frame, expression_weights, viseme_weights, morph_weights)
elif character_type == "LIGHT":
active,r,g,b,m,rng,angle,falloff,attenuation,darkness = struct.unpack_from("!?fffffffff", pose_data, offset)
color = Color((r,g,b))
if actor:
rlx.apply_light_pose(actor.object, loc, rot, sca, color, active, m, rng, angle, falloff, attenuation, darkness)
offset += 37
elif character_type == "CAMERA":
lens,enable,focus,rng,fb,nb,ft,nt,mbd = struct.unpack_from("!f?fffffff", pose_data, offset)
if actor:
rlx.apply_camera_pose(actor.object, loc, rot, sca, lens, enable, focus, rng, fb, nb, ft, nt, mbd)
offset += 33
if rig:
rig.pose.bones.update()
return actors
def reposition_prop_meshes(self, actors):
actor: LinkActor
for actor in actors:
if actor.get_type() == "PROP":
for mesh_name in actor.skin_meshes:
obj: bpy.types.Object
obj, loc, rot, sca = actor.skin_meshes[mesh_name]
rig = obj.parent
# do not adjust mesh transforms on skinned props
mod = modifiers.get_armature_modifier(obj)
if mod: continue
obj.matrix_world = utils.make_transform_matrix(loc, rot, sca)
def decode_lighting_data(self, data):
props = vars.props()
prefs = vars.prefs()
lights_data = decode_to_json(data)
utils.log_info(f"Light Decoded, Use Lights: {lights_data['use_lights']}")
ambient_color = utils.array_to_color(lights_data["ambient_color"])
ambient_strength = 0.125 + ambient_color.v
utils.object_mode()
use_lighting = lights_data.get("use_lights", False)
auto_lighting = lights_data.get("auto_lights", False)
# don't set up auto lighting (from CC Go-B) if not enabled
if auto_lighting and not prefs.datalink_auto_lighting:
return
container = rlx.add_light_container()
# create or modify existing lights
for light_data in lights_data["lights"]:
light = rlx.find_link_id(light_data["link_id"])
light = rlx.decode_rlx_light(light_data, light, container)
# clean up lights not found in scene
for obj in bpy.data.objects:
if obj.type == "LIGHT":
obj_link_id = utils.get_rl_link_id(obj)
if obj_link_id and obj_link_id not in lights_data["scene_lights"]:
utils.delete_light_object(obj)
bpy.context.scene.eevee.use_taa_reprojection = True
if utils.B420():
bpy.context.scene.eevee.use_shadows = True
bpy.context.scene.eevee.use_volumetric_shadows = True
bpy.context.scene.eevee.use_raytracing = True
bpy.context.scene.eevee.ray_tracing_options.resolution_scale = "1"
bpy.context.scene.eevee.ray_tracing_options.use_denoise = True
bpy.context.scene.eevee.use_shadow_jitter_viewport = True
bpy.context.scene.eevee.use_bokeh_jittered = True
bpy.context.scene.world.use_sun_shadow = True
bpy.context.scene.world.use_sun_shadow_jitter = True
else:
bpy.context.scene.eevee.use_gtao = True
bpy.context.scene.eevee.gtao_distance = 0.25
bpy.context.scene.eevee.gtao_factor = 0.5
bpy.context.scene.eevee.use_bloom = True
bpy.context.scene.eevee.bloom_threshold = 0.8
bpy.context.scene.eevee.bloom_knee = 0.5
bpy.context.scene.eevee.bloom_radius = 2.0
bpy.context.scene.eevee.bloom_intensity = 0.5
bpy.context.scene.eevee.use_ssr = True
bpy.context.scene.eevee.use_ssr_refraction = True
bpy.context.scene.eevee.bokeh_max_size = 32
view_transform = prefs.lighting_use_look if utils.B400() else "Filmic"
colorspace.set_view_settings(view_transform, "Medium Contrast", 0, 0.75)
if bpy.context.scene.cycles.transparent_max_bounces < 100:
bpy.context.scene.cycles.transparent_max_bounces = 100
view_space = utils.get_view_3d_space()
shading = utils.get_view_3d_shading()
if shading:
if shading.type != 'MATERIAL' and shading.type != "RENDERED":
shading.type = 'MATERIAL'
shading.use_scene_lights = True
shading.use_scene_lights_render = True
shading.use_scene_world = False
shading.use_scene_world_render = True
shading.studio_light = 'studio.exr'
shading.studiolight_rotate_z = -25 * 0.01745329
shading.studiolight_intensity = ambient_strength
shading.studiolight_background_alpha = 0.0
shading.studiolight_background_blur = 0.5
if view_space and self.is_cc():
# only hide the lights if it's from Character Creator
view_space.overlay.show_extras = False
if bpy.context.scene.view_settings.view_transform == "AgX":
c = props.light_filter
props.light_filter = (0.875, 1, 1, 1)
bpy.ops.cc3.scene(param="FILTER_LIGHTS")
props.light_filter = c
use_ibl = lights_data.get("use_ibl", False)
if use_ibl:
ibl_path = lights_data.get("ibl_path", "")
ibl_remote_id = lights_data.get("ibl_remote_id")
ibl_path = self.get_remote_file(ibl_remote_id, ibl_path)
ibl_strength = lights_data.get("ibl_strength", 0.5)
ibl_location = utils.array_to_vector(lights_data.get("ibl_location", [0,0,0])) / 100
ibl_rotation = utils.array_to_vector(lights_data.get("ibl_rotation", [0,0,0]))
ibl_scale = lights_data.get("ibl_scale", 1.0)
if ibl_path:
world.world_setup(None, ibl_path, ambient_color, ibl_location, ibl_rotation, ibl_scale, ibl_strength)
else:
world.world_setup(None, "", ambient_color, Vector((0,0,0)), Vector((0,0,0)), 1.0, ambient_strength)
def receive_lighting_sync(self, data):
props = vars.props()
update_link_status(f"Light Data Receveived")
state = utils.store_mode_selection_state()
props.lighting_brightness = 1.0
self.decode_lighting_data(data)
utils.restore_mode_selection_state(state)
# Camera
#
def get_view_camera_data(self):
view_space: bpy.types.Space
r3d: bpy.types.RegionView3D
view_space, r3d = utils.get_region_3d()
t = r3d.view_location
r = r3d.view_rotation
d = r3d.view_distance
dir = Vector((0,0,-1))
dir.rotate(r)
loc: Vector = t - (dir * d)
lens = view_space.lens
data = {
"link_id": "0",
"name": "Viewport Camera",
"loc": [loc.x, loc.y, loc.z],
"rot": [r.x, r.y, r.z, r.w],
"sca": [1, 1, 1],
"focal_length": lens,
}
return data
def get_view_camera_pivot(self):
view_space, r3d = utils.get_region_3d()
t = r3d.view_location
return t
def send_camera_sync(self):
update_link_status(f"Synchronizing View Camera")
self.send_notify(f"Sync View Camera")
camera_data = self.get_view_camera_data()
pivot = self.get_view_camera_pivot()
data = {
"view_camera": camera_data,
"pivot": [pivot.x, pivot.y, pivot.z],
}
self.send(OpCodes.CAMERA_SYNC, encode_from_json(data))
def decode_camera_sync_data(self, data):
data = decode_to_json(data)
camera_data = data["view_camera"]
pivot = utils.array_to_vector(data["pivot"]) / 100
view_space, r3d = utils.get_region_3d()
loc = utils.array_to_vector(camera_data["loc"]) / 100
rot = utils.array_to_quaternion(camera_data["rot"])
to_pivot = pivot - loc
dir = Vector((0,0,-1))
dir.rotate(rot)
dist = to_pivot.dot(dir)
if dist <= 0:
dist = 1.0
r3d.view_location = loc + dir * dist
r3d.view_rotation = rot
r3d.view_distance = dist
view_space.lens = camera_data["focal_length"] * 1.625
def receive_camera_sync(self, data):
update_link_status(f"Camera Data Receveived")
self.decode_camera_sync_data(data)
def send_frame_sync(self):
update_link_status(f"Sending Frame Sync")
fps = bpy.context.scene.render.fps
start_frame = BFA(bpy.context.scene.frame_start)
end_frame = BFA(bpy.context.scene.frame_end)
current_frame = BFA(bpy.context.scene.frame_current)
start_time = start_frame / fps
end_time = end_frame / fps
current_time = current_frame / fps
frame_data = {
"fps": fps,
"start_time": start_time,
"end_time": end_time,
"current_time": current_time,
"start_frame": start_frame,
"end_frame": end_frame,
"current_frame": current_frame,
}
self.send(OpCodes.FRAME_SYNC, encode_from_json(frame_data))
def receive_frame_sync(self, data):
update_link_status(f"Frame Sync Receveived")
frame_data = decode_to_json(data)
start_frame = frame_data["start_frame"]
end_frame = frame_data["end_frame"]
current_frame = frame_data["current_frame"]
utils.log_info(f"Receive Frame Sync: start: {start_frame} end: {end_frame} current: {current_frame}")
bpy.context.scene.frame_start = RLFA(start_frame)
bpy.context.scene.frame_end = RLFA(end_frame)
bpy.context.scene.frame_current = RLFA(current_frame)
def set_link_fps(self, fps: int=None):
if self.link_data:
if not fps:
fps = self.link_data.link_fps
fps = int(fps)
if self.link_data.link_fps != fps or bpy.context.scene.render.fps != fps:
self.link_data.link_fps = fps
bpy.context.scene.render.fps = fps
bpy.context.scene.render.fps_base = 1.0
return fps
def get_link_fps(self):
if self.link_data:
return self.link_data.link_fps
return bpy.context.scene.render.fps
def check_fps(self):
fps = bpy.context.scene.render.fps
if self.link_data.link_fps != fps:
self.send_fps()
def send_fps(self):
fps = bpy.context.scene.render.fps
self.link_data.link_fps = fps
fps_data = {
"fps": fps
}
self.send(OpCodes.FPS, encode_from_json(fps_data))
def receive_fps(self, data):
fps_data = decode_to_json(data)
fps = int(fps_data.get("fps", 60))
update_link_status(f"FPS Received: {fps}")
utils.log_info(f"Receive FPS: {fps}")
bpy.context.scene.render.fps = fps
bpy.context.scene.render.fps_base = 1.0
self.link_data.link_fps = fps
# Character Pose
#
def receive_actor_templates(self, data):
props = vars.props()
global LINK_DATA
state = utils.store_mode_selection_state()
props.validate_and_clean_up()
# decode character templates
template_json = decode_to_json(data)
count = template_json["count"]
utils.log_info(f"Receive Character Template: {count} actors")
# fetch actors and set templates
for actor_data in template_json["actors"]:
name = actor_data["name"]
character_type = actor_data["type"]
link_id = actor_data["link_id"]
actor = LINK_DATA.find_sequence_actor(link_id)
if actor:
if actor.get_type() == "AVATAR" or actor.get_type() == "PROP":
actor.set_template(actor_data)
utils.log_info(f"Preparing Actor: {actor.name} ({actor.get_link_id()})")
prep_pose_actor(actor, LINK_DATA.sequence_start_frame, LINK_DATA.sequence_end_frame)
else:
utils.log_error(f"Unable to find actor: {name} ({link_id})")
update_link_status(f"Character Templates Received")
utils.restore_mode_selection_state(state)
def select_actor_rigs(self, actors, start_frame=0, end_frame=0):
rigs = []
objects = []
actor: LinkActor
all_selected = True
# determine what needs to be selected
for actor in actors:
rig = actor.get_armature()
if rig:
rigs.append(rig)
if rig not in bpy.context.selected_objects:
all_selected = False
elif actor.object:
objects.append(actor.object)
if actor.object not in bpy.context.selected_objects:
all_selected = False
all_objects = rigs.copy()
all_objects.extend(objects)
# make sure only actors are selected
for obj in bpy.context.selected_objects:
if obj not in all_objects:
all_selected = False
# if there are armatures make sure we are in pose mode
if rigs and utils.get_mode() != "POSE":
all_selected = False
if not all_selected:
utils.object_mode()
utils.clear_selected_objects()
utils.try_select_objects(all_objects, True)
if rigs:
utils.set_active_object(rigs[0])
utils.set_mode("POSE")
return rigs, objects
def get_actors_frame_range(self, actors: any, frame, start_frame, end_frame, current_frame,
expand_range=False, expand_frame=False, set_preview=False,
set_current=False, set_start=False, set_end=False):
T = type(actors)
if T is list:
...
elif T is LinkActor:
actors = [ actors ]
elif vars.is_chr_cache(actors):
actors = [ LinkActor(actors) ]
else:
actors = None
if actors:
for actor in actors:
opt_start_frame = LinkActor.get_sequence_frame(actor, start_frame, start_frame, current_frame)
opt_end_frame = LinkActor.get_sequence_frame(actor, end_frame, start_frame, current_frame)
opt_frame = LinkActor.get_sequence_frame(actor, frame, start_frame, current_frame)
if expand_range:
if opt_start_frame < bpy.context.scene.frame_start:
bpy.context.scene.frame_start = opt_start_frame
if opt_end_frame > bpy.context.scene.frame_end:
bpy.context.scene.frame_end = opt_end_frame
if expand_frame:
if opt_frame < bpy.context.scene.frame_start:
bpy.context.scene.frame_start = opt_frame
if opt_frame > bpy.context.scene.frame_end:
bpy.context.scene.frame_end = opt_frame
opt_start_frame = LinkActor.get_sequence_frame(actors[0], start_frame, start_frame, current_frame)
opt_end_frame = LinkActor.get_sequence_frame(actors[0], end_frame, start_frame, current_frame)
opt_frame = LinkActor.get_sequence_frame(actors[0], frame, start_frame, current_frame)
else:
opt_start_frame = LinkActor.get_sequence_frame(None, start_frame, start_frame, current_frame)
opt_end_frame = LinkActor.get_sequence_frame(None, end_frame, start_frame, current_frame)
opt_frame = LinkActor.get_sequence_frame(None, frame, start_frame, current_frame)
if set_preview:
set_frame_range(opt_start_frame, opt_end_frame, preview=True)
if set_current:
set_frame(opt_frame)
elif set_start:
set_frame(opt_start_frame)
elif set_end:
set_frame(opt_end_frame)
return opt_frame, opt_start_frame, opt_end_frame
def receive_pose(self, data):
props = vars.props()
global LINK_DATA
props.validate_and_clean_up()
# decode pose data
json_data = decode_to_json(data)
start_frame = RLFA(json_data["start_frame"])
end_frame = RLFA(json_data["end_frame"])
frame = RLFA(json_data["frame"])
current_frame = bpy.context.scene.frame_current
motion_prefix = json_data.get("motion_prefix", "")
use_fake_user = json_data.get("use_fake_user", False)
set_keyframes = json_data.get("set_keyframes", True)
LINK_DATA.sequence_start_frame = frame
LINK_DATA.sequence_end_frame = frame
LINK_DATA.sequence_current_frame = frame
LINK_DATA.scene_current_frame = current_frame
LINK_DATA.set_action_settings(motion_prefix, use_fake_user, set_keyframes)
utils.log_info(f"Receive Pose: {frame}")
# fetch actors
actors_data = json_data["actors"]
actors = []
for actor_data in actors_data:
name = actor_data["name"]
character_type = actor_data["type"]
link_id = actor_data["link_id"]
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
if actor:
actors.append(actor)
opt_frame, opt_start_frame, opt_end_frame = \
self.get_actors_frame_range(None, frame, start_frame, end_frame, current_frame,
expand_frame=True, set_current=True)
# set pose frame
update_link_status(f"Receiving Pose Frame: {frame}")
LINK_DATA.sequence_actors = actors
LINK_DATA.sequence_type = "POSE"
bpy.ops.screen.animation_cancel()
bpy.context.view_layer.update()
def receive_pose_frame(self, data):
global LINK_DATA
state = utils.store_mode_selection_state()
# decode and cache pose
frame = self.decode_pose_frame_header(data)
actors = self.decode_pose_frame_data(data)
utils.log_info(f"Receive Pose Frame: {frame}")
opt_frame = LinkActor.get_sequence_frame(None, frame, LINK_DATA.sequence_start_frame, LINK_DATA.scene_current_frame)
opt_start_frame = LinkActor.get_sequence_frame(None, LINK_DATA.sequence_start_frame, LINK_DATA.sequence_start_frame, LINK_DATA.scene_current_frame)
# force recalculate all transforms (lights and cameras seem to need this)
bpy.context.view_layer.update()
self.reposition_prop_meshes(actors)
# store frame data
update_link_status(f"Pose Frame: {frame}")
self.select_actor_rigs(actors)
actor: LinkActor
if LINK_DATA.set_keyframes:
for actor in actors:
if actor.ready(require_cache=LINK_DATA.set_keyframes):
if actor.get_type() == "PROP" or actor.get_type() == "AVATAR":
store_bone_cache_keyframes(actor, opt_frame, opt_start_frame)
elif actor.get_type() == "LIGHT":
store_light_cache_keyframes(actor, opt_frame, opt_start_frame)
elif actor.get_type() == "CAMERA":
store_camera_cache_keyframes(actor, opt_frame, opt_start_frame)
# write pose action
for actor in actors:
if actor.ready(require_cache=LINK_DATA.set_keyframes):
if LINK_DATA.set_keyframes:
write_sequence_actions(actor, 1, opt_start_frame)
if actor.get_type() == "PROP" or actor.get_type() == "AVATAR":
remove_datalink_import_rig(actor, apply_constraints=not LINK_DATA.set_keyframes)
if actor.get_type() == "PROP":
rigutils.update_prop_rig(actor.get_armature())
elif actor.get_type() == "AVATAR":
rigutils.update_avatar_rig(actor.get_armature())
# finish
LINK_DATA.sequence_actors = None
LINK_DATA.sequence_type = None
if LINK_DATA.set_keyframes:
bpy.context.scene.frame_current = opt_frame
utils.restore_mode_selection_state(state, include_frames=False)
# doesn't work with existing actions, the pose is reset back to action after execution.
#for actor in actors:
# rig: bpy.types.Object = actor.get_armature()
# bone: bpy.types.PoseBone = rig.pose.bones["CC_Base_R_Upperarm"]
# bone.rotation_quaternion = (1,0,0,0)
def receive_sequence(self, data):
props = vars.props()
global LINK_DATA
props.validate_and_clean_up()
# decode sequence data
json_data = decode_to_json(data)
start_frame = RLFA(json_data["start_frame"])
end_frame = RLFA(json_data["end_frame"])
frame = RLFA(json_data["frame"])
current_frame = bpy.context.scene.frame_current
motion_prefix = json_data.get("motion_prefix", "")
use_fake_user = json_data.get("use_fake_user", False)
set_keyframes = json_data.get("set_keyframes", True)
LINK_DATA.sequence_start_frame = start_frame
LINK_DATA.sequence_end_frame = end_frame
LINK_DATA.sequence_current_frame = start_frame
LINK_DATA.scene_current_frame = current_frame
LINK_DATA.set_action_settings(motion_prefix, use_fake_user, set_keyframes)
num_frames = end_frame - start_frame + 1
utils.log_info(f"Receive Sequence: {start_frame} to {end_frame}, {num_frames} frames")
# fetch sequence actors
actors_data = json_data["actors"]
actors = []
for actor_data in actors_data:
name = actor_data["name"]
character_type = actor_data["type"]
link_id = actor_data["link_id"]
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
if actor:
actors.append(actor)
opt_frame, opt_start_frame, opt_end_frame = \
self.get_actors_frame_range(None, frame, start_frame, end_frame, current_frame,
expand_range=True, set_preview=True, set_start=True)
LINK_DATA.sequence_actors = actors
LINK_DATA.sequence_type = "SEQUENCE"
if not actors:
self.send_invalid("No valid sequence Actors!")
# update scene range
update_link_status(f"Receiving Live Sequence: {num_frames} frames")
bpy.ops.screen.animation_cancel()
utils.start_timer("FRAME")
utils.start_timer("DECODE")
utils.start_timer("LAYER_UPDATE")
utils.start_timer("REPOSITION")
utils.start_timer("SELECT_RIGS")
utils.start_timer("STORE_CACHE")
utils.start_timer("WRITE")
# start the sequence
self.start_sequence()
def receive_sequence_frame(self, data):
global LINK_DATA
utils.mark_timer("FRAME")
# decode and cache pose
utils.mark_timer("DECODE")
frame = self.decode_pose_frame_header(data)
utils.log_detail(f"Receive Sequence Frame: {frame}")
actors = self.decode_pose_frame_data(data)
utils.update_timer("DECODE")
opt_frame = LinkActor.get_sequence_frame(None, frame, LINK_DATA.sequence_start_frame, LINK_DATA.scene_current_frame)
opt_start_frame = LinkActor.get_sequence_frame(None, LINK_DATA.sequence_start_frame, LINK_DATA.sequence_start_frame, LINK_DATA.scene_current_frame)
utils.mark_timer("REPOSITION")
self.reposition_prop_meshes(actors)
utils.update_timer("REPOSITION")
# force recalculate all transforms
utils.mark_timer("LAYER_UPDATE")
bpy.context.view_layer.update()
utils.update_timer("LAYER_UPDATE")
# store frame data
utils.mark_timer("SELECT_RIGS")
update_link_status(f"Sequence Frame: {LINK_DATA.sequence_current_frame}")
self.select_actor_rigs(actors)
utils.update_timer("SELECT_RIGS")
utils.mark_timer("STORE_CACHE")
actor: LinkActor
for actor in actors:
if actor.ready(require_cache=LINK_DATA.set_keyframes):
if LINK_DATA.set_keyframes:
if actor.get_type() == "PROP" or actor.get_type() == "AVATAR":
store_bone_cache_keyframes(actor, opt_frame, opt_start_frame)
elif actor.get_type() == "LIGHT":
store_light_cache_keyframes(actor, opt_frame, opt_start_frame)
elif actor.get_type() == "CAMERA":
store_camera_cache_keyframes(actor, opt_frame, opt_start_frame)
utils.update_timer("STORE_CACHE")
# send sequence frame ack
self.send_sequence_ack(frame)
utils.update_timer("FRAME")
def receive_sequence_end(self, data):
global LINK_DATA
# decode sequence end
json_data = decode_to_json(data)
actors_data = json_data["actors"]
end_frame = RLFA(json_data["frame"])
aborted = json_data.get("aborted", False)
LINK_DATA.sequence_end_frame = end_frame
utils.log_info("Receive Sequence End")
# fetch actors
actors = []
actor: LinkActor
for actor_data in actors_data:
name = actor_data["name"]
character_type = actor_data["type"]
link_id = actor_data["link_id"]
actor = LINK_DATA.find_sequence_actor(link_id)
if actor:
actors.append(actor)
num_frames = LINK_DATA.sequence_end_frame - LINK_DATA.sequence_start_frame + 1
if not aborted:
utils.log_info(f"sequence complete: {LINK_DATA.sequence_start_frame} to {LINK_DATA.sequence_end_frame} = {num_frames}")
update_link_status(f"Live Sequence Complete: {num_frames} frames")
else:
update_link_status(f"Live Sequence Aborted!")
opt_start_frame = LinkActor.get_sequence_frame(None, LINK_DATA.sequence_start_frame, LINK_DATA.sequence_start_frame, LINK_DATA.scene_current_frame)
# write actions
utils.mark_timer("WRITE")
for actor in actors:
if LINK_DATA.set_keyframes:
write_sequence_actions(actor, num_frames, opt_start_frame)
if actor.get_type() == "PROP" or actor.get_type() == "AVATAR":
remove_datalink_import_rig(actor, apply_constraints=not LINK_DATA.set_keyframes)
if actor.get_type() == "PROP":
rigutils.update_prop_rig(actor.get_armature())
elif actor.get_type() == "AVATAR":
rigutils.update_avatar_rig(actor.get_armature())
utils.update_timer("WRITE")
utils.log_timer("Frame", name="FRAME")
utils.log_timer("Decode", name="DECODE")
utils.log_timer("Layer Update", name="LAYER_UPDATE")
utils.log_timer("Reposition", name="REPOSITION")
utils.log_timer("Select Rigs", name="SELECT_RIGS")
utils.log_timer("Store Cache", name="STORE_CACHE")
utils.log_timer("Write", name="WRITE")
# stop sequence
self.stop_sequence()
LINK_DATA.sequence_actors = None
LINK_DATA.sequence_type = None
#bpy.context.scene.frame_current = LINK_DATA.sequence_start_frame
# play the recorded sequence
if not aborted and LINK_DATA.set_keyframes:
bpy.ops.screen.animation_play()
def receive_sequence_ack(self, data):
prefs = vars.prefs()
global LINK_DATA
json_data = decode_to_json(data)
ack_frame = RLFA(json_data["frame"])
server_rate = json_data["rate"]
delta_frames = LINK_DATA.sequence_current_frame - ack_frame
if prefs.datalink_match_client_rate:
if LINK_DATA.ack_time == 0.0:
LINK_DATA.ack_time = time.time()
LINK_DATA.ack_rate = 120
count = 5
else:
t = time.time()
delta_time = max(t - LINK_DATA.ack_time, 1/120)
LINK_DATA.ack_time = t
ack_rate = (1.0 / delta_time)
LINK_DATA.ack_rate = utils.lerp(LINK_DATA.ack_rate, ack_rate, 0.5)
if delta_frames >= 20:
count = 0
elif delta_frames >= 10:
count = 1
elif delta_frames >= 5:
count = 2
else:
count = 4
self.update_sequence(count, delta_frames)
else:
self.update_sequence(5, delta_frames)
def get_remote_file(self, remote_id, source_path, file_override=None):
if remote_id:
remote_files_folder = get_unpacked_tar_file_folder(remote_id)
if file_override:
source_file = file_override
else:
source_folder, source_file = os.path.split(source_path)
source_path = os.path.join(remote_files_folder, source_file)
else:
if file_override:
source_folder = os.path.split(source_path)[0]
source_path = os.path.join(source_folder, file_override)
return source_path
def receive_actor_import(self, data):
props = vars.props()
prefs = vars.prefs()
global LINK_DATA
props.validate_and_clean_up()
# decode character import data
json_data = decode_to_json(data)
fbx_path = json_data.get("path")
remote_id = json_data.get("remote_id")
fbx_path = self.get_remote_file(remote_id, fbx_path)
fps = json_data.get("fps", 60)
name = json_data.get("name")
character_type = json_data.get("type")
link_id = json_data.get("link_id")
motion_prefix = json_data.get("motion_prefix", "")
use_fake_user = json_data.get("use_fake_user", False)
save_after_import = json_data.get("save_after_import", False)
LINK_DATA.set_action_settings(motion_prefix, use_fake_user, True)
self.set_link_fps(fps)
start_frame = RLFA(json_data.get("start_frame", 0))
end_frame = RLFA(json_data.get("end_frame", 0))
frame = RLFA(json_data.get("frame", 0))
current_frame = bpy.context.scene.frame_current
LINK_DATA.sequence_start_frame = start_frame
LINK_DATA.sequence_end_frame = end_frame
LINK_DATA.sequence_current_frame = frame
LINK_DATA.scene_current_frame = current_frame
utils.log_info(f"Receive Character Import: {name} / {link_id} / {fbx_path}")
if not os.path.exists(fbx_path):
update_link_status(f"Invalid Import Path!")
return
self.get_actors_frame_range(None, frame, start_frame, end_frame, current_frame,
expand_range=True, set_preview=True, set_start=True)
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
opt_start_frame = LinkActor.get_start_frame(actor, start_frame, current_frame)
if actor:
update_link_status(f"Character: {name} exists!")
utils.log_info(f"Actor {name} ({link_id}) already exists!")
if prefs.datalink_confirm_replace:
bpy.ops.ccic.link_confirm_dialog("INVOKE_DEFAULT",
message=f"Character {name} already exists in the scene. Do you want to replace the character?",
mode="REPLACE",
name=name,
filepath=fbx_path,
link_id=link_id,
start_frame=opt_start_frame,
character_type=character_type,
prefs="datalink_confirm_replace")
else:
self.do_update_replace(name, link_id, fbx_path, character_type, True, opt_start_frame,
objects_to_replace_names=None,
replace_actions=False)
else:
update_link_status(f"Receving Character Import: {name}")
self.do_file_import(fbx_path, link_id, save_after_import, opt_start_frame)
def do_file_import(self, file_path, link_id, save_after_import, start_frame):
fps = self.get_link_fps()
try:
bpy.ops.cc3.importer(param="IMPORT", filepath=file_path, link_id=link_id,
zoom=False, no_rigify=True,
start_frame=start_frame,
motion_prefix=LINK_DATA.motion_prefix,
use_fake_user=LINK_DATA.use_fake_user)
except Exception as e:
utils.log_error(f"Error importing {file_path}", e)
return
# imports without animations can reset fps to 60
utils.log_info(f"Re-applying FPS: {fps}")
self.set_link_fps(fps)
actor = LinkActor.find_actor(link_id)
# props have big ugly bones, so show them as wires
if actor and actor.get_type() == "PROP":
arm = actor.get_armature()
#rigutils.custom_prop_rig(arm)
#rigutils.de_pivot(actor.get_chr_cache())
elif actor and actor.get_type() == "AVATAR":
if actor.get_chr_cache().is_non_standard():
arm = actor.get_armature()
#rigutils.custom_avatar_rig(arm)
if actor:
update_link_status(f"Character Imported: {actor.name}")
if save_after_import:
self.receive_save()
# force frame update (for actions to apply)
bpy.context.scene.frame_current = bpy.context.scene.frame_current
def receive_camera_fbx_import(self, data):
props = vars.props()
prefs = vars.prefs()
global LINK_DATA
props.validate_and_clean_up()
# decode character import data
json_data = decode_to_json(data)
fbx_path = json_data.get("path")
remote_id = json_data.get("remote_id")
fbx_path = self.get_remote_file(remote_id, fbx_path)
name = json_data.get("name")
character_type = json_data.get("type")
link_id = json_data.get("link_id")
motion_prefix = json_data.get("motion_prefix", "")
use_fake_user = json_data.get("use_fake_user", False)
save_after_import = json_data.get("save_after_import", False)
LINK_DATA.set_action_settings(motion_prefix, use_fake_user, True)
start_frame = RLFA(json_data.get("start_frame", 0))
current_frame = bpy.context.scene.frame_current
utils.log_info(f"Receive Camera Import: {name} / {link_id} / {fbx_path}")
if not os.path.exists(fbx_path):
update_link_status(f"Invalid Import Path!")
utils.log_error(f"Invalid Import Path: {fbx_path}")
return
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
opt_start_frame = LinkActor.get_start_frame(actor, start_frame, current_frame)
if actor:
update_link_status(f"Camera: {name} exists!")
utils.log_info(f"Camera {name} ({link_id}) already exists!")
if prefs.datalink_confirm_replace:
bpy.ops.ccic.link_confirm_dialog("INVOKE_DEFAULT",
message=f"Camera {name} already exists in the scene. Do you want to replace the character?",
mode="CAMERA",
name=name,
filepath=fbx_path,
start_frame=opt_start_frame,
link_id=link_id,
character_type=character_type,
prefs="datalink_confirm_replace")
else:
self.do_motion_import(link_id, fbx_path, character_type, opt_start_frame)
else:
update_link_status(f"Receving Camera Import: {name}")
self.do_file_import(fbx_path, link_id, save_after_import, opt_start_frame)
def receive_rlx_import(self, data):
props = vars.props()
prefs = vars.prefs()
global LINK_DATA
props.validate_and_clean_up()
# decode character import data
json_data = decode_to_json(data)
base_path = json_data.get("path")
remote_id = json_data.get("remote_id")
names = json_data.get("names")
character_types = json_data.get("types")
link_ids = json_data.get("link_ids")
motion_prefix = json_data.get("motion_prefix", "")
start_frame = RLFA(json_data.get("start_frame", 0))
end_frame = RLFA(json_data.get("end_frame", 0))
frame = RLFA(json_data.get("frame", 0))
current_frame = bpy.context.scene.frame_current
use_fake_user = json_data.get("use_fake_user", False)
save_after_import = json_data.get("save_after_import", False)
LINK_DATA.set_action_settings(motion_prefix, use_fake_user, True)
opt_frame, opt_start_frame, opt_end_frame = \
self.get_actors_frame_range(None, frame, start_frame, end_frame, current_frame,
expand_range=True, set_preview=True, set_start=True)
for i, name in enumerate(names):
link_id = link_ids[i]
character_type = character_types[i]
file = name + ".rlx"
rlx_path = self.get_remote_file(remote_id, base_path, file_override=file)
utils.log_info(f"Receive Light / Camera Import: {name} / {link_id} / {rlx_path}")
if not os.path.exists(rlx_path):
update_link_status(f"Invalid Import Path!")
utils.log_error(f"Invalid Import Path: {rlx_path}")
continue
self.do_file_import(rlx_path, link_id, save_after_import, opt_start_frame)
def receive_motion_import(self, data):
props = vars.props()
prefs = vars.prefs()
global LINK_DATA
props.validate_and_clean_up()
SMSS = utils.store_mode_selection_state()
# decode character import data
json_data = decode_to_json(data)
fbx_path = json_data.get("path")
remote_id = json_data.get("remote_id")
fbx_path = self.get_remote_file(remote_id, fbx_path)
name = json_data["name"]
character_type = json_data["type"]
link_id = json_data["link_id"]
start_frame = RLFA(json_data["start_frame"])
end_frame = RLFA(json_data["end_frame"])
frame = RLFA(json_data["frame"])
current_frame = bpy.context.scene.frame_current
motion_prefix = json_data.get("motion_prefix", "")
use_fake_user = json_data.get("use_fake_user", False)
LINK_DATA.sequence_start_frame = start_frame
LINK_DATA.sequence_end_frame = end_frame
LINK_DATA.sequence_current_frame = frame
LINK_DATA.scene_current_frame = current_frame
LINK_DATA.set_action_settings(motion_prefix, use_fake_user, True)
num_frames = end_frame - start_frame + 1
utils.log_info(f"Receive Motion Import: {name} / {link_id} / {fbx_path}")
utils.log_info(f"Motion Range: {start_frame} to {end_frame}, {num_frames} frames")
# update scene range
bpy.ops.screen.animation_cancel()
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
if not actor:
chr_cache, obj, mat, obj_cache, mat_cache = utils.get_context_character(bpy.context)
update_link_status(f"Character: {name} not found!")
utils.log_info(f"Actor {name} ({link_id}) not found!")
if chr_cache and LinkActor.chr_cache_type(chr_cache) == character_type:
link_id = chr_cache.link_id
if link_id:
utils.log_info(f"Redirecting to active character: {chr_cache.character_name}")
opt_frame, opt_start_frame, opt_end_frame = \
self.get_actors_frame_range(chr_cache, frame, start_frame, end_frame, current_frame,
expand_range=True, set_preview=True, set_start=True)
if prefs.datalink_confirm_mismatch:
bpy.ops.ccic.link_confirm_dialog("INVOKE_DEFAULT",
message=f"Character {name} not found, do you want to apply the motion to the current character: {chr_cache.character_name}?",
mode="MOTION",
name=name,
filepath=fbx_path,
link_id=chr_cache.link_id,
character_type=character_type,
start_frame=opt_start_frame,
prefs="datalink_confirm_mismatch")
else:
self.do_motion_import(link_id, fbx_path, character_type, opt_start_frame)
utils.restore_mode_selection_state(SMSS)
return
link_id = actor.get_link_id()
opt_frame, opt_start_frame, opt_end_frame = \
self.get_actors_frame_range(actor, frame, start_frame, end_frame, current_frame,
expand_range=True, set_preview=True, set_start=True)
self.do_motion_import(link_id, fbx_path, character_type, opt_start_frame)
utils.restore_mode_selection_state(SMSS, include_frames=False)
def do_motion_import(self, link_id, fbx_path, character_type, start_frame):
actor = LinkActor.find_actor(link_id, search_type=character_type)
update_link_status(f"Receving Motion Import: {actor.name}")
if actor.get_type() != character_type:
update_link_status(f"Invalid character type for motion!")
return
if os.path.exists(fbx_path):
try:
bpy.ops.cc3.anim_importer(filepath=fbx_path, remove_meshes=False,
remove_materials_images=True, remove_shape_keys=False,
start_frame=start_frame,
motion_prefix=LINK_DATA.motion_prefix,
use_fake_user=LINK_DATA.use_fake_user)
except Exception as e:
utils.log_error(f"Error importing {fbx_path}", e)
motion_rig = utils.get_active_object()
if motion_rig:
self.replace_actor_motion(actor, motion_rig)
update_link_status(f"Motion Imported: {actor.name}")
else:
update_link_status(f"Motion Import Failed!: {actor.name}")
def replace_actor_motion(self, actor: LinkActor, motion_rig):
props = vars.props()
prefs = vars.prefs()
if actor and motion_rig:
incoming_motion_action = None
motion_rig_action, motion_rig_slot = utils.safe_get_action_slot(motion_rig)
use_slotted = prefs.use_action_slots()
motion_objects = utils.get_child_objects(motion_rig)
motion_id = rigutils.get_motion_id(motion_rig_action)
utils.log_info(f"Replacing Actor Motion:")
utils.log_indent()
utils.log_info(f"Motion rig action: {motion_rig_action.name}")
# fetch actor rig
actor_rig = actor.get_armature()
chr_cache = actor.get_chr_cache()
actor_rig_id = rigutils.get_rig_id(actor_rig)
arm_id = utils.get_rl_object_id(actor_rig)
# store existing motion
action_store_id = props.store_actions(actor_rig)
# generate new action set data
motion_id = rigutils.get_unique_set_motion_id(actor_rig_id, motion_id, LINK_DATA.motion_prefix, slotted=use_slotted)
set_id, set_generation = rigutils.generate_motion_set(actor_rig, motion_id, LINK_DATA.motion_prefix)
remove_actions = []
if actor_rig:
if actor.get_type() == "PROP":
# if it's a prop retarget the animation (or copy the rest pose):
# props have no bind pose so the rest pose is the first frame of
# the animation, which changes with every new animation import...
if prefs.datalink_retarget_prop_actions:
# retarget the action to the existing rig
update_link_status(f"Retargeting Motion...")
baked_action = rigutils.bake_rig_action_from_source(motion_rig, actor_rig)
baked_action.use_fake_user = LINK_DATA.use_fake_user
rigutils.add_motion_set_data(baked_action, set_id, set_generation, arm_id=arm_id, slotted=use_slotted)
rigutils.set_armature_action_name(baked_action, actor_rig_id, motion_id, LINK_DATA.motion_prefix, slotted=use_slotted)
rigutils.copy_action_shape_key_channels(actor_rig, motion_rig_action, baked_action, fake_user=LINK_DATA.use_fake_user)
rigutils.delete_motion_set(motion_rig_action)
rigutils.load_motion_set(actor_rig, baked_action)
incoming_motion_action = baked_action
else:
# update the existing rig bind pose from the new motion
rigutils.copy_rest_pose(motion_rig, actor_rig)
motion_rig_action.use_fake_user = LINK_DATA.use_fake_user
# load_motion_set will create a new motion set when loading onto a different prop rig
incoming_motion_action = rigutils.load_motion_set(actor_rig, motion_rig_action, move=True)
rigutils.update_prop_rig(actor_rig)
else: # Avatar
if chr_cache.rigified:
update_link_status(f"Retargeting Motion...")
baked_action = rigging.adv_bake_retarget_to_rigify(None, chr_cache, motion_rig, motion_rig_action)
baked_action.use_fake_user = LINK_DATA.use_fake_user
rigutils.add_motion_set_data(baked_action, set_id, set_generation, arm_id=arm_id, slotted=use_slotted)
rigutils.set_armature_action_name(baked_action, actor_rig_id, motion_id, LINK_DATA.motion_prefix, slotted=use_slotted)
rigutils.copy_action_shape_key_channels(actor_rig, motion_rig_action, baked_action, fake_user=LINK_DATA.use_fake_user)
rigutils.delete_motion_set(motion_rig_action)
rigutils.load_motion_set(actor_rig, baked_action)
incoming_motion_action = baked_action
else:
actor_rig_action = utils.safe_get_action(actor_rig)
motion_rig_action.use_fake_user = LINK_DATA.use_fake_user
# load_motion_set will create a new motion set when loading onto a different character rig
incoming_motion_action = rigutils.load_motion_set(actor_rig, motion_rig_action, move=True)
rigutils.update_avatar_rig(actor_rig)
for obj in motion_objects:
utils.delete_mesh_object(obj)
if motion_rig:
utils.delete_armature_object(motion_rig)
if actor_rig and incoming_motion_action:
action_mode, frame_mode = actor.get_import_modes()
rigutils.finalize_motion_import(actor_rig, incoming_motion_action, action_store_id, action_mode)
utils.log_recess()
def receive_actor_update(self, data):
props = vars.props()
global LINK_DATA
props.validate_and_clean_up()
# decode character update
json_data = decode_to_json(data)
old_name = json_data["old_name"]
old_link_id = json_data["old_link_id"]
character_type = json_data["type"]
new_name = json_data["new_name"]
new_link_id = json_data["new_link_id"]
utils.log_info(f"Receive Character Update: {old_name} -> {new_name} / {old_link_id} -> {new_link_id}")
# update character data
actor = LinkActor.find_actor(old_link_id, search_name=old_name, search_type=character_type)
utils.log_info(f"Updating Actor: {actor.name} {actor.get_link_id()}")
actor.update_name(new_name)
actor.update_link_id(new_link_id)
def receive_morph(self, data, update=False):
props = vars.props()
global LINK_DATA
props.validate_and_clean_up()
# decode receive morph
json_data = decode_to_json(data)
obj_path = json_data.get("path")
remote_id = json_data.get("remote_id")
obj_path = self.get_remote_file(remote_id, obj_path)
name = json_data["name"]
character_type = json_data["type"]
link_id = json_data["link_id"]
utils.log_info(f"Receive Character Morph: {name} / {link_id} / {obj_path}")
# fetch actor to update morph or import new morph character
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
if actor:
update = True
else:
update = False
if actor:
chr_cache = actor.get_chr_cache()
if not chr_cache.is_import_type("OBJ"):
update_link_status(f"Character is not for Morph editing!")
return
update_link_status(f"Receving Character Morph: {name}")
if os.path.exists(obj_path):
if update:
self.import_morph_update(actor, obj_path)
update_link_status(f"Morph Updated: {actor.name}")
else:
try:
bpy.ops.cc3.importer(param="IMPORT", filepath=obj_path, link_id=link_id)
except Exception as e:
utils.log_error(f"Error importing {obj_path}", e)
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
update_link_status(f"Morph Imported: {actor.name}")
def import_morph_update(self, actor: LinkActor, file_path):
utils.log_info(f"Import Morph Update: {actor.name} / {file_path}")
old_objects = utils.get_set(bpy.data.objects)
importer.obj_import(file_path, split_objects=False, split_groups=False, vgroups=True)
objects = utils.get_set_new(bpy.data.objects, old_objects)
if objects and actor and actor.get_chr_cache():
for source in objects:
source.scale = (0.01, 0.01, 0.01)
dest = actor.get_chr_cache().object_cache[0].object
geom.copy_vert_positions_by_index(source, dest)
utils.delete_mesh_object(source)
def receive_update_replace(self, data):
props = vars.props()
props.validate_and_clean_up()
json_data = decode_to_json(data)
fbx_path = json_data.get("path")
remote_id = json_data.get("remote_id")
fbx_path = self.get_remote_file(remote_id, fbx_path)
name = json_data["name"]
character_type = json_data["type"]
link_id = json_data["link_id"]
replace_all = json_data["replace"]
objects_to_replace_names = json_data["objects"]
start_frame = RLFA(json_data.get("start_frame", 0))
current_frame = bpy.context.scene.frame_current
utils.log_info(f"Receive Update / Replace: {name} - {objects_to_replace_names}")
context_chr_cache = props.get_context_character_cache()
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type, context_chr_cache=context_chr_cache)
opt_start_frame = LinkActor.get_start_frame(actor, start_frame, current_frame)
self.do_update_replace(name, link_id, fbx_path, character_type, replace_all, opt_start_frame,
objects_to_replace_names)
def do_update_replace(self, name, link_id, fbx_path, character_type, replace_all, start_frame,
objects_to_replace_names=None, replace_actions=False):
props = vars.props()
global LINK_DATA
context_chr_cache = props.get_context_character_cache()
process_only = ""
if not replace_all and objects_to_replace_names:
for n in objects_to_replace_names:
if process_only:
process_only += "|"
process_only += n
# import character assign new link_id
temp_link_id = utils.generate_random_id(20)
fps = self.get_link_fps()
utils.log_info(f"Importing replacement with temp link_id: {temp_link_id}")
try:
bpy.ops.cc3.importer(param="IMPORT",
filepath=fbx_path,
link_id=temp_link_id,
process_only=process_only,
start_frame=start_frame)
except Exception as e:
utils.log_error(f"Error importing {fbx_path}", e)
# imports without animations can reset fps to 60
self.set_link_fps(fps)
# the actor to replace
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type, context_chr_cache=context_chr_cache)
rig: bpy.types.Object = actor.get_armature()
rig_action = utils.safe_get_action(rig)
utils.log_info(f"Character Rig: {rig.name} / {rig_action.name if rig_action else 'No Action'}")
chr_cache = actor.get_chr_cache()
# the replacements
temp_actor = LinkActor.find_actor(temp_link_id, search_name=name, search_type=character_type)
temp_rig: bpy.types.Object = temp_actor.get_armature()
temp_rig_action = utils.safe_get_action(temp_rig)
temp_chr_cache = temp_actor.get_chr_cache()
utils.log_info(f"Replacement Rig: {temp_rig.name} / {temp_rig_action.name if temp_rig_action else 'No Action'}")
# can happen if the link_id's don't match
if chr_cache == temp_chr_cache:
utils.log_error("Character replacement and original are the same!")
update_link_status(f"Error! Character Mismatch")
temp_chr_cache.invalidate()
temp_chr_cache.delete()
return
if not replace_all:
# firstly convert the rest pose of the old rig to the new rig
# (so the new objects aren't modified by this process)
new_rest_pose = False
if not rigutils.is_rest_pose_same(temp_rig, rig):
utils.log_info(f"Incoming Rest Pose {temp_rig.name} is different: applying new rest pose...")
rigutils.copy_rest_pose(temp_rig, rig)
new_rest_pose = True
if rig and temp_rig:
# find and invalidate the cache data for the objects/materials being replaced
original_data = {}
done = []
# source cache objects and split meshes are treated separately here
for obj_cache in chr_cache.object_cache:
obj = obj_cache.get_object()
if obj not in done:
done.append(obj)
if obj_cache.source_name in objects_to_replace_names:
if obj:
original_data[obj_cache.source_name] = {
"name": obj.name,
"object_id": obj_cache.object_id
}
if obj.type == "MESH":
for mat in obj.data.materials:
if chr_cache.count_material(mat) <= 1:
mat_cache = chr_cache.get_material_cache(mat)
if mat_cache:
mat_cache.invalidate()
mat_cache.delete()
obj_cache.invalidate()
obj_cache.delete()
to_delete = []
for child in rig.children:
if child not in done and utils.object_exists_is_mesh(child):
done.append(child)
child_source_name = utils.strip_name(child.name)
if child_source_name in objects_to_replace_names:
obj_cache = chr_cache.get_object_cache(child)
if obj_cache:
original_data[child_source_name] = {
"name": child.name,
"object_id": obj_cache.object_id
}
if child.type == "MESH":
for mat in child.data.materials:
if chr_cache.count_material(mat) <= 1:
mat_cache = chr_cache.get_material_cache(mat)
if mat_cache:
mat_cache.invalidate()
mat_cache.delete()
to_delete.append(child)
utils.delete_objects(to_delete, log=True)
# reparent the replacements to the actor rig
new_objects = []
for child in temp_rig.children:
if utils.object_exists_is_mesh(child):
new_objects.append(child)
child.parent = rig
mod = modifiers.get_armature_modifier(child, armature=rig)
temp_obj_cache = temp_chr_cache.get_object_cache(child)
new_obj_cache = chr_cache.add_object_cache(child, copy_from=temp_obj_cache)
new_obj_cache.object = child
# restore object names and object id's
if temp_obj_cache.source_name in original_data:
utils.force_object_name(child, original_data[temp_obj_cache.source_name]["name"])
new_obj_cache.object_id = original_data[temp_obj_cache.source_name]["object_id"]
utils.set_rl_object_id(child, new_obj_cache.object_id)
for mat in child.data.materials:
if utils.material_exists(mat):
temp_mat_cache = temp_chr_cache.get_material_cache(mat)
material_type = temp_mat_cache.material_type
new_mat_cache = chr_cache.add_material_cache(mat, material_type, copy_from=temp_mat_cache)
new_mat_cache.material = mat
# generate a new json_local file with the updated data
chr_json = chr_cache.get_json_data()
chr_dir = chr_cache.get_import_dir()
tmp_json = temp_chr_cache.get_json_data()
tmp_dir = temp_chr_cache.get_import_dir()
chr_meshes, chr_phys_meshes = jsonutils.get_character_meshes_json(chr_json, chr_cache.get_character_id())
tmp_meshes, tmp_phys_meshes = jsonutils.get_character_meshes_json(tmp_json, temp_chr_cache.get_character_id())
chr_colliders = jsonutils.get_physics_collision_shapes_json(chr_json, chr_cache.get_character_id())
tmp_colliders = jsonutils.get_physics_collision_shapes_json(tmp_json, temp_chr_cache.get_character_id())
if not chr_meshes:
utils.log_error("No mesh data in character json!")
return
if not tmp_meshes:
utils.log_error("No mesh data in replacement character json!")
return
# make physics json if none in character (copy colliders over if none)
# ensures that chr_phys_meshes and chr_colliders exist
if tmp_phys_meshes or tmp_colliders:
if tmp_colliders and not chr_colliders:
chr_phys_meshes, chr_colliders = jsonutils.add_physics_json(chr_json, chr_cache.get_character_id(), tmp_json, temp_chr_cache.get_character_id())
else:
chr_phys_meshes, chr_colliders = jsonutils.add_physics_json(chr_json, chr_cache.get_character_id())
# replace the mesh json and soft physics mesh json data with the updates
for obj_name in objects_to_replace_names:
obj_json = None
phys_obj_json = None
if obj_name in tmp_meshes:
utils.log_info(f"Replacing {obj_name} in chr meshes json.")
obj_json = copy.deepcopy(tmp_meshes[obj_name])
chr_meshes[obj_name] = obj_json
else:
utils.log_info(f"{obj_name} not found in temp meshes json.")
if tmp_phys_meshes and obj_name in tmp_phys_meshes:
utils.log_info(f"Replacing {obj_name} in chr physics meshes json.")
phys_obj_json = copy.deepcopy(tmp_phys_meshes[obj_name])
chr_phys_meshes[obj_name] = phys_obj_json
# remap the texture paths relative to the new json_local file (in chr_dir)
jsonutils.remap_mesh_json_tex_paths(obj_json, phys_obj_json, tmp_dir, chr_dir)
# replace all the collider data if the rest pose has changed
if new_rest_pose and chr_colliders and tmp_colliders:
chr_colliders.clear()
for bone_name in tmp_colliders:
chr_colliders[bone_name] = copy.deepcopy(tmp_colliders[bone_name])
# write the changes to a .json_local
jsonutils.write_json(chr_json, chr_cache.import_file, is_fbx_path=True, is_json_local=True, update_cache=True)
# remove unused images/folders from the update import files
tmp_images = jsonutils.get_meshes_images(tmp_meshes)
keep_images = jsonutils.get_meshes_images(tmp_meshes, filter=objects_to_replace_names)
for img_path in tmp_images:
if img_path not in keep_images:
full_path = os.path.normpath(os.path.join(tmp_dir, img_path))
if os.path.exists(full_path):
utils.log_info(f"Deleting unused image file: {img_path}")
os.remove(full_path)
# replace_actions is True only when sending a new replacement character (through Send Actor)
# Send Update / Replace will not replace the action and should load the old one
if not replace_actions:
# remove temp chr actions (motion set)
if temp_rig_action:
rigutils.delete_motion_set(temp_rig_action)
# remap shapekey actions for the new objects
if rig_action:
rigutils.load_motion_set(rig, rig_action)
# invalidate and clean up but don't delete the objects & materials
# do this last as it invalidates the references
temp_chr_cache.invalidate()
temp_chr_cache.clean_up()
chr_cache.clean_up()
utils.remove_from_collection(props.import_cache, temp_chr_cache)
# delete the temp rig
if temp_rig:
utils.delete_object_tree(temp_rig)
else: # replace_all
if rig and temp_rig:
# copy old transform to new
temp_rig.location = rig.location
temp_rig.rotation_mode = rig.rotation_mode
temp_rig.rotation_quaternion = rig.rotation_quaternion
temp_rig.rotation_euler = rig.rotation_euler
temp_rig.rotation_axis_angle = rig.rotation_axis_angle
# replace_actions is True only when sending a new replacement character (through Send Actor)
# Send Update / Replace will not replace the action and should load the old one
if not replace_actions:
if temp_rig_action:
rigutils.delete_motion_set(temp_rig_action)
link_id = chr_cache.link_id
character_name = chr_cache.character_name
rig_name = rig.name
rig_data_name = rig.data.name
rl_armature_id = utils.get_rl_object_id(rig)
temp_chr_cache.set_link_id(link_id)
temp_chr_cache.character_name = character_name
utils.set_rl_object_id(temp_rig, rl_armature_id)
rig_obj_cache = temp_chr_cache.get_object_cache(temp_rig)
if rig_obj_cache:
rig_obj_cache.object_id = rl_armature_id
utils.force_object_name(temp_rig, rig_name)
utils.force_armature_name(temp_rig.data, rig_data_name)
if not replace_actions:
if rig_action:
# reload the old motion back into the new character
# rig will be destroyed and temp_rig will take it's place
rigutils.load_motion_set(temp_rig, rig_action)
# remove the original character
# do this last as it invalidates the references
chr_cache.invalidate()
chr_cache.delete()
chr_cache.clean_up()
utils.remove_from_collection(props.import_cache, chr_cache)
def receive_rigify_request(self, data):
props = vars.props()
props.validate_and_clean_up()
# decode rigify request
json_data = decode_to_json(data)
name = json_data["name"]
character_type = json_data["type"]
link_id = json_data["link_id"]
utils.log_info(f"Receive Rigify Request: {name} / {link_id}")
# rigify actor armature
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
if actor:
chr_cache = actor.get_chr_cache()
if chr_cache:
if chr_cache.rigified:
utils.log_error(f"Character {actor.name} already rigified!")
return
update_link_status(f"Rigifying: {actor.name}")
chr_cache.select(only=True)
cc3_rig = chr_cache.get_armature()
bpy.ops.cc3.rigifier(param="ALL", no_face_rig=True, auto_retarget=True)
rigutils.update_avatar_rig(chr_cache.get_armature())
update_link_status(f"Character Rigified: {actor.name}")
LINK_SERVICE: LinkService = None
def get_link_service():
global LINK_SERVICE
return LINK_SERVICE
def link_state_update():
global LINK_SERVICE
if LINK_SERVICE:
link_props = vars.link_props()
link_props.link_listening = LINK_SERVICE.is_listening
link_props.link_connected = LINK_SERVICE.is_connected
link_props.link_connecting = LINK_SERVICE.is_connecting
utils.update_ui()
def update_link_status(text):
link_props = vars.link_props()
link_props.link_status = text
utils.update_ui()
@persistent
def reconnect(file_path=None):
global LINK_SERVICE
link_props = vars.link_props()
prefs = vars.prefs()
utils.log_info("Reconnecting DataLink...")
connected = LINK_SERVICE.is_connected if LINK_SERVICE else False
connecting = LINK_SERVICE.is_connecting if LINK_SERVICE else False
if connected:
utils.log_info(" - DataLink already connected.")
elif connecting:
utils.log_info(" - DataLink Connecting...")
else:
if link_props.reconnect or link_props.connected:
link_props.reconnect = False
utils.log_info(" - DataLink was connected. Attempting to reconnect...")
bpy.ops.ccic.datalink(param="START")
elif prefs.datalink_auto_start:
utils.log_info(" - Auto-starting datalink...")
bpy.ops.ccic.datalink(param="START")
else:
utils.log_info(" - No previous DataLink to restart.")
return None
@persistent
def disconnect(file_path=None):
global LINK_SERVICE
link_props = vars.link_props()
if LINK_SERVICE:
utils.log_info("Disconnecting DataLink...")
link_props.reconnect = link_props.connected
LINK_SERVICE.service_disconnect()
return None
class CCICDataLink(bpy.types.Operator):
"""DataLink Control Operator"""
bl_idname = "ccic.datalink"
bl_label = "Listener"
bl_options = {"REGISTER"}
param: bpy.props.StringProperty(
name = "param",
default = "",
options={"HIDDEN"}
)
def execute(self, context):
global LINK_SERVICE
if self.param == "GOB_START":
self.link_start(is_go_b=True)
return {'FINISHED'}
if self.param == "START":
self.link_start()
return {'FINISHED'}
elif self.param == "DISCONNECT":
self.link_disconnect()
return {'FINISHED'}
elif self.param == "STOP":
self.link_stop()
return {'FINISHED'}
if self.param in ["SEND_POSE", "SEND_ANIM", "SEND_ACTOR", "SEND_MORPH",
"SEND_REPLACE_MESH", "SEND_TEXTURES", "SYNC_CAMERA"]:
if not LINK_SERVICE or not LINK_SERVICE.is_connected:
self.link_start()
if not LINK_SERVICE or not (LINK_SERVICE.is_connected or LINK_SERVICE.is_connecting):
self.report({"ERROR"}, "Server not listening!")
return {'FINISHED'}
if LINK_SERVICE:
if self.param == "SEND_POSE":
LINK_SERVICE.send_pose_request()
self.report({'INFO'}, f"Sending pose request ...")
return {'FINISHED'}
elif self.param == "SEND_ANIM":
LINK_SERVICE.send_sequence_request()
self.report({'INFO'}, f"Sending sequence request ...")
return {'FINISHED'}
elif self.param == "STOP_ANIM":
LINK_SERVICE.abort_sequence()
self.report({'INFO'}, f"Sequence stopped!")
return {'FINISHED'}
elif self.param == "SEND_ACTOR":
count = LINK_SERVICE.send_actor()
if count == 1:
self.report({'INFO'}, f"Actor sent ...")
elif count > 1:
self.report({'INFO'}, f"{count} Actors sent ...")
else:
self.report({'ERROR'}, f"No Actors sent!")
return {'FINISHED'}
elif self.param == "SEND_MORPH":
if LINK_SERVICE.send_morph():
self.report({'INFO'}, f"Morph sent ...")
else:
self.report({'ERROR'}, f"Morph not sent!")
return {'FINISHED'}
elif self.param == "SYNC_CAMERA":
LINK_SERVICE.send_camera_sync()
return {'FINISHED'}
elif self.param == "SEND_REPLACE_MESH":
count = LINK_SERVICE.send_replace_mesh()
if count == 1:
self.report({'INFO'}, f"Replace Mesh sent ...")
elif count > 1:
self.report({'INFO'}, f"{count} Replace Meshes sent ...")
else:
self.report({'ERROR'}, f"No Replace Meshes sent!")
return {'FINISHED'}
elif self.param == "SEND_MATERIAL_UPDATE":
count = LINK_SERVICE.send_material_update(context)
if count == 1:
self.report({'INFO'}, f"Material sent ...")
elif count > 1:
self.report({'INFO'}, f"{count} Materials sent ...")
else:
self.report({'ERROR'}, f"No Materials sent!")
return {'FINISHED'}
elif self.param == "DEPIVOT":
props = vars.props()
chr_cache = props.get_context_character_cache(context)
if chr_cache:
rigutils.de_pivot(chr_cache)
return {'FINISHED'}
elif self.param == "DEBUG":
LINK_SERVICE.send(OpCodes.DEBUG)
return {'FINISHED'}
elif self.param == "TEST":
test()
return {'FINISHED'}
if self.param == "SHOW_ACTOR_FILES":
props = vars.props()
chr_cache = props.get_context_character_cache(context)
if chr_cache:
utils.open_folder(chr_cache.get_import_dir())
return {'FINISHED'}
elif self.param == "SHOW_ACTOR_JSON":
props = vars.props()
chr_cache = props.get_context_character_cache(context)
if chr_cache:
os.startfile(chr_cache.get_character_json_path())
return {'FINISHED'}
elif self.param == "SHOW_PROJECT_FILES":
local_path = get_local_data_path()
if local_path:
utils.open_folder(local_path)
return {'FINISHED'}
return {'FINISHED'}
def prep_local_files(self):
data_path = get_local_data_path()
if data_path:
os.makedirs(data_path, exist_ok=True)
import_path = os.path.join(data_path, "imports")
export_path = os.path.join(data_path, "exports")
os.makedirs(import_path, exist_ok=True)
os.makedirs(export_path, exist_ok=True)
def link_start(self, is_go_b=False):
prefs = vars.prefs()
global LINK_SERVICE
self.prep_local_files()
if not LINK_SERVICE:
LINK_SERVICE = LinkService()
LINK_SERVICE.changed.connect(link_state_update)
if LINK_SERVICE:
link_ip = "127.0.0.1"
if is_go_b:
# go_b only to local host
prefs.datalink_target = "LOCAL"
try:
if prefs.datalink_target == "REMOTE":
link_ip = socket.gethostbyname(prefs.datalink_host)
prefs.datalink_bad_hostname = False
except:
prefs.datalink_bad_hostname = True
utils.log_error(f"Bad Remote DataLink Hostname! {prefs.datalink_host}")
return
LINK_SERVICE.service_start(link_ip, BLENDER_PORT)
def link_stop(self):
global LINK_SERVICE
if LINK_SERVICE:
LINK_SERVICE.service_stop()
def link_disconnect(self):
global LINK_SERVICE
if LINK_SERVICE:
LINK_SERVICE.service_disconnect()
@classmethod
def description(cls, context, properties):
if properties.param == "GOB_START":
return "Attempt to start the DataLink by connecting to the server running on CC4/iC8 to local host"
if properties.param == "START":
return "Attempt to start the DataLink by connecting to the server running on CC4/iC8"
elif properties.param == "DISCONNECT":
return "Disconnect from the DataLink server"
elif properties.param == "STOP":
return "Stop the DataLink on both client and server"
elif properties.param == "SEND_POSE":
return "Send the current pose (and frame) to CC4/iC8"
elif properties.param == "SEND_ANIM":
return "Send the animation on the character to CC4/iC8 as a live sequence"
elif properties.param == "STOP_ANIM":
return "Stop the live sequence"
elif properties.param == "SEND_ACTOR":
return "Send the character or prop to CC4/iC8"
elif properties.param == "SEND_MORPH":
return "Send the character body back to CC4 and create a morph slider for it"
elif properties.param == "SEND_ACTOR_INVALID":
return "This standard character has altered topology of the base body mesh and will not re-import into Character Creator"
elif properties.param == "SEND_MORPH_INVALID":
return "This standard character morph has altered topology of the base body mesh and will not re-import into Character Creator"
elif properties.param == "SYNC_CAMERA":
return "TBD"
elif properties.param == "SEND_REPLACE_MESH":
return "Send the mesh alterations back to CC4, only if the mesh topology has not changed"
elif properties.param == "SEND_REPLACE_MESH_INVALID":
return "*Warning* The selected (or one of the selected) mesh has changed in topology and cannot be sent back to CC4 via replace mesh.\n\n" \
"This mesh can now only be sent to CC4 with the entire character (Go CC)"
elif properties.param == "SEND_MATERIAL_UPDATE":
return "Send material data and textures for the currently selected meshe objects back to CC4"
elif properties.param == "DEPIVOT":
return "TBD"
elif properties.param == "DEBUG":
return "Debug!"
elif properties.param == "TEST":
return "Test!"
elif properties.param == "SHOW_ACTOR_FILES":
return "Open the actor imported files folder"
elif properties.param == "SHOW_PROJECT_FILES":
return "Open the project folder"
return ""
def debug(debug_json):
utils.log_always("")
utils.log_always("DEBUG")
utils.log_always("=====")
# simulate service crash
l = [0,1]
l[2] = 0
def test():
utils.log_always("")
utils.log_always("TEST")
utils.log_always("====")
class CCICLinkConfirmDialog(bpy.types.Operator):
bl_idname = "ccic.link_confirm_dialog"
bl_label = "Confirm Action"
message: bpy.props.StringProperty(default="")
name: bpy.props.StringProperty(default="")
filepath: bpy.props.StringProperty(default="")
link_id: bpy.props.StringProperty(default="")
start_frame: bpy.props.IntProperty(default=1)
character_type: bpy.props.StringProperty(default="")
mode: bpy.props.StringProperty(default="")
prefs: bpy.props.StringProperty(default="")
width=400
wrap_width = width / 5.5
def execute(self, context):
global LINK_SERVICE
props = vars.props()
prefs = vars.prefs()
if self.mode == "REPLACE":
LINK_SERVICE.do_update_replace(self.name, self.link_id, self.filepath,
self.character_type, True, self.start_frame,
objects_to_replace_names=None,
replace_actions=True)
if self.mode == "CAMERA":
LINK_SERVICE.do_motion_import(self.link_id, self.filepath, self.character_type, self.start_frame)
if self.mode == "LIGHT":
LINK_SERVICE.do_motion_import(self.link_id, self.filepath, self.character_type, self.start_frame)
if self.mode == "MOTION":
LINK_SERVICE.do_motion_import(self.link_id, self.filepath, self.character_type, self.start_frame)
return {"FINISHED"}
def invoke(self, context, event):
props = vars.props()
prefs = vars.prefs()
chr_cache = props.get_context_character_cache(context)
return context.window_manager.invoke_props_dialog(self, width=self.width)
def cancel(self, context):
#bpy.ops.ccic.link_confirm_dialog('INVOKE_DEFAULT',
# message=self.message,
# param=self.param)
return
def draw(self, context):
props = vars.props()
prefs = vars.prefs()
layout = self.layout
message: str = self.message
lines = message.splitlines()
wrapper = textwrap.TextWrapper(width=self.wrap_width)
for line in lines:
line = line.strip()
wrapped_lines = wrapper.wrap(line)
for wrapped_line in wrapped_lines:
layout.label(text=wrapped_line)
if self.prefs:
layout.separator()
if self.prefs == "datalink_confirm_mismatch":
layout.prop(prefs, self.prefs, text="Always Confirm Mismatch")
elif self.prefs == "datalink_confirm_replace":
layout.prop(prefs, self.prefs, text="Always Confirm Character Replace")
else:
layout.prop(prefs, self.prefs, text="Always Confirm")
layout.separator()
@classmethod
def description(cls, context, properties):
return "Edit the character name and non-standard type"
class CCICLinkTest(bpy.types.Operator):
bl_idname = "ccic.linktest"
bl_label = "Link Test"
def execute(self, context):
chr_cache, obj, mat, obj_cache, mat_cache = utils.get_context_character(context)
rig = chr_cache.get_armature()
pose_bone = rig.pose.bones["CC_Base_R_Upperarm"]
utils.log_always(pose_bone.rotation_quaternion)
pose_bone.rotation_quaternion = (1,0,0,0)
utils.log_always(pose_bone.rotation_quaternion)
return {"FINISHED"}