3811 lines
149 KiB
Python
3811 lines
149 KiB
Python
import bpy #, bpy_extras
|
|
#import bpy_extras.view3d_utils as v3d
|
|
import atexit
|
|
from enum import IntEnum
|
|
import os, socket, time, select, struct, json, copy
|
|
#import subprocess
|
|
from mathutils import Vector, Quaternion, Matrix
|
|
from . import (importer, exporter, bones, geom, colorspace,
|
|
world, rigging, rigutils, drivers, modifiers,
|
|
jsonutils, utils, vars)
|
|
import textwrap
|
|
|
|
BLENDER_PORT = 9334
|
|
UNITY_PORT = 9335
|
|
RL_PORT = 9333
|
|
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
|
|
|
|
class OpCodes(IntEnum):
|
|
NONE = 0
|
|
HELLO = 1
|
|
PING = 2
|
|
STOP = 10
|
|
DISCONNECT = 11
|
|
DEBUG = 15
|
|
NOTIFY = 50
|
|
SAVE = 60
|
|
MORPH = 90
|
|
MORPH_UPDATE = 91
|
|
REPLACE_MESH = 95
|
|
MATERIALS = 96
|
|
CHARACTER = 100
|
|
CHARACTER_UPDATE = 101
|
|
PROP = 102
|
|
PROP_UPDATE = 103
|
|
UPDATE_REPLACE = 108
|
|
RIGIFY = 110
|
|
TEMPLATE = 200
|
|
POSE = 210
|
|
POSE_FRAME = 211
|
|
SEQUENCE = 220
|
|
SEQUENCE_FRAME = 221
|
|
SEQUENCE_END = 222
|
|
SEQUENCE_ACK = 223
|
|
LIGHTS = 230
|
|
CAMERA_SYNC = 231
|
|
FRAME_SYNC = 232
|
|
MOTION = 240
|
|
|
|
|
|
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
|
|
bones: list = None
|
|
skin_meshes: dict = None
|
|
meshes: list = 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
|
|
|
|
def __init__(self, chr_cache):
|
|
self.chr_cache = chr_cache
|
|
self.name = chr_cache.character_name
|
|
self.bones = []
|
|
self.skin_meshes = {}
|
|
self.meshes = []
|
|
self.rig_bones = []
|
|
self.expressions = []
|
|
self.visemes = []
|
|
self.morphs = []
|
|
self.cache = None
|
|
self.alias = []
|
|
self.shape_keys = {}
|
|
return
|
|
|
|
def get_chr_cache(self):
|
|
return self.chr_cache
|
|
|
|
def get_link_id(self):
|
|
chr_cache = self.get_chr_cache()
|
|
if chr_cache:
|
|
return chr_cache.get_link_id()
|
|
return None
|
|
|
|
def get_armature(self):
|
|
chr_cache = self.get_chr_cache()
|
|
if chr_cache:
|
|
return chr_cache.get_armature()
|
|
return None
|
|
|
|
def select(self):
|
|
chr_cache = self.get_chr_cache()
|
|
if chr_cache:
|
|
chr_cache.select_all()
|
|
|
|
def get_type(self):
|
|
"""AVATAR|PROP|NONE"""
|
|
chr_cache = self.get_chr_cache()
|
|
return self.chr_cache_type(chr_cache)
|
|
|
|
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.link_id = link_id
|
|
return
|
|
if link_id not in self.alias and actor_link_id != link_id:
|
|
utils.log_info(f"Assigning actor alias: {chr_cache.character_name}: {link_id}")
|
|
self.alias.append(link_id)
|
|
return
|
|
|
|
@staticmethod
|
|
def find_actor(link_id, search_name=None, search_type=None, context_chr_cache=None):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
link_data = get_link_data()
|
|
|
|
utils.log_detail(f"Looking for LinkActor: {search_name} {link_id} {search_type}")
|
|
actor: LinkActor = None
|
|
|
|
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:
|
|
if chr_cache.is_avatar():
|
|
return "AVATAR"
|
|
else:
|
|
return "PROP"
|
|
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, bones, meshes, expressions, visemes, morphs):
|
|
self.bones = bones
|
|
self.meshes = meshes
|
|
self.expressions = expressions
|
|
self.visemes = self.remap_visemes(visemes)
|
|
self.morphs = morphs
|
|
rig = self.get_armature()
|
|
skin_meshes = {}
|
|
# rename pivot bones
|
|
for i, bone_name in enumerate(self.bones):
|
|
if bone_name == "_Object_Pivot_Node_":
|
|
self.bones[i] = "CC_Base_Pivot"
|
|
#for i, mesh_name in enumerate(meshes):
|
|
# if mesh_name in names:
|
|
# count = names[mesh_name]
|
|
# names[mesh_name] += 1
|
|
# meshes[i] = f"{mesh_name}.{count:03d}"
|
|
# else:
|
|
# names[mesh_name] == 1
|
|
names = {}
|
|
for i, mesh_name in enumerate(meshes):
|
|
obj = None
|
|
for child in rig.children:
|
|
child_source_name = utils.strip_name(child.name)
|
|
if child.type == "MESH" and child_source_name == mesh_name:
|
|
# determine the duplication suffix offset
|
|
if child_source_name in names:
|
|
count = names[child_source_name]
|
|
names[child_source_name] += 1
|
|
mesh_name = f"{child_source_name}.{count:03d}"
|
|
meshes[i] = mesh_name
|
|
else:
|
|
count = utils.get_duplication_suffix(child.name)
|
|
names[child_source_name] = count + 1
|
|
mesh_name = child.name
|
|
meshes[i] = mesh_name
|
|
obj = child
|
|
rot_mode = obj.rotation_mode
|
|
obj.rotation_mode = "QUATERNION"
|
|
skin_meshes[mesh_name] = [obj, obj.location.copy(), obj.rotation_quaternion.copy(), obj.scale.copy()]
|
|
obj.rotation_mode = rot_mode
|
|
self.skin_meshes = skin_meshes
|
|
|
|
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.link_id = new_link_id
|
|
|
|
def ready(self):
|
|
if self.cache and self.get_chr_cache() and self.get_armature():
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
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():
|
|
link_host: str = "localhost"
|
|
link_host_ip: str = "127.0.0.1"
|
|
link_target: str = "BLENDER"
|
|
link_port: int = 9333
|
|
actors: list = []
|
|
# Sequence/Pose Props
|
|
sequence_current_frame: int = 0
|
|
sequence_start_frame: int = 0
|
|
sequence_end_frame: int = 0
|
|
sequence_actors: list = None
|
|
sequence_type: str = None
|
|
#
|
|
preview_shape_keys: bool = True
|
|
preview_skip_frames: bool = False
|
|
# remote props
|
|
remote_app: str = None
|
|
remote_version: str = None
|
|
remote_path: str = None
|
|
remote_exe: str = None
|
|
#
|
|
ack_rate: float = 0.0
|
|
ack_time: float = 0.0
|
|
#
|
|
motion_prefix: str = ""
|
|
use_fake_user: bool = False
|
|
|
|
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):
|
|
self.motion_prefix = prefix.strip()
|
|
self.use_fake_user = fake_user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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_local_data_path():
|
|
local_path = utils.local_path()
|
|
blend_file_name = utils.blend_file_name()
|
|
data_path = ""
|
|
if local_path and blend_file_name:
|
|
data_path = local_path
|
|
return data_path
|
|
|
|
|
|
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):
|
|
"""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
|
|
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.unhide(chr_cache.rig_datalink_rig)
|
|
#utils.log_info(f"Using existing datalink transfer rig: {chr_cache.rig_datalink_rig.name}")
|
|
return chr_cache.rig_datalink_rig
|
|
|
|
no_constraints = True if chr_cache.rigified else False
|
|
|
|
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)
|
|
edit_bone: bpy.types.EditBone
|
|
arm: bpy.types.Armature = datalink_rig.data
|
|
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])
|
|
actor.rig_bones = actor.bones.copy()
|
|
for i, sk_bone_name in enumerate(actor.bones):
|
|
edit_bone = arm.edit_bones.new(sk_bone_name)
|
|
actor.rig_bones[i] = edit_bone.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)
|
|
|
|
# constraint character armature
|
|
l = len(actor.bones)
|
|
if not no_constraints:
|
|
for i, rig_bone_name in enumerate(actor.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)
|
|
else:
|
|
utils.log_warn(f"Could not find bone: {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,
|
|
rig_override=datalink_rig,
|
|
to_original_rig=True)
|
|
|
|
return datalink_rig
|
|
|
|
|
|
def remove_datalink_import_rig(actor: LinkActor):
|
|
if actor:
|
|
chr_cache = actor.get_chr_cache()
|
|
chr_rig = actor.get_armature()
|
|
|
|
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.restore_ik_stretch(actor.ik_store)
|
|
|
|
else:
|
|
# remove all contraints on the character rig
|
|
if utils.object_exists(chr_rig):
|
|
utils.unhide(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
|
|
|
|
#rigging.reset_shape_keys(chr_cache)
|
|
utils.object_mode_to(chr_rig)
|
|
|
|
|
|
def set_actor_expression_weight(objects, expression_name, weight):
|
|
global LINK_DATA
|
|
if objects and LINK_DATA.preview_shape_keys:
|
|
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 reset_action(chr_cache):
|
|
if chr_cache:
|
|
rig = chr_cache.get_armature()
|
|
if rig:
|
|
action = utils.safe_get_action(rig)
|
|
utils.clear_prop_collection(action.fcurves)
|
|
|
|
|
|
def create_fcurves_cache(count, indices, defaults):
|
|
curves = []
|
|
cache = {
|
|
"count": count,
|
|
"indices": indices,
|
|
"curves": curves,
|
|
}
|
|
for i in range(0, indices):
|
|
d = defaults[i]
|
|
cache_data = [d]*(count*2)
|
|
curves.append(cache_data)
|
|
return cache
|
|
|
|
|
|
def get_datalink_rig_action(rig, motion_id=None):
|
|
if not motion_id:
|
|
motion_id = "DataLink"
|
|
rig_id = rigutils.get_rig_id(rig)
|
|
action_name = rigutils.make_armature_action_name(rig_id, motion_id, LINK_DATA.motion_prefix)
|
|
if action_name in bpy.data.actions:
|
|
action = bpy.data.actions[action_name]
|
|
else:
|
|
action = bpy.data.actions.new(action_name)
|
|
utils.safe_set_action(rig, action)
|
|
action.use_fake_user = LINK_DATA.use_fake_user
|
|
return action
|
|
|
|
|
|
def prep_rig(actor: LinkActor, start_frame, end_frame):
|
|
"""Prepares the character rig for keyframing poses from the pose data stream."""
|
|
|
|
if actor and actor.get_chr_cache():
|
|
chr_cache = actor.get_chr_cache()
|
|
rig = actor.get_armature()
|
|
if not rig:
|
|
utils.log_error(f"Actor: {actor.name} invalid rig!")
|
|
return
|
|
objects, none_objects = actor.get_sequence_objects()
|
|
if rig:
|
|
rig_id = rigutils.get_rig_id(rig)
|
|
rl_arm_id = utils.get_rl_object_id(rig)
|
|
utils.log_info(f"Preparing Character Rig: {actor.name} {rig_id} / {len(actor.bones)} bones")
|
|
|
|
# set data
|
|
if LINK_DATA.sequence_type == "POSE":
|
|
motion_id = "Pose"
|
|
else:
|
|
motion_id = "Sequence"
|
|
set_id, set_generation = rigutils.generate_motion_set(rig, motion_id, LINK_DATA.motion_prefix)
|
|
# rig action
|
|
action = get_datalink_rig_action(rig, motion_id)
|
|
rigutils.add_motion_set_data(action, set_id, set_generation, rl_arm_id=rl_arm_id)
|
|
utils.log_info(f"Preparing rig action: {action.name}")
|
|
utils.clear_prop_collection(action.fcurves)
|
|
|
|
# shape key actions
|
|
num_expressions = len(actor.expressions)
|
|
num_visemes = len(actor.visemes)
|
|
if objects:
|
|
for obj in objects:
|
|
obj_id = rigutils.get_action_obj_id(obj)
|
|
action_name = rigutils.make_key_action_name(rig_id, motion_id, obj_id, LINK_DATA.motion_prefix)
|
|
utils.log_info(f"Preparing shape key action: {action_name} / {num_expressions}+{num_visemes} shape keys")
|
|
if action_name in bpy.data.actions:
|
|
action = bpy.data.actions[action_name]
|
|
else:
|
|
action = bpy.data.actions.new(action_name)
|
|
rigutils.add_motion_set_data(action, set_id, set_generation, obj_id=obj_id)
|
|
utils.clear_prop_collection(action.fcurves)
|
|
utils.safe_set_action(obj.data.shape_keys, action)
|
|
action.use_fake_user = LINK_DATA.use_fake_user
|
|
# remove actions from non sequence objects
|
|
for obj in none_objects:
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
|
|
if chr_cache.rigified:
|
|
# disable IK stretch
|
|
actor.ik_store = rigutils.disable_ik_stretch(rig)
|
|
BAKE_BONE_GROUPS = ["FK", "IK", "Special", "Root"] #not Tweak and Extra
|
|
BAKE_BONE_COLLECTIONS = ["Face", #"Face (Primary)", "Face (Secondary)",
|
|
"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"]
|
|
# TODO These bones may need to have their pose reset as they are damped tracked in the rig
|
|
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,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,28]
|
|
if utils.object_mode_to(rig):
|
|
bone: bpy.types.Bone
|
|
pose_bone: bpy.types.PoseBone
|
|
bones.make_bones_visible(rig, collections=BAKE_BONE_COLLECTIONS, layers=BAKE_BONE_LAYERS)
|
|
for pose_bone in rig.pose.bones:
|
|
bone = pose_bone.bone
|
|
bone.select = False
|
|
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
|
|
bone.hide_select = False
|
|
bone.select = True
|
|
pose_bone.rotation_mode = "QUATERNION"
|
|
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
|
|
bone.hide_select = False
|
|
bone.select = True
|
|
#pose_bone.rotation_mode = "QUATERNION"
|
|
|
|
# create keyframe cache for animation sequences
|
|
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
|
|
bone = rig.data.bones[bone_name]
|
|
if bone.select:
|
|
loc_cache = create_fcurves_cache(count, 3, [0,0,0])
|
|
sca_cache = create_fcurves_cache(count, 3, [1,1,1])
|
|
rot_cache = create_fcurves_cache(count, 4, [1,0,0,0])
|
|
bone_cache[bone_name] = {
|
|
"loc": loc_cache,
|
|
"sca": sca_cache,
|
|
"rot": rot_cache,
|
|
}
|
|
|
|
for expression_name in actor.expressions:
|
|
expression_cache[expression_name] = create_fcurves_cache(count, 1, [0])
|
|
|
|
for viseme_name in actor.visemes:
|
|
viseme_cache[viseme_name] = create_fcurves_cache(count, 1, [0])
|
|
|
|
for morph_name in actor.morphs:
|
|
pass
|
|
|
|
actor.set_cache(actor_cache)
|
|
|
|
|
|
def set_frame_range(start, end):
|
|
bpy.data.scenes["Scene"].frame_start = start
|
|
bpy.data.scenes["Scene"].frame_end = end
|
|
|
|
|
|
def set_frame(frame):
|
|
bpy.data.scenes["Scene"].frame_current = frame
|
|
bpy.context.view_layer.update()
|
|
|
|
|
|
def key_frame_pose_visual():
|
|
area = [a for a in bpy.context.screen.areas if a.type=="VIEW_3D"][0]
|
|
with bpy.context.temp_override(area=area):
|
|
bpy.ops.anim.keyframe_insert_menu(type='BUILTIN_KSI_VisualLocRot')
|
|
|
|
|
|
def store_bone_cache_keyframes(actor: LinkActor, frame):
|
|
"""Needs to be called after all constraints have been set and all bones in the pose positioned"""
|
|
|
|
if not actor.cache:
|
|
utils.log_error(f"No actor cache: {actor.name}")
|
|
return
|
|
|
|
rig = actor.get_armature()
|
|
start_frame = actor.cache["start"]
|
|
cache_index = (frame - start_frame) * 2
|
|
bone_cache = actor.cache["bones"]
|
|
for bone_name in bone_cache:
|
|
loc_cache = bone_cache[bone_name]["loc"]
|
|
sca_cache = bone_cache[bone_name]["sca"]
|
|
rot_cache = bone_cache[bone_name]["rot"]
|
|
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()
|
|
for i in range(0, 3):
|
|
curve = loc_cache["curves"][i]
|
|
curve[cache_index] = frame
|
|
curve[cache_index + 1] = loc[i]
|
|
for i in range(0, 3):
|
|
curve = sca_cache["curves"][i]
|
|
curve[cache_index] = frame
|
|
curve[cache_index + 1] = sca[i]
|
|
for i in range(0, 4):
|
|
curve = rot_cache["curves"][i]
|
|
curve[cache_index] = frame
|
|
curve[cache_index + 1] = rot[i]
|
|
|
|
|
|
def store_shape_key_cache_keyframes(actor: LinkActor, frame, expression_weights, viseme_weights, morph_weights):
|
|
if not actor.cache:
|
|
utils.log_error(f"No actor cache: {actor.name}")
|
|
return
|
|
|
|
start_frame = actor.cache["start"]
|
|
cache_index = (frame - start_frame) * 2
|
|
|
|
expression_cache = actor.cache["expressions"]
|
|
for i, expression_name in enumerate(expression_cache):
|
|
curve = expression_cache[expression_name]["curves"][0]
|
|
curve[cache_index] = frame
|
|
curve[cache_index + 1] = expression_weights[i]
|
|
|
|
viseme_cache = actor.cache["visemes"]
|
|
for i, viseme_name in enumerate(viseme_cache):
|
|
curve = viseme_cache[viseme_name]["curves"][0]
|
|
curve[cache_index] = frame
|
|
curve[cache_index + 1] = viseme_weights[i]
|
|
|
|
|
|
def write_sequence_actions(actor: LinkActor, num_frames):
|
|
if actor.cache:
|
|
rig = actor.cache["rig"]
|
|
rig_action = utils.safe_get_action(rig)
|
|
objects, none_objects = actor.get_sequence_objects()
|
|
set_count = num_frames * 2
|
|
|
|
if rig_action:
|
|
utils.clear_prop_collection(rig_action.fcurves)
|
|
bone_cache = actor.cache["bones"]
|
|
for bone_name in bone_cache:
|
|
pose_bone: bpy.types.PoseBone = rig.pose.bones[bone_name]
|
|
loc_cache = bone_cache[bone_name]["loc"]
|
|
sca_cache = bone_cache[bone_name]["sca"]
|
|
rot_cache = bone_cache[bone_name]["rot"]
|
|
fcurve: bpy.types.FCurve
|
|
for i in range(0, 3):
|
|
data_path = pose_bone.path_from_id("location")
|
|
fcurve = rig_action.fcurves.new(data_path, index=i, action_group="Location")
|
|
fcurve.keyframe_points.add(num_frames)
|
|
fcurve.keyframe_points.foreach_set('co', loc_cache["curves"][i][:set_count])
|
|
for i in range(0, 3):
|
|
data_path = pose_bone.path_from_id("scale")
|
|
fcurve = rig_action.fcurves.new(data_path, index=i, action_group="Scale")
|
|
fcurve.keyframe_points.add(num_frames)
|
|
fcurve.keyframe_points.foreach_set('co', sca_cache["curves"][i][:set_count])
|
|
for i in range(0, 4):
|
|
data_path = pose_bone.path_from_id("rotation_quaternion")
|
|
fcurve = rig_action.fcurves.new(data_path, index=i, action_group="Rotation Quaternion")
|
|
fcurve.keyframe_points.add(num_frames)
|
|
fcurve.keyframe_points.foreach_set('co', rot_cache["curves"][i][:set_count])
|
|
|
|
expression_cache = actor.cache["expressions"]
|
|
viseme_cache = actor.cache["visemes"]
|
|
for obj in objects:
|
|
obj_action = utils.safe_get_action(obj.data.shape_keys)
|
|
if obj_action:
|
|
utils.clear_prop_collection(obj_action.fcurves)
|
|
for expression_name in expression_cache:
|
|
if expression_name in obj.data.shape_keys.key_blocks:
|
|
key_cache = expression_cache[expression_name]
|
|
key = obj.data.shape_keys.key_blocks[expression_name]
|
|
data_path = key.path_from_id("value")
|
|
fcurve = obj_action.fcurves.new(data_path, action_group="Expression")
|
|
fcurve.keyframe_points.add(num_frames)
|
|
fcurve.keyframe_points.foreach_set('co', key_cache["curves"][0][:set_count])
|
|
for viseme_name in viseme_cache:
|
|
if viseme_name in obj.data.shape_keys.key_blocks:
|
|
key_cache = viseme_cache[viseme_name]
|
|
key = obj.data.shape_keys.key_blocks[viseme_name]
|
|
data_path = key.path_from_id("value")
|
|
fcurve = obj_action.fcurves.new(data_path, action_group="Viseme")
|
|
fcurve.keyframe_points.add(num_frames)
|
|
fcurve.keyframe_points.foreach_set('co', key_cache["curves"][0][:set_count])
|
|
# remove actions from non sequence objects
|
|
for obj in none_objects:
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
|
|
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)
|
|
|
|
|
|
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
|
|
|
|
def __init__(self):
|
|
global LINK_DATA
|
|
self.link_data = LINK_DATA
|
|
atexit.register(self.service_disconnect)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self):
|
|
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:
|
|
self.server_sock = None
|
|
self.server_sockets = []
|
|
self.is_listening = True
|
|
utils.log_error(f"Unable to start server on TCP *:{BLENDER_PORT}", e)
|
|
|
|
def stop_server(self):
|
|
if self.server_sock:
|
|
utils.log_info(f"Closing Server Socket")
|
|
try:
|
|
self.server_sock.shutdown()
|
|
self.server_sock.close()
|
|
except:
|
|
pass
|
|
self.is_listening = False
|
|
self.server_sock = None
|
|
self.server_sockets = []
|
|
self.server_stopped.emit()
|
|
self.changed.emit()
|
|
|
|
def start_timer(self):
|
|
self.time = time.time()
|
|
if not self.timer:
|
|
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:
|
|
bpy.app.timers.unregister(self.loop)
|
|
except:
|
|
pass
|
|
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
|
|
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
|
|
utils.log_info(f"connecting with data link server on {host}:{port}")
|
|
self.send_hello()
|
|
self.connecting.emit()
|
|
self.changed.emit()
|
|
return True
|
|
except:
|
|
self.client_sock = None
|
|
self.client_sockets = []
|
|
self.is_connected = False
|
|
link_props.connected = False
|
|
self.is_connecting = False
|
|
utils.log_info(f"Client socket connect failed!")
|
|
return False
|
|
else:
|
|
utils.log_info(f"Client already connected!")
|
|
return True
|
|
|
|
def send_hello(self):
|
|
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:],
|
|
}
|
|
utils.log_info(f"Send Hello: {self.local_path}")
|
|
self.send(OpCodes.HELLO, encode_from_json(json_data))
|
|
|
|
def stop_client(self):
|
|
if self.client_sock:
|
|
utils.log_info(f"Closing Client Socket")
|
|
try:
|
|
self.client_sock.shutdown()
|
|
self.client_sock.close()
|
|
except:
|
|
pass
|
|
self.is_connected = False
|
|
self.is_connecting = False
|
|
try:
|
|
link_props = vars.link_props()
|
|
link_props.connected = False
|
|
except:
|
|
pass
|
|
self.client_sock = None
|
|
self.client_sockets = []
|
|
if self.listening:
|
|
self.keepalive_timer = HANDSHAKE_TIMEOUT_S
|
|
self.client_stopped.emit()
|
|
self.changed.emit()
|
|
|
|
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)
|
|
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 sync every frame in sequence
|
|
if op_code == OpCodes.SEQUENCE_FRAME and prefs.datalink_frame_sync:
|
|
self.is_data = True
|
|
return
|
|
if op_code == OpCodes.CHARACTER or op_code == OpCodes.PROP:
|
|
# 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:
|
|
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.DEBUG:
|
|
self.receive_debug(data)
|
|
|
|
##
|
|
#
|
|
|
|
elif op_code == OpCodes.SAVE:
|
|
self.receive_save(data)
|
|
|
|
elif op_code == OpCodes.TEMPLATE:
|
|
self.receive_character_template(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_character_import(data)
|
|
|
|
elif op_code == OpCodes.PROP:
|
|
self.receive_character_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.LIGHTS:
|
|
self.receive_lights(data)
|
|
|
|
elif op_code == OpCodes.CAMERA_SYNC:
|
|
self.receive_camera_sync(data)
|
|
|
|
elif op_code == OpCodes.FRAME_SYNC:
|
|
self.receive_frame_sync(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 service_disconnect(self):
|
|
try:
|
|
self.send(OpCodes.DISCONNECT)
|
|
self.service_recv_disconnected()
|
|
except: ...
|
|
|
|
def service_recv_disconnected(self):
|
|
if CLIENT_ONLY:
|
|
self.stop_timer()
|
|
self.stop_client()
|
|
|
|
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 check_service(self):
|
|
global LINK_SERVICE
|
|
global LINK_DATA
|
|
if not LINK_SERVICE or not LINK_DATA:
|
|
utils.log_error("DataLink service data lost. Due to script reload?")
|
|
utils.log_error("Connection is maintained but actor data has been reset.")
|
|
LINK_SERVICE = self
|
|
LINK_DATA = self.link_data
|
|
LINK_DATA.reset()
|
|
return True
|
|
|
|
def check_paths(self):
|
|
local_path = get_local_data_path()
|
|
if local_path != self.local_path:
|
|
self.local_path = local_path
|
|
self.send_hello()
|
|
|
|
def loop(self):
|
|
try:
|
|
current_time = time.time()
|
|
delta_time = current_time - self.time
|
|
self.time = current_time
|
|
if delta_time > 0:
|
|
rate = 1.0 / delta_time
|
|
self.loop_rate = self.loop_rate * 0.75 + rate * 0.25
|
|
#if self.loop_count % 100 == 0:
|
|
# utils.log_detail(f"LinkServer loop timer rate: {self.loop_rate}")
|
|
self.loop_count += 1
|
|
|
|
self.check_paths()
|
|
|
|
if not self.check_service():
|
|
return None
|
|
|
|
if not self.timer:
|
|
return None
|
|
|
|
if self.is_connected:
|
|
self.ping_timer -= delta_time
|
|
self.keepalive_timer -= delta_time
|
|
|
|
if USE_PING and self.ping_timer <= 0:
|
|
self.send(OpCodes.PING)
|
|
|
|
if USE_KEEPALIVE and self.keepalive_timer <= 0:
|
|
utils.log_info("lost connection!")
|
|
self.service_stop()
|
|
return None
|
|
|
|
elif self.is_listening:
|
|
self.keepalive_timer -= delta_time
|
|
|
|
if USE_KEEPALIVE and self.keepalive_timer <= 0:
|
|
utils.log_info("no connection within time limit!")
|
|
self.service_stop()
|
|
return None
|
|
|
|
# accept incoming connections
|
|
self.accept()
|
|
|
|
# receive client data
|
|
self.recv()
|
|
|
|
# run anything in sequence
|
|
for i in range(0, self.sequence_send_count):
|
|
self.sequence.emit()
|
|
|
|
if self.is_import:
|
|
return 0.5
|
|
else:
|
|
interval = 0.0 if (self.is_data or self.is_sequence) else TIMER_INTERVAL
|
|
return interval
|
|
|
|
except Exception as e:
|
|
utils.log_error("LinkService timer loop crash!", e)
|
|
return TIMER_INTERVAL
|
|
|
|
|
|
def send(self, op_code, binary_data = None):
|
|
try:
|
|
if self.client_sock and (self.is_connected or self.is_connecting):
|
|
data_length = len(binary_data) if binary_data else 0
|
|
header = struct.pack("!II", op_code, data_length)
|
|
data = bytearray()
|
|
data.extend(header)
|
|
if binary_data:
|
|
data.extend(binary_data)
|
|
try:
|
|
self.client_sock.sendall(data)
|
|
except Exception as e:
|
|
utils.log_error("Client socket sendall failed!")
|
|
self.client_lost()
|
|
return
|
|
self.ping_timer = PING_INTERVAL_S
|
|
self.sent.emit()
|
|
|
|
except Exception as e:
|
|
utils.log_error("LinkService send failed!", e)
|
|
|
|
def 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 receive_notify(self, data):
|
|
notify_json = decode_to_json(data)
|
|
update_link_status(notify_json["message"])
|
|
|
|
def receive_save(self, data):
|
|
if bpy.data.filepath:
|
|
utils.log_info("Saving Mainfile")
|
|
bpy.ops.wm.save_mainfile()
|
|
|
|
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_path(self, folder_name, file_name, reuse_folder=False, reuse_file=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_folder)
|
|
export_path = utils.get_unique_file_path(character_export_folder, file_name, reuse=reuse_file)
|
|
return export_path
|
|
|
|
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)
|
|
|
|
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 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}...")
|
|
export_path = self.get_export_path(actor.name, actor.name + ".fbx")
|
|
self.send_notify(f"Exporting: {actor.name}")
|
|
if actor.get_type() == "PROP":
|
|
bpy.ops.cc3.exporter(param="EXPORT_CC3", link_id_override=actor.get_link_id(), filepath=export_path)
|
|
elif actor.get_type() == "AVATAR":
|
|
bpy.ops.cc3.exporter(param="EXPORT_CC3", link_id_override=actor.get_link_id(), filepath=export_path)
|
|
update_link_status(f"Sending: {actor.name}")
|
|
export_data = encode_from_json({
|
|
"path": export_path,
|
|
"name": actor.name,
|
|
"type": actor.get_type(),
|
|
"link_id": actor.get_link_id(),
|
|
})
|
|
if 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}...")
|
|
export_path = self.get_export_path("Morphs", actor.name + "_morph.obj",
|
|
reuse_folder=True, reuse_file=False)
|
|
key_path = self.get_key_path(export_path, ".ObjKey")
|
|
self.send_notify(f"Exporting: {actor.name}")
|
|
state = utils.store_mode_selection_state()
|
|
bpy.ops.cc3.exporter(param="EXPORT_CC3", filepath=export_path)
|
|
update_link_status(f"Sending: {actor.name}")
|
|
export_data = encode_from_json({
|
|
"path": export_path,
|
|
"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 os.path.exists(export_path):
|
|
self.send(OpCodes.MORPH, export_data)
|
|
update_link_status(f"Sent: {actor.name}")
|
|
return True
|
|
return False
|
|
|
|
def obj_export(self, file_path, use_selection=False, use_animation=False, global_scale=100,
|
|
use_vertex_colors=False, use_vertex_groups=False, apply_modifiers=True,
|
|
keep_vertex_order=False, use_materials=False):
|
|
if utils.B330():
|
|
bpy.ops.wm.obj_export(filepath=file_path,
|
|
global_scale=global_scale,
|
|
export_selected_objects=use_selection,
|
|
export_animation=use_animation,
|
|
export_materials=use_materials,
|
|
export_colors=use_vertex_colors,
|
|
export_vertex_groups=use_vertex_groups,
|
|
apply_modifiers=apply_modifiers)
|
|
else:
|
|
bpy.ops.export_scene.obj(filepath=file_path,
|
|
global_scale=global_scale,
|
|
use_selection=use_selection,
|
|
use_materials=use_materials,
|
|
use_animation=use_animation,
|
|
use_vertex_groups=use_vertex_groups,
|
|
use_mesh_modifiers=apply_modifiers,
|
|
keep_vertex_order=keep_vertex_order)
|
|
|
|
def send_replace_mesh(self):
|
|
state = utils.store_mode_selection_state()
|
|
objects = utils.get_selected_meshes()
|
|
# important that character is in the exact same pose on both sides,
|
|
# so make sure the character is on the same frame in the animation.
|
|
self.send_frame_sync()
|
|
count = 0
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
actor = self.get_actor_from_object(obj)
|
|
if actor:
|
|
obj_cache = actor.get_chr_cache().get_object_cache(obj)
|
|
object_name = obj.name
|
|
mesh_name = obj.data.name
|
|
if obj_cache:
|
|
object_name = obj_cache.source_name
|
|
mesh_name = obj_cache.source_name
|
|
export_path = self.get_export_path("Meshes", f"{obj.name}_mesh.obj",
|
|
reuse_folder=True, reuse_file=True)
|
|
utils.set_active_object(obj, deselect_all=True)
|
|
self.obj_export(export_path, use_selection=True, use_vertex_colors=True)
|
|
export_data = encode_from_json({
|
|
"path": export_path,
|
|
"actor_name": actor.name,
|
|
"object_name": object_name,
|
|
"mesh_name": mesh_name,
|
|
"type": actor.get_type(),
|
|
"link_id": actor.get_link_id(),
|
|
})
|
|
self.send(OpCodes.REPLACE_MESH, export_data)
|
|
update_link_status(f"Sent Mesh: {actor.name}")
|
|
count += 1
|
|
|
|
utils.restore_mode_selection_state(state)
|
|
|
|
return count
|
|
|
|
def export_object_material_data(self, context, actor: LinkActor, objects):
|
|
prefs = vars.prefs()
|
|
obj: bpy.types.Object
|
|
|
|
chr_cache = actor.get_chr_cache()
|
|
if chr_cache:
|
|
if prefs.datalink_send_mode == "ACTIVE":
|
|
materials = []
|
|
for obj in objects:
|
|
idx = obj.active_material_index
|
|
if len(obj.material_slots) > idx:
|
|
mat = obj.material_slots[idx].material
|
|
if mat:
|
|
materials.append(mat)
|
|
else:
|
|
materials = None
|
|
export_path = self.get_export_path("Materials", f"{actor.name}.json",
|
|
reuse_folder=True, reuse_file=True)
|
|
export_dir, json_file = os.path.split(export_path)
|
|
json_data = chr_cache.get_json_data()
|
|
if not json_data:
|
|
json_data = jsonutils.generate_character_base_json_data(actor.name)
|
|
exporter.set_character_generation(json_data, chr_cache, actor.name)
|
|
exporter.prep_export(context, chr_cache, actor.name, objects, json_data,
|
|
chr_cache.get_import_dir(), export_dir,
|
|
False, False, False, False, True,
|
|
materials=materials, sync=True, force_bake=True)
|
|
jsonutils.write_json(json_data, export_path)
|
|
export_data = encode_from_json({
|
|
"path": export_path,
|
|
"actor_name": actor.name,
|
|
"type": actor.get_type(),
|
|
"link_id": actor.get_link_id(),
|
|
})
|
|
self.send(OpCodes.MATERIALS, export_data)
|
|
|
|
def send_material_update(self, context):
|
|
state = utils.store_mode_selection_state()
|
|
|
|
selection = self.get_actor_mesh_selection()
|
|
count = 0
|
|
for chr_cache in selection:
|
|
actor = LinkActor(chr_cache)
|
|
meshes = selection[chr_cache]["meshes"]
|
|
armatures = selection[chr_cache]["armatures"]
|
|
if armatures:
|
|
# export material info for whole character
|
|
all_meshes = actor.get_mesh_objects()
|
|
self.export_object_material_data(context, actor, all_meshes)
|
|
count += 1
|
|
elif meshes:
|
|
# export material info just for selected meshes
|
|
self.export_object_material_data(context, actor, meshes)
|
|
count += 1
|
|
|
|
utils.restore_mode_selection_state(state)
|
|
|
|
return count
|
|
|
|
def encode_character_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:
|
|
chr_cache = actor.get_chr_cache()
|
|
bones = []
|
|
|
|
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-"):
|
|
bones.append(pose_bone.name)
|
|
driver_mode = "BONE"
|
|
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)
|
|
if drivers.has_facial_shape_key_bone_drivers(chr_cache):
|
|
driver_mode = "EXPRESSION"
|
|
else:
|
|
driver_mode = "BONE"
|
|
|
|
actor.collect_shape_keys()
|
|
shapes = [key for key in actor.shape_keys]
|
|
|
|
|
|
actor.bones = bones
|
|
actor_data.append({
|
|
"name": actor.name,
|
|
"type": actor.get_type(),
|
|
"link_id": actor.get_link_id(),
|
|
"bones": bones,
|
|
"shapes": shapes,
|
|
"drivers": driver_mode,
|
|
})
|
|
|
|
return encode_from_json(character_template)
|
|
|
|
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:
|
|
data += pack_string(actor.name)
|
|
data += pack_string(actor.get_type())
|
|
data += pack_string(actor.get_link_id())
|
|
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 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)
|
|
|
|
return data
|
|
|
|
def encode_sequence_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 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.rigified:
|
|
if actor.ik_store:
|
|
rigutils.restore_ik_stretch(actor.ik_store)
|
|
|
|
def send_pose(self):
|
|
global LINK_DATA
|
|
|
|
# get actors
|
|
actors = self.get_selected_actors()
|
|
count = 0
|
|
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_character_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)
|
|
count += len(actors)
|
|
return count
|
|
|
|
def send_animation(self):
|
|
return
|
|
|
|
def abort_sequence(self):
|
|
global LINK_DATA
|
|
# 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()
|
|
|
|
def send_sequence(self):
|
|
global LINK_DATA
|
|
|
|
# get actors
|
|
actors = self.get_selected_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_character_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):
|
|
sequence_data = self.encode_sequence_data(LINK_DATA.sequence_actors)
|
|
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)
|
|
ensure_current_frame(frame)
|
|
LINK_DATA.sequence_current_frame = frame
|
|
offset = 8
|
|
actors = []
|
|
for i in range(0, count):
|
|
offset, name = unpack_string(pose_data, offset)
|
|
offset, character_type = unpack_string(pose_data, offset)
|
|
offset, link_id = unpack_string(pose_data, offset)
|
|
actor = LINK_DATA.find_sequence_actor(link_id)
|
|
actor_ready = False
|
|
if actor:
|
|
objects, none_objects = actor.get_sequence_objects()
|
|
rig: bpy.types.Object = actor.get_armature()
|
|
actor_ready = actor.ready()
|
|
is_prop = actor.get_type() == "PROP"
|
|
else:
|
|
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)
|
|
offset += 40
|
|
if rig:
|
|
loc = Vector((tx, ty, tz)) * 0.01
|
|
rot = Quaternion((rw, rx, ry, rz))
|
|
sca = Vector((sx, sy, sz))
|
|
#
|
|
rig.location = Vector((0, 0, 0))
|
|
rot_mode = rig.rotation_mode
|
|
rig.rotation_mode = "QUATERNION"
|
|
rig.rotation_quaternion = 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
|
|
|
|
datalink_rig = None
|
|
if actor:
|
|
if actor_ready:
|
|
actors.append(actor)
|
|
datalink_rig = make_datalink_import_rig(actor)
|
|
else:
|
|
utils.log_error(f"Actor not ready: {name}/ {link_id}")
|
|
else:
|
|
utils.log_error(f"Could not find actor: {name}/ {link_id}")
|
|
|
|
# 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:
|
|
bone_name = actor.rig_bones[i]
|
|
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((sx, sy, sz))
|
|
rot_mode = pose_bone.rotation_mode
|
|
pose_bone.rotation_mode = "QUATERNION"
|
|
pose_bone.rotation_quaternion = rot
|
|
pose_bone.location = loc
|
|
pose_bone.scale = sca
|
|
pose_bone.rotation_mode = rot_mode
|
|
|
|
|
|
# unpack mesh transforms
|
|
num_meshes = struct.unpack_from("!I", pose_data, offset)[0]
|
|
offset += 4
|
|
|
|
# unpack the binary transform data directly into the mesh transform
|
|
for i in range(0, num_meshes):
|
|
tx,ty,tz,rx,ry,rz,rw,sx,sy,sz = struct.unpack_from("!ffffffffff", pose_data, offset)
|
|
offset += 40
|
|
if actor and datalink_rig:
|
|
mesh_name = actor.meshes[i]
|
|
if mesh_name in actor.skin_meshes:
|
|
obj = actor.skin_meshes[mesh_name][0]
|
|
actor.skin_meshes[mesh_name][1] = Vector((tx, ty, tz)) * 0.01
|
|
actor.skin_meshes[mesh_name][2] = Quaternion((rw, rx, ry, rz))
|
|
actor.skin_meshes[mesh_name][3] = Vector((sx, sy, sz))
|
|
|
|
|
|
# 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:
|
|
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:
|
|
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 actor_ready:
|
|
store_shape_key_cache_keyframes(actor, frame, expression_weights, viseme_weights, morph_weights)
|
|
|
|
return actors
|
|
|
|
def reposition_prop_meshes(self, actors):
|
|
actor: LinkActor
|
|
for actor in actors:
|
|
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, rig.scale)
|
|
|
|
def find_link_id(self, link_id: str):
|
|
for obj in bpy.data.objects:
|
|
if "link_id" in obj and obj["link_id"] == link_id:
|
|
return obj
|
|
return None
|
|
|
|
def add_spot_light(self, name, container):
|
|
bpy.ops.object.light_add(type="SPOT")
|
|
light = utils.get_active_object()
|
|
light.name = name
|
|
light.data.name = name
|
|
utils.set_ccic_id(light)
|
|
light.parent = container
|
|
light.matrix_parent_inverse = container.matrix_world.inverted()
|
|
return light
|
|
|
|
def add_area_light(self, name, container):
|
|
bpy.ops.object.light_add(type="AREA")
|
|
light = utils.get_active_object()
|
|
light.name = name
|
|
light.data.name = name
|
|
utils.set_ccic_id(light)
|
|
light.parent = container
|
|
light.matrix_parent_inverse = container.matrix_world.inverted()
|
|
return light
|
|
|
|
def add_point_light(self, name, container):
|
|
bpy.ops.object.light_add(type="POINT")
|
|
light = utils.get_active_object()
|
|
light.name = name
|
|
light.data.name = name
|
|
utils.set_ccic_id(light)
|
|
light.parent = container
|
|
light.matrix_parent_inverse = container.matrix_world.inverted()
|
|
return light
|
|
|
|
def add_dir_light(self, name, container):
|
|
bpy.ops.object.light_add(type="SUN")
|
|
light = utils.get_active_object()
|
|
light.name = name
|
|
light.data.name = name
|
|
utils.set_ccic_id(light)
|
|
light.parent = container
|
|
light.matrix_parent_inverse = container.matrix_world.inverted()
|
|
return light
|
|
|
|
def add_light_container(self):
|
|
container = None
|
|
for obj in bpy.data.objects:
|
|
if obj.type == "EMPTY" and "Lighting" in obj.name and utils.has_ccic_id(obj):
|
|
container = obj
|
|
if not container:
|
|
bpy.ops.object.empty_add(type="PLAIN_AXES", radius=0.01)
|
|
container = utils.get_active_object()
|
|
container.name = "Lighting"
|
|
utils.set_ccic_id(container)
|
|
children = utils.get_child_objects(container)
|
|
for child in children:
|
|
if utils.has_ccic_id(child) and child.type == "LIGHT":
|
|
utils.delete_object_tree(child)
|
|
return container
|
|
|
|
def decode_lights_data(self, data):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
lights_data = decode_to_json(data)
|
|
|
|
RECTANGULAR_AS_AREA = True
|
|
TUBE_AS_AREA = True
|
|
|
|
ambient_color = utils.array_to_color(lights_data["ambient_color"])
|
|
ambient_strength = 0.125 + ambient_color.v
|
|
|
|
utils.object_mode()
|
|
|
|
container = self.add_light_container()
|
|
|
|
for light_data in lights_data["lights"]:
|
|
light_type = light_data["type"]
|
|
if light_type == "DIR":
|
|
light_type = "SUN"
|
|
is_tube = light_data["is_tube"]
|
|
is_rectangle = light_data["is_rectangle"]
|
|
if TUBE_AS_AREA and is_tube:
|
|
light_type = "AREA"
|
|
if RECTANGULAR_AS_AREA and is_rectangle:
|
|
light_type = "AREA"
|
|
|
|
light = self.find_link_id(light_data["link_id"])
|
|
if light and (light.type != "LIGHT" or light.data.type != light_type):
|
|
utils.delete_light_object(light)
|
|
light = None
|
|
if not light:
|
|
if light_type == "AREA":
|
|
light = self.add_area_light(light_data["name"], container)
|
|
elif light_type == "POINT":
|
|
light = self.add_point_light(light_data["name"], container)
|
|
elif light_type == "SUN":
|
|
light = self.add_dir_light(light_data["name"], container)
|
|
else:
|
|
light = self.add_spot_light(light_data["name"], container)
|
|
light["link_id"] = light_data["link_id"]
|
|
|
|
loc = utils.array_to_vector(light_data["loc"]) / 100
|
|
light.location = loc
|
|
rot_mode = light.rotation_mode
|
|
light.rotation_mode = "QUATERNION"
|
|
light.rotation_quaternion = utils.array_to_quaternion(light_data["rot"])
|
|
light.rotation_mode = rot_mode
|
|
light.scale = utils.array_to_vector(light_data["sca"])
|
|
desat_ambient_color = ambient_color.copy()
|
|
desat_ambient_color.s *= 0.2
|
|
light.data.color = utils.color_filter(utils.array_to_color(light_data["color"]), desat_ambient_color)
|
|
|
|
# range and falloff modifiers
|
|
fm = 2 - pow(light_data["falloff"] / 100, 2)
|
|
mult = light_data["multiplier"]
|
|
r = light_data["range"] / 5000
|
|
P = 4
|
|
range_curve = 1.0 - 1.0/pow((r + 1.0),P)
|
|
S = 1.0
|
|
E = 1.0
|
|
# area energy modifier
|
|
if light_data["is_tube"]:
|
|
m = 0.75 + light_data["tube_length"] * light_data["tube_radius"] / 10000
|
|
E *= m
|
|
elif light_data["is_rectangle"]:
|
|
a = 0.75 + light_data["rect"][0] * light_data["rect"][1] / 10000
|
|
E *= a
|
|
|
|
# non-inverse square light modifier
|
|
# CC4 lighting is pointed at the center so we can guess the linear->square light difference
|
|
if self.is_cc() and not light_data["inverse_square"]:
|
|
dist = max(loc.magnitude * 0.4, 0.01)
|
|
inv_linear_atten = 1 / dist
|
|
inv_square_atten = 1 / (dist * dist)
|
|
E *= max(1, inv_linear_atten / inv_square_atten)
|
|
|
|
if light_type == "SUN":
|
|
#light.data.energy = 450 * pow(light_data["multiplier"]/20, 2) * fm * mmod
|
|
light.data.energy = 3 * mult
|
|
elif light_type == "SPOT":
|
|
light.data.energy = 25 * mult * fm * range_curve * E
|
|
elif light_type == "POINT":
|
|
light.data.energy = 15 * mult * fm * range_curve * E
|
|
elif light_type == "AREA":
|
|
light.data.energy = 13 * mult * fm * range_curve * E
|
|
if light_type != "SUN":
|
|
light.data.use_custom_distance = True
|
|
light.data.cutoff_distance = light_data["range"] / 100
|
|
if light_type == "SPOT":
|
|
light.data.spot_size = light_data["angle"] * 0.01745329
|
|
light.data.spot_blend = 0.01 * light_data["falloff"] * (0.5 + 0.01 * light_data["attenuation"] * 0.5)
|
|
if light_data["is_rectangle"]:
|
|
light.data.shadow_soft_size = (light_data["rect"][0] + light_data["rect"][0]) / 200
|
|
elif light_data["is_tube"]:
|
|
light.data.shadow_soft_size = light_data["tube_radius"] / 100
|
|
if light_type == "AREA":
|
|
if light_data["is_rectangle"]:
|
|
light.data.shape = "RECTANGLE"
|
|
light.data.size = S * light_data["rect"][0] / 100
|
|
light.data.size_y = S * light_data["rect"][1] / 100
|
|
elif light_data["is_tube"]:
|
|
light.data.shape = "ELLIPSE"
|
|
light.data.size = S * 10 * max(1, light_data["tube_length"]) / 100
|
|
light.data.size_y = S * light_data["tube_radius"] / 100
|
|
light.data.use_shadow = light_data["cast_shadow"]
|
|
if light_data["cast_shadow"]:
|
|
if utils.B420():
|
|
light.data.use_shadow_jitter = True
|
|
else:
|
|
if light_type != "SUN":
|
|
light.data.shadow_buffer_clip_start = 0.0025
|
|
light.data.shadow_buffer_bias = 1.0
|
|
light.data.use_contact_shadow = True
|
|
light.data.contact_shadow_distance = 0.1
|
|
light.data.contact_shadow_bias = 0.03
|
|
light.data.contact_shadow_thickness = 0.001
|
|
utils.hide(light, not light_data["active"])
|
|
|
|
# clean up lights not found in scene
|
|
for obj in bpy.data.objects:
|
|
if obj.type == "LIGHT":
|
|
if "link_id" in obj 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.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 = 1.0
|
|
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 High 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_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_lights(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_lights_data(data)
|
|
utils.restore_mode_selection_state(state)
|
|
|
|
|
|
# Camera
|
|
#
|
|
|
|
def get_view_camera_data(self):
|
|
view_space: bpy.types.Space
|
|
r3d: bpy.types.RegionView3D
|
|
view_space, r3d = utils.get_region_3d()
|
|
t = r3d.view_location
|
|
r = r3d.view_rotation
|
|
d = r3d.view_distance
|
|
dir = Vector((0,0,-1))
|
|
dir.rotate(r)
|
|
loc: Vector = t - (dir * d)
|
|
lens = view_space.lens
|
|
data = {
|
|
"link_id": "0",
|
|
"name": "Viewport Camera",
|
|
"loc": [loc.x, loc.y, loc.z],
|
|
"rot": [r.x, r.y, r.z, r.w],
|
|
"sca": [1, 1, 1],
|
|
"focal_length": lens,
|
|
}
|
|
return data
|
|
|
|
def get_view_camera_pivot(self):
|
|
view_space, r3d = utils.get_region_3d()
|
|
t = r3d.view_location
|
|
return t
|
|
|
|
def send_camera_sync(self):
|
|
#
|
|
update_link_status(f"Synchronizing View Camera")
|
|
self.send_notify(f"Sync View Camera")
|
|
camera_data = self.get_view_camera_data()
|
|
pivot = self.get_view_camera_pivot()
|
|
data = {
|
|
"view_camera": camera_data,
|
|
"pivot": [pivot.x, pivot.y, pivot.z],
|
|
}
|
|
self.send(OpCodes.CAMERA_SYNC, encode_from_json(data))
|
|
|
|
def decode_camera_sync_data(self, data):
|
|
data = decode_to_json(data)
|
|
camera_data = data["view_camera"]
|
|
pivot = utils.array_to_vector(data["pivot"]) / 100
|
|
view_space, r3d = utils.get_region_3d()
|
|
loc = utils.array_to_vector(camera_data["loc"]) / 100
|
|
rot = utils.array_to_quaternion(camera_data["rot"])
|
|
to_pivot = pivot - loc
|
|
dir = Vector((0,0,-1))
|
|
dir.rotate(rot)
|
|
dist = to_pivot.dot(dir)
|
|
if dist <= 0:
|
|
dist = 1.0
|
|
r3d.view_location = loc + dir * dist
|
|
r3d.view_rotation = rot
|
|
r3d.view_distance = dist
|
|
view_space.lens = camera_data["focal_length"] * 1.625
|
|
|
|
def receive_camera_sync(self, data):
|
|
update_link_status(f"Camera Data Receveived")
|
|
self.decode_camera_sync_data(data)
|
|
|
|
def send_frame_sync(self):
|
|
update_link_status(f"Sending Frame Sync")
|
|
fps = bpy.context.scene.render.fps
|
|
start_frame = BFA(bpy.context.scene.frame_start)
|
|
end_frame = BFA(bpy.context.scene.frame_end)
|
|
current_frame = BFA(bpy.context.scene.frame_current)
|
|
start_time = start_frame / fps
|
|
end_time = end_frame / fps
|
|
current_time = current_frame / fps
|
|
frame_data = {
|
|
"fps": fps,
|
|
"start_time": start_time,
|
|
"end_time": end_time,
|
|
"current_time": current_time,
|
|
"start_frame": start_frame,
|
|
"end_frame": end_frame,
|
|
"current_frame": current_frame,
|
|
}
|
|
self.send(OpCodes.FRAME_SYNC, encode_from_json(frame_data))
|
|
|
|
def receive_frame_sync(self, data):
|
|
update_link_status(f"Frame Sync Receveived")
|
|
frame_data = decode_to_json(data)
|
|
start_frame = frame_data["start_frame"]
|
|
end_frame = frame_data["end_frame"]
|
|
current_frame = frame_data["current_frame"]
|
|
bpy.context.scene.frame_start = RLFA(start_frame)
|
|
bpy.context.scene.frame_end = RLFA(end_frame)
|
|
bpy.context.scene.frame_current = RLFA(current_frame)
|
|
|
|
|
|
# Character Pose
|
|
#
|
|
|
|
def receive_character_template(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:
|
|
actor.set_template(actor_data["bones"],
|
|
actor_data["meshes"],
|
|
actor_data["expressions"],
|
|
actor_data["visemes"],
|
|
actor_data["morphs"])
|
|
utils.log_info(f"Preparing Actor: {actor.name} ({actor.get_link_id()})")
|
|
prep_rig(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 = []
|
|
actor: LinkActor
|
|
for actor in actors:
|
|
rig = actor.get_armature()
|
|
if rig:
|
|
rigs.append(rig)
|
|
if rigs:
|
|
all_selected = True
|
|
if not (utils.get_mode() == "POSE" and len(bpy.context.selected_objects) == len(rigs)):
|
|
all_selected = False
|
|
else:
|
|
for rig in rigs:
|
|
if rig not in bpy.context.selected_objects:
|
|
all_selected = False
|
|
break
|
|
if not all_selected:
|
|
utils.object_mode()
|
|
utils.clear_selected_objects()
|
|
utils.try_select_objects(rigs, True, make_active=True)
|
|
utils.set_mode("POSE")
|
|
return rigs
|
|
|
|
def receive_pose(self, data):
|
|
props = vars.props()
|
|
global LINK_DATA
|
|
|
|
props.validate_and_clean_up()
|
|
|
|
# decode pose data
|
|
json_data = decode_to_json(data)
|
|
start_frame = RLFA(json_data["start_frame"])
|
|
end_frame = RLFA(json_data["end_frame"])
|
|
frame = RLFA(json_data["frame"])
|
|
motion_prefix = json_data.get("motion_prefix", "")
|
|
use_fake_user = json_data.get("use_fake_user", False)
|
|
LINK_DATA.sequence_start_frame = frame
|
|
LINK_DATA.sequence_end_frame = frame
|
|
LINK_DATA.sequence_current_frame = frame
|
|
LINK_DATA.set_action_settings(motion_prefix, use_fake_user)
|
|
utils.log_info(f"Receive Pose: {frame}")
|
|
|
|
# fetch actors
|
|
actors_data = json_data["actors"]
|
|
actors = []
|
|
for actor_data in actors_data:
|
|
name = actor_data["name"]
|
|
character_type = actor_data["type"]
|
|
link_id = actor_data["link_id"]
|
|
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
|
|
if actor:
|
|
actors.append(actor)
|
|
|
|
# set pose frame
|
|
update_link_status(f"Receiving Pose Frame: {frame}")
|
|
LINK_DATA.sequence_actors = actors
|
|
LINK_DATA.sequence_type = "POSE"
|
|
bpy.ops.screen.animation_cancel()
|
|
set_frame_range(start_frame, end_frame)
|
|
set_frame(frame)
|
|
|
|
def receive_pose_frame(self, data):
|
|
global LINK_DATA
|
|
|
|
state = utils.store_mode_selection_state()
|
|
|
|
# decode and cache pose
|
|
frame = self.decode_pose_frame_header(data)
|
|
utils.log_info(f"Receive Pose Frame: {frame}")
|
|
actors = self.decode_pose_frame_data(data)
|
|
|
|
# force recalculate all transforms
|
|
bpy.context.view_layer.update()
|
|
|
|
self.reposition_prop_meshes(actors)
|
|
|
|
# store frame data
|
|
update_link_status(f"Pose Frame: {frame}")
|
|
rigs = self.select_actor_rigs(actors)
|
|
if rigs:
|
|
actor: LinkActor
|
|
for actor in actors:
|
|
if actor.ready():
|
|
store_bone_cache_keyframes(actor, frame)
|
|
|
|
# write pose action
|
|
for actor in actors:
|
|
if actor.ready():
|
|
remove_datalink_import_rig(actor)
|
|
write_sequence_actions(actor, 1)
|
|
pass
|
|
|
|
rig = actor.get_armature()
|
|
if actor.get_type() == "PROP":
|
|
rigutils.update_prop_rig(rig)
|
|
elif actor.get_type() == "AVATAR":
|
|
rigutils.update_avatar_rig(rig)
|
|
|
|
# finish
|
|
LINK_DATA.sequence_actors = None
|
|
LINK_DATA.sequence_type = None
|
|
bpy.context.scene.frame_current = frame
|
|
utils.restore_mode_selection_state(state)
|
|
|
|
def receive_sequence(self, data):
|
|
props = vars.props()
|
|
global LINK_DATA
|
|
|
|
props.validate_and_clean_up()
|
|
|
|
# decode sequence data
|
|
json_data = decode_to_json(data)
|
|
start_frame = RLFA(json_data["start_frame"])
|
|
end_frame = RLFA(json_data["end_frame"])
|
|
motion_prefix = json_data.get("motion_prefix", "")
|
|
use_fake_user = json_data.get("use_fake_user", False)
|
|
LINK_DATA.sequence_start_frame = start_frame
|
|
LINK_DATA.sequence_end_frame = end_frame
|
|
LINK_DATA.sequence_current_frame = start_frame
|
|
LINK_DATA.set_action_settings(motion_prefix, use_fake_user)
|
|
num_frames = end_frame - start_frame + 1
|
|
utils.log_info(f"Receive Sequence: {start_frame} to {end_frame}, {num_frames} frames")
|
|
|
|
# fetch sequence actors
|
|
actors_data = json_data["actors"]
|
|
actors = []
|
|
for actor_data in actors_data:
|
|
name = actor_data["name"]
|
|
character_type = actor_data["type"]
|
|
link_id = actor_data["link_id"]
|
|
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
|
|
if actor:
|
|
actors.append(actor)
|
|
LINK_DATA.sequence_actors = actors
|
|
LINK_DATA.sequence_type = "SEQUENCE"
|
|
|
|
# update scene range
|
|
update_link_status(f"Receiving Live Sequence: {num_frames} frames")
|
|
bpy.ops.screen.animation_cancel()
|
|
set_frame_range(LINK_DATA.sequence_start_frame, LINK_DATA.sequence_end_frame)
|
|
set_frame(LINK_DATA.sequence_start_frame)
|
|
|
|
# start the sequence
|
|
self.start_sequence()
|
|
|
|
def receive_sequence_frame(self, data):
|
|
global LINK_DATA
|
|
|
|
# decode and cache pose
|
|
frame = self.decode_pose_frame_header(data)
|
|
utils.log_detail(f"Receive Sequence Frame: {frame}")
|
|
actors = self.decode_pose_frame_data(data)
|
|
|
|
# force recalculate all transforms
|
|
bpy.context.view_layer.update()
|
|
|
|
self.reposition_prop_meshes(actors)
|
|
|
|
# store frame data
|
|
update_link_status(f"Sequence Frame: {LINK_DATA.sequence_current_frame}")
|
|
rigs = self.select_actor_rigs(actors)
|
|
if rigs:
|
|
for actor in actors:
|
|
if actor.ready():
|
|
store_bone_cache_keyframes(actor, frame)
|
|
|
|
# send sequence frame ack
|
|
self.send_sequence_ack(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"])
|
|
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
|
|
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")
|
|
|
|
# write actions
|
|
for actor in actors:
|
|
remove_datalink_import_rig(actor)
|
|
write_sequence_actions(actor, num_frames)
|
|
rig = actor.get_armature()
|
|
if actor.get_type() == "PROP":
|
|
rigutils.update_prop_rig(rig)
|
|
elif actor.get_type() == "AVATAR":
|
|
rigutils.update_avatar_rig(rig)
|
|
|
|
# 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
|
|
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 receive_character_import(self, data):
|
|
props = vars.props()
|
|
global LINK_DATA
|
|
|
|
props.validate_and_clean_up()
|
|
|
|
# decode character import data
|
|
json_data = decode_to_json(data)
|
|
fbx_path = json_data["path"]
|
|
name = json_data["name"]
|
|
character_type = json_data["type"]
|
|
link_id = json_data["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)
|
|
|
|
utils.log_info(f"Receive Character Import: {name} / {link_id} / {fbx_path}")
|
|
|
|
if not os.path.exists(fbx_path):
|
|
update_link_status(f"Invalid Import Path!")
|
|
return
|
|
|
|
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
|
|
if actor:
|
|
update_link_status(f"Character: {name} exists!")
|
|
utils.log_info(f"Actor {name} ({link_id}) already exists!")
|
|
bpy.ops.ccic.link_confirm_dialog("INVOKE_DEFAULT",
|
|
message=f"Character {name} already exists in the scene. Do you want to replace the character?",
|
|
mode="REPLACE",
|
|
name=name,
|
|
filepath=fbx_path,
|
|
link_id=link_id,
|
|
character_type=character_type)
|
|
return
|
|
|
|
update_link_status(f"Receving Character Import: {name}")
|
|
self.do_character_import(fbx_path, link_id, save_after_import)
|
|
|
|
def do_character_import(self, fbx_path, link_id, save_after_import):
|
|
try:
|
|
bpy.ops.cc3.importer(param="IMPORT", filepath=fbx_path, link_id=link_id,
|
|
zoom=False, no_rigify=True,
|
|
motion_prefix=LINK_DATA.motion_prefix,
|
|
use_fake_user=LINK_DATA.use_fake_user)
|
|
except:
|
|
utils.log_error(f"Error importing {fbx_path}")
|
|
return
|
|
actor = LinkActor.find_actor(link_id)
|
|
# props have big ugly bones, so show them as wires
|
|
if actor and actor.get_type() == "PROP":
|
|
arm = actor.get_armature()
|
|
#rigutils.custom_prop_rig(arm)
|
|
#rigutils.de_pivot(actor.get_chr_cache())
|
|
elif actor and actor.get_type() == "AVATAR":
|
|
if actor.get_chr_cache().is_non_standard():
|
|
arm = actor.get_armature()
|
|
#rigutils.custom_avatar_rig(arm)
|
|
update_link_status(f"Character Imported: {actor.name}")
|
|
if save_after_import:
|
|
self.receive_save()
|
|
|
|
def receive_motion_import(self, data):
|
|
props = vars.props()
|
|
global LINK_DATA
|
|
|
|
props.validate_and_clean_up()
|
|
|
|
# decode character import data
|
|
json_data = decode_to_json(data)
|
|
fbx_path = json_data["path"]
|
|
name = json_data["name"]
|
|
character_type = json_data["type"]
|
|
link_id = json_data["link_id"]
|
|
start_frame = RLFA(json_data["start_frame"])
|
|
end_frame = RLFA(json_data["end_frame"])
|
|
frame = RLFA(json_data["frame"])
|
|
motion_prefix = json_data.get("motion_prefix", "")
|
|
use_fake_user = json_data.get("use_fake_user", False)
|
|
LINK_DATA.sequence_start_frame = start_frame
|
|
LINK_DATA.sequence_end_frame = end_frame
|
|
LINK_DATA.sequence_current_frame = frame
|
|
LINK_DATA.set_action_settings(motion_prefix, use_fake_user)
|
|
num_frames = end_frame - start_frame + 1
|
|
utils.log_info(f"Receive Motion Import: {name} / {link_id} / {fbx_path}")
|
|
utils.log_info(f"Motion Range: {start_frame} to {end_frame}, {num_frames} frames")
|
|
|
|
# update scene range
|
|
bpy.ops.screen.animation_cancel()
|
|
set_frame_range(LINK_DATA.sequence_start_frame, LINK_DATA.sequence_end_frame)
|
|
set_frame(LINK_DATA.sequence_start_frame)
|
|
bpy.context.scene.frame_current = frame
|
|
|
|
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
|
|
if not actor:
|
|
update_link_status(f"Character: {name} not found!")
|
|
utils.log_info(f"Actor {name} ({link_id}) not found!")
|
|
return
|
|
|
|
update_link_status(f"Receving Motion Import: {name}")
|
|
if os.path.exists(fbx_path):
|
|
#try:
|
|
bpy.ops.cc3.anim_importer(filepath=fbx_path, remove_meshes=False,
|
|
remove_materials_images=True, remove_shape_keys=False,
|
|
motion_prefix=LINK_DATA.motion_prefix,
|
|
use_fake_user=LINK_DATA.use_fake_user)
|
|
motion_rig = utils.get_active_object()
|
|
if motion_rig:
|
|
self.replace_actor_motion(actor, motion_rig)
|
|
#except:
|
|
# utils.log_error(f"Error importing motion {fbx_path}")
|
|
# return
|
|
update_link_status(f"Motion Imported: {actor.name}")
|
|
else:
|
|
update_link_status(f"Motion Import Failed!: {actor.name}")
|
|
|
|
def replace_actor_motion(self, actor: LinkActor, motion_rig):
|
|
prefs = vars.prefs()
|
|
|
|
if actor and motion_rig:
|
|
motion_rig_action = utils.safe_get_action(motion_rig)
|
|
motion_objects = utils.get_child_objects(motion_rig)
|
|
motion_id = rigutils.get_action_motion_id(motion_rig_action)
|
|
utils.log_info(f"Replacing Actor Motion:")
|
|
utils.log_indent()
|
|
utils.log_info(f"Motion rig action: {motion_rig_action.name}")
|
|
# fetch all associated actions...
|
|
source_actions = rigutils.find_source_actions(motion_rig_action, motion_rig)
|
|
# fetch actor rig
|
|
actor_rig = actor.get_armature()
|
|
chr_cache = actor.get_chr_cache()
|
|
actor_rig_id = rigutils.get_rig_id(actor_rig)
|
|
rl_arm_id = utils.get_rl_object_id(actor_rig)
|
|
motion_id = rigutils.get_unique_set_motion_id(actor_rig_id, motion_id, LINK_DATA.motion_prefix)
|
|
# generate new action set data
|
|
set_id, set_generation = rigutils.generate_motion_set(actor_rig, motion_id, LINK_DATA.motion_prefix)
|
|
remove_actions = []
|
|
if actor_rig:
|
|
if actor.get_type() == "PROP":
|
|
# if it's a prop retarget the animation (or copy the rest pose):
|
|
# props have no bind pose so the rest pose is the first frame of
|
|
# the animation, which changes with every new animation import...
|
|
if prefs.datalink_retarget_prop_actions:
|
|
action = get_datalink_rig_action(actor_rig, motion_id)
|
|
rigutils.add_motion_set_data(action, set_id, set_generation, rl_arm_id=rl_arm_id)
|
|
update_link_status(f"Retargeting Motion...")
|
|
armature_action = rigutils.bake_rig_action_from_source(motion_rig, actor_rig)
|
|
armature_action.use_fake_user = LINK_DATA.use_fake_user
|
|
remove_actions.append(motion_rig_action)
|
|
else:
|
|
rigutils.add_motion_set_data(motion_rig_action, set_id, set_generation, rl_arm_id=rl_arm_id)
|
|
rigutils.set_armature_action_name(motion_rig_action, actor_rig_id, motion_id, LINK_DATA.motion_prefix)
|
|
motion_rig_action.use_fake_user = LINK_DATA.use_fake_user
|
|
rigutils.copy_rest_pose(motion_rig, actor_rig)
|
|
utils.safe_set_action(actor_rig, motion_rig_action)
|
|
rigutils.update_prop_rig(actor_rig)
|
|
else: # Avatar
|
|
if chr_cache.rigified:
|
|
update_link_status(f"Retargeting Motion...")
|
|
armature_action = rigging.adv_bake_retarget_to_rigify(None, chr_cache, motion_rig, motion_rig_action)[0]
|
|
armature_action.use_fake_user = LINK_DATA.use_fake_user
|
|
rigutils.add_motion_set_data(armature_action, set_id, set_generation, rl_arm_id=rl_arm_id)
|
|
rigutils.set_armature_action_name(armature_action, actor_rig_id, motion_id, LINK_DATA.motion_prefix)
|
|
remove_actions.append(motion_rig_action)
|
|
else:
|
|
rigutils.add_motion_set_data(motion_rig_action, set_id, set_generation, rl_arm_id=rl_arm_id)
|
|
rigutils.set_armature_action_name(motion_rig_action, actor_rig_id, motion_id, LINK_DATA.motion_prefix)
|
|
motion_rig_action.use_fake_user = LINK_DATA.use_fake_user
|
|
utils.safe_set_action(actor_rig, motion_rig_action)
|
|
rigutils.update_avatar_rig(actor_rig)
|
|
# assign motion object shape key actions:
|
|
key_actions = rigutils.apply_source_key_actions(actor_rig,
|
|
source_actions, copy=True,
|
|
motion_id=motion_id,
|
|
motion_prefix=LINK_DATA.motion_prefix,
|
|
all_matching=True,
|
|
set_id=set_id, set_generation=set_generation)
|
|
for action in key_actions.values():
|
|
action.use_fake_user = LINK_DATA.use_fake_user
|
|
# remove unused motion key actions
|
|
for obj_action in source_actions["keys"].values():
|
|
if obj_action not in key_actions.values():
|
|
remove_actions.append(obj_action)
|
|
# delete imported motion rig and objects
|
|
for obj in motion_objects:
|
|
utils.delete_mesh_object(obj)
|
|
if motion_rig:
|
|
utils.delete_armature_object(motion_rig)
|
|
# remove old actions
|
|
for old_action in remove_actions:
|
|
if old_action:
|
|
utils.log_info(f"Removing unused Action: {old_action.name}")
|
|
bpy.data.actions.remove(old_action)
|
|
utils.log_recess()
|
|
|
|
def receive_actor_update(self, data):
|
|
props = vars.props()
|
|
global LINK_DATA
|
|
|
|
props.validate_and_clean_up()
|
|
|
|
# decode character update
|
|
json_data = decode_to_json(data)
|
|
old_name = json_data["old_name"]
|
|
old_link_id = json_data["old_link_id"]
|
|
character_type = json_data["type"]
|
|
new_name = json_data["new_name"]
|
|
new_link_id = json_data["new_link_id"]
|
|
utils.log_info(f"Receive Character Update: {old_name} -> {new_name} / {old_link_id} -> {new_link_id}")
|
|
|
|
# update character data
|
|
actor = LinkActor.find_actor(old_link_id, search_name=old_name, search_type=character_type)
|
|
utils.log_info(f"Updating Actor: {actor.name} {actor.get_link_id()}")
|
|
actor.update_name(new_name)
|
|
actor.update_link_id(new_link_id)
|
|
|
|
def receive_morph(self, data, update=False):
|
|
props = vars.props()
|
|
global LINK_DATA
|
|
|
|
props.validate_and_clean_up()
|
|
|
|
# decode receive morph
|
|
json_data = decode_to_json(data)
|
|
obj_path = json_data["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:
|
|
bpy.ops.cc3.importer(param="IMPORT", filepath=obj_path, link_id=link_id)
|
|
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["path"]
|
|
name = json_data["name"]
|
|
character_type = json_data["type"]
|
|
link_id = json_data["link_id"]
|
|
replace_all = json_data["replace"]
|
|
objects_to_replace_names = json_data["objects"]
|
|
utils.log_info(f"Receive Update / Replace: {name} - {objects_to_replace_names}")
|
|
|
|
self.do_update_replace(name, link_id, fbx_path, character_type, replace_all, objects_to_replace_names)
|
|
|
|
def do_update_replace(self, name, link_id, fbx_path, character_type, replace_all, objects_to_replace_names=None, replace_actions=False):
|
|
props = vars.props()
|
|
global LINK_DATA
|
|
context_chr_cache = props.get_context_character_cache()
|
|
|
|
process_only = ""
|
|
if not replace_all and objects_to_replace_names:
|
|
for n in objects_to_replace_names:
|
|
if process_only:
|
|
process_only += "|"
|
|
process_only += n
|
|
|
|
# import character assign new link_id
|
|
temp_link_id = utils.generate_random_id(20)
|
|
utils.log_info(f"Importing replacement with temp link_id: {temp_link_id}")
|
|
bpy.ops.cc3.importer(param="IMPORT", filepath=fbx_path, link_id=temp_link_id, process_only=process_only)
|
|
|
|
# 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)
|
|
|
|
# remove unused images/folders from the update import files
|
|
tmp_images = jsonutils.get_meshes_images(tmp_meshes)
|
|
keep_images = jsonutils.get_meshes_images(tmp_meshes, filter=objects_to_replace_names)
|
|
for img_path in tmp_images:
|
|
if img_path not in keep_images:
|
|
full_path = os.path.normpath(os.path.join(tmp_dir, img_path))
|
|
if os.path.exists(full_path):
|
|
utils.log_info(f"Deleting unused image file: {img_path}")
|
|
os.remove(full_path)
|
|
|
|
if not replace_actions:
|
|
# remove temp chr actions (motion set)
|
|
if temp_rig_action:
|
|
rigutils.delete_motion_set(temp_rig_action)
|
|
|
|
# remap shapekey actions for the new objects
|
|
if rig_action:
|
|
source_actions = rigutils.find_source_actions(rig_action, rig)
|
|
rigutils.apply_source_key_actions(rig, source_actions, all_matching=True, filter=new_objects)
|
|
|
|
# invalidate and clean up but don't delete the objects & materials
|
|
# do this last as it invalidates the references
|
|
temp_chr_cache.invalidate()
|
|
temp_chr_cache.clean_up()
|
|
chr_cache.clean_up()
|
|
utils.remove_from_collection(props.import_cache, temp_chr_cache)
|
|
|
|
# delete the temp rig
|
|
if temp_rig:
|
|
utils.delete_object_tree(temp_rig)
|
|
|
|
else: # replace_all
|
|
|
|
if rig and temp_rig:
|
|
|
|
# copy old transform to new
|
|
temp_rig.location = rig.location
|
|
temp_rig.rotation_mode = rig.rotation_mode
|
|
temp_rig.rotation_quaternion = rig.rotation_quaternion
|
|
temp_rig.rotation_euler = rig.rotation_euler
|
|
temp_rig.rotation_axis_angle = rig.rotation_axis_angle
|
|
|
|
if not replace_actions:
|
|
# remove temp chr actions (motion set)
|
|
if temp_rig_action:
|
|
rigutils.delete_motion_set(temp_rig_action)
|
|
|
|
# copy/retarget actions from original rig to the replacement
|
|
if rig_action:
|
|
source_actions = rigutils.find_source_actions(rig_action, rig)
|
|
rigutils.apply_source_armature_action(temp_rig, source_actions)
|
|
rigutils.apply_source_key_actions(temp_rig, source_actions, all_matching=True)
|
|
|
|
link_id = chr_cache.link_id
|
|
character_name = chr_cache.character_name
|
|
rig_name = rig.name
|
|
rig_data_name = rig.data.name
|
|
rl_armature_id = utils.get_rl_object_id(rig)
|
|
temp_chr_cache.link_id = link_id
|
|
temp_chr_cache.character_name = character_name
|
|
|
|
utils.set_rl_object_id(temp_rig, rl_armature_id)
|
|
rig_obj_cache = temp_chr_cache.get_object_cache(temp_rig)
|
|
if rig_obj_cache:
|
|
rig_obj_cache.object_id = rl_armature_id
|
|
utils.force_object_name(temp_rig, rig_name)
|
|
utils.force_armature_name(temp_rig.data, rig_data_name)
|
|
|
|
# remove the original character
|
|
# do this last as it invalidates the references
|
|
chr_cache.invalidate()
|
|
chr_cache.delete()
|
|
chr_cache.clean_up()
|
|
utils.remove_from_collection(props.import_cache, chr_cache)
|
|
|
|
def receive_rigify_request(self, data):
|
|
props = vars.props()
|
|
props.validate_and_clean_up()
|
|
|
|
# decode rigify request
|
|
json_data = decode_to_json(data)
|
|
name = json_data["name"]
|
|
character_type = json_data["type"]
|
|
link_id = json_data["link_id"]
|
|
utils.log_info(f"Receive Rigify Request: {name} / {link_id}")
|
|
|
|
# rigify actor armature
|
|
actor = LinkActor.find_actor(link_id, search_name=name, search_type=character_type)
|
|
if actor:
|
|
chr_cache = actor.get_chr_cache()
|
|
if chr_cache:
|
|
if chr_cache.rigified:
|
|
utils.log_error(f"Character {actor.name} already rigified!")
|
|
return
|
|
update_link_status(f"Rigifying: {actor.name}")
|
|
chr_cache.select(only=True)
|
|
cc3_rig = chr_cache.get_armature()
|
|
bpy.ops.cc3.rigifier(param="ALL", no_face_rig=True, auto_retarget=True)
|
|
rigutils.update_avatar_rig(chr_cache.get_armature())
|
|
update_link_status(f"Character Rigified: {actor.name}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LINK_SERVICE: LinkService = None
|
|
|
|
|
|
def get_link_service():
|
|
global LINK_SERVICE
|
|
return LINK_SERVICE
|
|
|
|
|
|
def link_state_update():
|
|
global LINK_SERVICE
|
|
if LINK_SERVICE:
|
|
link_props = vars.link_props()
|
|
link_props.link_listening = LINK_SERVICE.is_listening
|
|
link_props.link_connected = LINK_SERVICE.is_connected
|
|
link_props.link_connecting = LINK_SERVICE.is_connecting
|
|
utils.update_ui()
|
|
|
|
|
|
def update_link_status(text):
|
|
link_props = vars.link_props()
|
|
link_props.link_status = text
|
|
utils.update_ui()
|
|
|
|
|
|
def reconnect():
|
|
global LINK_SERVICE
|
|
link_props = vars.link_props()
|
|
prefs = vars.prefs()
|
|
|
|
if link_props.connected:
|
|
if LINK_SERVICE and LINK_SERVICE.is_connected:
|
|
utils.log_info("DataLink remains connected.")
|
|
elif not LINK_SERVICE or not LINK_SERVICE.is_connected:
|
|
utils.log_info("DataLink was connected. Attempting to reconnect...")
|
|
bpy.ops.ccic.datalink(param="START")
|
|
|
|
elif prefs.datalink_auto_start:
|
|
if LINK_SERVICE and LINK_SERVICE.is_connected:
|
|
utils.log_info("DataLink already connected.")
|
|
elif not LINK_SERVICE or not LINK_SERVICE.is_connected:
|
|
utils.log_info("Auto-starting datalink...")
|
|
bpy.ops.ccic.datalink(param="START")
|
|
|
|
|
|
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 == "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":
|
|
count = LINK_SERVICE.send_pose()
|
|
if count == 1:
|
|
self.report({'INFO'}, f"Pose sent...")
|
|
elif count > 1:
|
|
self.report({'INFO'}, f"{count} Poses sent...")
|
|
else:
|
|
self.report({'ERROR'}, f"No Pose sent!")
|
|
return {'FINISHED'}
|
|
|
|
elif self.param == "SEND_ANIM":
|
|
LINK_SERVICE.send_sequence()
|
|
self.report({'INFO'}, f"Sequence started...")
|
|
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_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):
|
|
link_props = vars.link_props()
|
|
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_SERVICE.service_start(link_props.link_host_ip, link_props.link_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 == "START":
|
|
return "Attempt to start the DataLink by connecting to the server running on CC4/iC8"
|
|
|
|
elif properties.param == "DISCONNECT":
|
|
return "Disconnect from the DataLink server"
|
|
|
|
elif properties.param == "STOP":
|
|
return "Stop the DataLink on both client and server"
|
|
|
|
elif properties.param == "SEND_POSE":
|
|
return "Send the current pose (and frame) to CC4/iC8"
|
|
|
|
elif properties.param == "SEND_ANIM":
|
|
return "Send the animation on the character to CC4/iC8 as a live sequence"
|
|
|
|
elif properties.param == "STOP_ANIM":
|
|
return "Stop the live sequence"
|
|
|
|
elif properties.param == "SEND_ACTOR":
|
|
return "Send the character or prop to CC4/iC8"
|
|
|
|
elif properties.param == "SEND_MORPH":
|
|
return "Send the character body back to CC4 and create a morph slider for it"
|
|
|
|
elif properties.param == "SEND_ACTOR_INVALID":
|
|
return "This standard character has altered topology of the base body mesh and will not re-import into Character Creator"
|
|
|
|
elif properties.param == "SEND_MORPH_INVALID":
|
|
return "This standard character morph has altered topology of the base body mesh and will not re-import into Character Creator"
|
|
|
|
elif properties.param == "SYNC_CAMERA":
|
|
return "TBD"
|
|
|
|
elif properties.param == "SEND_REPLACE_MESH":
|
|
return "Send the mesh alterations back to CC4, only if the mesh topology has not changed"
|
|
|
|
elif properties.param == "SEND_REPLACE_MESH_INVALID":
|
|
return "*Warning* The selected (or one of the selected) mesh has changed in topology and cannot be sent back to CC4 via replace mesh.\n\n" \
|
|
"This mesh can now only be sent to CC4 with the entire character (Go CC)"
|
|
|
|
elif properties.param == "SEND_MATERIAL_UPDATE":
|
|
return "Send material data and textures for the currently selected meshe objects back to CC4"
|
|
|
|
elif properties.param == "DEPIVOT":
|
|
return "TBD"
|
|
|
|
elif properties.param == "DEBUG":
|
|
return "Debug!"
|
|
|
|
elif properties.param == "TEST":
|
|
return "Test!"
|
|
|
|
elif properties.param == "SHOW_ACTOR_FILES":
|
|
return "Open the actor imported files folder"
|
|
|
|
elif properties.param == "SHOW_PROJECT_FILES":
|
|
return "Open the project folder"
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def debug(debug_json):
|
|
utils.log_always("")
|
|
utils.log_always("DEBUG")
|
|
utils.log_always("=====")
|
|
|
|
# simulate service crash
|
|
l = [0,1]
|
|
l[2] = 0
|
|
|
|
|
|
def test():
|
|
utils.log_always("")
|
|
utils.log_always("TEST")
|
|
utils.log_always("====")
|
|
|
|
|
|
class CCICLinkConfirmDialog(bpy.types.Operator):
|
|
bl_idname = "ccic.link_confirm_dialog"
|
|
bl_label = "Confirm Action"
|
|
|
|
message: bpy.props.StringProperty(default="")
|
|
name: bpy.props.StringProperty(default="")
|
|
filepath: bpy.props.StringProperty(default="")
|
|
link_id: bpy.props.StringProperty(default="")
|
|
character_type: bpy.props.StringProperty(default="")
|
|
mode: bpy.props.StringProperty(default = "")
|
|
|
|
width=400
|
|
wrap_width = width / 5.5
|
|
|
|
def execute(self, context):
|
|
global LINK_SERVICE
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
if self.mode:
|
|
LINK_SERVICE.do_update_replace(self.name, self.link_id, self.filepath,
|
|
self.character_type, True,
|
|
objects_to_replace_names=None,
|
|
replace_actions=True)
|
|
|
|
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()
|
|
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)
|
|
layout.separator()
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Edit the character name and non-standard type" |