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