Files
blender-portable-repo/scripts/addons/cc_blender_tools-2_3_3/link.py
T
2026-03-17 15:25:32 -06:00

4733 lines
192 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, bones, geom, colorspace,
world, rigging, rigutils, drivers, modifiers,
cc, jsonutils, utils, vars)
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
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
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_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 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
#
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
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, datalink_rig,
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_contraints=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_contraints and chr_rig:
if utils.set_active_object(chr_rig):
if utils.pose_mode_to(chr_rig):
action = utils.safe_get_action(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)
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_contraints 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):
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)
if action_name in bpy.data.actions:
action = bpy.data.actions[action_name]
else:
action = bpy.data.actions.new(action_name)
utils.safe_set_action(rig, action)
action.use_fake_user = LINK_DATA.use_fake_user
return action
def get_datalink_obj_actions(obj, motion_id=None):
if not motion_id:
motion_id = "DataLink"
name = obj.name
f_prefix = rigutils.get_formatted_prefix(LINK_DATA.motion_prefix)
ob_name = f"{f_prefix}{name}|O|{motion_id}"
data_name = f"{f_prefix}{name}|{obj.type[0]}|{motion_id}"
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 utils.B440():
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."""
motion_id = "Pose" if LINK_DATA.sequence_type == "POSE" else "Sequence"
if actor and actor.get_type() == "LIGHT":
# create keyframe cache for light animation sequences
if LINK_DATA.set_keyframes:
rlx.prep_rlx_actions(actor.object, actor.name, motion_id,
reuse_existing=True,
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.object, actor.name, motion_id,
reuse_existing=True,
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)
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
chr_cache = actor.get_chr_cache()
rig = actor.get_armature()
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)
utils.log_info(f"Preparing Character Rig: {actor.name} {rig_id} / {len(actor.bones)} bones")
if LINK_DATA.set_keyframes:
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)
rigutils.add_motion_set_data(action, set_id, set_generation, rl_arm_id=rl_arm_id)
utils.log_info(f"Preparing rig action: {action.name}")
utils.clear_action(action)
# shape key actions
num_expressions = len(actor.expressions)
num_visemes = len(actor.visemes)
if objects:
for obj in objects:
obj_id = rigutils.get_action_obj_id(obj)
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")
if action_name in bpy.data.actions:
action = bpy.data.actions[action_name]
else:
action = bpy.data.actions.new(action_name)
rigutils.add_motion_set_data(action, set_id, set_generation, obj_id=obj_id)
utils.clear_action(action)
utils.safe_set_action(obj.data.shape_keys, action)
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)
BAKE_BONE_GROUPS = ["FK", "IK", "Special", "Root", "Face"] #not Tweak and Extra
BAKE_BONE_COLLECTIONS = ["Face", #"Face (Primary)", "Face (Secondary)",
"Face (Expressions)",
"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"]
SHOW_BONE_COLLECTIONS = [ "Face (UI)" ]
SHOW_BONE_COLLECTIONS.extend(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):
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):
bpy.data.scenes["Scene"].frame_start = start
bpy.data.scenes["Scene"].frame_end = end
def set_frame(frame):
bpy.data.scenes["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):
"""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()
start = actor.cache["start"]
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, expression_weights, viseme_weights, morph_weights):
if not actor.cache:
utils.log_error(f"No actor cache: {actor.name}")
return
start = actor.cache["start"]
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):
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"]
start = actor.cache["start"]
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):
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"]
start = actor.cache["start"]
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):
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)
def write_action_cache_curve(action: bpy.types.Action, cache, prop, data_path, num_frames, group_name, slot=None, slot_type=None):
if not LINK_DATA.set_keyframes: return
prop_cache = cache[prop]
num_curves = len(prop_cache["curves"])
channels = utils.get_action_channels(action, slot=slot, slot_type=slot_type)
if channels:
fcurve: bpy.types.FCurve = None
if group_name not in channels.groups:
channels.groups.new(group_name)
for i in range(0, num_curves):
cache_curve = prop_cache["curves"][i]
fcurve = channels.fcurves.new(data_path, index=i)
fcurve.keyframe_points.add(num_frames)
set_count = num_frames * 2
if set_count < len(cache_curve):
# if setting fewer frames than are in the cache (sequence was stopped early)
fcurve.keyframe_points.foreach_set('co', cache_curve[:set_count])
else:
fcurve.keyframe_points.foreach_set('co', cache_curve)
def write_sequence_actions(actor: LinkActor, num_frames):
if actor.cache:
if actor.get_type() == "PROP" or actor.get_type() == "AVATAR":
rig = actor.cache["rig"]
rig_action = utils.safe_get_action(rig)
objects, none_objects = actor.get_sequence_objects()
if rig_action:
utils.clear_action(rig_action, "OBJECT", rig_action.name)
bone_cache = actor.cache["bones"]
rig_slot = utils.get_action_slot(rig_action, "OBJECT")
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)
expression_cache = actor.cache["expressions"]
viseme_cache = actor.cache["visemes"]
for obj in objects:
obj_action = utils.safe_get_action(obj.data.shape_keys)
key_slot = utils.get_action_slot(obj_action, "KEY")
if obj_action:
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(obj_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(obj_action, viseme_cache, viseme_name,
key.path_from_id("value"), num_frames, "Viseme",
slot=key_slot)
utils.safe_set_action(obj.data.shape_keys, obj_action) # re-apply action to fix slot
# remove actions from non sequence objects
for obj in none_objects:
utils.safe_set_action(obj.data.shape_keys, None)
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)
write_action_rotation_cache_curve(ob_action, actor.cache["transform"], "rot", light, num_frames, slot=ob_slot)
write_action_cache_curve(ob_action, actor.cache["transform"], "sca", "scale", num_frames, "Scale", slot=ob_slot)
write_action_cache_curve(light_action, actor.cache["light"], "color", "color", num_frames, "Light", slot=light_slot)
write_action_cache_curve(light_action, actor.cache["light"], "energy", "energy", num_frames, "Light", slot=light_slot)
write_action_cache_curve(light_action, actor.cache["light"], "cutoff_distance", "cutoff_distance", num_frames, "Light", slot=light_slot)
if light.type == "SPOT":
write_action_cache_curve(light_action, actor.cache["light"], "spot_blend", "spot_blend", num_frames, "Spotlight", slot=light_slot)
write_action_cache_curve(light_action, actor.cache["light"], "spot_size", "spot_size", num_frames, "Spotlight", slot=light_slot)
# re-apply actions to fix slot
utils.safe_set_action(light, ob_action)
utils.safe_set_action(light.data, light_action)
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)
write_action_rotation_cache_curve(ob_action, actor.cache["transform"], "rot", camera, num_frames, slot=ob_slot)
write_action_cache_curve(ob_action, actor.cache["transform"], "sca", "scale", num_frames, "Scale", slot=ob_slot)
write_action_cache_curve(cam_action, actor.cache["camera"], "lens", "lens", num_frames, "Light", slot=cam_slot)
write_action_cache_curve(cam_action, actor.cache["camera"], "dof", "dof.use_dof", num_frames, "Light", slot=cam_slot)
write_action_cache_curve(cam_action, actor.cache["camera"], "focus_distance", "dof.focus_distance", num_frames, "Light", slot=cam_slot)
write_action_cache_curve(cam_action, actor.cache["camera"], "f_stop", "dof.aperture_f_stop", num_frames, "Light", slot=cam_slot)
# re-apply actions to fix slot
utils.safe_set_action(camera, ob_action)
utils.safe_set_action(camera.data, cam_action)
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,
}
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.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
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,
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,
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=True)
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)
if LINK_DATA.set_keyframes:
ensure_current_frame(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, 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"]
bpy.context.scene.frame_start = RLFA(start_frame)
bpy.context.scene.frame_end = RLFA(end_frame)
bpy.context.scene.frame_current = RLFA(current_frame)
# 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 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"])
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.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)
# 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()
if LINK_DATA.set_keyframes:
set_frame_range(start_frame, end_frame)
set_frame(frame)
else:
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)
utils.log_info(f"Receive Pose Frame: {frame}")
actors = self.decode_pose_frame_data(data)
# force recalculate all transforms
#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, frame)
elif actor.get_type() == "LIGHT":
store_light_cache_keyframes(actor, frame)
elif actor.get_type() == "CAMERA":
store_camera_cache_keyframes(actor, 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)
if actor.get_type() == "PROP" or actor.get_type() == "AVATAR":
remove_datalink_import_rig(actor, apply_contraints=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 = 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"])
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.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)
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()
set_frame_range(LINK_DATA.sequence_start_frame, LINK_DATA.sequence_end_frame)
set_frame(LINK_DATA.sequence_start_frame)
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")
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, frame)
elif actor.get_type() == "LIGHT":
store_light_cache_keyframes(actor, frame)
elif actor.get_type() == "CAMERA":
store_camera_cache_keyframes(actor, 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!")
# write actions
utils.mark_timer("WRITE")
for actor in actors:
if LINK_DATA.set_keyframes:
write_sequence_actions(actor, num_frames)
if actor.get_type() == "PROP" or actor.get_type() == "AVATAR":
remove_datalink_import_rig(actor, apply_contraints=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)
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)
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
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
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,
character_type=character_type,
prefs="datalink_confirm_replace")
else:
self.do_update_replace(name, link_id, fbx_path, character_type, True,
objects_to_replace_names=None,
replace_actions=True)
else:
update_link_status(f"Receving Character Import: {name}")
self.do_file_import(fbx_path, link_id, save_after_import)
def do_file_import(self, file_path, link_id, save_after_import):
try:
bpy.ops.cc3.importer(param="IMPORT", filepath=file_path, link_id=link_id,
zoom=False, no_rigify=True,
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
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)
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)
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,
link_id=link_id,
character_type=character_type,
prefs="datalink_confirm_replace")
else:
self.do_motion_import(link_id, fbx_path, character_type)
else:
update_link_status(f"Receving Camera Import: {name}")
self.do_file_import(fbx_path, link_id, save_after_import)
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", "")
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)
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)
def receive_motion_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["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"])
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.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()
set_frame_range(LINK_DATA.sequence_start_frame, LINK_DATA.sequence_end_frame)
set_frame(LINK_DATA.sequence_start_frame)
bpy.context.scene.frame_current = frame
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}")
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,
prefs="datalink_confirm_mismatch")
else:
self.do_motion_import(link_id, fbx_path, character_type)
return
link_id = actor.get_link_id()
self.do_motion_import(link_id, fbx_path, character_type)
def do_motion_import(self, link_id, fbx_path, character_type):
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,
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)
#except:
# utils.log_error(f"Error importing motion {fbx_path}")
# return
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):
prefs = vars.prefs()
if actor and motion_rig:
motion_rig_action = utils.safe_get_action(motion_rig)
motion_objects = utils.get_child_objects(motion_rig)
motion_id = rigutils.get_action_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 all associated actions...
source_actions = rigutils.find_source_actions(motion_rig_action, motion_rig)
# fetch actor rig
actor_rig = actor.get_armature()
chr_cache = actor.get_chr_cache()
actor_rig_id = rigutils.get_rig_id(actor_rig)
rl_arm_id = utils.get_rl_object_id(actor_rig)
motion_id = rigutils.get_unique_set_motion_id(actor_rig_id, motion_id, LINK_DATA.motion_prefix)
# generate new action set data
set_id, set_generation = rigutils.generate_motion_set(actor_rig, motion_id, LINK_DATA.motion_prefix)
remove_actions = []
action_pairs = []
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:
action = get_datalink_rig_action(actor_rig, motion_id)
rigutils.add_motion_set_data(action, set_id, set_generation, rl_arm_id=rl_arm_id)
update_link_status(f"Retargeting Motion...")
armature_action = rigutils.bake_rig_action_from_source(motion_rig, actor_rig)
armature_action.use_fake_user = LINK_DATA.use_fake_user
remove_actions.append(motion_rig_action)
else:
rigutils.add_motion_set_data(motion_rig_action, set_id, set_generation, rl_arm_id=rl_arm_id)
rigutils.set_armature_action_name(motion_rig_action, actor_rig_id, motion_id, LINK_DATA.motion_prefix)
motion_rig_action.use_fake_user = LINK_DATA.use_fake_user
rigutils.copy_rest_pose(motion_rig, actor_rig)
utils.safe_set_action(actor_rig, motion_rig_action)
rigutils.update_prop_rig(actor_rig)
else: # Avatar
if chr_cache.rigified:
update_link_status(f"Retargeting Motion...")
armature_action = rigging.adv_bake_retarget_to_rigify(None, chr_cache, motion_rig, motion_rig_action)[0]
armature_action.use_fake_user = LINK_DATA.use_fake_user
rigutils.add_motion_set_data(armature_action, set_id, set_generation, rl_arm_id=rl_arm_id)
rigutils.set_armature_action_name(armature_action, actor_rig_id, motion_id, LINK_DATA.motion_prefix)
remove_actions.append(motion_rig_action)
else:
actor_rig_action = utils.safe_get_action(actor_rig)
rigutils.add_motion_set_data(motion_rig_action, set_id, set_generation, rl_arm_id=rl_arm_id)
rigutils.set_armature_action_name(motion_rig_action, actor_rig_id, motion_id, LINK_DATA.motion_prefix)
motion_rig_action.use_fake_user = LINK_DATA.use_fake_user
utils.safe_set_action(actor_rig, motion_rig_action)
action_pairs.append((actor_rig_action, motion_rig_action))
rigutils.update_avatar_rig(actor_rig)
# assign motion object shape key actions:
key_actions = rigutils.apply_source_key_actions(actor_rig,
source_actions, copy=True,
motion_id=motion_id,
motion_prefix=LINK_DATA.motion_prefix,
all_matching=True,
set_id=set_id, set_generation=set_generation)
actions = [ p[0] for p in key_actions.values() ]
for action in actions:
action.use_fake_user = LINK_DATA.use_fake_user
# remove unused motion key actions
for obj_action in source_actions["keys"].values():
if obj_action not in actions:
remove_actions.append(obj_action)
# delete imported motion rig and objects
for obj in motion_objects:
utils.delete_mesh_object(obj)
if motion_rig:
utils.delete_armature_object(motion_rig)
# remove old actions
for old_action in remove_actions:
if old_action:
utils.log_info(f"Removing unused Action: {old_action.name}")
bpy.data.actions.remove(old_action)
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"]
utils.log_info(f"Receive Update / Replace: {name} - {objects_to_replace_names}")
self.do_update_replace(name, link_id, fbx_path, character_type, replace_all, objects_to_replace_names)
def do_update_replace(self, name, link_id, fbx_path, character_type, replace_all, 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)
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)
except Exception as e:
utils.log_error(f"Error importing {fbx_path}", e)
# 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)
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:
source_actions = rigutils.find_source_actions(rig_action, rig)
rigutils.apply_source_key_actions(rig, source_actions, all_matching=True, filter=new_objects)
# 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
if not replace_actions:
# remove temp chr actions (motion set)
if temp_rig_action:
rigutils.delete_motion_set(temp_rig_action)
# copy/retarget actions from original rig to the replacement
if rig_action:
source_actions = rigutils.find_source_actions(rig_action, rig)
rigutils.apply_source_armature_action(temp_rig, source_actions)
rigutils.apply_source_key_actions(temp_rig, source_actions, all_matching=True)
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)
# 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="")
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,
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)
if self.mode == "LIGHT":
LINK_SERVICE.do_motion_import(self.link_id, self.filepath, self.character_type)
if self.mode == "MOTION":
LINK_SERVICE.do_motion_import(self.link_id, self.filepath, self.character_type)
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"}