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