6516 lines
261 KiB
Python
6516 lines
261 KiB
Python
# Copyright (C) 2021 Victor Soupday
|
|
# This file is part of CC/iC Blender Tools <https://github.com/soupday/cc_blender_tools>
|
|
#
|
|
# CC/iC Blender Tools is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# CC/iC Blender Tools is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with CC/iC Blender Tools. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import bpy
|
|
from mathutils import Vector, Matrix, Quaternion, Euler
|
|
from random import random
|
|
import re, time, os, json
|
|
from . import springbones, bones, facerig, modifiers, rigify_mapping_data, lib, ui, utils, vars
|
|
from typing import List
|
|
|
|
|
|
def edit_rig(rig):
|
|
if rig and utils.edit_mode_to(rig):
|
|
return True
|
|
utils.log_error(f"Unable to edit rig: {rig}!")
|
|
return False
|
|
|
|
|
|
def select_rig(rig):
|
|
if rig and utils.object_mode_to(rig):
|
|
return True
|
|
utils.log_error(f"Unable to select rig: {rig}!")
|
|
return False
|
|
|
|
|
|
def pose_rig(rig):
|
|
if rig and utils.pose_mode_to(rig):
|
|
return True
|
|
utils.log_error(f"Unable to pose rig: {rig}!")
|
|
return False
|
|
|
|
|
|
def name_in_data_paths(action, name, slot_type=None):
|
|
channel = utils.get_action_channelbag(action, slot_type=slot_type)
|
|
if channel:
|
|
for fcurve in channel.fcurves:
|
|
if name in fcurve.data_path:
|
|
return True
|
|
return False
|
|
|
|
|
|
def name_in_pose_bone_data_paths_regex(action, name, slot_type=None):
|
|
channel = utils.get_action_channelbag(action, slot_type=slot_type)
|
|
if channel:
|
|
name = ".*" + name
|
|
for fcurve in channel.fcurves:
|
|
if re.match(name, fcurve.data_path):
|
|
return True
|
|
return False
|
|
|
|
|
|
def bone_name_in_armature_regex(arm, name):
|
|
for bone in arm.data.bones:
|
|
if re.match(name, bone.name):
|
|
return True
|
|
return False
|
|
|
|
|
|
def rig_has_bones(armature: bpy.types.Object, bones: list):
|
|
if armature:
|
|
if len(armature.data.bones) > 0:
|
|
for bone_name in bones:
|
|
if type(bone_name) is list:
|
|
found = False
|
|
for bn in bone_name:
|
|
if bn in armature.data.bones:
|
|
found = True
|
|
if not found:
|
|
return False
|
|
elif bone_name not in armature.data.bones:
|
|
return False
|
|
return True
|
|
return False
|
|
|
|
|
|
def action_has_bones(action: bpy.types.Action, bones: list):
|
|
channel = utils.get_action_channelbag(action, slot_type="OBJECT")
|
|
if channel:
|
|
if len(channel.fcurves) > 0:
|
|
for bone_name in bones:
|
|
if type(bone_name) is list:
|
|
found = False
|
|
for bn in bone_name:
|
|
if name_in_data_paths(action, bn, slot_type="OBJECT"):
|
|
found = True
|
|
if not found:
|
|
return False
|
|
elif not name_in_data_paths(action, bone_name, slot_type="OBJECT"):
|
|
return False
|
|
return True
|
|
return False
|
|
|
|
|
|
def is_G3_action(action):
|
|
return action_has_bones(action, rigify_mapping_data.CC3_BONE_NAMES)
|
|
|
|
|
|
def is_G3_armature(armature):
|
|
return rig_has_bones(armature, rigify_mapping_data.CC3_BONE_NAMES)
|
|
|
|
|
|
def is_iClone_action(action):
|
|
return action_has_bones(action, rigify_mapping_data.ICLONE_BONE_NAMES)
|
|
|
|
|
|
def is_iClone_armature(armature):
|
|
return rig_has_bones(armature, rigify_mapping_data.ICLONE_BONE_NAMES)
|
|
|
|
|
|
def is_ActorCore_action(action):
|
|
return action_has_bones(action, rigify_mapping_data.ACTOR_CORE_BONE_NAMES)
|
|
|
|
|
|
def is_ActorCore_armature(armature):
|
|
return rig_has_bones(armature, rigify_mapping_data.ACTOR_CORE_BONE_NAMES)
|
|
|
|
|
|
def is_GameBase_action(action):
|
|
return action_has_bones(action, rigify_mapping_data.GAME_BASE_BONE_NAMES)
|
|
|
|
|
|
def is_GameBase_armature(armature):
|
|
return rig_has_bones(armature, rigify_mapping_data.GAME_BASE_BONE_NAMES)
|
|
|
|
|
|
def is_Mixamo_action(action):
|
|
return action_has_bones(action, rigify_mapping_data.MIXAMO_BONE_NAMES)
|
|
|
|
|
|
def is_Mixamo_armature(armature):
|
|
return rig_has_bones(armature, rigify_mapping_data.MIXAMO_BONE_NAMES)
|
|
|
|
|
|
def is_rigify_action(action):
|
|
return action_has_bones(action, rigify_mapping_data.RIGIFY_BONE_NAMES)
|
|
|
|
|
|
def is_rigify_armature(armature):
|
|
return rig_has_bones(armature, rigify_mapping_data.RIGIFY_BONE_NAMES)
|
|
|
|
|
|
def is_rl_rigify_action(action):
|
|
return action_has_bones(action, rigify_mapping_data.RIGIFY_PLUS_BONE_NAMES)
|
|
|
|
|
|
def is_rigify_plus_armature(armature):
|
|
return rig_has_bones(armature, rigify_mapping_data.RIGIFY_PLUS_BONE_NAMES)
|
|
|
|
|
|
def is_rl_armature(armature):
|
|
if (is_ActorCore_armature(armature) or
|
|
is_G3_armature(armature) or
|
|
is_GameBase_armature(armature) or
|
|
is_iClone_armature(armature)):
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_rig_generation(armature):
|
|
if is_ActorCore_armature(armature):
|
|
return "ActorCore"
|
|
elif is_G3_armature(armature):
|
|
return "G3"
|
|
elif is_GameBase_armature(armature):
|
|
return "GameBase"
|
|
elif is_iClone_armature(armature):
|
|
return "G3"
|
|
else:
|
|
return "Unknown"
|
|
|
|
|
|
def is_unity_action(action):
|
|
return "_Unity" in action.name and "|A|" in action.name
|
|
|
|
|
|
def get_armature_action_source_type(armature, action=None):
|
|
if armature and not action and armature.type == "ARMATURE":
|
|
if is_G3_armature(armature):
|
|
return "G3", "G3 (CC3/CC3+)"
|
|
if is_iClone_armature(armature):
|
|
return "G3", "G3 (iClone)"
|
|
if is_ActorCore_armature(armature):
|
|
return "G3", "G3 (ActorCore)"
|
|
if is_GameBase_armature(armature):
|
|
return "GameBase", "GameBase (CC3/CC3+)"
|
|
if is_Mixamo_armature(armature):
|
|
return "Mixamo", "Mixamo"
|
|
if is_rigify_plus_armature(armature):
|
|
return "Rigify+", "Rigify+"
|
|
if is_rigify_armature(armature):
|
|
return "Rigify", "Rigify"
|
|
if armature and action and armature.type == "ARMATURE":
|
|
if is_G3_armature(armature) and is_G3_action(action):
|
|
return "G3", "G3 (CC3/CC3+)"
|
|
if is_iClone_armature(armature) and is_iClone_action(action):
|
|
return "G3", "G3 (iClone)"
|
|
if is_ActorCore_armature(armature) and is_ActorCore_action(action):
|
|
return "G3", "G3 (ActorCore)"
|
|
if is_GameBase_armature(armature) and is_GameBase_action(action):
|
|
return "GameBase", "GameBase (CC3/CC3+)"
|
|
if is_Mixamo_armature(armature) and is_Mixamo_action(action):
|
|
return "Mixamo", "Mixamo"
|
|
if is_rigify_plus_armature(armature) and is_rl_rigify_action(action):
|
|
return "Rigify+", "Rigify+"
|
|
if is_rigify_armature(armature) and is_rigify_action(action):
|
|
return "Rigify", "Rigify"
|
|
# detect other types as they become available...
|
|
return "Unknown", "Unknown"
|
|
|
|
|
|
def find_source_actions(source_action, source_rig=None):
|
|
src_set_id, src_set_gen, src_type_id, src_object_id = get_motion_set_ids(source_action)
|
|
src_rlx_id = utils.get_prop(source_action, "rl_rlx_id")
|
|
src_prefix = get_motion_prefix(source_action)
|
|
src_prefix, src_rig_id, src_type_id, src_object_id, src_motion_id = decode_action_name(source_action)
|
|
if not src_motion_id:
|
|
src_motion_id = get_motion_id(source_action)
|
|
|
|
actions = {
|
|
"motion_info": {
|
|
"prefix": src_prefix,
|
|
"motion_id": src_motion_id,
|
|
"set_id": src_set_id,
|
|
"set_generation": src_set_gen,
|
|
"slotted": False,
|
|
},
|
|
"count": 0,
|
|
"armature": None,
|
|
"armatures": [],
|
|
"keys": {},
|
|
"rlx": None,
|
|
"light": None,
|
|
"camera": None,
|
|
"objects": {},
|
|
}
|
|
|
|
if src_set_id:
|
|
utils.log_info(f"Looking for motion set id: {src_set_id}")
|
|
for action in bpy.data.actions:
|
|
if "rl_set_id" in action:
|
|
set_id, set_gen, type_id, object_id = get_motion_set_ids(action)
|
|
rlx_id = utils.get_prop(action, "rl_rlx_id")
|
|
unused = utils.get_prop(action, "rl_unused_action", False)
|
|
if set_id == src_set_id:
|
|
if type_id == "KEY":
|
|
utils.log_info(f" - Found shape-key action: {action.name} for {object_id}")
|
|
actions["keys"][object_id] = action
|
|
elif type_id == "RLX" or (type_id == "SLOTTED" and rlx_id):
|
|
utils.log_info(f" - Found rlx action: {action.name}")
|
|
actions["rlx"] = action
|
|
elif type_id == "LIGHT" and rlx_id:
|
|
utils.log_info(f" - Found rlx light action: {action.name}")
|
|
actions["light"] = action
|
|
elif type_id == "CAMERA" and rlx_id:
|
|
utils.log_info(f" - Found rlx camera action: {action.name}")
|
|
actions["camera"] = action
|
|
elif type_id == "ARM" or type_id == "SLOTTED":
|
|
utils.log_info(f" - Found armature action: {action.name}")
|
|
actions["armatures"].append(action)
|
|
if not unused and not actions["armature"]:
|
|
actions["armature"] = action
|
|
actions["slotted"] = (type_id == "SLOTTED")
|
|
return actions
|
|
|
|
# try matching actions by action name pattern
|
|
if src_type_id and src_motion_id:
|
|
if is_slotted_action(source_action):
|
|
actions["armature"] = source_action
|
|
actions["armatures"].append(source_action)
|
|
actions["slotted"] = True
|
|
return actions
|
|
else:
|
|
# match actions by name pattern
|
|
found = False
|
|
utils.log_info(f"Looking for shape-key actions matching: [<{src_prefix}>|]{src_rig_id}|[K|A]|[<obj>|]{src_motion_id}")
|
|
for action in bpy.data.actions:
|
|
motion_prefix, rig_id, type_id, object_id, motion_id = decode_action_name(action)
|
|
if (motion_id and object_id not in actions and
|
|
motion_prefix == src_prefix and
|
|
rig_id == src_rig_id and
|
|
utils.partial_match(motion_id, src_motion_id)):
|
|
if type_id == "K":
|
|
utils.log_info(f" - Found shape-key action: {action.name} for {object_id}")
|
|
actions["keys"][object_id] = action
|
|
found = True
|
|
elif type_id == "A":
|
|
utils.log_info(f" - Found armature action: {action.name}")
|
|
actions["armature"] = action
|
|
found = True
|
|
if found:
|
|
return actions
|
|
|
|
# try to fetch shape-key actions from source armature child objects, if supplied
|
|
elif source_rig:
|
|
utils.log_info(f"Looking for shape-key actions in armature child objects: {source_rig.name}")
|
|
action = utils.safe_get_action(source_rig)
|
|
if action:
|
|
utils.log_info(f" - Found armature action: {action.name}")
|
|
actions["armature"] = action
|
|
for obj in source_rig.children:
|
|
obj_id = get_obj_id(obj)
|
|
if obj.type == "MESH":
|
|
action = utils.safe_get_action(obj.data.shape_keys)
|
|
if action:
|
|
utils.log_info(f" - Found shape-key action: {action.name} for {obj_id}")
|
|
actions["keys"][obj_id] = action
|
|
actions["objects"][obj_id] = obj
|
|
return actions
|
|
|
|
return actions
|
|
|
|
|
|
def get_main_body_action(source_actions):
|
|
# find the "Body" action
|
|
for obj_id in source_actions["keys"]:
|
|
action: bpy.types.Action = source_actions["keys"][obj_id]
|
|
l_name = obj_id.lower()
|
|
if l_name == "body":
|
|
utils.log_info(f" - Using main body action: {action.name}")
|
|
return action
|
|
# find the action with the most shape keys
|
|
action_with_most_keys = None
|
|
num_keys = 0
|
|
for obj_id in source_actions["keys"]:
|
|
action = source_actions["keys"][obj_id]
|
|
channel = utils.get_action_channelbag(action, slot_type="KEY")
|
|
if channel:
|
|
if len(channel.fcurves) > num_keys:
|
|
num_keys = len(channel.fcurves)
|
|
action_with_most_keys = action
|
|
if action_with_most_keys:
|
|
utils.log_info(f" - Using action with most shape keys: {action_with_most_keys.name}")
|
|
else:
|
|
utils.log_info(f" - No shape key actions in this Motion set!")
|
|
return action_with_most_keys
|
|
|
|
|
|
"""
|
|
# TODO: No longer used.
|
|
def apply_source_actions(dst_rig, source_actions, copy=False,
|
|
motion_id=None, motion_prefix=None,
|
|
set_id=None, set_generation=None,
|
|
all_matching=False, filter=None):
|
|
apply_source_armature_action(dst_rig, source_actions, copy=copy,
|
|
motion_id=motion_id, motion_prefix=motion_prefix,
|
|
set_id=set_id, set_generation=set_generation)
|
|
apply_source_key_actions(dst_rig, source_actions, copy=copy,
|
|
all_matching=all_matching,
|
|
motion_id=motion_id, motion_prefix=motion_prefix,
|
|
set_id=set_id, set_generation=set_generation,
|
|
filter=filter)
|
|
|
|
# TODO: No longer used.
|
|
def apply_source_armature_action(dst_rig, source_actions, copy=False,
|
|
motion_id=None, motion_prefix=None,
|
|
set_id=None, set_generation=None):
|
|
obj_used = []
|
|
actions_used = []
|
|
rig_id = get_rig_id(dst_rig)
|
|
rl_arm_id = utils.get_rl_object_id(dst_rig)
|
|
if not motion_id:
|
|
motion_id = source_actions["motion_info"]["motion_id"]
|
|
if motion_prefix is None:
|
|
motion_prefix = source_actions["motion_info"]["prefix"]
|
|
utils.log_info(f"Applying source armature action:")
|
|
action = source_actions["armature"]
|
|
is_slotted = source_actions["slotted"]
|
|
if action:
|
|
if copy and motion_id:
|
|
action_name = make_armature_action_name(rig_id, motion_id, motion_prefix)
|
|
utils.log_info(f" - Copying action: {action.name} to {action_name}")
|
|
action = utils.copy_action(action, action_name)
|
|
if set_id and set_generation:
|
|
add_motion_set_data(action, set_id, set_generation, rl_arm_id=rl_arm_id, slotted=is_slotted)
|
|
utils.log_info(f" - Applying action: {action.name} to {rig_id}")
|
|
load_slotted_action(dst_rig, action)
|
|
obj_used.append(dst_rig)
|
|
actions_used.append(action)
|
|
return obj_used, actions_used
|
|
|
|
|
|
# TODO: No longer used.
|
|
def apply_source_key_actions(dst_rig, source_actions, all_matching=False, copy=False,
|
|
motion_id=None, motion_prefix=None,
|
|
set_id=None, set_generation=None,
|
|
filter=None):
|
|
obj_used = []
|
|
key_actions = {}
|
|
if source_actions["slotted"]:
|
|
return key_actions
|
|
rig_id = get_rig_id(dst_rig)
|
|
if motion_id == "" or motion_id == "TEMP":
|
|
motion_id = "TEMP_" + utils.generate_random_id(12)
|
|
if motion_id is None:
|
|
motion_id = source_actions["motion_info"]["motion_id"]
|
|
if motion_prefix is None:
|
|
motion_prefix = source_actions["motion_info"]["prefix"]
|
|
|
|
# TODO this should really collect all named shape key animation tracks across all source objects
|
|
# and create actions specific to each target object.
|
|
# e.g. facial hair meshes have tongue shape keys which the body action alone doesn't have.
|
|
|
|
# apply to exact matches first
|
|
utils.log_info(f"Applying source key actions: (copy={copy}, motion_id={motion_id})")
|
|
objects = utils.get_child_objects(dst_rig)
|
|
for obj in objects:
|
|
if filter and obj not in filter: continue
|
|
if obj.type == "MESH":
|
|
if utils.object_has_shape_keys(obj):
|
|
obj_id = get_action_obj_id(obj)
|
|
old_action = utils.safe_get_action(obj.data.shape_keys)
|
|
if (obj_id in source_actions["keys"] and
|
|
obj_has_action_shape_keys(obj, source_actions["keys"][obj_id])):
|
|
action = source_actions["keys"][obj_id]
|
|
if copy and motion_id:
|
|
action_name = make_key_action_name(rig_id, motion_id, obj_id, motion_prefix)
|
|
utils.log_info(f" - Copying action: {action.name} to {action_name}")
|
|
action = utils.copy_action(action, action_name)
|
|
if set_id and set_generation:
|
|
add_motion_set_data(action, set_id, set_generation, obj_id=obj_id)
|
|
utils.log_info(f" - Applying action: {action.name} to {obj_id}")
|
|
utils.safe_set_action(obj.data.shape_keys, action)
|
|
obj_used.append(obj)
|
|
key_actions[obj_id] = (action, old_action)
|
|
else:
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
|
|
# apply to other compatible shape key objects
|
|
if all_matching:
|
|
utils.log_info(f"Applying other matching source key actions:")
|
|
body_action = get_main_body_action(source_actions)
|
|
for obj in objects:
|
|
if filter and obj not in filter: continue
|
|
if obj not in obj_used and utils.object_has_shape_keys(obj):
|
|
obj_id = get_action_obj_id(obj)
|
|
old_action = utils.safe_get_action(obj.data.shape_keys)
|
|
if body_action:
|
|
if obj_has_action_shape_keys(obj, body_action):
|
|
action = body_action
|
|
if copy and motion_id:
|
|
action_name = make_key_action_name(rig_id, motion_id, obj_id, motion_prefix)
|
|
utils.log_info(f" - Copying action: {action.name} to {action_name}")
|
|
action = utils.copy_action(action, action_name)
|
|
if set_id and set_generation:
|
|
add_motion_set_data(action, set_id, set_generation, obj_id=obj_id)
|
|
utils.log_info(f" - Applying action: {action.name} to {obj_id}")
|
|
utils.safe_set_action(obj.data.shape_keys, action)
|
|
obj_used.append(obj)
|
|
key_actions[obj_id] = (action, old_action)
|
|
return key_actions
|
|
"""
|
|
|
|
|
|
def obj_has_action_shape_keys(obj, action: bpy.types.Action):
|
|
channel = utils.get_action_channelbag(action, slot_type="KEY")
|
|
if channel:
|
|
if obj.data.shape_keys and obj.data.shape_keys.key_blocks:
|
|
for key in obj.data.shape_keys.key_blocks:
|
|
for fcurve in channel.fcurves:
|
|
if key.name in fcurve.data_path:
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_rig_id(rig):
|
|
if rig:
|
|
rig_id = utils.strip_name(rig.name.strip()).replace("|", "_")
|
|
return rig_id
|
|
else:
|
|
return ""
|
|
|
|
|
|
def get_armature_id(rig):
|
|
if rig:
|
|
return utils.get_prop(rig, "rl_armature_id", None)
|
|
else:
|
|
return ""
|
|
|
|
|
|
def get_motion_id(action):
|
|
if type(action) is str:
|
|
action_name = action
|
|
else:
|
|
action_name = action.name
|
|
ids = action_name.split("|")
|
|
if ids:
|
|
return ids[-1]
|
|
return action_name
|
|
|
|
|
|
def decode_action_name(action):
|
|
"""Decode action name into prefix, rig_id, type("A"|"K"), object_id, motion_id.
|
|
if the action name does not follow this naming pattern all values return None."""
|
|
if type(action) is str:
|
|
action_name = action
|
|
else:
|
|
action_name = action.name
|
|
#utils.log_detail(f"Decoding Action name: {action_name}")
|
|
motion_id = action_name
|
|
prefix = ""
|
|
rig_id = ""
|
|
type_id = "A"
|
|
obj_id = ""
|
|
try:
|
|
ids = action_name.split("|")
|
|
for i, id in enumerate(ids):
|
|
ids[i] = id.strip()
|
|
L = len(ids)
|
|
K = L
|
|
TYPE_IDS = ["A", "UA", "S", "O", "UO", "L", "UL", "R", "UR", "C", "UC"]
|
|
if utils.any_in(ids, TYPE_IDS):
|
|
K -= 1
|
|
elif "K" in ids or "UK" in ids:
|
|
K -= 2
|
|
if L > 0:
|
|
motion_id = ids[-1]
|
|
if K >= 3:
|
|
prefix = ids[0]
|
|
rig_id = ids[1]
|
|
elif K >= 2:
|
|
rig_id = ids[0]
|
|
if "rl_action_type" in action:
|
|
if action["rl_action_type"] == "ARM":
|
|
type_id = "A"
|
|
elif action["rl_action_type"] == "KEY":
|
|
type_id = "K"
|
|
if "rl_key_object" in action:
|
|
obj_id = action["rl_key_object"]
|
|
elif action["rl_action_type"] == "SLOTTED":
|
|
type_id = "S"
|
|
else:
|
|
if "A" in ids:
|
|
type_id = "A"
|
|
elif "K" in ids:
|
|
type_id = "K"
|
|
i = ids.index("K")
|
|
if L > i+1:
|
|
obj_id = ids[i+1]
|
|
elif "S" in ids:
|
|
type_id = "S"
|
|
except Exception as e:
|
|
prefix = None
|
|
rig_id = None
|
|
type_id = None
|
|
obj_id = None
|
|
motion_id = action_name
|
|
return prefix, rig_id, type_id, obj_id, motion_id
|
|
|
|
|
|
def get_motion_id(action, default_name="Motion"):
|
|
if action:
|
|
motion_id = action.name.split("|")[-1].strip()
|
|
else:
|
|
motion_id = ""
|
|
if not motion_id and default_name:
|
|
motion_id = f"{default_name}_{utils.generate_random_id(8)}"
|
|
return motion_id
|
|
|
|
|
|
def get_motion_prefix(action, default_prefix=""):
|
|
prefix = decode_action_name(action)[0]
|
|
if prefix is None:
|
|
return default_prefix
|
|
elif prefix:
|
|
return prefix.strip()
|
|
else:
|
|
return prefix
|
|
|
|
|
|
def get_obj_id(obj):
|
|
obj_id = utils.strip_cc_base_name(obj.name).replace("|", "_")
|
|
return obj_id
|
|
|
|
|
|
def get_formatted_prefix(motion_prefix):
|
|
if motion_prefix is None:
|
|
motion_prefix = ""
|
|
motion_prefix = motion_prefix.strip().replace("|", "_")
|
|
while motion_prefix.endswith("_"):
|
|
motion_prefix = motion_prefix[:-1]
|
|
return motion_prefix
|
|
|
|
|
|
def get_unique_set_motion_id(rig_id, motion_id, motion_prefix, exclude_set_id=None,
|
|
slotted=False, rlob=None, rlx=False):
|
|
if rlx or (rlob and rlob.type != "ARMATURE"):
|
|
test_name = make_rlx_action_name(rig_id, motion_id, motion_prefix, slotted=slotted)
|
|
else:
|
|
test_name = make_armature_action_name(rig_id, motion_id, motion_prefix, slotted=slotted)
|
|
base_name = test_name
|
|
num_suffix = 0
|
|
while test_name in bpy.data.actions:
|
|
if exclude_set_id and "rl_set_id" in bpy.data.actions[test_name]:
|
|
if exclude_set_id == bpy.data.actions[test_name]["rl_set_id"]:
|
|
break
|
|
num_suffix += 1
|
|
test_name = f"{base_name}_{num_suffix:03d}"
|
|
if num_suffix > 0:
|
|
motion_id += f"_{num_suffix:03d}"
|
|
return motion_id
|
|
|
|
|
|
def generate_action_name(rig_id, code_id, obj_id, motion_id, motion_prefix):
|
|
f_prefix = get_formatted_prefix(motion_prefix)
|
|
name = ""
|
|
if f_prefix:
|
|
name += f"{f_prefix}"
|
|
if rig_id:
|
|
if name: name += "|"
|
|
name += f"{rig_id}"
|
|
if code_id:
|
|
if name: name += "|"
|
|
name += f"{code_id}"
|
|
if obj_id:
|
|
if name: name += "|"
|
|
name += f"{obj_id}"
|
|
if motion_id:
|
|
if name: name += "|"
|
|
name += f"{motion_id}"
|
|
return name
|
|
#return f"{f_prefix}{rig_id}|{code_id}|{motion_id}"
|
|
|
|
|
|
def make_armature_action_name(rig_id, motion_id, motion_prefix, slotted=False):
|
|
if slotted:
|
|
return generate_action_name(rig_id, "", "", motion_id, motion_prefix)
|
|
else:
|
|
return generate_action_name(rig_id, "A", "", motion_id, motion_prefix)
|
|
|
|
|
|
def make_key_action_name(rig_id, motion_id, obj_id, motion_prefix):
|
|
return generate_action_name(rig_id, "K", obj_id, motion_id, motion_prefix)
|
|
|
|
|
|
def make_rlx_action_name(rlob_id, motion_id, motion_prefix, slotted=False):
|
|
if slotted:
|
|
return generate_action_name(rlob_id, "", "", motion_id, motion_prefix)
|
|
else:
|
|
return generate_action_name(rlob_id, "O", "", motion_id, motion_prefix)
|
|
|
|
|
|
def make_data_action_name(obj, rlob_id, motion_id, motion_prefix):
|
|
code_id = obj.type[0] if type(obj) is bpy.types.Object else code_id[0]
|
|
return generate_action_name(rlob_id, code_id, "", motion_id, motion_prefix)
|
|
|
|
|
|
def set_armature_action_name(action, rig_id, motion_id, motion_prefix, slotted=False):
|
|
action.name = make_armature_action_name(rig_id, motion_id, motion_prefix, slotted=slotted)
|
|
|
|
|
|
def set_key_action_name(action, rig_id, motion_id, obj_id, motion_prefix):
|
|
action.name = make_key_action_name(rig_id, motion_id, obj_id, motion_prefix)
|
|
|
|
|
|
def get_set_generation(rig):
|
|
set_generation = utils.get_prop(rig, "rl_set_generation", None)
|
|
return set_generation
|
|
|
|
|
|
def update_rig_set_generation(rig):
|
|
rl_set_generation, source_label = get_armature_action_source_type(rig)
|
|
if ("rl_set_generation" not in rig or
|
|
rig["rl_set_generation"] != rl_set_generation):
|
|
rig["rl_set_generation"] = rl_set_generation
|
|
return rl_set_generation
|
|
|
|
|
|
def generate_set_id():
|
|
return utils.generate_random_id(32)
|
|
|
|
|
|
def generate_motion_set(rig, motion_id, motion_prefix):
|
|
rl_set_id = generate_set_id()
|
|
rl_set_generation = update_rig_set_generation(rig)
|
|
return rl_set_id, rl_set_generation
|
|
|
|
|
|
def get_motion_set_ids(action):
|
|
set_id = utils.get_prop(action, "rl_set_id", None)
|
|
set_generation = utils.get_prop(action, "rl_set_generation", None)
|
|
action_type_id = utils.get_prop(action, "rl_action_type", None)
|
|
key_object = utils.get_prop(action, "rl_key_object", None)
|
|
return set_id, set_generation, action_type_id, key_object
|
|
|
|
|
|
def get_motion_set_actions(action_or_set_id):
|
|
actions = []
|
|
if type(action_or_set_id) is bpy.types.Action:
|
|
set_id = utils.get_prop(action_or_set_id, "rl_set_id", None)
|
|
else:
|
|
set_id = action_or_set_id
|
|
if set_id:
|
|
for action in bpy.data.actions:
|
|
if utils.get_prop(action, "rl_set_id") == set_id:
|
|
actions.append(action)
|
|
return actions
|
|
|
|
|
|
def is_rlx_motion(actions_or_set_id):
|
|
T = type(actions_or_set_id)
|
|
if T is list:
|
|
actions = actions_or_set_id
|
|
else:
|
|
actions = get_motion_set_actions(actions_or_set_id)
|
|
for action in actions:
|
|
if "rl_rlx_id" in action:
|
|
return True
|
|
return False
|
|
|
|
|
|
def get_actions_frame_range(actions):
|
|
frame_start = None
|
|
frame_end = None
|
|
if actions:
|
|
if not type(actions) is list:
|
|
actions = [actions]
|
|
action: bpy.types.Action = None
|
|
for action in actions:
|
|
if action:
|
|
start = int(action.frame_range[0])
|
|
end = int(action.frame_range[1])
|
|
if frame_start is None:
|
|
frame_start = start
|
|
if frame_end is None:
|
|
frame_end = end
|
|
if start < frame_start:
|
|
frame_start = start
|
|
if end > frame_end:
|
|
frame_end = end
|
|
if frame_start is None or frame_end is None:
|
|
frame_start = 0
|
|
frame_end = 0
|
|
return frame_start, frame_end
|
|
|
|
|
|
def get_motion_set_frame_range(action_or_set_id):
|
|
actions = get_motion_set_actions(action_or_set_id)
|
|
return get_actions_frame_range(actions)
|
|
|
|
|
|
def replace_motion_set(set_id, old_set_id):
|
|
actions = get_motion_set_actions(set_id)
|
|
old_actions = get_motion_set_actions(old_set_id)
|
|
temp_id = generate_set_id()
|
|
name = old_actions[0].name if len(old_actions) == 1 else None
|
|
for action in actions:
|
|
if name:
|
|
utils.force_action_name(action, name)
|
|
utils.set_prop(action, "rl_set_id", old_set_id)
|
|
for action in old_actions:
|
|
utils.set_prop(action, "rl_set_id", temp_id)
|
|
delete_motion_set(temp_id)
|
|
|
|
|
|
def add_motion_set_data(action, set_id, set_generation,
|
|
key_id=None, arm_id=None, rlx_id=None,
|
|
is_light=False, is_camera=False,
|
|
slotted=False):
|
|
action["rl_set_id"] = set_id
|
|
action["rl_set_generation"] = set_generation
|
|
|
|
if slotted:
|
|
action["rl_action_type"] = "SLOTTED"
|
|
elif arm_id and key_id:
|
|
action["rl_action_type"] = "KEY"
|
|
elif arm_id:
|
|
action["rl_action_type"] = "ARM"
|
|
elif rlx_id and is_light:
|
|
action["rl_action_type"] = "LIGHT"
|
|
elif rlx_id and is_camera:
|
|
action["rl_action_type"] = "CAMERA"
|
|
elif rlx_id:
|
|
action["rl_action_type"] = "RLX"
|
|
|
|
if arm_id:
|
|
action["rl_armature_id"] = arm_id
|
|
if key_id:
|
|
action["rl_key_object"] = key_id
|
|
if rlx_id:
|
|
action["rl_rlx_id"] = rlx_id
|
|
|
|
|
|
|
|
def update_motion_set_index(action):
|
|
props = vars.props()
|
|
props.action_set_list_index = utils.index_in_collection(action, bpy.data.actions)
|
|
|
|
|
|
def load_motion_set(rig, action, move=False, temp=None, force=None):
|
|
prefs = vars.prefs()
|
|
if action:
|
|
utils.log_info(f"Load Motion Set: {action.name} {'Move ' if move else ''}{'Temp ' if temp else ''}")
|
|
else:
|
|
utils.log_info(f"Clearing Motion Set ...")
|
|
# clear the pose and shape-keys first before loading the motion set
|
|
clear_motion_set(rig)
|
|
arm_action = action
|
|
if action:
|
|
utils.log_indent()
|
|
if prefs.use_action_slots():
|
|
arm_action = load_slotted_action(rig, action, move=move, temp=temp, force=force)
|
|
else:
|
|
arm_action = load_separate_actions(rig, action, move=move, temp=temp, force=force)
|
|
if arm_action:
|
|
update_motion_set_index(arm_action)
|
|
utils.log_recess()
|
|
|
|
return arm_action
|
|
|
|
|
|
def new_motion_set(rig: bpy.types.Object, motion_id=None, motion_prefix=""):
|
|
prefs = vars.prefs()
|
|
|
|
slotted = prefs.use_action_slots()
|
|
rig_id = get_rig_id(rig)
|
|
rl_id = utils.get_rl_id(rig)
|
|
set_id = generate_set_id()
|
|
set_generation = utils.get_prop(rig, "rl_set_generation")
|
|
if not motion_id:
|
|
motion_id = "New"
|
|
motion_id = get_unique_set_motion_id(rig_id, motion_id, motion_prefix, slotted=slotted, rlob=rig)
|
|
if rig.type == "ARMATURE":
|
|
action_name = make_armature_action_name(rig_id, motion_id, motion_prefix, slotted=slotted)
|
|
else:
|
|
action_name = make_rlx_action_name(rig_id, motion_id, motion_prefix, slotted=slotted)
|
|
action = bpy.data.actions.new(action_name)
|
|
slot, channel = add_action_ob_slot_channelbag(action, rig)
|
|
if rig.type == "ARMATURE":
|
|
add_motion_set_data(action, set_id, set_generation, arm_id=rl_id, slotted=slotted)
|
|
elif rig.type == "LIGHT" or rig.type == "CAMERA":
|
|
add_motion_set_data(action, set_id, set_generation, rlx_id=rl_id, slotted=slotted)
|
|
utils.safe_set_action(rig, action, slot=slot)
|
|
utils.safe_set_action(rig.data, None, create=False)
|
|
if rig.type == "ARMATURE":
|
|
for child in rig.children:
|
|
if utils.object_exists_is_mesh(child) and utils.object_has_shape_keys(child):
|
|
utils.safe_set_action(child, None, create=False)
|
|
utils.safe_set_action(child.data, None, create=False)
|
|
utils.safe_set_action(child.data.shape_keys, None, create=False)
|
|
elif rig.type == "LIGHT" or rig.type == "CAMERA":
|
|
utils.safe_set_action(rig.data, None, create=False)
|
|
update_motion_set_index(action)
|
|
return action
|
|
|
|
|
|
def ensure_motion_set(rlob, motion_id="New", motion_prefix=""):
|
|
action = utils.safe_get_action(rlob)
|
|
if not action:
|
|
action = new_motion_set(rlob, motion_id, motion_prefix)
|
|
if action:
|
|
load_motion_set(rlob, action)
|
|
|
|
|
|
def clear_motion_set(rig):
|
|
mode_selection = utils.store_mode_selection_state()
|
|
has_actions = utils.safe_get_action(rig)
|
|
if rig.type == "ARMATURE" and not has_actions:
|
|
reset_pose(rig)
|
|
utils.safe_set_action(rig, None)
|
|
if rig.type == "LIGHT" or rig.type == "CAMERA":
|
|
utils.safe_set_action(rig.data, None, create=False)
|
|
objects = utils.get_child_objects(rig)
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
if utils.object_has_shape_keys(obj):
|
|
utils.safe_set_action(obj.data.shape_keys, None, create=False)
|
|
if not has_actions:
|
|
reset_shape_keys(obj)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
|
|
def clear_all_actions(objects):
|
|
for obj in objects:
|
|
if utils.object_exists_is_armature(obj):
|
|
utils.safe_set_action(obj, None)
|
|
elif utils.object_exists_is_mesh(obj):
|
|
if utils.object_has_shape_keys(obj):
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
|
|
|
|
def push_motion_set(rig: bpy.types.Object, set_armature_action, push_index = 0):
|
|
prefs = vars.prefs()
|
|
source_actions = find_source_actions(set_armature_action, None)
|
|
frame = bpy.context.scene.frame_current
|
|
set_arm_action: bpy.types.Action = source_actions["armature"]
|
|
start = int(set_arm_action.frame_range[0])
|
|
end = int(set_arm_action.frame_range[1])
|
|
length = end - start
|
|
is_slotted = prefs.use_action_slots() and utils.get_prop(set_arm_action, "rl_action_type") == "SLOTTED"
|
|
objects = utils.get_child_objects(rig)
|
|
# find all available NLA tracks
|
|
nla_data = []
|
|
if rig.animation_data.nla_tracks:
|
|
nla_data.append(rig.animation_data.nla_tracks)
|
|
for obj in objects:
|
|
if obj.data.shape_keys and obj.data.shape_keys.animation_data:
|
|
if obj.data.shape_keys.animation_data.nla_tracks:
|
|
nla_data.append(obj.data.shape_keys.animation_data.nla_tracks)
|
|
# count the mininum number of shared tracks across all action objects
|
|
min_tracks = 0
|
|
for nla_tracks in nla_data:
|
|
l = len(nla_tracks)
|
|
if l > 0:
|
|
if min_tracks == 0:
|
|
min_tracks = l
|
|
min_tracks = min(min_tracks, l)
|
|
# find the first available track that can fit the motion set
|
|
available_tracks = [True] * min_tracks
|
|
for nla_tracks in nla_data:
|
|
for i in range(0, min_tracks):
|
|
track: bpy.types.NlaTrack = nla_tracks[i]
|
|
strip: bpy.types.NlaStrip
|
|
for strip in track.strips:
|
|
if frame >= strip.frame_start and frame < strip.frame_end:
|
|
available_tracks[i] = False
|
|
elif (frame + length) >= strip.frame_start and (frame + length) < strip.frame_end:
|
|
available_tracks[i] = False
|
|
elif strip.frame_start < frame and strip.frame_end >= frame + length:
|
|
available_tracks[i] = False
|
|
track_index = -1
|
|
for i, available in enumerate(available_tracks):
|
|
if available:
|
|
track_index = i
|
|
break
|
|
# push the actions
|
|
action: bpy.types.Action = source_actions["armature"]
|
|
rig: bpy.types.Object
|
|
if rig.animation_data is None:
|
|
rig.animation_data_create()
|
|
if not rig.animation_data.nla_tracks or track_index == -1:
|
|
track = rig.animation_data.nla_tracks.new()
|
|
else:
|
|
track = rig.animation_data.nla_tracks[track_index]
|
|
try:
|
|
strip = track.strips.new(action.name, frame, action)
|
|
except:
|
|
track = rig.animation_data.nla_tracks.new()
|
|
strip = track.strips.new(action.name, frame, action)
|
|
strip.action_frame_start = start
|
|
strip.action_frame_end = end
|
|
strip.name = f"{action.name}|{push_index:03d}"
|
|
for obj in objects:
|
|
if is_slotted:
|
|
if "KEAll Keys" in action.slots:
|
|
obj_id = "KEAll Keys"
|
|
else:
|
|
obj_id = "KE" + obj.name
|
|
if obj.type == "MESH" and obj_id in action.slots:
|
|
slot = action.slots[obj_id]
|
|
if obj.data.shape_keys:
|
|
if not obj.data.shape_keys.animation_data:
|
|
obj.data.shape_keys.animation_data_create()
|
|
if not obj.data.shape_keys.animation_data.nla_tracks or track_index == -1:
|
|
track = obj.data.shape_keys.animation_data.nla_tracks.new()
|
|
else:
|
|
track = obj.data.shape_keys.animation_data.nla_tracks[track_index]
|
|
try:
|
|
strip = track.strips.new(action.name, frame, action)
|
|
except:
|
|
track = obj.data.shape_keys.animation_data.nla_tracks.new()
|
|
strip = track.strips.new(action.name, frame, action)
|
|
strip.action_slot = slot
|
|
strip.action_frame_start = start
|
|
strip.action_frame_end = end
|
|
strip.name = f"{action.name}|{push_index:03d}"
|
|
else:
|
|
obj_id = get_obj_id(obj)
|
|
if obj.type == "MESH" and obj_id in source_actions["keys"]:
|
|
action = source_actions["keys"][obj_id]
|
|
if obj.data.shape_keys:
|
|
if not obj.data.shape_keys.animation_data:
|
|
obj.data.shape_keys.animation_data_create()
|
|
if not obj.data.shape_keys.animation_data.nla_tracks or track_index == -1:
|
|
track = obj.data.shape_keys.animation_data.nla_tracks.new()
|
|
else:
|
|
track = obj.data.shape_keys.animation_data.nla_tracks[track_index]
|
|
try:
|
|
strip = track.strips.new(action.name, frame, action)
|
|
except:
|
|
track = obj.data.shape_keys.animation_data.nla_tracks.new()
|
|
strip = track.strips.new(action.name, frame, action)
|
|
strip.action_frame_start = start
|
|
strip.action_frame_end = end
|
|
strip.name = f"{action.name}|{push_index:03d}"
|
|
|
|
|
|
def clear_animation_data(obj: bpy.types.Object):
|
|
if obj.type == "ARMATURE" or obj.type == "MESH":
|
|
# remove action
|
|
utils.safe_set_action(obj, None)
|
|
# remove strips
|
|
# this removes drivers too...
|
|
#obj.animation_data_clear()
|
|
ad = obj.animation_data
|
|
if ad:
|
|
while ad.nla_tracks:
|
|
ad.nla_tracks.remove(ad.nla_tracks[0])
|
|
if obj.type == "MESH":
|
|
# remove shape key action
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
# remove shape key strips
|
|
if obj.data.shape_keys and obj.data.shape_keys.animation_data:
|
|
obj.data.shape_keys.animation_data_clear()
|
|
ad = obj.data.shape_keys.animation_data
|
|
if ad:
|
|
while ad.nla_tracks:
|
|
ad.nla_tracks.remove(ad.nla_tracks[0])
|
|
|
|
|
|
def reset_nla_tracks(obj):
|
|
track = None
|
|
if obj.type == "ARMATURE" or obj.type == "MESH":
|
|
# remove action
|
|
utils.safe_set_action(obj, None)
|
|
# remove strips
|
|
ad = obj.animation_data
|
|
if ad:
|
|
while ad.nla_tracks:
|
|
ad.nla_tracks.remove(ad.nla_tracks[0])
|
|
if obj.type == "MESH":
|
|
# remove shape key action
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
# remove shape key strips
|
|
if obj.data.shape_keys and obj.data.shape_keys.animation_data:
|
|
ad = obj.data.shape_keys.animation_data
|
|
if ad:
|
|
while ad.nla_tracks:
|
|
ad.nla_tracks.remove(ad.nla_tracks[0])
|
|
return track
|
|
|
|
|
|
def create_key_proxy_object(obj_id, action: bpy.types.Action=None,
|
|
shape_keys=None, parent=None, channel=None):
|
|
# create object
|
|
bpy.ops.mesh.primitive_cube_add(size=0.1, enter_editmode=False,
|
|
align='WORLD',
|
|
location=(0, 0, 0),
|
|
scale=(1, 1, 1))
|
|
obj: bpy.types.Object = utils.get_active_object()
|
|
obj.shape_key_add(name="Basis")
|
|
name = f"Key_Proxy_{obj_id}"
|
|
obj.name = name
|
|
obj.data.name = name
|
|
obj["key_proxy"] = "WqebNXksi9wLQwco1hyFQMlIYcbqWGZF"
|
|
obj.data["key_proxy"] = "WqebNXksi9wLQwco1hyFQMlIYcbqWGZF"
|
|
if parent:
|
|
obj.parent = parent
|
|
obj.hide_set(True)
|
|
|
|
if channel:
|
|
for fcurve in channel.fcurves:
|
|
data_path = fcurve.data_path
|
|
if data_path.startswith("key_blocks["):
|
|
key_name = data_path[12:-8]
|
|
key = obj.shape_key_add(name=key_name)
|
|
key.slider_max = 1.5
|
|
key.slider_min = -1.5
|
|
|
|
elif action:
|
|
channel = utils.get_action_channelbag(action, slot_type="KEY")
|
|
for fcurve in channel.fcurves:
|
|
data_path = fcurve.data_path
|
|
if data_path.startswith("key_blocks["):
|
|
key_name = data_path[12:-8]
|
|
key = obj.shape_key_add(name=key_name)
|
|
key.slider_max = 1.5
|
|
key.slider_min = -1.5
|
|
|
|
elif shape_keys:
|
|
if "Basis" in shape_keys:
|
|
shape_keys.remove("Basis")
|
|
for key_name in shape_keys:
|
|
key = obj.shape_key_add(name=key_name)
|
|
key.slider_max = 1.5
|
|
key.slider_min = -1.5
|
|
|
|
return obj
|
|
|
|
|
|
def get_shape_key_action_objects(rigify_rig, source_rig, source_action=None, shape_keys=None):
|
|
objects = []
|
|
|
|
if source_rig and source_action:
|
|
|
|
source_actions = find_source_actions(source_action, source_rig)
|
|
|
|
if source_actions["slotted"]:
|
|
channelbags = utils.get_action_channelbags(source_action)
|
|
for channel in channelbags:
|
|
slot = channel.slot
|
|
if slot.target_id_type == "KEY":
|
|
obj_id = slot.name_display
|
|
obj = create_key_proxy_object(obj_id, parent=source_rig, channel=channel)
|
|
utils.safe_set_action(obj.data.shape_keys, source_action, slot=slot)
|
|
objects.append(obj)
|
|
|
|
else:
|
|
for obj_id, obj_action in source_actions["keys"].items():
|
|
# we don't need all the objects, just these three
|
|
if obj_id in ["Body", "Tongue", "Eye"]:
|
|
obj = create_key_proxy_object(obj_id, action=obj_action, parent=source_rig)
|
|
utils.safe_set_action(obj.data.shape_keys, obj_action)
|
|
objects.append(obj)
|
|
|
|
elif shape_keys:
|
|
obj = create_key_proxy_object(f"Key_Proxy_{rigify_rig.name}", shape_keys=shape_keys, parent=source_rig)
|
|
objects.append(obj)
|
|
|
|
return objects
|
|
|
|
|
|
def apply_fast_key_proxies(objects=None):
|
|
store = {}
|
|
if not objects:
|
|
objects = list(bpy.data.objects)
|
|
RBWC = bpy.data.collections["RigidBodyWorld"] if "RigidBodyWorld" in bpy.data.collections else None
|
|
for obj in objects:
|
|
if utils.object_exists_is_mesh(obj):
|
|
|
|
# don't replace widgets
|
|
if obj.name.startswith("WGT-"): continue
|
|
# don't replace rigid body world meshes
|
|
if RBWC and obj.name in RBWC.objects: continue
|
|
# don't replace existing key_proxies
|
|
if "key_proxy" in obj: continue
|
|
|
|
key_action, key_slot = utils.safe_get_action_slot(obj.data.shape_keys)
|
|
|
|
if utils.object_has_shape_keys(obj):
|
|
keys = [ key.name for key in obj.data.shape_keys.key_blocks ]
|
|
else:
|
|
keys = None
|
|
values = None
|
|
proxy = create_key_proxy_object(obj.name, shape_keys=keys)
|
|
proxy_mesh = proxy.data
|
|
bpy.data.objects.remove(proxy)
|
|
store[obj.name] = obj.data
|
|
obj.data = proxy_mesh
|
|
|
|
utils.safe_set_action(proxy_mesh.shape_keys, key_action, slot=key_slot)
|
|
return store
|
|
|
|
|
|
def restore_fast_key_proxies(store):
|
|
if store:
|
|
for obj_name in store:
|
|
if obj_name in bpy.data.objects:
|
|
obj = bpy.data.objects[obj_name]
|
|
if utils.get_prop(obj.data, "key_proxy") == "WqebNXksi9wLQwco1hyFQMlIYcbqWGZF":
|
|
proxy_mesh = obj.data
|
|
obj.data = store[obj_name]
|
|
if obj.data.shape_keys:
|
|
action, slot = utils.safe_get_action_slot(proxy_mesh.shape_keys)
|
|
utils.safe_set_action(obj.data.shape_keys, action, slot=slot)
|
|
bpy.data.meshes.remove(proxy_mesh)
|
|
|
|
|
|
def clean_up_shape_key_proxy_objects():
|
|
for obj in bpy.data.objects:
|
|
if utils.get_prop(obj, "key_proxy") == "WqebNXksi9wLQwco1hyFQMlIYcbqWGZF":
|
|
utils.delete_object(obj)
|
|
|
|
|
|
def get_nla_tracks(data):
|
|
try:
|
|
if data and data.animation_data and data.animation_data.nla_tracks:
|
|
return data.animation_data.nla_tracks
|
|
except:
|
|
return None
|
|
|
|
|
|
def get_all_nla_strips(data, obj, strips=None):
|
|
if strips is None:
|
|
strips = {}
|
|
tracks = get_nla_tracks(data)
|
|
if tracks:
|
|
for track in tracks:
|
|
for strip in track.strips:
|
|
strips[strip] = (obj, track)
|
|
return strips
|
|
|
|
|
|
def get_strips_by_sets(set_ids: set):
|
|
all_strips = {}
|
|
for obj in bpy.data.objects:
|
|
if utils.object_exists(obj):
|
|
if obj.type == "ARMATURE":
|
|
get_all_nla_strips(obj, obj, all_strips)
|
|
elif obj.type == "MESH":
|
|
get_all_nla_strips(obj.data.shape_keys, obj, all_strips)
|
|
strip: bpy.types.NlaStrip
|
|
strips = {}
|
|
for strip in all_strips:
|
|
strip_set_id = utils.get_prop(strip.action, "rl_set_id")
|
|
for sel_set_id, sel_auto_index in set_ids:
|
|
if strip_set_id == sel_set_id:
|
|
strip_auto_index = utils.get_auto_index_suffix(strip.name)
|
|
if strip_auto_index == sel_auto_index:
|
|
obj, track = all_strips[strip]
|
|
strips[strip] = (obj, track)
|
|
return strips
|
|
|
|
|
|
def unselect_all_but_strip(active_strip):
|
|
all_strips = {}
|
|
for obj in bpy.data.objects:
|
|
if utils.object_exists(obj):
|
|
if obj.type == "ARMATURE":
|
|
get_all_nla_strips(obj, obj, all_strips)
|
|
elif obj.type == "MESH":
|
|
get_all_nla_strips(obj.data.shape_keys, obj, all_strips)
|
|
for strip in all_strips:
|
|
if strip != active_strip and strip.select:
|
|
strip.select = False
|
|
|
|
|
|
def select_strips_by_set(active_strip: bpy.types.NlaStrip):
|
|
strips = bpy.context.selected_nla_strips.copy()
|
|
set_ids = set()
|
|
for strip in strips:
|
|
set_id = utils.get_prop(strip.action, "rl_set_id")
|
|
strip_auto_index = utils.get_auto_index_suffix(strip.name)
|
|
if set_id and strip_auto_index:
|
|
set_ids.add((set_id, strip_auto_index))
|
|
if not active_strip and bpy.context.selected_nla_strips:
|
|
active_strip = bpy.context.selected_nla_strips[0]
|
|
if active_strip:
|
|
unselect_all_but_strip(active_strip)
|
|
strips = get_strips_by_sets(set_ids)
|
|
for strip in strips:
|
|
strip.select = True
|
|
|
|
|
|
def align_strips(strips, to_strip: bpy.types.NlaStrip=None, left=True):
|
|
strip: bpy.types.NlaStrip
|
|
left_frame = None if not to_strip else to_strip.frame_start
|
|
right_frame = None if not to_strip else to_strip.frame_end
|
|
# if no active strip, get the left most and right most frame in all strips
|
|
if not to_strip:
|
|
for strip in strips:
|
|
if left_frame is None:
|
|
left_frame = strip.frame_start
|
|
if right_frame is None:
|
|
right_frame = strip.frame_end
|
|
left_frame = min(strip.frame_start, left_frame)
|
|
right_frame = max(strip.frame_end, right_frame)
|
|
# align strips
|
|
# TODO sort strips in reverse order of direction
|
|
for strip in strips:
|
|
length = strip.frame_end - strip.frame_start
|
|
if left:
|
|
strip.frame_start = left_frame
|
|
strip.frame_end = strip.frame_start + length
|
|
strip.frame_start = strip.frame_end - length
|
|
else:
|
|
strip.frame_end = right_frame
|
|
strip.frame_start = strip.frame_end - length
|
|
strip.frame_end = strip.frame_start + length
|
|
|
|
|
|
def size_strips(strips, to_strip: bpy.types.NlaStrip=None, longest=True, reset=False):
|
|
to_length = 0
|
|
if to_strip:
|
|
to_length = to_strip.frame_end - to_strip.frame_start
|
|
min_length = None
|
|
max_length = None
|
|
strip: bpy.types.NlaStrip
|
|
# find the shortest and longest strip lengths
|
|
for strip in strips:
|
|
length = strip.frame_end - strip.frame_start
|
|
if min_length is None:
|
|
min_length = length
|
|
if max_length is None:
|
|
max_length = length
|
|
min_length = min(length, min_length)
|
|
max_length = max(length, max_length)
|
|
if not to_strip:
|
|
to_length = max_length if longest else min_length
|
|
for strip in strips:
|
|
if reset:
|
|
action_length = int(strip.action.frame_range[1] - strip.action.frame_range[0])
|
|
strip.frame_end = strip.frame_start + action_length
|
|
strip.frame_start = strip.frame_end - action_length
|
|
elif to_length > 0:
|
|
strip.frame_end = strip.frame_start + to_length
|
|
strip.frame_start = strip.frame_end - to_length
|
|
|
|
|
|
def set_action_set_fake_user(action, use_fake_user):
|
|
set_id = utils.get_prop(action, "rl_set_id")
|
|
if set_id:
|
|
for action in bpy.data.actions:
|
|
action_set_id = utils.get_prop(action, "rl_set_id")
|
|
if action_set_id == set_id:
|
|
action.use_fake_user = use_fake_user
|
|
utils.update_ui(all=True)
|
|
|
|
|
|
def delete_motion_set(action_or_set_id):
|
|
if type(action_or_set_id) is bpy.types.Action:
|
|
set_id = utils.get_prop(action_or_set_id, "rl_set_id")
|
|
else:
|
|
set_id = action_or_set_id
|
|
to_remove = []
|
|
if set_id:
|
|
for action in bpy.data.actions:
|
|
action_set_id = utils.get_prop(action, "rl_set_id")
|
|
if action_set_id == set_id:
|
|
to_remove.append(action)
|
|
for action in to_remove:
|
|
bpy.data.actions.remove(action)
|
|
utils.update_ui(all=True)
|
|
|
|
|
|
def duplicate_motion_set(source_action):
|
|
set_id = utils.get_prop(source_action, "rl_set_id")
|
|
src_set_generation = utils.get_prop(source_action, "rl_set_generation", None)
|
|
src_action_type_id = utils.get_prop(source_action, "rl_action_type", None)
|
|
new_set_id = generate_set_id()
|
|
duplicates = []
|
|
new_action = None
|
|
if set_id:
|
|
for action in bpy.data.actions:
|
|
action_set_id = utils.get_prop(action, "rl_set_id")
|
|
if action_set_id == set_id:
|
|
utils.log_detail(f"Duplicating action: {action.name}")
|
|
set_generation = utils.get_prop(action, "rl_set_generation", src_set_generation)
|
|
action_type_id = utils.get_prop(action, "rl_action_type", src_action_type_id)
|
|
obj_id = utils.get_prop(action, "rl_key_object")
|
|
rlob_id = utils.get_rl_id(action)
|
|
rlx_id = utils.get_prop(action, "rl_rlx_id")
|
|
arm_id = utils.get_prop(action, "rl_armature_id")
|
|
is_slotted = action_type_id == "SLOTTED"
|
|
is_light = action_type_id == "LIGHT"
|
|
is_camera = action_type_id == "CAMERA"
|
|
is_rlx = action_type_id == "RLX" or rlx_id is not None
|
|
rlx_id = rlob_id if is_rlx else None
|
|
arm_id = None if is_rlx else rlob_id
|
|
duplicate_action = action.copy()
|
|
add_motion_set_data(duplicate_action, new_set_id, set_generation,
|
|
key_id=obj_id, arm_id=arm_id, rlx_id=rlx_id,
|
|
is_light=is_light, is_camera=is_camera,
|
|
slotted=is_slotted),
|
|
duplicates.append(duplicate_action)
|
|
if action_type_id == "SLOTTED" or action_type_id == "ARM" or action_type_id == "RLX":
|
|
new_action = duplicate_action
|
|
if new_action:
|
|
update_motion_set_index(new_action)
|
|
utils.log_detail(f"Using duplicate main action: {new_action.name}")
|
|
else:
|
|
utils.log_error(f"Duplicate main action not found!")
|
|
utils.update_ui(all=True)
|
|
#duplicate_set = []
|
|
#if new_action:
|
|
# duplicate_set.append(new_action)
|
|
#for action in duplicates:
|
|
# if action not in duplicate_set:
|
|
# duplicate_set.append(action)
|
|
return new_action #, duplicate_set
|
|
|
|
|
|
def get_new_motion_set_action_name(action_type, set_id, prefix, rig_id, motion_id, rlob=None):
|
|
motion_id = get_unique_set_motion_id(rig_id, motion_id, prefix,
|
|
exclude_set_id=set_id,
|
|
slotted=(action_type == "SLOTTED"), rlob=rlob)
|
|
name = ""
|
|
if action_type == "SLOTTED":
|
|
name = make_armature_action_name(rig_id, motion_id, prefix, slotted=True)
|
|
else:
|
|
name = make_armature_action_name(rig_id, motion_id, prefix, slotted=False)
|
|
return name
|
|
|
|
|
|
def rename_motion_set(action_type, set_id, prefix, rig_id, motion_id):
|
|
actions = get_motion_set_actions(set_id)
|
|
is_rlx = is_rlx_motion(actions)
|
|
motion_id = get_unique_set_motion_id(rig_id, motion_id, prefix,
|
|
exclude_set_id=set_id,
|
|
slotted=(action_type == "SLOTTED"), rlx=is_rlx)
|
|
for action in actions:
|
|
a_set_id, a_set_generation, action_type_id, obj_id = get_motion_set_ids(action)
|
|
if action_type_id == "ARM":
|
|
name = make_armature_action_name(rig_id, motion_id, prefix, slotted=False)
|
|
action.name = name
|
|
elif action_type_id == "KEY":
|
|
name = make_key_action_name(rig_id, motion_id, obj_id, prefix)
|
|
action.name = name
|
|
elif action_type_id == "SLOTTED":
|
|
name = make_armature_action_name(rig_id, motion_id, prefix, slotted=True)
|
|
action.name = name
|
|
elif action_type_id == "RLX":
|
|
name = make_rlx_action_name(rig_id, motion_id, prefix, slotted=True)
|
|
action.name = name
|
|
|
|
|
|
def rename_armature(arm, name):
|
|
armature_object = None
|
|
armature_data = None
|
|
try:
|
|
armature_object = bpy.data.objects[name]
|
|
except:
|
|
pass
|
|
try:
|
|
armature_data = bpy.data.armatures[name]
|
|
except:
|
|
pass
|
|
utils.force_object_name(arm, name)
|
|
utils.force_armature_name(arm.data, name)
|
|
return armature_object, armature_data
|
|
|
|
|
|
def restore_armature_names(armature_object, armature_data, name):
|
|
if armature_object:
|
|
utils.force_object_name(armature_object, name)
|
|
if armature_data:
|
|
utils.force_armature_name(armature_data, name)
|
|
|
|
|
|
def get_rigify_ik_fk_influence_avg(rig):
|
|
ik_fk = 0
|
|
num_bones = 0
|
|
ik_fk_control_bones = ["upper_arm_parent.L", "upper_arm_parent.R", "thigh_parent.L", "thigh_parent.R"]
|
|
for bone_name in ik_fk_control_bones:
|
|
if bone_name in rig.pose.bones:
|
|
num_bones += 1
|
|
pose_bone = rig.pose.bones[bone_name]
|
|
ik_fk += pose_bone["IK_FK"]
|
|
if num_bones > 0:
|
|
ik_fk /= num_bones
|
|
return ik_fk
|
|
|
|
|
|
def get_rigify_ik_fk_influence(rig):
|
|
ik_fk_control_bones = ["upper_arm_parent.L", "upper_arm_parent.R", "thigh_parent.L", "thigh_parent.R"]
|
|
ik_fk = [0,0,0,0]
|
|
for i, bone_name in enumerate(ik_fk_control_bones):
|
|
if bone_name in rig.pose.bones:
|
|
pose_bone = rig.pose.bones[bone_name]
|
|
ik_fk[i] = pose_bone["IK_FK"]
|
|
return ik_fk
|
|
|
|
|
|
def set_rigify_ik_fk_influence(rig, ik_fk):
|
|
ik_fk_control_bones = ["upper_arm_parent.L", "upper_arm_parent.R", "thigh_parent.L", "thigh_parent.R"]
|
|
if type(ik_fk) is list:
|
|
for i, bone_name in enumerate(ik_fk_control_bones):
|
|
if bone_name in rig.pose.bones:
|
|
pose_bone = rig.pose.bones[bone_name]
|
|
pose_bone["IK_FK"] = ik_fk[i]
|
|
else:
|
|
for bone_name in ik_fk_control_bones:
|
|
if bone_name in rig.pose.bones:
|
|
pose_bone = rig.pose.bones[bone_name]
|
|
pose_bone["IK_FK"] = ik_fk
|
|
|
|
|
|
def poke_rig(rig):
|
|
"""Switches modes on the armature and sets the root pose bone location to force updates
|
|
after changing custom paramters i.e. IK_FK on the limbs."""
|
|
state = utils.store_mode_selection_state()
|
|
select_rig(rig)
|
|
pose_bone: bpy.types.PoseBone = rig.pose.bones[0]
|
|
loc = pose_bone.location
|
|
pose_bone.location = loc
|
|
pose_rig(rig)
|
|
utils.restore_mode_selection_state(state)
|
|
|
|
|
|
def set_bone_tail_length(bone: bpy.types.EditBone, tail):
|
|
"""Set the length based on a new tail position, but don't set the tail directly
|
|
as it may cause changes in the bone roll and angles."""
|
|
if bone and tail:
|
|
if type(tail) is bpy.types.EditBone:
|
|
new_tail = tail.head.copy()
|
|
elif type(tail) is Vector:
|
|
new_tail = tail.copy()
|
|
length = (new_tail - bone.head).length
|
|
bone.length = length
|
|
|
|
|
|
def set_bone_deform(bone: bpy.types.EditBone, use_deform, objects):
|
|
try:
|
|
if bone:
|
|
# don't set non deform on rig bones if there are objects weighted to it
|
|
for obj in objects:
|
|
if bone.name in obj.vertex_groups:
|
|
return
|
|
bone.use_deform = use_deform
|
|
except: ...
|
|
|
|
|
|
def fix_cc3_standard_rig(cc3_rig: bpy.types.Object):
|
|
if edit_rig(cc3_rig):
|
|
cc3_rig.show_in_front = False
|
|
objects = utils.get_child_objects(cc3_rig, of_type="MESH")
|
|
left_eye = bones.get_edit_bone(cc3_rig, "CC_Base_L_Eye")
|
|
right_eye = bones.get_edit_bone(cc3_rig, "CC_Base_R_Eye")
|
|
left_hand = bones.get_edit_bone(cc3_rig, ["CC_Base_L_Hand", "hand_l"])
|
|
right_hand = bones.get_edit_bone(cc3_rig, ["CC_Base_R_Hand", "hand_r"])
|
|
left_foot = bones.get_edit_bone(cc3_rig, ["CC_Base_L_Foot", "foot_l"])
|
|
right_foot = bones.get_edit_bone(cc3_rig, ["CC_Base_R_Foot", "foot_r"])
|
|
head = bones.get_edit_bone(cc3_rig, ["CC_Base_Head", "head"])
|
|
left_upper_arm = bones.get_edit_bone(cc3_rig, ["CC_Base_L_Upperarm", "upperarm_l"])
|
|
right_upper_arm = bones.get_edit_bone(cc3_rig, ["CC_Base_R_Upperarm", "upperarm_r"])
|
|
left_lower_arm = bones.get_edit_bone(cc3_rig, ["CC_Base_L_Forearm", "lowerarm_l"])
|
|
right_lower_arm = bones.get_edit_bone(cc3_rig, ["CC_Base_R_Forearm", "lowerarm_r"])
|
|
left_thigh = bones.get_edit_bone(cc3_rig, ["CC_Base_L_Thigh", "thigh_l"])
|
|
right_thigh = bones.get_edit_bone(cc3_rig, ["CC_Base_R_Thigh", "thigh_r"])
|
|
left_calf = bones.get_edit_bone(cc3_rig, ["CC_Base_L_Calf", "calf_l"])
|
|
right_calf = bones.get_edit_bone(cc3_rig, ["CC_Base_R_Calf", "calf_r"])
|
|
# fix deform state
|
|
for bone_name in bones.NONE_DEFORM_BONES:
|
|
edit_bone = bones.get_edit_bone(cc3_rig, bone_name)
|
|
if edit_bone:
|
|
set_bone_deform(edit_bone, False, objects)
|
|
# eyes
|
|
eye_z = None
|
|
if left_eye and right_eye:
|
|
eye_z = ((left_eye.head + right_eye.head) * 0.5).z
|
|
# head
|
|
if head:
|
|
head_tail = head.tail.copy()
|
|
head_tail.z = eye_z + (eye_z - head.head.z) * 0.5
|
|
set_bone_tail_length(head, head_tail)
|
|
# arms
|
|
set_bone_tail_length(left_upper_arm, left_lower_arm)
|
|
set_bone_tail_length(right_upper_arm, right_lower_arm)
|
|
set_bone_tail_length(left_lower_arm, left_hand)
|
|
set_bone_tail_length(right_lower_arm, right_hand)
|
|
# legs
|
|
set_bone_tail_length(left_thigh, left_calf)
|
|
set_bone_tail_length(right_thigh, right_calf)
|
|
set_bone_tail_length(left_calf, left_foot)
|
|
set_bone_tail_length(right_calf, right_foot)
|
|
select_rig(cc3_rig)
|
|
|
|
|
|
def reset_rotation_modes(rig, rotation_mode = "QUATERNION"):
|
|
pose_bone: bpy.types.PoseBone
|
|
for pose_bone in rig.pose.bones:
|
|
pose_bone.rotation_mode = rotation_mode
|
|
|
|
|
|
def is_skinned_rig(rig):
|
|
meshes = utils.get_child_objects(rig)
|
|
for mesh in meshes:
|
|
mod = None
|
|
for m in mesh.modifiers:
|
|
if m and m.type == "ARMATURE":
|
|
mod = m
|
|
if mod:
|
|
return True
|
|
return False
|
|
|
|
|
|
def is_face_rig(rig):
|
|
return ("facerig" in rig.pose.bones)
|
|
|
|
|
|
BASE_RIG_COLLECTION_CCIC240 = ["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" ]
|
|
BASE_RIG_COLLECTION = ["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" ]
|
|
BASE_RIG_LAYERS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,28]
|
|
BASE_DEF_COLLECTION = ["DEF"]
|
|
BASE_DEF_LAYERS = [29]
|
|
|
|
FULL_RIG_COLLECTION_CCIC240 = ["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",
|
|
"Spring (IK)", "Spring (FK)", "Spring (Tweak)"]
|
|
FULL_RIG_COLLECTION = ["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",
|
|
"Spring (IK)", "Spring (FK)", "Spring (Tweak)",
|
|
"Face (Expressions)", "Face (UI)"]
|
|
FULL_RIG_LAYERS = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,28]
|
|
FULL_DEF_COLLECTION = ["DEF", "Spring (Edit)", "Spring (Root)"]
|
|
FULL_DEF_LAYERS = [24, 25, 29]
|
|
|
|
SPRING_RIG_COLLECTION = ["Spring (IK)", "Spring (FK)", "Spring (Tweak)"]
|
|
SPRING_RIG_LAYERS = [19,20,21]
|
|
SPRING_DEF_COLLECTION = ["Spring (Edit)", "Spring (Root)"]
|
|
SPRING_DEF_LAYERS = [24,25]
|
|
|
|
FACE_RIG_COLLECTION_CCIC240 = ["Face"]
|
|
FACE_RIG_COLLECTION = ["Face (Expressions)", "Face (UI)"]
|
|
FACE_RIG_LAYERS = [22,23]
|
|
FACE_RIG_HIDE_CCIC240 = ["Face (Primary)", "Face (Secondary)"]
|
|
FACE_RIG_HIDE = ["Face", "Face (Primary)", "Face (Secondary)"]
|
|
FACE_RIG_HIDE_LAYERS = [0,1,2]
|
|
|
|
|
|
def show_hide_collections_layers(rig, collections, layers, show=True):
|
|
if rig:
|
|
if utils.B400():
|
|
for collection in rig.data.collections:
|
|
if collection.name in collections:
|
|
collection.is_visible = show
|
|
else:
|
|
for i in range(0, 32):
|
|
if i in layers:
|
|
rig.data.layers[i] = show
|
|
|
|
|
|
def is_full_rigify_rig_shown(rig):
|
|
if rig:
|
|
FRC = FULL_RIG_COLLECTION_CCIC240 if facerig.is_ccic_240_rig(rig) else FULL_RIG_COLLECTION
|
|
FRH = FACE_RIG_HIDE_CCIC240 if facerig.is_ccic_240_rig(rig) else FACE_RIG_HIDE
|
|
face_rig = is_face_rig(rig)
|
|
if utils.B400():
|
|
for collection in rig.data.collections:
|
|
if face_rig and collection.name in FRH:
|
|
continue
|
|
if collection.name in FRC and not collection.is_visible:
|
|
return False
|
|
else:
|
|
for i in range(0, 32):
|
|
if face_rig and i in FACE_RIG_HIDE_LAYERS:
|
|
continue
|
|
if i in FULL_RIG_LAYERS and rig.data.layers[i] == False:
|
|
return False
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def toggle_show_full_rig(rig):
|
|
if rig:
|
|
FRC = FULL_RIG_COLLECTION_CCIC240 if facerig.is_ccic_240_rig(rig) else FULL_RIG_COLLECTION
|
|
BRC = BASE_RIG_COLLECTION_CCIC240 if facerig.is_ccic_240_rig(rig) else BASE_RIG_COLLECTION
|
|
FRH = FACE_RIG_HIDE_CCIC240 if facerig.is_ccic_240_rig(rig) else FACE_RIG_HIDE
|
|
face_rig = is_face_rig(rig)
|
|
show = not is_full_rigify_rig_shown(rig)
|
|
if utils.B400():
|
|
if show:
|
|
for collection in rig.data.collections:
|
|
if face_rig and collection.name in FRH:
|
|
collection.is_visible = False
|
|
else:
|
|
collection.is_visible = collection.name in FRC
|
|
else:
|
|
for collection in rig.data.collections:
|
|
if face_rig and collection.name in FRH:
|
|
collection.is_visible = False
|
|
else:
|
|
collection.is_visible = collection.name in BRC
|
|
else:
|
|
if show:
|
|
rig.data.layers[vars.ROOT_BONE_LAYER] = True
|
|
else:
|
|
rig.data.layers[vars.DEF_BONE_LAYER] = True
|
|
for i in range(0, 32):
|
|
if show:
|
|
rig.data.layers[i] = i in FULL_RIG_LAYERS
|
|
else:
|
|
rig.data.layers[i] = i in BASE_RIG_LAYERS
|
|
if face_rig:
|
|
for layer in FACE_RIG_HIDE_LAYERS:
|
|
rig.data.layers[layer] = False
|
|
|
|
|
|
def is_base_rig_shown(rig):
|
|
if rig:
|
|
FRH = FACE_RIG_HIDE_CCIC240 if facerig.is_ccic_240_rig(rig) else FACE_RIG_HIDE
|
|
BRC = BASE_RIG_COLLECTION_CCIC240 if facerig.is_ccic_240_rig(rig) else BASE_RIG_COLLECTION
|
|
face_rig = is_face_rig(rig)
|
|
if utils.B400():
|
|
for collection in rig.data.collections:
|
|
if face_rig and collection.name in FRH:
|
|
continue
|
|
if collection.name in BRC and not collection.is_visible:
|
|
return False
|
|
else:
|
|
for i in range(0, 32):
|
|
if face_rig and i in FACE_RIG_HIDE_LAYERS:
|
|
continue
|
|
if i in BASE_RIG_LAYERS and rig.data.layers[i] == False:
|
|
return False
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def toggle_show_base_rig(rig):
|
|
if rig:
|
|
FRH = FACE_RIG_HIDE_CCIC240 if facerig.is_ccic_240_rig(rig) else FACE_RIG_HIDE
|
|
BRC = BASE_RIG_COLLECTION_CCIC240 if facerig.is_ccic_240_rig(rig) else BASE_RIG_COLLECTION
|
|
show = True
|
|
face_rig = is_face_rig(rig)
|
|
if is_full_rigify_rig_shown(rig):
|
|
show = True
|
|
elif is_base_rig_shown(rig):
|
|
show = False
|
|
if utils.B400():
|
|
if show:
|
|
for collection in rig.data.collections:
|
|
if face_rig and collection.name in FRH:
|
|
collection.is_visible = False
|
|
else:
|
|
collection.is_visible = collection.name in BRC
|
|
else:
|
|
for collection in rig.data.collections:
|
|
collection.is_visible = collection.name in BASE_DEF_COLLECTION
|
|
else:
|
|
if show:
|
|
rig.data.layers[vars.ROOT_BONE_LAYER] = True
|
|
else:
|
|
rig.data.layers[vars.DEF_BONE_LAYER] = True
|
|
for i in range(0, 32):
|
|
if show:
|
|
rig.data.layers[i] = i in BASE_RIG_LAYERS
|
|
else:
|
|
rig.data.layers[i] = i in BASE_DEF_LAYERS
|
|
if face_rig:
|
|
for layer in FACE_RIG_HIDE_LAYERS:
|
|
rig.data.layers[layer] = False
|
|
|
|
|
|
def is_spring_rig_shown(rig):
|
|
if rig:
|
|
if utils.B400():
|
|
for collection in rig.data.collections:
|
|
if collection.name in SPRING_RIG_COLLECTION and not collection.is_visible:
|
|
return False
|
|
else:
|
|
for i in range(0, 32):
|
|
if i in SPRING_RIG_LAYERS and rig.data.layers[i] == False:
|
|
return False
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def toggle_show_spring_rig(rig):
|
|
if rig:
|
|
show = True
|
|
if is_full_rigify_rig_shown(rig):
|
|
show = True
|
|
elif is_spring_rig_shown(rig):
|
|
show = False
|
|
if utils.B400():
|
|
if show:
|
|
for collection in rig.data.collections:
|
|
collection.is_visible = collection.name in SPRING_RIG_COLLECTION
|
|
else:
|
|
for collection in rig.data.collections:
|
|
collection.is_visible = collection.name in SPRING_DEF_COLLECTION
|
|
else:
|
|
if show:
|
|
rig.data.layers[vars.SPRING_IK_LAYER] = True
|
|
else:
|
|
rig.data.layers[vars.DEF_BONE_LAYER] = True
|
|
|
|
for i in range(0, 32):
|
|
if show:
|
|
rig.data.layers[i] = i in SPRING_RIG_LAYERS
|
|
else:
|
|
rig.data.layers[i] = i in SPRING_DEF_LAYERS
|
|
|
|
|
|
def is_only_face_rig_shown(rig):
|
|
only_shown = False
|
|
shown = True
|
|
if rig:
|
|
FRC = FACE_RIG_COLLECTION_CCIC240 if facerig.is_ccic_240_rig(rig) else FACE_RIG_COLLECTION
|
|
if utils.B400():
|
|
|
|
for collection in rig.data.collections:
|
|
if collection.name in FRC:
|
|
if collection.is_visible:
|
|
only_shown = True
|
|
else:
|
|
shown = False
|
|
for collection in rig.data.collections:
|
|
if collection.name not in FRC:
|
|
if collection.is_visible:
|
|
return shown, False
|
|
else:
|
|
for layer in range(0, 32):
|
|
if layer in FRC:
|
|
if rig.data.layers[layer]:
|
|
only_shown = True
|
|
else:
|
|
shown = False
|
|
for layer in range(0, 32):
|
|
if layer not in FRC:
|
|
if rig.data.layers[layer]:
|
|
return shown, False
|
|
return shown, only_shown
|
|
|
|
|
|
def toggle_show_only_face_rig(rig):
|
|
if rig:
|
|
FRC = FACE_RIG_COLLECTION_CCIC240 if facerig.is_ccic_240_rig(rig) else FACE_RIG_COLLECTION
|
|
BRC = BASE_RIG_COLLECTION_CCIC240 if facerig.is_ccic_240_rig(rig) else BASE_RIG_COLLECTION
|
|
FRH = FACE_RIG_HIDE_CCIC240 if facerig.is_ccic_240_rig(rig) else FACE_RIG_HIDE
|
|
face_rig = is_face_rig(rig)
|
|
show_only = False
|
|
if utils.B400():
|
|
for collection in rig.data.collections:
|
|
if collection.name not in FRC and collection.is_visible:
|
|
show_only = True
|
|
else:
|
|
for layer in range(0, 32):
|
|
if rig.data.layers[layer] and layer not in FACE_RIG_LAYERS:
|
|
show_only = True
|
|
|
|
if utils.B400():
|
|
if show_only:
|
|
for collection in rig.data.collections:
|
|
collection.is_visible = collection.name in FRC
|
|
else:
|
|
for collection in rig.data.collections:
|
|
if face_rig and collection.name in FRH:
|
|
collection.is_visible = False
|
|
else:
|
|
collection.is_visible = collection.name in BRC
|
|
else:
|
|
if show_only:
|
|
rig.data.layers[22] = True
|
|
rig.data.layers[23] = True
|
|
for layer in range(0, 32):
|
|
rig.data.layers[layer] = layer in FACE_RIG_LAYERS
|
|
else:
|
|
rig.data.layers[22] = False
|
|
rig.data.layers[23] = False
|
|
for layer in range(0, 32):
|
|
rig.data.layers[layer] = layer in BASE_RIG_LAYERS and layer not in FACE_RIG_HIDE_LAYERS
|
|
|
|
|
|
def reset_pose(rig, exceptions=None, use_selected=False):
|
|
if rig:
|
|
utils.pose_mode_to(rig)
|
|
rig.data.pose_position = "POSE"
|
|
bones_data = {}
|
|
for pose_bone in rig.pose.bones:
|
|
bone = pose_bone.bone
|
|
if exceptions and pose_bone.name in exceptions:
|
|
bones.select_bone(rig, pose_bone, False)
|
|
continue
|
|
selected = bones.get_bone_selected(rig, bone)
|
|
bones_data[bone] = (selected, bone.hide, bone.hide_select)
|
|
if not use_selected:
|
|
bones.select_bone(rig, bone, True)
|
|
bone.hide = False
|
|
if bones.can_unlock(pose_bone):
|
|
bone.hide_select = False
|
|
bpy.ops.pose.transforms_clear()
|
|
for bone in rig.data.bones:
|
|
if bone in bones_data:
|
|
selected, bone.hide, bone.hide_select = bones_data[bone]
|
|
bones.select_bone(rig, bone, selected)
|
|
|
|
|
|
def reset_shape_keys(mesh):
|
|
if mesh and utils.object_has_shape_keys(mesh):
|
|
key: bpy.types.ShapeKey
|
|
for key in mesh.data.shape_keys.key_blocks:
|
|
key.value = 0.0
|
|
|
|
|
|
def is_rig_rest_position(rig):
|
|
if rig:
|
|
if rig.data.pose_position == "REST":
|
|
return True
|
|
return False
|
|
|
|
|
|
def toggle_rig_rest_position(rig):
|
|
if rig:
|
|
if rig.data.pose_position == "POSE":
|
|
rig.data.pose_position = "REST"
|
|
else:
|
|
rig.data.pose_position = "POSE"
|
|
|
|
|
|
def get_local_pose_bone_transform(M: Matrix, pose_bone: bpy.types.PoseBone):
|
|
"""M: Matrix - object space matrix of the transform to convert
|
|
pose_bone: bpy.types.PoseBone - pose bone to calculate local space transform for."""
|
|
L: Matrix # local space matrix we want
|
|
NL: Matrix # non-local space matrix we want (if not using local location or inherit rotation)
|
|
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()
|
|
return loc, rot, sca, L, NL
|
|
|
|
|
|
def apply_as_rest_pose(rig):
|
|
if rig and select_rig(rig):
|
|
objects = utils.get_child_objects(rig)
|
|
for obj in objects:
|
|
if utils.object_exists(obj):
|
|
vis = obj.visible_get()
|
|
if not vis:
|
|
utils.unhide(obj)
|
|
mod: bpy.types.ArmatureModifier = modifiers.get_object_modifier(obj, "ARMATURE")
|
|
if mod:
|
|
# apply armature modifier with preserve settings and mod order
|
|
modifiers.apply_modifier(obj, modifier=mod, preserving=True)
|
|
modifiers.get_armature_modifier(obj, create=True, armature=rig)
|
|
utils.hide(obj, not vis)
|
|
if pose_rig(rig):
|
|
bpy.ops.pose.armature_apply(selected=False)
|
|
utils.object_mode_to(rig)
|
|
|
|
|
|
def constrain_pose_rigs(src_rig, dst_rig):
|
|
# constrain the destination rig rest pose to the source rig pose
|
|
constraints = {}
|
|
if select_rig(src_rig):
|
|
src_bone: bpy.types.PoseBone
|
|
dst_bone: bpy.types.PoseBone
|
|
for src_bone in src_rig.pose.bones:
|
|
if src_bone.name in dst_rig.pose.bones:
|
|
dst_bone = dst_rig.pose.bones[src_bone.name]
|
|
con = bones.add_copy_transforms_constraint(src_rig, dst_rig, src_bone.name, dst_bone.name)
|
|
constraints[dst_bone] = con
|
|
return constraints
|
|
|
|
|
|
def unconstrain_pose_rigs(constraints):
|
|
# remove the constraints
|
|
for dst_bone in constraints:
|
|
con = constraints[dst_bone]
|
|
dst_bone.constraints.remove(con)
|
|
|
|
|
|
def cmp_matrix(A: Matrix, B: Matrix):
|
|
rows = len(A.row)
|
|
cols = len(A.col)
|
|
delta = 0
|
|
for i in range(0, rows):
|
|
for j in range(0, cols):
|
|
delta += abs(A[i][j] - B[i][j])
|
|
if delta < 0.001:
|
|
return True
|
|
return False
|
|
|
|
|
|
def is_rest_pose_same(src_rig, dst_rig):
|
|
if len(src_rig.data.bones) != len(dst_rig.data.bones):
|
|
return False
|
|
src_bone: bpy.types.Bone
|
|
dst_bone: bpy.types.Bone
|
|
for src_bone in src_rig.data.bones:
|
|
if src_bone.name not in dst_rig.data.bones:
|
|
return False
|
|
dst_bone = dst_rig.data.bones[src_bone.name]
|
|
if not cmp_matrix(src_bone.matrix, dst_bone.matrix):
|
|
return False
|
|
return True
|
|
|
|
|
|
def copy_rest_pose(src_rig, dst_rig):
|
|
# TODO make everything visible...
|
|
temp_collection = utils.force_visible_in_scene("TMP_COPY_POSE", src_rig, dst_rig)
|
|
TS = utils.store_object_transform(src_rig)
|
|
TD = utils.store_object_transform(dst_rig)
|
|
utils.reset_object_transform(src_rig)
|
|
utils.reset_object_transform(dst_rig)
|
|
src_action, src_slot = utils.safe_get_action_slot(src_rig)
|
|
dst_action, dst_slot = utils.safe_get_action_slot(dst_rig)
|
|
utils.safe_set_action(src_rig, None)
|
|
utils.safe_set_action(dst_rig, None)
|
|
|
|
utils.try_select_objects([src_rig, dst_rig], clear_selection=True)
|
|
bones.clear_pose(src_rig)
|
|
bones.clear_pose(dst_rig)
|
|
|
|
# constrain the destination rig rest pose to the source rig pose
|
|
constraints = constrain_pose_rigs(src_rig, dst_rig)
|
|
|
|
# apply the destination pose as rest pose
|
|
apply_as_rest_pose(dst_rig)
|
|
|
|
# remove the constraints
|
|
unconstrain_pose_rigs(constraints)
|
|
|
|
utils.restore_object_transform(src_rig, TS)
|
|
utils.restore_object_transform(dst_rig, TD)
|
|
utils.safe_set_action(src_rig, src_action, slot=src_slot)
|
|
utils.safe_set_action(dst_rig, dst_action, slot=dst_slot)
|
|
|
|
utils.restore_visible_in_scene(temp_collection)
|
|
|
|
|
|
def bake_rig_action(src_rig, dst_rig, reuse_action=False):
|
|
src_action: bpy.types.Action = utils.safe_get_action(src_rig)
|
|
baked_action = None
|
|
|
|
if utils.try_select_object(dst_rig, True) and utils.set_active_object(dst_rig):
|
|
utils.log_info(f"Baking action: {src_action.name} to {dst_rig.name}")
|
|
# frame range
|
|
bpy.context.scene.use_preview_range = False
|
|
if src_action:
|
|
start_frame = int(src_action.frame_range[0])
|
|
end_frame = int(src_action.frame_range[1])
|
|
else:
|
|
start_frame = int(bpy.context.scene.frame_start)
|
|
end_frame = int(bpy.context.scene.frame_end)
|
|
|
|
# limit view layer to dst rig (bakes faster)
|
|
tmp_collection, layer_collections, to_hide = utils.limit_view_layer_to_collection("TMP_BAKE", dst_rig)
|
|
|
|
utils.set_active_object(dst_rig)
|
|
utils.set_mode("POSE")
|
|
|
|
# bake
|
|
bpy.ops.nla.bake(frame_start=start_frame,
|
|
frame_end=end_frame,
|
|
only_selected=True,
|
|
visual_keying=True,
|
|
use_current_action=reuse_action,
|
|
clear_constraints=False,
|
|
clean_curves=False,
|
|
bake_types={'POSE'})
|
|
|
|
# armature action
|
|
baked_action = utils.safe_get_action(dst_rig)
|
|
|
|
utils.object_mode()
|
|
|
|
# restore view layers
|
|
utils.restore_limited_view_layers(tmp_collection, layer_collections, to_hide)
|
|
|
|
# return the baked action
|
|
return baked_action
|
|
|
|
|
|
def bake_rig_action_from_source(src_rig, dst_rig):
|
|
"""Bakes the action from one rig to another, for when rigs have different bind poses but have the same bones.
|
|
Also copies any slotted key actions from the source.
|
|
Used by DataLink for retargeting prop animations."""
|
|
#temp_collection = utils.force_visible_in_scene("TMP_Bake_Retarget", src_rig, dst_rig)
|
|
rig_settings = bones.store_armature_settings(dst_rig)
|
|
# constrain the destination rig rest pose to the source rig pose
|
|
constraints = constrain_pose_rigs(src_rig, dst_rig)
|
|
baked_action = None
|
|
if select_rig(dst_rig):
|
|
bones.make_bones_visible(dst_rig)
|
|
bone : bpy.types.Bone
|
|
bones.select_all_bones(dst_rig, True)
|
|
baked_action = bake_rig_action(src_rig, dst_rig)
|
|
# remove contraints
|
|
unconstrain_pose_rigs(constraints)
|
|
bones.restore_armature_settings(dst_rig, rig_settings)
|
|
#load_slotted_action(dst_rig, baked_action)
|
|
#utils.restore_visible_in_scene(temp_collection)
|
|
return baked_action
|
|
|
|
|
|
DISABLE_TWEAK_STRETCH_IN = [
|
|
"DEF-thigh.R",
|
|
"DEF-thigh.R.001",
|
|
"DEF-shin.R",
|
|
"DEF-shin.R.001",
|
|
"DEF-foot.R",
|
|
"DEF-thigh.L",
|
|
"DEF-thigh.L.001",
|
|
"DEF-shin.L",
|
|
"DEF-shin.L.001",
|
|
"DEF-foot.L",
|
|
]
|
|
|
|
DISABLE_TWEAK_STRETCH_FOR = [
|
|
"thigh_tweak.R",
|
|
"thigh_tweak.R.001",
|
|
"shin_tweak.R",
|
|
"shin_tweak.R.001",
|
|
"foot_tweak.R",
|
|
"thigh_tweak.L",
|
|
"thigh_tweak.L.001",
|
|
"shin_tweak.L",
|
|
"shin_tweak.L.001",
|
|
"foot_tweak.L",
|
|
]
|
|
|
|
|
|
def set_ik_stretch_control(rigify_rig, fac):
|
|
"""Set the default state of the IK Stretch controls"""
|
|
|
|
control_bone_names = [ "upper_arm_parent.L", "upper_arm_parent.R", "thigh_parent.L", "thigh_parent.R" ]
|
|
for bone_name in control_bone_names:
|
|
control_bone = bones.get_pose_bone(rigify_rig, bone_name)
|
|
if control_bone:
|
|
if "IK_Stretch" in control_bone:
|
|
control_bone["IK_Stretch"] = fac
|
|
|
|
|
|
def disable_ik_stretch(rigify_rig, bone_names=None):
|
|
con_store = {}
|
|
ik_store = { "constraints": con_store }
|
|
for pose_bone in rigify_rig.pose.bones:
|
|
if bone_names and pose_bone.name not in bone_names:
|
|
continue
|
|
for con in pose_bone.constraints:
|
|
if con and con.type == "IK":
|
|
con_store[con] = con.use_stretch
|
|
# disabling this for now, as it can cause really bad IK solving...
|
|
#con.use_stretch = False
|
|
return ik_store
|
|
|
|
|
|
DEFAULT_IK_STRETCH_BONES = {
|
|
"MCH-shin_ik.L": True,
|
|
"MCH-shin_ik.R": True,
|
|
"MCH-forearm_ik.L": True,
|
|
"MCH-forearm_ik.R": True
|
|
}
|
|
|
|
|
|
def is_stretch_enabled(rigify_rig):
|
|
for bone_name, ik_stretch in DEFAULT_IK_STRETCH_BONES.items():
|
|
if bone_name in rigify_rig.pose.bones:
|
|
pose_bone = rigify_rig.pose.bones[bone_name]
|
|
for con in pose_bone.constraints:
|
|
if con and con.type == "IK":
|
|
if con.use_stretch:
|
|
return True
|
|
return False
|
|
|
|
|
|
def restore_ik_stretch(ik_store=None, rigify_rig=None):
|
|
if ik_store:
|
|
con_store = ik_store["constraints"]
|
|
for con in con_store:
|
|
con.use_stretch = con_store[con]
|
|
elif rigify_rig:
|
|
for bone_name, ik_stretch in DEFAULT_IK_STRETCH_BONES.items():
|
|
if bone_name in rigify_rig.pose.bones:
|
|
pose_bone = rigify_rig.pose.bones[bone_name]
|
|
for con in pose_bone.constraints:
|
|
if con and con.type == "IK":
|
|
con.use_stretch = ik_stretch
|
|
|
|
|
|
|
|
def update_avatar_rig(rig):
|
|
prefs = vars.prefs()
|
|
|
|
utils.log_info("Updating avatar rig...")
|
|
|
|
if is_rigify_armature(rig):
|
|
# disable all stretch-to tweak constraints and hide tweak bones...
|
|
# tweak bones are not fully compatible with CC/iC animation (probably Blender only)
|
|
# and cause positioning errors as they stretch/compress the bones.
|
|
# NOTE: Seems to have been because of a bug in Blender 4.1, fixed in 4.2 so disabling this...
|
|
if False:
|
|
if prefs.datalink_disable_tweak_bones:
|
|
disable = True
|
|
influence = 0.0
|
|
else:
|
|
disable = False
|
|
influence = 1.0
|
|
pose_bone: bpy.types.PoseBone
|
|
for pose_bone in rig.pose.bones:
|
|
if pose_bone.name in DISABLE_TWEAK_STRETCH_FOR:
|
|
if disable:
|
|
bones.set_bone_color(rig, pose_bone, "TWEAK_DISABLED")
|
|
else:
|
|
bones.set_bone_color(rig, pose_bone, "TWEAK")
|
|
elif prefs.datalink_disable_tweak_bones and pose_bone.name in DISABLE_TWEAK_STRETCH_IN:
|
|
for con in pose_bone.constraints:
|
|
if con.type == "STRETCH_TO":
|
|
if "tweak" in con.subtarget:
|
|
con.influence = influence
|
|
# disable IK stretch
|
|
if "IK_Stretch" in pose_bone:
|
|
pose_bone["IK_Stretch"] = 0.0
|
|
else: # just disable IK Stretch...
|
|
pose_bone: bpy.types.PoseBone
|
|
for pose_bone in rig.pose.bones:
|
|
# disable IK stretch
|
|
if "IK_Stretch" in pose_bone:
|
|
pose_bone["IK_Stretch"] = 0.0
|
|
|
|
|
|
def update_prop_rig(rig):
|
|
prefs = vars.prefs()
|
|
|
|
if not rig: return
|
|
|
|
utils.log_info("Updating prop rig...")
|
|
|
|
skin_bones = set()
|
|
rigid_bones = set()
|
|
mesh_bones = set()
|
|
root_bones = set()
|
|
skinned_root_bones = set()
|
|
|
|
root_bones.add(rig.data.bones[0])
|
|
USE_JSON_BONE_DATA = True
|
|
|
|
meshes = utils.get_child_objects(rig)
|
|
for obj in meshes:
|
|
if (obj.parent_type == "BONE" and obj.parent_bone in rig.data.bones):
|
|
bone = rig.data.bones[obj.parent_bone]
|
|
if (bone.parent and bone.parent.parent and
|
|
"CC_Base_Pivot" in bone.parent.name):
|
|
mesh_bones.add(bone.name)
|
|
rigid_bones.add(bone.parent.parent.name)
|
|
elif (bone.parent and
|
|
"CC_Base_Pivot" in bone.name):
|
|
rigid_bones.add(bone.parent.name)
|
|
elif bone.parent:
|
|
rigid_bones.add(bone.parent.name)
|
|
else:
|
|
rigid_bones.add(bone.name)
|
|
|
|
elif (obj.parent_type == "OBJECT" and obj.vertex_groups and len(obj.vertex_groups) == 1 and
|
|
utils.strip_name(obj.vertex_groups[0].name) == bones.rl_export_bone_name(utils.strip_name(obj.name))):
|
|
bone = rig.data.bones[obj.vertex_groups[0].name]
|
|
if (bone.parent and bone.parent.parent and
|
|
"CC_Base_Pivot" in bone.parent.name):
|
|
mesh_bones.add(bone.name)
|
|
rigid_bones.add(bone.parent.parent.name)
|
|
elif (bone.parent and
|
|
"CC_Base_Pivot" in bone.name):
|
|
rigid_bones.add(bone.parent.name)
|
|
elif bone.parent:
|
|
rigid_bones.add(bone.parent.name)
|
|
else:
|
|
rigid_bones.add(bone.name)
|
|
|
|
elif (obj.parent_type == "OBJECT" and obj.vertex_groups and len(obj.vertex_groups) > 0):
|
|
for vg in obj.vertex_groups:
|
|
skin_bones.add(vg.name)
|
|
if USE_JSON_BONE_DATA:
|
|
first_name = obj.vertex_groups[0].name
|
|
if first_name in rig.pose.bones:
|
|
pose_bone = rig.pose.bones[first_name]
|
|
while pose_bone.parent:
|
|
if "root_id" and "root_type" in pose_bone:
|
|
skinned_root_bones.add(pose_bone.name)
|
|
break
|
|
pose_bone = pose_bone.parent
|
|
|
|
if USE_JSON_BONE_DATA:
|
|
for pose_bone in rig.pose.bones:
|
|
if "root_id" in pose_bone and "root_type" in pose_bone:
|
|
root_bones.add(pose_bone.name)
|
|
|
|
pose_bone: bpy.types.PoseBone
|
|
for pose_bone in rig.pose.bones:
|
|
bone = pose_bone.bone
|
|
pivot_bone = "CC_Base_Pivot" in pose_bone.name
|
|
skin_bone = bone.name in skin_bones
|
|
rigid_bone = bone.name in rigid_bones
|
|
mesh_bone = bone.name in mesh_bones
|
|
root_bone = bone.name in root_bones
|
|
dummy_bone = not (skin_bone or rigid_bone or mesh_bone) and len(bone.children) == 0
|
|
node_bone = not (skin_bone or rigid_bone or mesh_bone) and len(bone.children) > 0
|
|
if root_bone:
|
|
bone.hide = False
|
|
elif pivot_bone:
|
|
bone.hide = True
|
|
elif skin_bone:
|
|
bone.hide = prefs.datalink_hide_prop_bones
|
|
elif mesh_bone:
|
|
bone.hide = True
|
|
elif rigid_bone:
|
|
bone.hide = False
|
|
elif dummy_bone:
|
|
bone.hide = True
|
|
elif node_bone:
|
|
bone.hide = prefs.datalink_hide_prop_bones
|
|
|
|
"""
|
|
import bpy
|
|
|
|
b = bpy.context.active_bone
|
|
c = b.children[0]
|
|
print(f"{b.head_local} {c.head_local} {b.length}")
|
|
hi = b.matrix_local.inverted() @ b.head_local
|
|
ti = b.matrix_local.inverted() @ b.tail_local
|
|
db = (ti - hi)/b.length
|
|
print(db)
|
|
|
|
|
|
ci = b.matrix_local.inverted() @ c.head_local
|
|
dc = (ci - hi)/b.length
|
|
print(dc)
|
|
print(db.dot(dc))
|
|
print(abs(db.length - dc.length))
|
|
q = db.rotation_difference(dc)
|
|
print(q)
|
|
print(q.to_euler())
|
|
"""
|
|
|
|
def get_bone_orientation(rig, bone_set: set):
|
|
B: bpy.types.Bone
|
|
C: bpy.types.Bone
|
|
for bone_name in bone_set:
|
|
B = rig.data.bones[bone_name]
|
|
if B.children and B.parent:
|
|
for C in B.children:
|
|
if B.length > 0.01:
|
|
# convert heads and tail to B local space
|
|
bhl = B.matrix_local.inverted() @ B.head_local
|
|
btl = B.matrix_local.inverted() @ B.tail_local
|
|
chl = B.matrix_local.inverted() @ C.head_local
|
|
# get bone axis in B local space
|
|
db: Vector = (btl - bhl) / B.length
|
|
# get direction to child in B local space
|
|
dc: Vector = (chl - bhl) / B.length
|
|
# if the distance to the child is ~= the same as the bone length
|
|
# (this should mean a chain of bones)
|
|
if abs(db.length - dc.length) < 0.01:
|
|
# get the rotation difference between the bone axis and the direction to the child
|
|
q = db.rotation_difference(dc)
|
|
euler = q.to_euler()
|
|
return euler
|
|
return Euler((0,0,0), "XYZ")
|
|
|
|
|
|
def get_widget_rig_collection(chr_cache):
|
|
try_names = [ chr_cache.get_name() ]
|
|
rig = chr_cache.get_armature()
|
|
rig_name = utils.strip_name(rig.name)
|
|
if rig_name.endswith("_Rigify"):
|
|
rig_name = rig_name[:-7]
|
|
try_names.append(rig_name)
|
|
if utils.object_exists_is_armature(chr_cache.rig_original_rig):
|
|
rig_name = chr_cache.rig_original_rig.name
|
|
try_names.append(rig_name)
|
|
for name in try_names:
|
|
try_collection_name = f"WGTS_{name}_rig"
|
|
if try_collection_name in bpy.data.collections:
|
|
return try_collection_name
|
|
for name in try_names:
|
|
for collection in bpy.data.collections:
|
|
if name in collection.name:
|
|
return collection.name
|
|
return None
|
|
|
|
|
|
def get_expression_widgets(chr_cache, collection_name):
|
|
chr_name = chr_cache.get_name()
|
|
facial_profile, viseme_profile = chr_cache.get_facial_profile()
|
|
tag = ""
|
|
if facial_profile == "EXT":
|
|
tag = "Ext"
|
|
elif facial_profile == "STD":
|
|
tag = "Std"
|
|
elif facial_profile == "TRA":
|
|
tag = "Tra"
|
|
elif facial_profile == "MH":
|
|
tag = "MH"
|
|
else:
|
|
raise Exception("Unknown facial profile!")
|
|
WGT_LINES = lib.get_object(f"WGT-RL_FaceRig_{tag}_Control_Lines", "RL_Custom_Widget",
|
|
names=f"WGT-{chr_name}_rig_{tag}_Control_Lines")
|
|
WGT_GROUPS = lib.get_object(f"WGT-RL_FaceRig_{tag}_Groups", "RL_Custom_Widget",
|
|
names=f"WGT-{chr_name}_rig_{tag}_Groups")
|
|
WGT_LABELS = lib.get_object(f"WGT-RL_FaceRig_{tag}_Labels", "RL_Custom_Widget",
|
|
names=f"WGT-{chr_name}_rig_{tag}_Labels")
|
|
WGT_OUTLINE = lib.get_object(f"WGT-RL_FaceRig_{tag}_Outline", "RL_Custom_Widget",
|
|
names=f"WGT-{chr_name}_rig_{tag}_Outline")
|
|
WGT_SLIDER = bones.make_line_widget(f"WGT-{chr_name}_rig_{tag}_Slider", 2.0)
|
|
WGT_RECT = bones.make_box_widget(f"WGT-{chr_name}_rig_{tag}_Rect", 2.0)
|
|
WGT_NUB = bones.make_sphere_widget(f"WGT-{chr_name}_rig_{tag}_Slider_Nub", 0.01666)
|
|
WGT_NAME = bones.make_text_widget(f"WGT-{chr_name}_rig_{tag}_Name", chr_name, 2.0, (0, 0.87, 0), 0.05)
|
|
bones.add_widget_to_collection(WGT_LINES, collection_name)
|
|
bones.add_widget_to_collection(WGT_GROUPS, collection_name)
|
|
bones.add_widget_to_collection(WGT_LABELS, collection_name)
|
|
bones.add_widget_to_collection(WGT_OUTLINE, collection_name)
|
|
bones.add_widget_to_collection(WGT_SLIDER, collection_name)
|
|
bones.add_widget_to_collection(WGT_RECT, collection_name)
|
|
bones.add_widget_to_collection(WGT_NUB, collection_name)
|
|
bones.add_widget_to_collection(WGT_NAME, collection_name)
|
|
return WGT_OUTLINE, WGT_GROUPS, WGT_LABELS, WGT_LINES, WGT_SLIDER, WGT_RECT, WGT_NUB, WGT_NAME
|
|
|
|
|
|
def get_expression_widgets_2(chr_cache, collection_name):
|
|
facial_profile, viseme_profile = chr_cache.get_facial_profile()
|
|
if facial_profile == "MH":
|
|
tag = "MH2"
|
|
WGT_LINES_2 = lib.get_object(f"WGT-RL_FaceRig_{tag}_Control_Lines", "RL_Custom_Widget")
|
|
WGT_GROUPS_2 = lib.get_object(f"WGT-RL_FaceRig_{tag}_Groups", "RL_Custom_Widget")
|
|
WGT_LABELS_2 = lib.get_object(f"WGT-RL_FaceRig_{tag}_Labels", "RL_Custom_Widget")
|
|
WGT_OUTLINE_2 = lib.get_object(f"WGT-RL_FaceRig_{tag}_Outline", "RL_Custom_Widget")
|
|
bones.add_widget_to_collection(WGT_LINES_2, collection_name)
|
|
bones.add_widget_to_collection(WGT_GROUPS_2, collection_name)
|
|
bones.add_widget_to_collection(WGT_LABELS_2, collection_name)
|
|
bones.add_widget_to_collection(WGT_OUTLINE_2, collection_name)
|
|
return WGT_OUTLINE_2, WGT_GROUPS_2, WGT_LABELS_2, WGT_LINES_2
|
|
else:
|
|
return None, None, None, None
|
|
|
|
|
|
def get_custom_widgets():
|
|
wgt_pivot = bones.make_axes_widget("WGT-datalink_pivot", 1)
|
|
wgt_mesh = bones.make_cone_widget("WGT-datalink_mesh", 1)
|
|
wgt_default = bones.make_sphere_widget("WGT-datalink_default", 1)
|
|
wgt_root = bones.make_root_widget("WGT-datalink_root", 2.5)
|
|
wgt_skin = bones.make_spike_widget("WGT-datalink_skin", 1)
|
|
bones.add_widget_to_collection(wgt_pivot, "WGTS_Datalink")
|
|
bones.add_widget_to_collection(wgt_mesh, "WGTS_Datalink")
|
|
bones.add_widget_to_collection(wgt_default, "WGTS_Datalink")
|
|
bones.add_widget_to_collection(wgt_root, "WGTS_Datalink")
|
|
bones.add_widget_to_collection(wgt_skin, "WGTS_Datalink")
|
|
widgets = {
|
|
"pivot": wgt_pivot,
|
|
"mesh": wgt_mesh,
|
|
"default": wgt_default,
|
|
"root": wgt_root,
|
|
"skin": wgt_skin,
|
|
}
|
|
return widgets
|
|
|
|
|
|
def set_bone_shape_scale(pose_bone: bpy.types.PoseBone, scale):
|
|
try:
|
|
if type(scale) is float or type(scale) is int:
|
|
S = Vector((scale, scale, scale))
|
|
elif type(scale) is list or type(scale) is tuple:
|
|
S = Vector(scale)
|
|
elif type(scale) is Vector:
|
|
S = scale
|
|
else:
|
|
return False
|
|
pose_bone.custom_shape_scale_xyz = S
|
|
return True
|
|
except:
|
|
pass
|
|
try:
|
|
pose_bone.custom_shape_scale = scale
|
|
return True
|
|
except:
|
|
pass
|
|
utils.log_error(f"Unable to set bone shape scale: {pose_bone.name} / {scale}")
|
|
return False
|
|
|
|
|
|
def custom_prop_rig(rig):
|
|
prefs = vars.prefs()
|
|
|
|
if not rig: return
|
|
|
|
utils.log_info("Applying custom prop rig...")
|
|
|
|
widgets = get_custom_widgets()
|
|
rig.show_in_front = True #not is_skinned_rig(rig)
|
|
rig.data.display_type = 'WIRE'
|
|
|
|
skin_bones = set()
|
|
rigid_bones = set()
|
|
mesh_bones = set()
|
|
root_bones = set()
|
|
skinned_root_bones = set()
|
|
|
|
root_bones.add(rig.data.bones[0].name)
|
|
USE_JSON_BONE_DATA = True
|
|
|
|
meshes = utils.get_child_objects(rig)
|
|
for obj in meshes:
|
|
if (obj.parent_type == "BONE" and obj.parent_bone in rig.data.bones):
|
|
bone = rig.data.bones[obj.parent_bone]
|
|
if (bone.parent and bone.parent.parent and
|
|
"CC_Base_Pivot" in bone.parent.name):
|
|
mesh_bones.add(bone.name)
|
|
rigid_bones.add(bone.parent.parent.name)
|
|
elif (bone.parent and
|
|
"CC_Base_Pivot" in bone.name):
|
|
rigid_bones.add(bone.parent.name)
|
|
elif bone.parent:
|
|
rigid_bones.add(bone.parent.name)
|
|
else:
|
|
rigid_bones.add(bone.name)
|
|
|
|
elif (obj.parent_type == "OBJECT" and obj.vertex_groups and len(obj.vertex_groups) == 1 and
|
|
utils.strip_name(obj.vertex_groups[0].name) == bones.rl_export_bone_name(utils.strip_name(obj.name))):
|
|
bone = rig.data.bones[obj.vertex_groups[0].name]
|
|
if (bone.parent and bone.parent.parent and
|
|
"CC_Base_Pivot" in bone.parent.name):
|
|
mesh_bones.add(bone.name)
|
|
rigid_bones.add(bone.parent.parent.name)
|
|
elif (bone.parent and
|
|
"CC_Base_Pivot" in bone.name):
|
|
rigid_bones.add(bone.parent.name)
|
|
elif bone.parent:
|
|
rigid_bones.add(bone.parent.name)
|
|
else:
|
|
rigid_bones.add(bone.name)
|
|
|
|
elif (obj.parent_type == "OBJECT" and obj.vertex_groups and len(obj.vertex_groups) > 0):
|
|
for vg in obj.vertex_groups:
|
|
skin_bones.add(vg.name)
|
|
if USE_JSON_BONE_DATA:
|
|
first_name = obj.vertex_groups[0].name
|
|
if first_name in rig.pose.bones:
|
|
pose_bone = rig.pose.bones[first_name]
|
|
while pose_bone.parent:
|
|
if "root_id" and "root_type" in pose_bone:
|
|
skinned_root_bones.add(pose_bone.name)
|
|
break
|
|
pose_bone = pose_bone.parent
|
|
|
|
skin_bone_orientation = get_bone_orientation(rig, skin_bones)
|
|
|
|
if USE_JSON_BONE_DATA:
|
|
for pose_bone in rig.pose.bones:
|
|
if "root_id" in pose_bone and "root_type" in pose_bone:
|
|
root_bones.add(pose_bone.name)
|
|
|
|
if select_rig(rig):
|
|
pose_bone: bpy.types.PoseBone
|
|
for pose_bone in rig.pose.bones:
|
|
bone = pose_bone.bone
|
|
pivot_bone = "CC_Base_Pivot" in pose_bone.name
|
|
skin_bone = bone.name in skin_bones
|
|
rigid_bone = bone.name in rigid_bones
|
|
mesh_bone = bone.name in mesh_bones
|
|
root_bone = bone.name in root_bones
|
|
dummy_bone = not (skin_bone or rigid_bone or mesh_bone) and len(bone.children) == 0
|
|
node_bone = not (skin_bone or rigid_bone or mesh_bone) and len(bone.children) > 0
|
|
if root_bone:
|
|
if not pose_bone.parent:
|
|
pose_bone.custom_shape = widgets["root"]
|
|
set_bone_shape_scale(pose_bone, 20)
|
|
else:
|
|
pose_bone.custom_shape = widgets["default"]
|
|
set_bone_shape_scale(pose_bone, 15)
|
|
bone.hide = False
|
|
pose_bone.use_custom_shape_bone_size = False
|
|
bones.set_bone_color(rig, pose_bone, "ROOT")
|
|
elif pivot_bone:
|
|
pose_bone.custom_shape = widgets["pivot"]
|
|
bone.hide = True
|
|
pose_bone.use_custom_shape_bone_size = False
|
|
set_bone_shape_scale(pose_bone, 10)
|
|
bones.set_bone_color(rig, pose_bone, "SPECIAL")
|
|
elif skin_bone:
|
|
pose_bone.custom_shape = widgets["skin"]
|
|
bone.hide = prefs.datalink_hide_prop_bones
|
|
pose_bone.use_custom_shape_bone_size = True
|
|
pose_bone.use
|
|
#pose_bone.bone.show_wire = True
|
|
pose_bone.custom_shape_rotation_euler = skin_bone_orientation
|
|
bones.set_bone_color(rig, pose_bone, "SKIN")
|
|
elif mesh_bone:
|
|
pose_bone.custom_shape = widgets["mesh"]
|
|
bone.hide = True
|
|
pose_bone.use_custom_shape_bone_size = False
|
|
set_bone_shape_scale(pose_bone, 10)
|
|
bones.set_bone_color(rig, pose_bone, "SPECIAL")
|
|
elif rigid_bone:
|
|
pose_bone.custom_shape = widgets["default"]
|
|
bone.hide = False
|
|
pose_bone.use_custom_shape_bone_size = False
|
|
set_bone_shape_scale(pose_bone, 10)
|
|
bones.set_bone_color(rig, pose_bone, "TWEAK")
|
|
elif dummy_bone:
|
|
pose_bone.custom_shape = widgets["pivot"]
|
|
bone.hide = True
|
|
pose_bone.use_custom_shape_bone_size = False
|
|
set_bone_shape_scale(pose_bone, 10)
|
|
bones.set_bone_color(rig, pose_bone, "IK")
|
|
elif node_bone:
|
|
pose_bone.custom_shape = widgets["default"]
|
|
bone.hide = prefs.datalink_hide_prop_bones
|
|
pose_bone.use_custom_shape_bone_size = False
|
|
set_bone_shape_scale(pose_bone, 10)
|
|
bones.set_bone_color(rig, pose_bone, "SPECIAL")
|
|
|
|
|
|
def custom_avatar_rig(rig):
|
|
prefs = vars.prefs()
|
|
|
|
if not rig: return
|
|
|
|
utils.log_info("Applying custom avatar rig...")
|
|
|
|
widgets = get_custom_widgets()
|
|
rig.show_in_front = False
|
|
rig.data.display_type = 'OCTAHEDRAL'
|
|
|
|
skin_bones = set()
|
|
root_bones = set()
|
|
|
|
root_bones.add(rig.data.bones[0].name)
|
|
for bone in rig.data.bones:
|
|
if bone not in root_bones:
|
|
skin_bones.add(bone.name)
|
|
|
|
skin_bone_orientation = get_bone_orientation(rig, skin_bones)
|
|
|
|
if select_rig(rig):
|
|
pose_bone: bpy.types.PoseBone
|
|
for pose_bone in rig.pose.bones:
|
|
bone = pose_bone.bone
|
|
if bone.parent is None:
|
|
if not pose_bone.parent:
|
|
pose_bone.custom_shape = widgets["root"]
|
|
set_bone_shape_scale(pose_bone, 20)
|
|
else:
|
|
pose_bone.custom_shape = widgets["default"]
|
|
set_bone_shape_scale(pose_bone, 15)
|
|
bone.hide = False
|
|
pose_bone.use_custom_shape_bone_size = False
|
|
bones.set_bone_color(rig, pose_bone, "ROOT")
|
|
else:
|
|
pose_bone.custom_shape = widgets["skin"]
|
|
bone.hide = False
|
|
pose_bone.use_custom_shape_bone_size = True
|
|
#pose_bone.bone.show_wire = True
|
|
pose_bone.custom_shape_rotation_euler = skin_bone_orientation
|
|
bones.set_bone_color(rig, pose_bone, "SKIN")
|
|
|
|
|
|
def de_pivot(chr_cache):
|
|
"""Removes the pivot bones and corrects the parenting of the mesh objects
|
|
from a CC/iC character or prop"""
|
|
|
|
if chr_cache:
|
|
rig = chr_cache.get_armature()
|
|
objects = chr_cache.get_all_objects(include_armature=False,
|
|
of_type="MESH")
|
|
|
|
if rig and objects:
|
|
|
|
true_parents = {}
|
|
if select_rig(rig):
|
|
obj: bpy.types.Object
|
|
for obj in objects:
|
|
if obj.parent == rig:
|
|
if obj.parent_type == "BONE":
|
|
parent_bone_name = obj.parent_bone
|
|
parent_bone: bpy.types.PoseBone = rig.pose.bones[parent_bone_name]
|
|
if "CC_Base_Pivot" in parent_bone.name:
|
|
true_parent = parent_bone.parent
|
|
M = obj.matrix_world.copy()
|
|
true_parents[obj] = (true_parent, M)
|
|
|
|
|
|
to_remove = []
|
|
if edit_rig(rig):
|
|
for edit_bone in rig.data.edit_bones:
|
|
if "CC_Base_Pivot" in edit_bone.name:
|
|
to_remove.append(edit_bone)
|
|
|
|
for edit_bone in to_remove:
|
|
rig.data.edit_bones.remove(edit_bone)
|
|
|
|
for obj in true_parents:
|
|
true_parent, M = true_parents[obj]
|
|
obj.parent_bone = true_parent.name
|
|
obj.matrix_world = M
|
|
|
|
select_rig(rig)
|
|
|
|
|
|
|
|
|
|
|
|
#region Actions
|
|
def add_action_slot_channelbag(action, name: str, slot_type: str, reuse=False, clear=False):
|
|
slot_name = f"{name} ({slot_type.capitalize()})"
|
|
if utils.B440():
|
|
channelbags = utils.get_action_channelbags(action)
|
|
if reuse:
|
|
for channelbag in channelbags:
|
|
if channelbag.slot and channelbag.slot.target_id_type == slot_type and channelbag.slot.name_display == slot_name:
|
|
slot = channelbag.slot
|
|
channel = channelbag
|
|
if clear:
|
|
channel.fcurves.clear()
|
|
return slot, channel
|
|
slot = action.slots.new(slot_type, slot_name)
|
|
channel = channelbags.new(slot)
|
|
else:
|
|
slot = None
|
|
channel = action
|
|
return slot, channel
|
|
|
|
|
|
def add_action_key_slot_channelbag(action, obj: bpy.types.Object, reuse=False, clear=False):
|
|
return add_action_slot_channelbag(action, obj.name, "KEY", reuse=reuse, clear=clear)
|
|
|
|
|
|
def add_action_ob_slot_channelbag(action, obj: bpy.types.Object, reuse=False, clear=False):
|
|
return add_action_slot_channelbag(action, obj.name, "OBJECT", reuse=reuse, clear=clear)
|
|
|
|
|
|
def add_action_data_slot_channelbag(action, obj: bpy.types.Object, slot_type=None, reuse=False, clear=False):
|
|
if not slot_type:
|
|
slot_type = obj.type
|
|
return add_action_slot_channelbag(action, obj.name, slot_type, reuse=reuse, clear=clear)
|
|
|
|
|
|
def is_slotted_action(action):
|
|
return utils.B440() and len(action.slots) > 1
|
|
|
|
|
|
def copy_slot_actions(src_action, dst_action, slot_type):
|
|
src_channelbags = utils.get_action_channelbags(src_action)
|
|
for src_channel in src_channelbags:
|
|
slot = src_channel.slot
|
|
if slot.target_id_type == slot_type:
|
|
dst_slot, dst_channel = add_action_slot_channelbag(dst_action, slot.name_display, slot_type)
|
|
for fcurve in src_channel.fcurves:
|
|
copy_fcurve_to_channel(fcurve, dst_channel)
|
|
|
|
|
|
def copy_action_shape_key_channels(rig, src_action, dst_action, fake_user=True):
|
|
prefs = vars.prefs()
|
|
copy_actions = [dst_action]
|
|
if prefs.use_action_slots():
|
|
copy_slot_actions(src_action, dst_action, "KEY")
|
|
else:
|
|
rig_id = get_rig_id(rig)
|
|
rl_id = utils.get_rl_id(rig)
|
|
motion_set = get_motion_set_ids(dst_action)
|
|
set_id = motion_set[0]
|
|
set_generation = motion_set[1]
|
|
motion_prefix = get_motion_prefix(dst_action)
|
|
motion_id = get_motion_id(dst_action)
|
|
key_actions = fetch_key_actions(src_action)
|
|
for key_action in key_actions:
|
|
key_object = utils.get_prop(key_action, "rl_key_object")
|
|
action_name = make_key_action_name(rig_id, motion_id, key_object, motion_prefix)
|
|
utils.log_info(f" - Copying Key Action: {key_action.name} to {action_name}")
|
|
copy_action: bpy.types.Action = key_action.copy()
|
|
copy_action.name = action_name
|
|
copy_action.use_fake_user = fake_user
|
|
add_motion_set_data(copy_action, set_id, set_generation, key_id=key_object, arm_id=rl_id, slotted=False)
|
|
copy_actions.append(copy_action)
|
|
return copy_actions
|
|
|
|
|
|
def fetch_key_actions(source_action):
|
|
source_actions = find_source_actions(source_action)
|
|
return source_actions["keys"].values()
|
|
|
|
|
|
def fetch_action_curve_database(source_action):
|
|
database = {}
|
|
SOURCE_CHANNELS = ["CC_Base_Body", "CC_Game_Body", "CC_Base_Tongue"]
|
|
source_actions = find_source_actions(source_action)
|
|
if source_actions["armature"]:
|
|
source_action = source_actions["armature"]
|
|
elif source_actions["rlx"]:
|
|
source_action = source_actions["rlx"]
|
|
source_arm_actions = [a for a in source_actions["armatures"] if a is not None]
|
|
source_key_actions = [a for a in source_actions["keys"].values() if a is not None]
|
|
source_rlx_action = source_actions["rlx"]
|
|
source_light_action = source_actions["light"]
|
|
source_camera_action = source_actions["camera"]
|
|
arm_actions = [a.copy() for a in source_arm_actions if a is not None]
|
|
key_actions = [a.copy() for a in source_key_actions if a is not None]
|
|
rlx_action = source_rlx_action.copy() if source_rlx_action else None
|
|
light_action = source_light_action.copy() if source_light_action else None
|
|
camera_action = source_camera_action.copy() if source_camera_action else None
|
|
source_rlx_actions = utils.list_if(source_rlx_action, source_light_action, source_camera_action)
|
|
rlx_actions = utils.list_if(rlx_action, light_action, camera_action)
|
|
for arm_action in arm_actions:
|
|
arm_action.name = "DB_AC_TMP"
|
|
for key_action in key_actions:
|
|
key_action.name = "DB_KC_TMP"
|
|
if rlx_action:
|
|
rlx_action.name = "DB_RC_TMP"
|
|
if light_action:
|
|
light_action.name = "DB_LC_TMP"
|
|
if camera_action:
|
|
camera_action.name = "DB_CC_TMP"
|
|
data_curves = { "OBJECT": {}, "KEY": {}, "LIGHT": {}, "CAMERA": {} }
|
|
database["source_action"] = source_action
|
|
database["source_arm_actions"] = source_arm_actions
|
|
database["source_key_actions"] = source_key_actions
|
|
database["source_rlx_actions"] = source_rlx_actions
|
|
database["arm_actions"] = arm_actions
|
|
database["key_actions"] = key_actions
|
|
database["rlx_actions"] = rlx_actions
|
|
database["curves"] = data_curves
|
|
first_frame = None
|
|
if utils.B440():
|
|
all_actions = arm_actions + key_actions + rlx_actions
|
|
a: bpy.types.Action
|
|
done_channelbags = []
|
|
done_actions = []
|
|
for a in all_actions:
|
|
if a in done_actions: continue
|
|
done_actions.append(a)
|
|
channelbags = utils.get_action_channelbags(a)
|
|
# build database of fcurves by slot
|
|
for channel in channelbags:
|
|
if channel in done_channelbags: continue
|
|
if channel.slot == None:
|
|
utils.log_info(f"No slot in channel {channel}")
|
|
continue
|
|
done_channelbags.append(channel)
|
|
slot = channel.slot
|
|
is_source_channel = False
|
|
for MC in SOURCE_CHANNELS:
|
|
if slot.name_display.startswith(MC):
|
|
is_source_channel = True
|
|
slot_type = slot.target_id_type
|
|
if slot_type not in data_curves:
|
|
utils.log_info(f"Found unexpected slot type: {slot_type}")
|
|
data_curves[slot_type] = {}
|
|
slot_curves = data_curves[slot_type]
|
|
fcurve: bpy.types.FCurve = None
|
|
for fcurve in channel.fcurves:
|
|
data_path = fcurve.data_path
|
|
index = fcurve.array_index
|
|
id = (data_path, index)
|
|
if id not in slot_curves or is_source_channel:
|
|
slot_curves[id] = fcurve
|
|
if fcurve.keyframe_points and len(fcurve.keyframe_points) > 0:
|
|
frame = fcurve.keyframe_points[0].co.x
|
|
if first_frame is None or frame < first_frame:
|
|
first_frame = frame
|
|
else:
|
|
all_actions = []
|
|
for arm_action in arm_actions:
|
|
all_actions.append(("OBJECT", arm_action))
|
|
for key_action in key_actions:
|
|
all_actions.append(("KEY", key_action))
|
|
if rlx_action:
|
|
all_actions.append(("OBJECT", rlx_action))
|
|
if light_action:
|
|
all_actions.append(("LIGHT", light_action))
|
|
if camera_action:
|
|
all_actions.append(("CAMERA", camera_action))
|
|
done_actions = []
|
|
for slot_type, a in all_actions:
|
|
if a in done_actions: continue
|
|
done_actions.append(a)
|
|
# build database of fcurves by slot
|
|
if slot_type not in data_curves:
|
|
utils.log_info(f"Found unexpected slot type: {slot_type}")
|
|
data_curves[slot_type] = {}
|
|
slot_curves = data_curves[slot_type]
|
|
fcurve: bpy.types.FCurve = None
|
|
for fcurve in a.fcurves:
|
|
data_path = fcurve.data_path
|
|
index = fcurve.array_index
|
|
id = (data_path, index)
|
|
if id not in slot_curves:
|
|
slot_curves[id] = fcurve
|
|
if fcurve.keyframe_points and len(fcurve.keyframe_points) > 0:
|
|
frame = fcurve.keyframe_points[0].co.x
|
|
if first_frame is None or frame < first_frame:
|
|
first_frame = frame
|
|
database["first_frame"] = first_frame
|
|
return database
|
|
|
|
|
|
def get_channelbag_group(channelbag, group_name):
|
|
group = None
|
|
if group_name:
|
|
if group_name in channelbag.groups:
|
|
group = channelbag.groups[group_name]
|
|
else:
|
|
group = channelbag.groups.new(group_name)
|
|
return group
|
|
|
|
|
|
def load_slotted_action(rig, action: bpy.types.Action, move=False, temp=None, force=False):
|
|
prefs = vars.prefs()
|
|
|
|
# determine motion set info
|
|
rig_id = get_rig_id(rig)
|
|
rl_id = utils.get_rl_id(rig)
|
|
old_action = utils.safe_get_action(rig)
|
|
set_id = utils.get_prop(action, "rl_set_id")
|
|
set_generation = utils.get_prop(action, "rl_set_generation")
|
|
motion_prefix = get_motion_prefix(action)
|
|
motion_id = get_motion_id(action)
|
|
src_rl_id = utils.get_rl_id(action)
|
|
use_fake_user = action.use_fake_user
|
|
|
|
# if loading onto a different rig_id, we want to create a new motion set ...
|
|
use_new_motion_set = not force and (((rl_id or src_rl_id) and rl_id != src_rl_id) or temp or move)
|
|
if temp:
|
|
motion_prefix = "TEMP"
|
|
motion_id = temp
|
|
if use_new_motion_set:
|
|
set_id = generate_set_id()
|
|
utils.log_info(f"Generating new motion set: {set_id}")
|
|
|
|
# generate a database of fcurves in the motion set
|
|
database = fetch_action_curve_database(action)
|
|
|
|
# re-use the existing source armature action
|
|
if use_new_motion_set:
|
|
|
|
# Point light, Export_20260630130902, Point light_1
|
|
motion_id = get_unique_set_motion_id(rig_id, motion_id, motion_prefix, slotted=True, rlob=rig)
|
|
action_name = generate_action_name(rig_id, "", "", motion_id, motion_prefix)
|
|
|
|
# if temp, re-use any existing temp action
|
|
if temp and action_name in bpy.data.actions:
|
|
action = bpy.data.actions[action_name]
|
|
else:
|
|
action = action.copy()
|
|
utils.force_action_name(action, action_name)
|
|
|
|
# add motion set data
|
|
arm_id = rl_id if rig.type == "ARMATURE" else None
|
|
rlx_id = None if rig.type == "ARMATURE" else rl_id
|
|
add_motion_set_data(action, set_id, set_generation, arm_id=arm_id, rlx_id=rlx_id, slotted=True)
|
|
action.use_fake_user = use_fake_user
|
|
|
|
# if this is a temporary action, store the action it replaced
|
|
if temp:
|
|
action["rl_temp_action"] = old_action.name if old_action else ""
|
|
elif force:
|
|
# add motion set data
|
|
arm_id = rl_id if rig.type == "ARMATURE" else None
|
|
rlx_id = None if rig.type == "ARMATURE" else rl_id
|
|
add_motion_set_data(action, set_id, set_generation, arm_id=arm_id, rlx_id=rlx_id, slotted=True)
|
|
else:
|
|
action: bpy.types.Action = database["source_action"]
|
|
|
|
# build channels for each object in rig
|
|
utils.log_info(f"Rebuilding Slotted Action: {action.name}")
|
|
keep_slots = []
|
|
keep_channels = []
|
|
data_curves = database["curves"]
|
|
first_frame = database["first_frame"]
|
|
|
|
# ARMATURE ACTION
|
|
#
|
|
if "OBJECT" in data_curves:
|
|
slot_curves = data_curves["OBJECT"]
|
|
used_ids = set()
|
|
|
|
# add armature (object) slot
|
|
rig_slot, rig_channel = add_action_ob_slot_channelbag(action, rig, reuse=True, clear=True)
|
|
|
|
if rig.type == "ARMATURE":
|
|
# for each bone fcurve in the database in the order of the pose bones on the rig ...
|
|
for bone in rig.pose.bones:
|
|
for id, fcurve in slot_curves.items():
|
|
curve_path, index = id
|
|
# that exist on this rig:
|
|
if id not in used_ids and bone.name in curve_path and utils.has_data_path(rig, curve_path):
|
|
clone = copy_fcurve_to_channel(fcurve, rig_channel)
|
|
# add fcurve to bone group
|
|
group_name = utils.get_data_path_object_name(curve_path)
|
|
clone.group = get_channelbag_group(rig_channel, group_name)
|
|
# store used id's
|
|
used_ids.add(id)
|
|
|
|
if True: #if rig.type == "LIGHT" or rig.type == "CAMERA":
|
|
# for each remaining fcurve data path in the database
|
|
for id, fcurve in slot_curves.items():
|
|
curve_path, index = id
|
|
# that have valid data paths on this rig:
|
|
if id not in used_ids and utils.has_data_path(rig, curve_path):
|
|
clone = copy_fcurve_to_channel(fcurve, rig_channel)
|
|
# add fcurve to Object group
|
|
group_name = utils.get_data_path_object_name(curve_path)
|
|
clone.group = get_channelbag_group(rig_channel, group_name)
|
|
# store used id's
|
|
used_ids.add(id)
|
|
|
|
# keep this slot/channel and load onto rig slot
|
|
keep_slots.append(rig_slot)
|
|
keep_channels.append(rig_channel)
|
|
utils.safe_set_action(rig, action, create=True, slot=rig_slot)
|
|
|
|
# determine unused fcurves
|
|
# NOTE: unused fcurves are added after the action is loaded on to the rig,
|
|
# as the unused curves can get stripped from the action if added before.
|
|
unused_ids = set()
|
|
for id in slot_curves:
|
|
if id not in used_ids:
|
|
unused_ids.add(id)
|
|
|
|
if unused_ids:
|
|
|
|
# add slot for unused fcurves
|
|
rig_slot, rig_channel = add_action_slot_channelbag(action, "Unused Object", "OBJECT",
|
|
reuse=True, clear=True)
|
|
|
|
# add unused fcurves
|
|
for id in unused_ids:
|
|
fcurve = slot_curves[id]
|
|
copy_fcurve_to_channel(fcurve, rig_channel)
|
|
|
|
# always keep this slot/channel
|
|
keep_slots.append(rig_slot)
|
|
keep_channels.append(rig_channel)
|
|
|
|
# SHAPE KEY ACTIONS
|
|
#
|
|
EXCLUDE_KEYS = ["Basis"]
|
|
SOURCE_MESH_NAMES = ["CC_Base_Body", "CC_Game_Body", "CC_Base_Tongue", "CC_Base_Eye"]
|
|
if "KEY" in data_curves and rig.type == "ARMATURE":
|
|
slot_curves = data_curves["KEY"]
|
|
used_ids = set()
|
|
|
|
# determine source key names from source mesh shape keys
|
|
# (i.e. shape keys from the body/tongue/eyes)
|
|
source_keys = set()
|
|
for smn in SOURCE_MESH_NAMES:
|
|
for obj in rig.children:
|
|
if obj.name.startswith(smn):
|
|
if utils.object_exists_is_mesh(obj) and utils.object_has_shape_keys(obj):
|
|
for key in obj.data.shape_keys.key_blocks:
|
|
source_keys.add(key.name)
|
|
|
|
# if generating a single key slot for all meshes
|
|
if not prefs.action_add_key_slots_per_obj:
|
|
|
|
# add shape key slot/channel for all mesh objects
|
|
key_slot, key_channel = add_action_slot_channelbag(action, "All Keys", "KEY",
|
|
reuse=True, clear=True)
|
|
|
|
# done id's across all meshes
|
|
done_ids = set()
|
|
done_keys = set()
|
|
|
|
for obj in rig.children:
|
|
|
|
if utils.object_exists_is_mesh(obj) and utils.object_has_shape_keys(obj):
|
|
|
|
# if generating key actions per mesh
|
|
if prefs.action_add_key_slots_per_obj:
|
|
|
|
# generate a specific shape key slot for each mesh object
|
|
key_slot, key_channel = add_action_key_slot_channelbag(action, obj,
|
|
reuse=True, clear=True)
|
|
|
|
# done id's per mesh
|
|
done_ids = set()
|
|
done_keys = set()
|
|
|
|
has_keys = False
|
|
|
|
# determine data paths of all driven shape keys in this mesh
|
|
driver_paths = set()
|
|
if obj.data.shape_keys.animation_data:
|
|
for driver in obj.data.shape_keys.animation_data.drivers:
|
|
driver_paths.add(driver.data_path)
|
|
|
|
# for each key curve in the database ...
|
|
for id, fcurve in slot_curves.items():
|
|
curve_path, index = id
|
|
|
|
# whose data path exists on this object and is not driven
|
|
if utils.has_data_path(obj.data.shape_keys, curve_path) and curve_path not in driver_paths:
|
|
|
|
# this slot/channel has keys for this mesh
|
|
has_keys = True
|
|
|
|
# if not already added for this mesh
|
|
if id not in done_ids:
|
|
fcurve = slot_curves[id]
|
|
key_name = utils.get_data_path_object_name(curve_path)
|
|
clone = copy_fcurve_to_channel(fcurve, key_channel)
|
|
# add fcurve to expression group
|
|
group_name = get_shape_key_group_name(key_name)
|
|
clone.group = get_channelbag_group(key_channel, group_name)
|
|
# store used id's
|
|
done_keys.add(key_name)
|
|
used_ids.add(id)
|
|
done_ids.add(id)
|
|
|
|
if prefs.action_add_empty_key_channels and first_frame is not None:
|
|
|
|
# for any shape keys without fcurves that are source shape keys
|
|
# (i.e. shape keys from the body/tongue/eyes)
|
|
for key in obj.data.shape_keys.key_blocks:
|
|
if (key.name not in done_keys and
|
|
key.name in source_keys and
|
|
key.name not in EXCLUDE_KEYS):
|
|
|
|
data_path = f"key_blocks[\"{key.name}\"].value"
|
|
|
|
# that are not driven
|
|
if data_path not in driver_paths:
|
|
|
|
# this action has keys for this mesh
|
|
has_keys = True
|
|
|
|
# add a zero single keyframe track
|
|
fcurve = key_channel.fcurves.new(data_path)
|
|
fcurve.keyframe_points.add(1)
|
|
fcurve.keyframe_points.foreach_set('co', [first_frame, 0.0])
|
|
reset_fcurve_interpolation(fcurve)
|
|
|
|
# add fcurve to expression group
|
|
group_name = get_shape_key_group_name(key.name)
|
|
fcurve.group = get_channelbag_group(key_channel, group_name)
|
|
|
|
# keep slots/channels that have assigned keys and load onto mesh slot,
|
|
# remove slots/channels that have no assigned keys
|
|
if has_keys:
|
|
keep_slots.append(key_slot)
|
|
keep_channels.append(key_channel)
|
|
utils.safe_set_action(obj.data.shape_keys, action, create=True, slot=key_slot)
|
|
else:
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
|
|
# determine unused shape key fcurves
|
|
unused_ids = set()
|
|
for id in slot_curves:
|
|
if id not in used_ids:
|
|
unused_ids.add(id)
|
|
|
|
if unused_ids:
|
|
|
|
# add slot/channel for unused shape key fcuves
|
|
key_slot, key_channel = add_action_slot_channelbag(action, "Unused Keys", "KEY", reuse=True, clear=True)
|
|
|
|
# add unused shape key fcurves to action
|
|
for id in unused_ids:
|
|
fcurve = slot_curves[id]
|
|
copy_fcurve_to_channel(fcurve, key_channel)
|
|
|
|
# always keep this slot/channel
|
|
keep_slots.append(key_slot)
|
|
keep_channels.append(key_channel)
|
|
|
|
# LIGHT / CAMERA (DATA) ACTIONS
|
|
#
|
|
slot_types = ["LIGHT", "CAMERA"]
|
|
for slot_type in slot_types:
|
|
|
|
if slot_type in data_curves:
|
|
|
|
if rig.type != slot_type:
|
|
continue
|
|
|
|
slot_curves = data_curves[slot_type]
|
|
used_ids = set()
|
|
|
|
# add data slot
|
|
data_slot, data_channel = add_action_slot_channelbag(action, f"{rig.name} ({slot_type.capitalize()})", slot_type,
|
|
reuse=True, clear=True)
|
|
|
|
add_action_data_slot_channelbag(action, rig, slot_type=slot_type, reuse=True, clear=True)
|
|
|
|
# for each fcurve data path in the database
|
|
for id, fcurve in slot_curves.items():
|
|
curve_path, index = id
|
|
# that exist on this rig:
|
|
if id not in used_ids and utils.has_data_path(rig.data, curve_path):
|
|
clone = copy_fcurve_to_channel(fcurve, data_channel)
|
|
# add fcurve to Object group
|
|
group_name = utils.get_data_path_object_name(curve_path)
|
|
clone.group = get_channelbag_group(data_channel, group_name)
|
|
# store used id's
|
|
used_ids.add(id)
|
|
|
|
# keep this slot/channel and load onto data slot
|
|
keep_slots.append(data_slot)
|
|
keep_channels.append(data_channel)
|
|
utils.safe_set_action(rig.data, action, create=True, slot=data_slot)
|
|
|
|
# determine unused fcurves
|
|
unused_ids = set()
|
|
for id in slot_curves:
|
|
if id not in used_ids:
|
|
unused_ids.add(id)
|
|
|
|
if unused_ids:
|
|
|
|
# add slot for unused fcurves
|
|
data_slot, data_channel = add_action_slot_channelbag(action, "Unused Data", slot_type,
|
|
reuse=True, clear=True)
|
|
|
|
# add unused fcurves
|
|
for id in unused_ids:
|
|
fcurve = slot_curves[id]
|
|
copy_fcurve_to_channel(fcurve, data_channel)
|
|
|
|
# always keep this slot/channel
|
|
keep_slots.append(data_slot)
|
|
keep_channels.append(data_channel)
|
|
|
|
# remove old and unused slots and channels
|
|
delete_slots = []
|
|
delete_channels = []
|
|
channelbags = utils.get_action_channelbags(action)
|
|
for slot in action.slots:
|
|
if slot not in keep_slots:
|
|
delete_slots.append(slot)
|
|
for channel in channelbags:
|
|
if channel not in keep_channels:
|
|
delete_channels.append(channel)
|
|
for channel in delete_channels:
|
|
channelbags.remove(channel)
|
|
for slot in delete_slots:
|
|
action.slots.remove(slot)
|
|
|
|
# remove all database copies
|
|
delete_actions = database["arm_actions"] + database["key_actions"] + database["rlx_actions"]
|
|
if move or not use_new_motion_set:
|
|
# remove old actions if moving to a new motion set
|
|
delete_actions += database["source_arm_actions"] + database["source_key_actions"] + database["source_rlx_actions"]
|
|
# remove actions, except this action
|
|
utils.log_detail(f"Deleting actions: {[a.name for a in delete_actions]}, except: {action.name}")
|
|
for delete_action in delete_actions:
|
|
if utils.action_exists(delete_action):
|
|
if delete_action != action:
|
|
bpy.data.actions.remove(delete_action)
|
|
|
|
# ensure the action knows it is slotted
|
|
utils.set_prop(action, "rl_action_type", "SLOTTED")
|
|
|
|
# optional: clean up action key frames
|
|
if prefs.action_clean_actions:
|
|
if utils.action_exists(action):
|
|
clean_action_keyframes(action, channels=True)
|
|
|
|
return action
|
|
|
|
|
|
def load_separate_actions(rig: bpy.types.Object, action: bpy.types.Action, move=False, temp=None, force=False):
|
|
prefs = vars.prefs()
|
|
|
|
# determine motion set info
|
|
rig_id = get_rig_id(rig)
|
|
rl_id = utils.get_rl_id(rig)
|
|
old_action = utils.safe_get_action(rig)
|
|
set_id = utils.get_prop(action, "rl_set_id")
|
|
set_generation = utils.get_prop(action, "rl_set_generation")
|
|
motion_prefix = get_motion_prefix(action)
|
|
motion_id = get_motion_id(action)
|
|
src_rl_id = utils.get_rl_id(action)
|
|
use_fake_user = action.use_fake_user
|
|
|
|
# if loading onto a different rig_id, we want to create a new motion set ...
|
|
use_new_motion_set = not force and (((rl_id or src_rl_id) and rl_id != src_rl_id) or temp or move)
|
|
if temp:
|
|
motion_prefix = "TEMP"
|
|
motion_id = temp
|
|
if use_new_motion_set:
|
|
set_id = generate_set_id()
|
|
|
|
# generate database of fcurves in the motion set
|
|
database = fetch_action_curve_database(action)
|
|
|
|
# build channels for each object in rig
|
|
utils.log_info(f"Retargetting Separate Actions: {action.name}")
|
|
keep_actions = []
|
|
remove_actions = []
|
|
data_curves = database["curves"]
|
|
first_frame = database["first_frame"]
|
|
|
|
# ARMATURE ACTION
|
|
#
|
|
if "OBJECT" in data_curves:
|
|
slot_curves = data_curves["OBJECT"]
|
|
used_ids = set()
|
|
|
|
# re-use the existing source armature action
|
|
if use_new_motion_set:
|
|
motion_id = get_unique_set_motion_id(rig_id, motion_id, motion_prefix, slotted=False, rlob=rig)
|
|
action_name = generate_action_name(rig_id, "A" if rig.type == "ARMATURE" else "O",
|
|
"", motion_id, motion_prefix)
|
|
# if temp, re-use any existing temp action
|
|
if temp and action_name in bpy.data.actions:
|
|
rig_action = bpy.data.actions[action_name]
|
|
else:
|
|
rig_action = bpy.data.actions.new(action_name)
|
|
utils.force_action_name(rig_action, action_name)
|
|
else:
|
|
rig_action: bpy.types.Action = database["source_action"]
|
|
utils.clear_action(rig_action)
|
|
|
|
# add slot and motion set data
|
|
rig_slot, rig_channel = add_action_ob_slot_channelbag(rig_action, rig, reuse=True, clear=True)
|
|
add_motion_set_data(rig_action, set_id, set_generation, arm_id=rl_id, slotted=False)
|
|
rig_action.use_fake_user = use_fake_user
|
|
|
|
# if this is a temporary action, store the action it replaced
|
|
if temp:
|
|
rig_action["rl_temp_action"] = old_action.name if old_action else ""
|
|
|
|
if rig.type == "ARMATURE":
|
|
# for each bone fcurve in the database in the order of the pose bones on the rig ...
|
|
for bone in rig.pose.bones:
|
|
for id, fcurve in slot_curves.items():
|
|
curve_path, index = id
|
|
# that exist on this rig:
|
|
if id not in used_ids and bone.name in curve_path and utils.has_data_path(rig, curve_path):
|
|
clone = copy_fcurve_to_channel(fcurve, rig_channel)
|
|
# add fcurve to bone group
|
|
group_name = utils.get_data_path_object_name(curve_path)
|
|
clone.group = get_channelbag_group(rig_channel, group_name)
|
|
# store used id's
|
|
used_ids.add(id)
|
|
|
|
if True: #if rig.type == "LIGHT" or rig.type == "CAMERA":
|
|
# for each remaining fcurve data path in the database
|
|
for id, fcurve in slot_curves.items():
|
|
curve_path, index = id
|
|
# that have valid data paths on this rig:
|
|
if id not in used_ids and utils.has_data_path(rig, curve_path):
|
|
clone = copy_fcurve_to_channel(fcurve, rig_channel)
|
|
# add fcurve to Object group
|
|
group_name = utils.get_data_path_object_name(curve_path)
|
|
clone.group = get_channelbag_group(rig_channel, group_name)
|
|
# store used id's
|
|
used_ids.add(id)
|
|
|
|
# keep this and load onto rig
|
|
keep_actions.append(rig_action)
|
|
utils.safe_set_action(rig, rig_action, create=True, slot=rig_slot)
|
|
|
|
# determine unused bone fcurves
|
|
unused_ids = set()
|
|
for id in slot_curves:
|
|
if id not in used_ids:
|
|
unused_ids.add(id)
|
|
|
|
if unused_ids:
|
|
|
|
# generate an action for unused bones
|
|
action_name = generate_action_name(rig_id, "UA", "", motion_id, motion_prefix)
|
|
if temp and action_name in bpy.data.actions:
|
|
rig_action = bpy.data.actions[action_name]
|
|
else:
|
|
rig_action = bpy.data.actions.new(action_name)
|
|
utils.force_action_name(rig_action, action_name)
|
|
rig_action["rl_unused_action"] = True
|
|
|
|
# add slot and motion set data
|
|
rig_slot, rig_channel = add_action_slot_channelbag(rig_action, "Unused Object", "OBJECT", reuse=True, clear=True)
|
|
add_motion_set_data(rig_action, set_id, set_generation, arm_id=rl_id, slotted=False)
|
|
rig_action.use_fake_user = use_fake_user
|
|
|
|
# if this is a temporary action, store the action it replaced
|
|
if temp:
|
|
rig_action["rl_temp_action"] = old_action.name if old_action else ""
|
|
|
|
# add unused fcurves
|
|
for id in unused_ids:
|
|
fcurve = slot_curves[id]
|
|
copy_fcurve_to_channel(fcurve, rig_channel)
|
|
|
|
# always keep this
|
|
keep_actions.append(rig_action)
|
|
|
|
# SHAPE KEY ACTIONS
|
|
#
|
|
EXCLUDE_KEYS = ["Basis"]
|
|
SOURCE_MESH_NAMES = ["CC_Base_Body", "CC_Game_Body", "CC_Base_Tongue", "CC_Base_Eye"]
|
|
if "KEY" in data_curves and rig.type == "ARMATURE":
|
|
slot_curves = data_curves["KEY"]
|
|
used_ids = set()
|
|
|
|
# determine source key names from source mesh shape keys
|
|
# (i.e. shape keys from the body/tongue/eyes)
|
|
source_keys = set()
|
|
for smn in SOURCE_MESH_NAMES:
|
|
for obj in rig.children:
|
|
if obj.name.startswith(smn):
|
|
if utils.object_exists_is_mesh(obj) and utils.object_has_shape_keys(obj):
|
|
for key in obj.data.shape_keys.key_blocks:
|
|
source_keys.add(key.name)
|
|
|
|
# if generating a single key action for all meshes
|
|
if not prefs.action_add_key_slots_per_obj:
|
|
|
|
# use a single action for all mesh objects
|
|
action_name = generate_action_name(rig_id, "K", "ALL", motion_id, motion_prefix)
|
|
if temp and action_name in bpy.data.actions:
|
|
key_action = bpy.data.actions[action_name]
|
|
else:
|
|
key_action = bpy.data.actions.new(action_name)
|
|
utils.force_action_name(key_action, action_name)
|
|
|
|
# add slot and motion set data
|
|
add_motion_set_data(key_action, set_id, set_generation, key_id="ALL", arm_id=rl_id, slotted=False)
|
|
key_slot, key_channel = add_action_slot_channelbag(key_action, "Key", "KEY", reuse=True, clear=True)
|
|
key_action.use_fake_user = use_fake_user
|
|
|
|
# if this is a temporary action, store the action it replaced
|
|
if temp:
|
|
key_action["rl_temp_action"] = old_action.name if old_action else ""
|
|
|
|
# done id's across all meshes
|
|
done_ids = set()
|
|
done_keys = set()
|
|
|
|
for obj in rig.children:
|
|
|
|
if utils.object_exists_is_mesh(obj) and utils.object_has_shape_keys(obj):
|
|
obj_id = get_obj_id(obj)
|
|
|
|
# if generating key actions per mesh
|
|
if prefs.action_add_key_slots_per_obj:
|
|
|
|
# generate a specific shape key slot for each mesh object
|
|
action_name = make_key_action_name(rig_id, motion_id, obj_id, motion_prefix)
|
|
if temp and action_name in bpy.data.actions:
|
|
key_action = bpy.data.actions[action_name]
|
|
else:
|
|
key_action = bpy.data.actions.new(action_name)
|
|
utils.force_action_name(key_action, action_name)
|
|
|
|
# add slot and motion set data
|
|
key_slot, key_channel = add_action_key_slot_channelbag(key_action, obj, reuse=True, clear=True)
|
|
add_motion_set_data(key_action, set_id, set_generation, key_id=obj_id, arm_id=rl_id, slotted=False)
|
|
key_action.use_fake_user = use_fake_user
|
|
|
|
# if this is a temporary action, store the action it replaced
|
|
if temp:
|
|
key_action["rl_temp_action"] = old_action.name if old_action else ""
|
|
|
|
# done id's per mesh
|
|
done_ids = set()
|
|
done_keys = set()
|
|
|
|
has_keys = False
|
|
|
|
# determine data paths of all driven shape keys in this mesh
|
|
driver_paths = set()
|
|
if obj.data.shape_keys.animation_data:
|
|
for driver in obj.data.shape_keys.animation_data.drivers:
|
|
driver_paths.add(driver.data_path)
|
|
|
|
# for each key curve in the database
|
|
for id, fcurve in slot_curves.items():
|
|
curve_path, index = id
|
|
|
|
# whose data path exists on this object and is not driven
|
|
if utils.has_data_path(obj.data.shape_keys, curve_path) and curve_path not in driver_paths:
|
|
# this action has keys for this mesh
|
|
has_keys = True
|
|
# if not already added for this mesh
|
|
if id not in done_ids:
|
|
fcurve = slot_curves[id]
|
|
key_name = utils.get_data_path_object_name(curve_path)
|
|
clone = copy_fcurve_to_channel(fcurve, key_channel)
|
|
# add fcurve to expression group
|
|
group_name = get_shape_key_group_name(key_name)
|
|
clone.group = get_channelbag_group(key_channel, group_name)
|
|
# store used id's
|
|
done_keys.add(key_name)
|
|
used_ids.add(id)
|
|
done_ids.add(id)
|
|
|
|
if prefs.action_add_empty_key_channels and first_frame is not None:
|
|
|
|
# for any shape keys without fcurves that are source shape keys ...
|
|
# (i.e. shape keys from the body/tongue/eyes)
|
|
for key in obj.data.shape_keys.key_blocks:
|
|
if (key.name not in done_keys and
|
|
key.name in source_keys and
|
|
key.name not in EXCLUDE_KEYS):
|
|
|
|
data_path = f"key_blocks[\"{key.name}\"].value"
|
|
|
|
# that are not driven:
|
|
if data_path not in driver_paths:
|
|
|
|
# this action has keys for this mesh
|
|
has_keys = True
|
|
|
|
# add a zero single keyframe track
|
|
fcurve = key_channel.fcurves.new(data_path)
|
|
fcurve.keyframe_points.add(1)
|
|
fcurve.keyframe_points.foreach_set('co', [first_frame, 0.0])
|
|
reset_fcurve_interpolation(fcurve)
|
|
|
|
# add fcurve to expression group
|
|
group_name = get_shape_key_group_name(key.name)
|
|
fcurve.group = get_channelbag_group(key_channel, group_name)
|
|
|
|
# keep actions that have assigned keys and load onto mesh,
|
|
# remove actions that have no assigned keys
|
|
if has_keys:
|
|
keep_actions.append(key_action)
|
|
utils.safe_set_action(obj.data.shape_keys, key_action, create=True, slot=key_slot)
|
|
else:
|
|
remove_actions.append(key_action)
|
|
utils.safe_set_action(obj.data.shape_keys, None)
|
|
|
|
# determine unused shape key fcurves
|
|
unused_ids = set()
|
|
for id in slot_curves:
|
|
if id not in used_ids:
|
|
unused_ids.add(id)
|
|
|
|
if unused_ids:
|
|
|
|
# generate an action for all unused shape keys
|
|
action_name = generate_action_name(rig_id, "UK", "UNUSED", motion_id, motion_prefix)
|
|
if temp and action_name in bpy.data.actions:
|
|
key_action = bpy.data.actions[action_name]
|
|
else:
|
|
key_action = bpy.data.actions.new(action_name)
|
|
utils.force_action_name(key_action, action_name)
|
|
key_action["rl_unused_action"] = True
|
|
|
|
# add slot and motion set data
|
|
key_slot, key_channel = add_action_slot_channelbag(key_action, "Unused Keys", "KEY", reuse=True, clear=True)
|
|
add_motion_set_data(key_action, set_id, set_generation, key_id="UNUSED", arm_id=rl_id, slotted=False)
|
|
key_action.use_fake_user = use_fake_user
|
|
|
|
# if this is a temporary action, store the action it replaced
|
|
if temp:
|
|
key_action["rl_temp_action"] = old_action.name if old_action else ""
|
|
|
|
# add unused shape key fcurves to action
|
|
for id in unused_ids:
|
|
fcurve = slot_curves[id]
|
|
copy_fcurve_to_channel(fcurve, key_channel)
|
|
|
|
# always keep this
|
|
keep_actions.append(key_action)
|
|
|
|
# LIGHT / CAMERA (DATA) ACTIONS
|
|
#
|
|
slot_types = ["LIGHT", "CAMERA"]
|
|
for slot_type in slot_types:
|
|
|
|
if slot_type in data_curves:
|
|
|
|
if rig.type != slot_type: continue
|
|
|
|
slot_curves = data_curves[slot_type]
|
|
used_ids = set()
|
|
|
|
action_name = generate_action_name(rig_id, slot_type[0], "", motion_id, motion_prefix)
|
|
# if temp, re-use any existing temp action
|
|
if temp and action_name in bpy.data.actions:
|
|
data_action = bpy.data.actions[action_name]
|
|
else:
|
|
data_action = bpy.data.actions.new(action_name)
|
|
utils.force_action_name(data_action, action_name)
|
|
# add slot and motion set data
|
|
data_slot, data_channel = add_action_data_slot_channelbag(data_action, rig, slot_type=slot_type, reuse=True, clear=True)
|
|
add_motion_set_data(rig_action, set_id, set_generation, rlx_id=rl_id, slotted=False)
|
|
data_action.use_fake_user = use_fake_user
|
|
# if this is a temporary action, store the action it replaced
|
|
if temp:
|
|
data_action["rl_temp_action"] = old_action.name if old_action else ""
|
|
|
|
# for each fcurve data path in the database
|
|
for id, fcurve in slot_curves.items():
|
|
curve_path, index = id
|
|
# that exist on this rig:
|
|
if id not in used_ids and utils.has_data_path(rig.data, curve_path):
|
|
clone = copy_fcurve_to_channel(fcurve, data_channel)
|
|
# add fcurve to Object group
|
|
group_name = utils.get_data_path_object_name(curve_path)
|
|
clone.group = get_channelbag_group(data_channel, group_name)
|
|
# store used id's
|
|
used_ids.add(id)
|
|
|
|
# keep this slot/channel and load onto rig slot
|
|
keep_actions.append(data_action)
|
|
utils.safe_set_action(rig.data, data_action, create=True, slot=data_slot)
|
|
|
|
# determine unused fcurves
|
|
unused_ids = set()
|
|
for id in slot_curves:
|
|
if id not in used_ids:
|
|
unused_ids.add(id)
|
|
|
|
if unused_ids:
|
|
|
|
# generate an action for unused bones
|
|
action_name = generate_action_name(rig_id, "U"+slot_type[0], "", motion_id, motion_prefix)
|
|
if temp and action_name in bpy.data.actions:
|
|
data_action = bpy.data.actions[action_name]
|
|
else:
|
|
data_action = bpy.data.actions.new(action_name)
|
|
utils.force_action_name(data_action, action_name)
|
|
data_action["rl_unused_action"] = True
|
|
|
|
# add slot and motion set data
|
|
data_slot, data_channel = add_action_slot_channelbag(data_action, "Unused Data", slot_type, reuse=True, clear=True)
|
|
add_motion_set_data(data_action, set_id, set_generation, rlx_id=rl_id, slotted=False)
|
|
data_action.use_fake_user = use_fake_user
|
|
|
|
# if this is a temporary action, store the action it replaced
|
|
if temp:
|
|
data_action["rl_temp_action"] = old_action.name if old_action else ""
|
|
|
|
# add unused fcurves
|
|
for id in unused_ids:
|
|
fcurve = slot_curves[id]
|
|
copy_fcurve_to_channel(fcurve, data_channel)
|
|
|
|
# always keep this slot/channel
|
|
keep_actions.append(data_action)
|
|
|
|
# remove database copies ...
|
|
remove_actions += database["arm_actions"]
|
|
remove_actions += database["key_actions"]
|
|
remove_actions += database["rlx_actions"]
|
|
if move or not use_new_motion_set:
|
|
remove_actions += database["source_arm_actions"]
|
|
remove_actions += database["source_key_actions"]
|
|
remove_actions += database["source_rlx_actions"]
|
|
for del_action in remove_actions:
|
|
if utils.action_exists(del_action) and del_action not in keep_actions:
|
|
bpy.data.actions.remove(del_action)
|
|
|
|
# optional: clean up action key-frames
|
|
if prefs.action_clean_actions:
|
|
for action in keep_actions:
|
|
if utils.action_exists(action):
|
|
clean_action_keyframes(action, channels=True)
|
|
|
|
return rig_action
|
|
|
|
|
|
def get_shape_key_group_name(key_name):
|
|
groups = {
|
|
"Basis": "Basis",
|
|
"Viseme": "V_",
|
|
"Brow": "Brow_",
|
|
"Eye": "Eye_",
|
|
"Eyelashes": "Eyelashes_",
|
|
"Ear": "Ear_",
|
|
"Nose": "Nose_",
|
|
"Mouth": "Mouth_",
|
|
"Jaw": "Jaw_",
|
|
"Neck": "Neck_",
|
|
"Head": "Head_",
|
|
"Tongue": "Tongue_",
|
|
"Eyelid": "Eyelid_",
|
|
"Constraint": "C_",
|
|
"Eyeball": "Eyeball_",
|
|
}
|
|
for group_name, prefix in groups.items():
|
|
if key_name.startswith(prefix):
|
|
return group_name
|
|
return "Custom"
|
|
|
|
|
|
def copy_fcurve_to_channel(from_fcurve: bpy.types.FCurve, to_channel, no_frames=False) -> bpy.types.FCurve:
|
|
if from_fcurve and to_channel:
|
|
if utils.B500():
|
|
to_fcurve: bpy.types.FCurve = to_channel.fcurves.new_from_fcurve(from_fcurve)
|
|
if no_frames:
|
|
to_fcurve.keyframe_points.clear()
|
|
else:
|
|
data_path = from_fcurve.data_path
|
|
index = from_fcurve.array_index
|
|
to_fcurve: bpy.types.FCurve = to_channel.fcurves.new(data_path, index=index)
|
|
to_fcurve.auto_smoothing = from_fcurve.auto_smoothing
|
|
to_fcurve.color = from_fcurve.color
|
|
to_fcurve.color_mode = from_fcurve.color_mode
|
|
to_fcurve.extrapolation = from_fcurve.extrapolation
|
|
to_fcurve.mute = from_fcurve.mute
|
|
to_fcurve.lock = from_fcurve.lock
|
|
if not no_frames:
|
|
from_keyframes, num_points = get_fcurve_keyframe_data(from_fcurve)
|
|
apply_fcurve_keyframe_data(to_fcurve, from_keyframes)
|
|
to_fcurve.update()
|
|
return to_fcurve
|
|
else:
|
|
return None
|
|
|
|
|
|
def clone_fcurve_to_channel(from_fcurve: bpy.types.FCurve, to_channel) -> bpy.types.FCurve:
|
|
if from_fcurve and to_channel:
|
|
data_path = from_fcurve.data_path
|
|
index = from_fcurve.array_index
|
|
to_fcurve: bpy.types.FCurve = to_channel.fcurves.new(data_path, index=index)
|
|
to_fcurve.auto_smoothing = from_fcurve.auto_smoothing
|
|
to_fcurve.color = from_fcurve.color
|
|
to_fcurve.color_mode = from_fcurve.color_mode
|
|
to_fcurve.extrapolation = from_fcurve.extrapolation
|
|
to_fcurve.mute = from_fcurve.mute
|
|
to_fcurve.lock = from_fcurve.lock
|
|
return to_fcurve
|
|
else:
|
|
return None
|
|
|
|
|
|
def refactor_to_slotted_action(objects, actions):
|
|
|
|
if not utils.B440():
|
|
return actions
|
|
|
|
combined = []
|
|
|
|
# build a database of armatures and their child mesh actions
|
|
data = {}
|
|
for obj in objects:
|
|
if obj.type == "ARMATURE":
|
|
action = utils.safe_get_action(obj)
|
|
data[obj.name] = {
|
|
"armature": obj,
|
|
"action": action,
|
|
"meshes": {},
|
|
}
|
|
for obj in objects:
|
|
if obj.type == "MESH":
|
|
action = utils.safe_get_action(obj.data.shape_keys)
|
|
if action:
|
|
if action in actions:
|
|
armature = obj.parent
|
|
if armature and action and armature.name in data:
|
|
meshes = data[armature.name]["meshes"]
|
|
meshes[obj.name] = {
|
|
"mesh": obj,
|
|
"action": action
|
|
}
|
|
else:
|
|
utils.log_error(f"Mesh action {action}, not found in actions!")
|
|
|
|
combined = []
|
|
|
|
for armature_name, armature_data in data.items():
|
|
armature = armature_data["armature"]
|
|
armature_action = armature_data["action"]
|
|
armature_channelbags = utils.get_action_channelbags(armature_action)
|
|
meshes = armature_data["meshes"]
|
|
for mesh_name, mesh_data in meshes.items():
|
|
mesh = mesh_data["mesh"]
|
|
mesh_action = mesh_data["action"]
|
|
# if already combined, ignore
|
|
if mesh_action == armature_action:
|
|
continue
|
|
# copy mesh action channels into slotted action
|
|
mesh_channelbags = utils.get_action_channelbags(mesh_action)
|
|
for i, mesh_channel in enumerate(mesh_channelbags):
|
|
slot_type = mesh_channel.slot.target_id_type
|
|
slot = armature_action.slots.new(slot_type, mesh_name)
|
|
channel = armature_channelbags.new(slot)
|
|
mesh_fcurve: bpy.types.FCurve = None
|
|
for mesh_fcurve in mesh_channel.fcurves:
|
|
copy_fcurve_to_channel(mesh_fcurve, channel)
|
|
mesh.data.shape_keys.animation_data.action = armature_action
|
|
mesh.data.shape_keys.animation_data.action_slot = slot
|
|
combined.append(armature_action)
|
|
|
|
# clean up old actions
|
|
for action in actions:
|
|
if action and action not in combined:
|
|
try:
|
|
bpy.data.actions.remove(action)
|
|
except: ...
|
|
|
|
return combined
|
|
|
|
|
|
def clean_actions(obj):
|
|
if utils.object_exists_is_camera(obj) or utils.object_exists_is_light(obj):
|
|
object_action = utils.safe_get_action(obj)
|
|
data_action = utils.safe_get_action(obj.data)
|
|
clean_action_keyframes(object_action)
|
|
clean_action_keyframes(data_action)
|
|
elif utils.object_exists_is_armature(obj):
|
|
rig_action = utils.safe_get_action(obj)
|
|
clean_action_keyframes(rig_action)
|
|
for child in obj.children:
|
|
if utils.object_exists_is_mesh(child) and utils.object_has_shape_keys(obj):
|
|
key_action = utils.safe_get_action(child)
|
|
clean_action_keyframes(key_action)
|
|
|
|
|
|
def clean_action_keyframes(action: bpy.types.Action, bone_threshold=0.0001, key_threshold=0.001, facerig_threshold=0.00001, other_threshold=0.0001, channels=True):
|
|
"""Note: bone threshold value of 0.0001 will clean bone key-frames to 1/200th of a degree 1/10th of a mm
|
|
key threshold value of 0.01 will clean shape key key-frames to 1/1000th of their range
|
|
facerig threshold value of about 0.00001 will clean key-frames on the
|
|
face rig controls to within 1/1000th on it's driven shape-key value."""
|
|
|
|
if not action:
|
|
return
|
|
fcurves = utils.get_action_fcurves(action)
|
|
bone_curves = []
|
|
key_curves = []
|
|
facerig_curves = []
|
|
other_curves = []
|
|
fcurve: bpy.types.FCurve
|
|
for fcurve in fcurves:
|
|
if fcurve.data_path.startswith("key_blocks"):
|
|
key_curves.append(fcurve)
|
|
elif fcurve.data_path.startswith("pose.bones"):
|
|
if "CTRL_" in fcurve.data_path:
|
|
facerig_curves.append(fcurve)
|
|
else:
|
|
bone_curves.append(fcurve)
|
|
else:
|
|
other_curves.append(fcurve)
|
|
groups = {}
|
|
groups["Pose Bone"] = (bone_curves, bone_threshold)
|
|
groups["Shape Keys"] = (key_curves, key_threshold)
|
|
groups["Face Rig Control"] = (facerig_curves, facerig_threshold)
|
|
groups["Other"] = (other_curves, other_threshold)
|
|
|
|
if bpy.context.screen.areas:
|
|
area = bpy.context.screen.areas[0]
|
|
area_type = area.type
|
|
area.type = "DOPESHEET_EDITOR"
|
|
|
|
if utils.B320():
|
|
with bpy.context.temp_override(area=area):
|
|
|
|
# deselect all key-frames
|
|
bpy.ops.action.select_all(action="DESELECT")
|
|
|
|
# deselect all channel groups and fcurves,
|
|
# otherwise everything gets cleaned (in channel mode) ...
|
|
for action in bpy.data.actions:
|
|
action_groups = utils.get_action_groups(action)
|
|
for group in action_groups:
|
|
group.select = False
|
|
action_fcurves = utils.get_action_fcurves(action)
|
|
for fcurve in action_fcurves:
|
|
fcurve.select = False
|
|
|
|
for group_name, (curves, threshold) in groups.items():
|
|
if curves:
|
|
curve_count = 0
|
|
keyframe_count = 0
|
|
|
|
# select the channels & groups to be cleaned
|
|
for fcurve in curves:
|
|
if utils.id_exists(fcurve):
|
|
fcurve.select = True
|
|
if utils.id_exists(fcurve.group):
|
|
fcurve.group.select = True
|
|
curve_count += 1
|
|
|
|
# if not cleaning by channel, select all the keyframes in each fcurve
|
|
if not channels:
|
|
kf: bpy.types.Keyframe
|
|
for kf in fcurve.keyframe_points:
|
|
kf.select_control_point = True
|
|
keyframe_count += 1
|
|
|
|
if channels:
|
|
utils.log_info(f"Cleaning action {group_name} key-frames: {action.name} ({curve_count} channels) Threshold: {threshold}")
|
|
else:
|
|
utils.log_info(f"Cleaning action {group_name} key-frames: {action.name} ({curve_count} channels, {keyframe_count} key-frames) Threshold: {threshold}")
|
|
|
|
# clean the selected channels / key-frames
|
|
bpy.ops.action.clean(threshold=threshold, channels=channels)
|
|
|
|
# deselect the channels we just cleaned
|
|
for fcurve in curves:
|
|
if utils.id_exists(fcurve):
|
|
fcurve.select = False
|
|
if utils.id_exists(fcurve.group) and fcurve.group.name != "":
|
|
fcurve.group.select = False
|
|
|
|
# deselect all key-frames
|
|
bpy.ops.action.select_all(action="DESELECT")
|
|
|
|
else:
|
|
utils.log_info(f"Cleaning action keyframes: {action.name} ({len(fcurves)} fcurves)")
|
|
context_override = bpy.context.copy()
|
|
context_override["area"] = area
|
|
context_override["selected_editable_fcurves"] = fcurves
|
|
context_override["selected_visible_fcurves"] = fcurves
|
|
bpy.ops.action.select_all(context_override, action="DESELECT")
|
|
bpy.ops.action.clean(context_override, threshold=threshold, channels=channels)
|
|
#except:
|
|
# utils.log_error(f"Unable to clean action keyframes: {action.name}")
|
|
area.type = area_type
|
|
|
|
|
|
def reset_fcurve_interpolation(fcurve: bpy.types.FCurve, interpolation="LINEAR"):
|
|
if interpolation != "BEZIER":
|
|
for keyframe in fcurve.keyframe_points:
|
|
keyframe.interpolation = interpolation
|
|
else:
|
|
L = len(fcurve.keyframe_points)
|
|
for i, keyframe in enumerate(fcurve.keyframe_points):
|
|
co = keyframe.co
|
|
prev: Vector = fcurve.keyframe_points[i-1].co if i > 0 else keyframe.co
|
|
next: Vector = fcurve.keyframe_points[i+1].co if i < L-1 else keyframe.co
|
|
keyframe.handle_left_type = "AUTO"
|
|
keyframe.handle_right_type = "AUTO"
|
|
keyframe.interpolation = interpolation
|
|
|
|
if i == L-1:
|
|
next = co + Vector((1, 0))
|
|
if i == 0:
|
|
prev = co + Vector((-1, 0))
|
|
|
|
forward: Vector = co * 0.5 + next * 0.5
|
|
back: Vector = co * 0.5 + prev * 0.5
|
|
foward_delta: Vector = forward - co
|
|
back_delta: Vector = co - back
|
|
|
|
if (foward_delta.y >= 0 and back_delta.y < 0) or (foward_delta.y < 0 and back_delta.y >= 0):
|
|
forward.y = keyframe.co.y
|
|
back.y = keyframe.co.y
|
|
else:
|
|
dy = (forward.y - back.y) * 0.5
|
|
forward.y = co.y + dy
|
|
back.y = co.y - dy
|
|
|
|
keyframe.handle_left[0] = back.x
|
|
keyframe.handle_left[1] = back.y
|
|
keyframe.handle_right[0] = forward.x
|
|
keyframe.handle_right[1] = forward.y
|
|
|
|
fcurve.update()
|
|
|
|
|
|
def shift_action(action, to_frame, frame_start = 1):
|
|
if to_frame == frame_start:
|
|
return
|
|
fcurves = utils.get_action_fcurves(action)
|
|
fcurve: bpy.types.FCurve = None
|
|
for fcurve in fcurves:
|
|
num_points = len(fcurve.keyframe_points)
|
|
points_data = [0.0,0.0]*num_points
|
|
fcurve.keyframe_points.foreach_get('co', points_data)
|
|
for i in range(0, num_points):
|
|
frame = points_data[i*2]
|
|
points_data[i*2] = frame - frame_start + to_frame
|
|
fcurve.keyframe_points.foreach_set('co', points_data)
|
|
reset_fcurve_interpolation(fcurve)
|
|
|
|
|
|
def mix_actions(src_action, src_slot, dst_action, dst_slot, frame_start, frame_end):
|
|
src_channelbag = utils.get_action_channelbag(src_action, src_slot)
|
|
dst_channelbag = utils.get_action_channelbag(dst_action, dst_slot)
|
|
|
|
#root_bone = "CC_Base_BoneRoot"
|
|
#src_root_transform_set = fetch_action_bone_transform_set(src_channelbag, root_bone)
|
|
#dst_root_transform_set = fetch_action_bone_transform_set(dst_channelbag, root_bone)
|
|
|
|
if not src_channelbag:
|
|
utils.log_error(f"Mix Actions: No source channels!")
|
|
return
|
|
|
|
if not dst_channelbag:
|
|
utils.log_error(f"Mix Actions: No destination channels!")
|
|
return
|
|
|
|
# determine which src curves correspond to which dst curves
|
|
fcurve_map = {}
|
|
used_dst = set()
|
|
for i, src_curve in enumerate(src_channelbag.fcurves):
|
|
for j, dst_curve in enumerate(dst_channelbag.fcurves):
|
|
if src_curve.data_path == dst_curve.data_path and src_curve.array_index == dst_curve.array_index:
|
|
fcurve_map[i] = j
|
|
used_dst.add(j)
|
|
break
|
|
|
|
# mix the src curves into the dst curve over the range
|
|
new_src = []
|
|
for i, src_curve in enumerate(src_channelbag.fcurves):
|
|
if i in fcurve_map:
|
|
j = fcurve_map[i]
|
|
dst_curve = dst_channelbag.fcurves[j]
|
|
mix_fcurve(src_curve, dst_curve, frame_start, frame_end)
|
|
else:
|
|
new_src.append(src_curve)
|
|
|
|
# these dst_curves have no corresponding curve in the src action, remove the mix keyframes?
|
|
for j, dst_curve in enumerate(dst_channelbag.fcurves):
|
|
if j not in used_dst:
|
|
mix_fcurve(None, dst_curve, frame_start, frame_end)
|
|
|
|
# for src curves not in the dst action, copy the fcurve in for that range
|
|
for src_curve in new_src:
|
|
copy_fcurve_to_channel(src_curve, dst_channelbag)
|
|
|
|
|
|
KEYFRAME_DATA_SIZE = {
|
|
"co": 2,
|
|
"type": 1,
|
|
"back": 1,
|
|
"easing": 1,
|
|
"period": 1,
|
|
"amplitude": 1,
|
|
"interpolation": 1,
|
|
"handle_left": 2,
|
|
"handle_right": 2,
|
|
"handle_left_type": 1,
|
|
"handle_right_type": 1,
|
|
}
|
|
|
|
|
|
def get_fcurve_keyframe_data(fcurve: bpy.types.FCurve):
|
|
if fcurve:
|
|
num_points = len(fcurve.keyframe_points)
|
|
else:
|
|
num_points = 0
|
|
|
|
keyframe_data = {}
|
|
for attr, size in KEYFRAME_DATA_SIZE.items():
|
|
data = [0]*size*num_points
|
|
if fcurve:
|
|
fcurve.keyframe_points.foreach_get(attr, data)
|
|
keyframe_data[attr] = data
|
|
|
|
return keyframe_data, num_points
|
|
|
|
|
|
def overwrite_fcurve_keyframe_data(src_keyframes, dst_keyframes, frame_before, frame_after):
|
|
result_keyframes = {}
|
|
for attr in src_keyframes:
|
|
src_data = src_keyframes[attr]
|
|
dst_data = dst_keyframes[attr]
|
|
size = KEYFRAME_DATA_SIZE[attr]
|
|
result_keyframes[attr] = dst_data[:(frame_before+1)*size] + src_data + dst_data[frame_after*size:]
|
|
return result_keyframes
|
|
|
|
|
|
def apply_fcurve_keyframe_data(fcurve: bpy.types.FCurve, keyframe_data):
|
|
num_points = len(keyframe_data["type"])
|
|
fcurve.keyframe_points.clear()
|
|
fcurve.keyframe_points.add(num_points)
|
|
for attr, data in keyframe_data.items():
|
|
fcurve.keyframe_points[0].back
|
|
fcurve.keyframe_points.foreach_set(attr, data)
|
|
|
|
|
|
def get_fcurve_data(fcurve: bpy.types.FCurve):
|
|
fcurve_data = {
|
|
"array_index": fcurve.array_index,
|
|
"auto_smoothing": fcurve.auto_smoothing,
|
|
"color": fcurve.color.copy(),
|
|
"color_mode": fcurve.color_mode,
|
|
"data_path": fcurve.data_path,
|
|
"extrapolation": fcurve.extrapolation,
|
|
}
|
|
return fcurve_data
|
|
|
|
|
|
def mix_fcurve(src_curve: bpy.types.FCurve, dst_curve: bpy.types.FCurve, frame_start, frame_end):
|
|
src_keyframes, num_src_points = get_fcurve_keyframe_data(src_curve)
|
|
dst_keyframes, num_dst_points = get_fcurve_keyframe_data(dst_curve)
|
|
dst_frames = dst_keyframes["co"][::2]
|
|
dst_index_before = 0
|
|
dst_index_after = num_dst_points
|
|
for i in range(0, num_dst_points):
|
|
frame = dst_frames[i]
|
|
if frame < frame_start:
|
|
dst_index_before = i
|
|
if frame > frame_end:
|
|
dst_index_after = i
|
|
break
|
|
result_keyframes = overwrite_fcurve_keyframe_data(src_keyframes, dst_keyframes, dst_index_before, dst_index_after)
|
|
apply_fcurve_keyframe_data(dst_curve, result_keyframes)
|
|
dst_curve.update()
|
|
|
|
|
|
def target_action(target):
|
|
try:
|
|
return target.animation_data.action
|
|
except: ...
|
|
return None
|
|
|
|
|
|
def target_slot(target):
|
|
try:
|
|
if utils.B440():
|
|
return target.animation_data.action_slot
|
|
except: ...
|
|
return None
|
|
|
|
|
|
def fetch_target_channels(rig, rig_action=None):
|
|
rig_slot = target_slot(rig)
|
|
if not rig_action:
|
|
rig_action = utils.safe_get_action(rig)
|
|
rig_channelbag = utils.get_action_channelbag(rig_action, slot=rig_slot)
|
|
rig_channel = (rig_action, rig_slot, rig_channelbag)
|
|
key_channels = {}
|
|
for child in rig.children:
|
|
if utils.object_exists_has_shape_keys(child):
|
|
key_slot = target_slot(child.data.shape_keys)
|
|
key_action = utils.safe_get_action(child.data.shape_keys)
|
|
key_channelbag = utils.get_action_channelbag(key_action, slot=key_slot)
|
|
key_channels[child.name] = (key_action, key_slot, key_channelbag)
|
|
data_slot = target_slot(rig.data)
|
|
if rig.type == "LIGHT" or rig.type == "CAMERA":
|
|
data_action = utils.safe_get_action(rig.data)
|
|
else:
|
|
data_action = None
|
|
data_channelbag = utils.get_action_channelbag(data_action, slot=data_slot)
|
|
data_channel = (data_action, data_slot, data_channelbag)
|
|
return rig_channel, key_channels, data_channel
|
|
|
|
|
|
def fetch_key_targets(rig):
|
|
key_targets = []
|
|
for child in rig.children:
|
|
if utils.object_exists_has_shape_keys(child):
|
|
key_targets.append(child.name)
|
|
return key_targets
|
|
|
|
|
|
def mix_motion_set(rig, action_store_id, frame_start, frame_end):
|
|
props = vars.props()
|
|
stored_main_action, stored_main_slot = props.fetch_stored_rig_channel(action_store_id)
|
|
done = False
|
|
objects = [rig]
|
|
if utils.object_exists_is_armature(rig):
|
|
for child in rig.children:
|
|
if utils.object_exists_is_mesh(child) and utils.object_has_shape_keys(child):
|
|
objects.append(child)
|
|
|
|
mix_pairs = []
|
|
for obj in objects:
|
|
action_store = props.get_action_store_for(action_store_id, obj)
|
|
if utils.object_exists_is_armature(obj):
|
|
if obj.animation_data:
|
|
mix_pairs.append((obj, action_store, "OBJECT",
|
|
target_action(obj),
|
|
target_slot(obj),
|
|
action_store.object_action if action_store else None,
|
|
action_store.object_slot_id if action_store else None))
|
|
elif utils.object_exists_is_mesh(obj):
|
|
if obj.data.shape_keys.animation_data:
|
|
mix_pairs.append((obj, action_store, "KEY",
|
|
target_action(obj.data.shape_keys),
|
|
target_slot(obj.data.shape_keys),
|
|
action_store.object_action if action_store else None,
|
|
action_store.object_slot_id if action_store else None))
|
|
elif utils.object_exists_is_light(obj) or utils.object_exists_is_camera(obj):
|
|
if obj.animation_data:
|
|
mix_pairs.append((obj, action_store, "OBJECT",
|
|
target_action(obj),
|
|
target_slot(obj),
|
|
action_store.object_action if action_store else None,
|
|
action_store.object_slot_id if action_store else None))
|
|
if obj.data.animation_data:
|
|
mix_pairs.append((obj, action_store, "LIGHT" if utils.object_exists_is_light(obj) else "CAMERA",
|
|
target_action(obj.data),
|
|
target_slot(obj.data),
|
|
action_store.data_action if action_store else None,
|
|
action_store.data_slot_id if action_store else None))
|
|
|
|
for obj, action_store, slot_type, new_action, new_slot, old_action, old_slot_id in mix_pairs:
|
|
if new_action:
|
|
valid_slot = (not utils.B440() or old_slot_id)
|
|
if old_action and valid_slot:
|
|
# mix new action slot over old action slot
|
|
old_slot = utils.find_action_slot(old_action, slot_id=old_slot_id)
|
|
mix_actions(new_action, new_slot, old_action, old_slot, frame_start, frame_end)
|
|
done = True
|
|
else:
|
|
copy_channels_to_rig_motion(rig, obj, slot_type, new_action, new_slot, stored_main_action)
|
|
# invalidate the actions in the store (if there is one) to prevent it restoring an action (with the wrong slot)
|
|
if action_store:
|
|
if slot_type in ["LIGHT", "CAMERA"]:
|
|
action_store.data_action_valid = False
|
|
else:
|
|
action_store.object_action_valid = False
|
|
|
|
|
|
if done:
|
|
props.restore_actions(action_store_id)
|
|
#delete_motion_set(set_id)
|
|
|
|
|
|
def shift_motion_frames(motion_action, to_frame):
|
|
if motion_action:
|
|
actions = get_motion_set_actions(motion_action)
|
|
action_range = get_actions_frame_range(actions)
|
|
utils.log_info(f"Moving motion frames: {motion_action.name} {action_range} to {to_frame}")
|
|
for action in actions:
|
|
shift_action(action, to_frame, action_range[0])
|
|
|
|
|
|
def add_slot_channels_to_rig_motion(rig, obj, slot_type, action, reuse=False, clear=False):
|
|
prefs = vars.prefs()
|
|
|
|
is_rig = utils.object_exists_is_armature(rig)
|
|
arm_id = utils.get_rl_id(rig)
|
|
|
|
if prefs.use_action_slots():
|
|
|
|
# add slot channel for src action slot channel
|
|
slot, channel = add_action_slot_channelbag(action, obj.name, slot_type, reuse=reuse, clear=clear)
|
|
|
|
else:
|
|
|
|
if is_rig:
|
|
# determine motion set info
|
|
rig_id = get_rig_id(rig)
|
|
obj_id = get_obj_id(obj)
|
|
set_id = utils.get_prop(action, "rl_set_id")
|
|
set_generation = utils.get_prop(action, "rl_set_generation")
|
|
motion_prefix = get_motion_prefix(action)
|
|
motion_id = get_motion_id(action)
|
|
use_fake_user = action.use_fake_user
|
|
|
|
# generate a slot channel for the object
|
|
if set_id:
|
|
if slot_type == "OBJECT":
|
|
action_name = make_armature_action_name(rig_id, motion_id, motion_prefix, slotted=False)
|
|
elif slot_type == "KEY":
|
|
action_name = make_key_action_name(rig_id, motion_id, obj_id, motion_prefix)
|
|
|
|
else:
|
|
set_id = None
|
|
motion_id = get_motion_id(action, f"{utils.datetimes()}")
|
|
motion_prefix = get_motion_prefix(action)
|
|
|
|
if slot_type == "OBJECT":
|
|
action_name = generate_action_name(obj.name, "O", "", motion_id, motion_prefix)
|
|
if slot_type == "LIGHT" or slot_type == "CAMERA":
|
|
action_name = generate_action_name(obj.name, slot_type[0], "", motion_id, motion_prefix)
|
|
|
|
# make a new dst_action for this object
|
|
if reuse and action_name in bpy.data.actions:
|
|
action = bpy.data.actions[action_name]
|
|
else:
|
|
action = bpy.data.actions.new(action_name)
|
|
utils.force_action_name(action, action_name)
|
|
if clear:
|
|
utils.clear_action(action)
|
|
action.use_fake_user = use_fake_user
|
|
|
|
if set_id:
|
|
add_motion_set_data(action, set_id, set_generation,
|
|
key_id=obj_id, arm_id=arm_id, slotted=False)
|
|
|
|
# add slot and motion set data
|
|
if slot_type == "OBJECT":
|
|
slot, channel = add_action_ob_slot_channelbag(action, rig, reuse=True, clear=True)
|
|
elif slot_type == "KEY":
|
|
slot, channel = add_action_key_slot_channelbag(action, obj, reuse=True, clear=True)
|
|
elif slot_type == "LIGHT" or slot_type == "CAMERA":
|
|
slot, channel = add_action_slot_channelbag(action, obj.name, slot_type, reuse=True, clear=True)
|
|
|
|
return action, slot, channel
|
|
|
|
|
|
def copy_channels_to_rig_motion(rig, obj, slot_type, src_action, src_slot, dst_action):
|
|
src_channelbag = utils.get_action_channelbag(src_action, src_slot)
|
|
dst_action, dst_slot, dst_channel = add_slot_channels_to_rig_motion(rig, obj, slot_type, dst_action, reuse=True, clear=True)
|
|
|
|
# copy the source fcurves into the dst_channel
|
|
for fcurve in src_channelbag.fcurves:
|
|
copy_fcurve_to_channel(fcurve, dst_channel)
|
|
|
|
if slot_type == "KEY":
|
|
utils.safe_set_action(obj.data.shape_keys, dst_action, slot=dst_slot)
|
|
elif slot_type == "OBJECT":
|
|
utils.safe_set_action(obj, dst_action, slot=dst_slot)
|
|
elif slot_type == "LIGHT" or slot_type == "CAMERA":
|
|
utils.safe_set_action(obj.data, dst_action, slot=dst_slot)
|
|
else:
|
|
utils.log_error(f"Unknown slot target for {dst_action} {dst_slot} {dst_channel}")
|
|
dst_action = None
|
|
dst_slot = None
|
|
|
|
return dst_action, dst_slot
|
|
|
|
|
|
def finalize_rlx_import(rlx_cache, obj, actions, action_store_id, frame_start=None, frame_end=None, overwrite=False):
|
|
"""TODO can this be merged with finalize_motion_import"""
|
|
props = vars.props()
|
|
|
|
motion_action = actions[0] if actions else None
|
|
motion_start_frame, motion_end_frame = get_actions_frame_range(actions)
|
|
if frame_start is None:
|
|
frame_start = motion_start_frame
|
|
if frame_end is None:
|
|
frame_end = motion_end_frame
|
|
|
|
utils.log_info("Finalize RLX import:")
|
|
utils.log_info(f"motion actions: {[a.name for a in actions]}")
|
|
utils.log_info(f"start frame: {frame_start}")
|
|
utils.log_info(f"end frame: {frame_end}")
|
|
|
|
if frame_start is None or frame_end is None:
|
|
utils.log_error(f"Unable to fetch frame range from actions!: {actions}")
|
|
props.restore_actions(action_store_id)
|
|
props.delete_action_store(action_store_id)
|
|
return
|
|
|
|
if actions:
|
|
stored_actions = props.fetch_stored_actions(action_store_id)
|
|
motion_rig_channel, motion_key_channels, motion_data_channel = fetch_target_channels(obj, motion_action)
|
|
key_targets = []
|
|
stored_rig_channel = props.fetch_stored_rig_channel(action_store_id)
|
|
stored_key_channels = props.fetch_stored_key_channels(action_store_id)
|
|
stored_data_channel = props.fetch_stored_data_channel(action_store_id)
|
|
opts, blend_in_data, blend_out_data, use_masked = get_import_opts(rlx_cache)
|
|
if opts.relative_root or opts.current_root or opts.use_blend or use_masked or overwrite:
|
|
resample_blend(rlx_cache, obj,
|
|
motion_rig_channel, motion_key_channels, motion_data_channel,
|
|
stored_rig_channel, stored_key_channels, stored_data_channel,
|
|
key_targets,
|
|
frame_start, frame_end,
|
|
relative_root=opts.relative_root,
|
|
current_root=opts.current_root,
|
|
use_blend=opts.use_blend,
|
|
overwrite=overwrite,
|
|
blend_in_frames=opts.blend_in_frames,
|
|
blend_out_frames=opts.blend_out_frames,
|
|
blend_in_data=blend_in_data,
|
|
blend_out_data=blend_out_data,
|
|
overall_strength=opts.blend_strength)
|
|
|
|
if overwrite:
|
|
stored_action = props.fetch_stored_rig_action(action_store_id)
|
|
# reload target (stored) motion set
|
|
load_motion_set(obj, stored_action)
|
|
# delete the source motion set
|
|
if motion_action and motion_action != stored_action:
|
|
delete_motion_set(motion_action)
|
|
utils.log_info(f"Re-Loaded target motion set: {stored_action.name}")
|
|
else:
|
|
# Do nothing else
|
|
utils.log_info(f"Loaded new motion set: {motion_action.name}")
|
|
|
|
#if action_mode == "BLEND":
|
|
# old_action = stored_actions[0]
|
|
# utils.log_info(f"Action Mode BLEND: blending motion set: {motion_action.name} over existing motion: {old_action.name}")
|
|
# mix_motion_set(obj, action_store_id, frame_start, frame_end)
|
|
# utils.delete_actions(actions)
|
|
|
|
props.delete_action_store(action_store_id)
|
|
|
|
|
|
def get_import_opts(chr_rlx_cache):
|
|
blend_in = None
|
|
blend_out = None
|
|
opts = chr_rlx_cache.action_options if chr_rlx_cache else None
|
|
use_masked = opts and (opts.use_bone_masking or opts.use_key_masking)
|
|
if opts and opts.use_blend:
|
|
blend_in = ui.get_fcurve_data(chr_rlx_cache, "motion_blend_in", shape="IN")
|
|
blend_out = ui.get_fcurve_data(chr_rlx_cache, "motion_blend_out", shape="OUT")
|
|
return opts, blend_in, blend_out, use_masked
|
|
|
|
|
|
def finalize_motion_import(chr_cache, rig, motion_rig_action, action_store_id,
|
|
frame_start=None, frame_end=None, overwrite=False):
|
|
props = vars.props()
|
|
motion_start_frame, motion_end_frame = get_motion_set_frame_range(motion_rig_action)
|
|
if frame_start is None:
|
|
frame_start = motion_start_frame
|
|
if frame_end is None:
|
|
frame_end = motion_end_frame
|
|
|
|
utils.log_info("Finalize motion import:")
|
|
utils.log_info(f"motion action: {motion_rig_action.name}")
|
|
utils.log_info(f"start frame: {frame_start}")
|
|
utils.log_info(f"end frame: {frame_end}")
|
|
|
|
if frame_start is None or frame_end is None:
|
|
utils.log_error(f"Unable to fetch frame range from action!: {motion_rig_action}")
|
|
props.restore_actions(action_store_id)
|
|
props.delete_action_store(action_store_id)
|
|
return
|
|
|
|
if motion_rig_action:
|
|
set_id = get_motion_set_ids(motion_rig_action)[0]
|
|
stored_actions = props.fetch_stored_actions(action_store_id)
|
|
motion_rig_channel, motion_key_channels, motion_data_channel = fetch_target_channels(rig, motion_rig_action)
|
|
key_targets = fetch_key_targets(rig)
|
|
stored_rig_channel = props.fetch_stored_rig_channel(action_store_id)
|
|
stored_key_channels = props.fetch_stored_key_channels(action_store_id)
|
|
stored_data_channel = props.fetch_stored_data_channel(action_store_id)
|
|
#masked_bones: list = []
|
|
#mask_bones(rig, motion_rig_action, motion_rig_slot, masked_bones)
|
|
opts, blend_in_data, blend_out_data, use_masked = get_import_opts(chr_cache)
|
|
if opts.relative_root or opts.current_root or opts.use_blend or use_masked or overwrite:
|
|
resample_blend(chr_cache, rig,
|
|
motion_rig_channel, motion_key_channels, motion_data_channel,
|
|
stored_rig_channel, stored_key_channels, stored_data_channel,
|
|
key_targets,
|
|
frame_start, frame_end,
|
|
relative_root=opts.relative_root,
|
|
current_root=opts.current_root,
|
|
use_blend=opts.use_blend,
|
|
overwrite=overwrite,
|
|
blend_in_frames=opts.blend_in_frames,
|
|
blend_out_frames=opts.blend_out_frames,
|
|
blend_in_data=blend_in_data,
|
|
blend_out_data=blend_out_data,
|
|
overall_strength=opts.blend_strength)
|
|
|
|
if overwrite:
|
|
stored_rig_action = props.fetch_stored_rig_action(action_store_id)
|
|
# reload target (stored) motion set
|
|
load_motion_set(rig, stored_rig_action)
|
|
# delete the source motion set
|
|
if motion_rig_action and motion_rig_action != stored_rig_action:
|
|
delete_motion_set(motion_rig_action)
|
|
utils.log_info(f"Re-Loaded target motion set: {stored_rig_action.name}")
|
|
else:
|
|
# already loaded, do nothing else
|
|
utils.log_info(f"Loaded new motion set: {motion_rig_action.name} / ({set_id})")
|
|
|
|
#if action_mode == "BLEND":
|
|
# old_action = stored_actions[0]
|
|
# old_set_id = get_motion_set_ids(old_action)[0]
|
|
# utils.log_info(f"Action Mode BLEND: blending motion set: {motion_rig_action.name} / ({set_id}) over existing motion: {old_action.name} / ({old_set_id})")
|
|
# mix_motion_set(rig, action_store_id, frame_start, frame_end)
|
|
# delete_motion_set(set_id)
|
|
|
|
props.delete_action_store(action_store_id)
|
|
|
|
|
|
def resample_relative_root(rig, motion_action, motion_slot, stored_action, stored_slot, from_frame):
|
|
root_bone_name = rig.pose.bones[0].name
|
|
motion_channelbag = utils.get_action_channelbag(motion_action, slot=motion_slot)
|
|
stored_channelbag = utils.get_action_channelbag(stored_action, slot=stored_slot)
|
|
stored_transform_set, stored_has_curves = fetch_action_bone_transform_set(stored_channelbag, root_bone_name)
|
|
motion_transform_set, motion_has_curves = fetch_action_bone_transform_set(motion_channelbag, root_bone_name)
|
|
stored_start_loc, stored_start_rot = evaluate_action_bone_transform_set(stored_transform_set, from_frame)
|
|
motion_start_loc, motion_start_rot = evaluate_action_bone_transform_set(motion_transform_set, 1)
|
|
|
|
utils.log_info(f"Resampling root motion: {motion_action.name}")
|
|
|
|
# relative transformation between from_frame of stored motion and start of new motion
|
|
MS = utils.make_transform_matrix(stored_start_loc, stored_start_rot)
|
|
MM = utils.make_transform_matrix(motion_start_loc, motion_start_rot)
|
|
MD = MS @ MM.inverted()
|
|
|
|
# build a list of motion root transform fcurves
|
|
motion_loc_curves, motion_rot_curves = motion_transform_set
|
|
motion_curves = motion_loc_curves + motion_rot_curves
|
|
# should be 7
|
|
num_curves = len(motion_curves)
|
|
|
|
# go through motion root transform fcurves and fetch the raw data,
|
|
# and build a list of discrete frames
|
|
frames = set()
|
|
fcurve: bpy.types.FCurve = None
|
|
for fcurve in motion_curves:
|
|
num_points = len(fcurve.keyframe_points)
|
|
data = [0.0, 0.0] * num_points
|
|
fcurve.keyframe_points.foreach_get('co', data)
|
|
for i in range(0, len(data), 2):
|
|
frames.add(data[i])
|
|
|
|
# sort the frames in order
|
|
num_frames = len(frames)
|
|
sorted_frames = list(frames)
|
|
sorted_frames.sort()
|
|
utils.log_info(f" - {num_frames} resampled frames")
|
|
|
|
# generate resampled data lists for the curves in the motion root transform
|
|
resampled_data = []
|
|
for i in range(0, num_curves):
|
|
data = [0.0, 0.0] * num_frames
|
|
resampled_data.append(data)
|
|
|
|
# evaluate the motion root transform at each frame and
|
|
# transpose relative to the stored root transform
|
|
for f, frame in enumerate(sorted_frames):
|
|
motion_loc, motion_rot = evaluate_action_bone_transform_set(motion_transform_set, frame)
|
|
MF = utils.make_transform_matrix(motion_loc, motion_rot)
|
|
MR = MD @ MF
|
|
rel_loc = MR.to_translation()
|
|
rel_rot = MR.to_quaternion()
|
|
flat_transform = [rel_loc.x, rel_loc.y, rel_loc.z,
|
|
rel_rot.w, rel_rot.x, rel_rot.y, rel_rot.z]
|
|
# write to the resampled curve data
|
|
index = f * 2
|
|
for i in range(0, num_curves):
|
|
resampled_data[i][index] = frame
|
|
resampled_data[i][index+1] = flat_transform[i]
|
|
|
|
# write the resampled transposed curve data back to the motion root transform curves
|
|
for i, fcurve in enumerate(motion_curves):
|
|
fcurve.keyframe_points.clear()
|
|
fcurve.keyframe_points.add(num_frames)
|
|
fcurve.keyframe_points.foreach_set('co', resampled_data[i])
|
|
reset_fcurve_interpolation(fcurve)
|
|
|
|
return
|
|
|
|
|
|
def get_blend_mask_bones(chr_cache):
|
|
mask_bones = {}
|
|
use_blend = False
|
|
props = chr_cache.action_options
|
|
if props.use_bone_masking:
|
|
for item in props.import_bones:
|
|
bone_name = item.name
|
|
bone_weight = item.weight
|
|
mask_bones[bone_name] = bone_weight
|
|
if bone_weight < 1.0:
|
|
use_blend = True
|
|
return mask_bones, use_blend, props.use_bone_masking
|
|
|
|
|
|
def get_blend_mask_keys(chr_cache):
|
|
mask_keys = {}
|
|
use_blend = False
|
|
props = chr_cache.action_options
|
|
if props.use_key_masking:
|
|
for item in props.import_keys:
|
|
key_name = item.name
|
|
key_weight = item.weight
|
|
mask_keys[key_name] = key_weight
|
|
if key_weight < 1.0:
|
|
use_blend = True
|
|
return mask_keys, use_blend, props.use_key_masking
|
|
|
|
|
|
def get_shape_key_names(rig) -> set:
|
|
keys = set()
|
|
for obj in rig.children:
|
|
if utils.object_exists_has_shape_keys(obj):
|
|
for i, key in enumerate(obj.data.shape_keys.key_blocks):
|
|
if i > 0:
|
|
keys.add(key.name)
|
|
return keys
|
|
|
|
|
|
def remove_bone_from_channelbag(channelbag, bone_name):
|
|
path = f"pose.bones[\"{bone_name}\"]"
|
|
to_remove = []
|
|
# TODO which Blender versions does this work in?
|
|
fcurves = channelbag.fcurves
|
|
for fcurve in fcurves:
|
|
if fcurve.data_path.startswith(path):
|
|
to_remove.append(fcurve)
|
|
for fcurve in to_remove:
|
|
fcurves.remove(fcurve)
|
|
|
|
|
|
def remove_key_from_channelbag(channelbag, key_name):
|
|
path = f"key_blocks[\"{key_name}\"]"
|
|
to_remove = []
|
|
# TODO which Blender versions does this work in?
|
|
fcurves = channelbag.fcurves
|
|
for fcurve in fcurves:
|
|
if fcurve.data_path.startswith(path):
|
|
to_remove.append(fcurve)
|
|
for fcurve in to_remove:
|
|
fcurves.remove(fcurve)
|
|
|
|
|
|
def resample_blend(chr_rlx_cache, rig,
|
|
motion_rig_channel, motion_key_channels, motion_data_channel,
|
|
stored_rig_channel, stored_key_channels, stored_data_channel,
|
|
key_targets,
|
|
from_frame, to_frame,
|
|
relative_root=False,
|
|
current_root=False,
|
|
bone_names=None,
|
|
key_names=None,
|
|
data_paths=None,
|
|
use_blend=False,
|
|
overwrite=False,
|
|
blend_in_frames=0,
|
|
blend_out_frames=0,
|
|
blend_in_data=None,
|
|
blend_out_data=None,
|
|
overall_strength=1.0):
|
|
"""Resamples the motion set, and blends with the stored motion set"""
|
|
|
|
# clamp overall_strength
|
|
overall_strength = max(0, min(1, overall_strength))
|
|
|
|
blend_mask_bones = {}
|
|
use_mask_bones = False
|
|
blend_mask_keys = {}
|
|
use_mask_keys = False
|
|
blend_mask_data = {}
|
|
use_mask_data = False
|
|
|
|
if chr_rlx_cache and rig.type == "ARMATURE":
|
|
blend_mask_bones, use_mask_bone_blend, use_mask_bones = get_blend_mask_bones(chr_rlx_cache)
|
|
blend_mask_keys, use_mask_key_blend, use_mask_keys = get_blend_mask_keys(chr_rlx_cache)
|
|
|
|
if chr_rlx_cache and rig.type == "LIGHT":
|
|
...
|
|
|
|
if chr_rlx_cache and rig.type == "CAMERA":
|
|
...
|
|
|
|
overall_strength = overall_strength if use_blend else 1.0
|
|
blend_in_frames = blend_in_frames if use_blend else 0
|
|
blend_out_frames = blend_out_frames if use_blend else 0
|
|
|
|
frame_range = to_frame - from_frame + 1
|
|
total_blend_frames = blend_in_frames + blend_out_frames
|
|
if total_blend_frames > frame_range:
|
|
blend_in_frames = round(frame_range * blend_in_frames / total_blend_frames)
|
|
blend_out_frames = frame_range - blend_in_frames
|
|
utils.log_warn(f"Blend range larger than motion. Rescaling to - in: {blend_in_frames}, out: {blend_out_frames}")
|
|
|
|
if (not relative_root and
|
|
not current_root and
|
|
not use_blend and
|
|
not overwrite):
|
|
return
|
|
|
|
utils.log_info(f"Resampling motion: {from_frame} - {to_frame}, blend in: {blend_in_frames}, blend out: {blend_out_frames}, use_blend: {use_blend}")
|
|
|
|
processed_fcurves: List[bpy.types.FCurve] = []
|
|
|
|
# resample bone tracks
|
|
#
|
|
if rig.type == "ARMATURE":
|
|
|
|
if not bone_names:
|
|
if use_blend or overwrite:
|
|
bone_names = [ b.name for b in rig.pose.bones ]
|
|
elif relative_root or current_root:
|
|
bone_names = [ rig.pose.bones[0].name ]
|
|
|
|
blend_bone_curves(motion_rig_channel, stored_rig_channel,
|
|
from_frame, to_frame,
|
|
rig, bone_names, relative_root, current_root,
|
|
use_blend, overwrite, overall_strength,
|
|
blend_in_frames, blend_out_frames,
|
|
blend_in_data, blend_out_data,
|
|
use_mask_bones, blend_mask_bones,
|
|
processed_fcurves=processed_fcurves)
|
|
|
|
blend_data_curves(motion_rig_channel, stored_rig_channel,
|
|
from_frame, to_frame,
|
|
use_blend, overwrite, overall_strength,
|
|
blend_in_frames, blend_out_frames,
|
|
blend_in_data, blend_out_data,
|
|
processed_fcurves=processed_fcurves)
|
|
|
|
# resample shape key tracks
|
|
#
|
|
if rig.type == "ARMATURE":
|
|
|
|
if not key_names:
|
|
key_names = get_shape_key_names(rig) if (use_blend or overwrite) else []
|
|
|
|
for obj_name in key_targets:
|
|
|
|
motion_key_channel = motion_key_channels[obj_name] \
|
|
if obj_name in motion_key_channels \
|
|
else (None, None, None)
|
|
stored_key_channel = stored_key_channels[obj_name] \
|
|
if obj_name in stored_key_channels \
|
|
else (None, None, None)
|
|
|
|
blend_key_curves(motion_key_channel, stored_key_channel,
|
|
from_frame, to_frame,
|
|
obj_name, key_names,
|
|
use_blend, overwrite, overall_strength,
|
|
blend_in_frames, blend_out_frames,
|
|
blend_in_data, blend_out_data,
|
|
use_mask_keys, blend_mask_keys,
|
|
processed_fcurves=processed_fcurves)
|
|
|
|
# process data
|
|
#
|
|
if rig.type == "LIGHT" or rig.type == "CAMERA":
|
|
|
|
blend_data_curves(motion_rig_channel, stored_rig_channel,
|
|
from_frame, to_frame,
|
|
use_blend, overwrite, overall_strength,
|
|
blend_in_frames, blend_out_frames,
|
|
blend_in_data, blend_out_data,
|
|
processed_fcurves=processed_fcurves)
|
|
|
|
blend_data_curves(motion_data_channel, stored_data_channel,
|
|
from_frame, to_frame,
|
|
use_blend, overwrite, overall_strength,
|
|
blend_in_frames, blend_out_frames,
|
|
blend_in_data, blend_out_data,
|
|
processed_fcurves=processed_fcurves)
|
|
return
|
|
|
|
|
|
def blend_bone_curves(motion_channel, stored_channel,
|
|
from_frame, to_frame,
|
|
rig, bone_names, relative_root, current_root,
|
|
use_blend, overwrite, overall_strength,
|
|
blend_in_frames, blend_out_frames,
|
|
blend_in_data, blend_out_data,
|
|
use_mask_bones, blend_mask_bones,
|
|
processed_fcurves: List[bpy.types.FCurve]=None):
|
|
|
|
motion_action, motion_slot, motion_channelbag = motion_channel
|
|
stored_action, stored_slot, stored_channelbag = stored_channel
|
|
|
|
blend_in_frames = blend_in_frames if use_blend else 0
|
|
blend_out_frames = blend_out_frames if use_blend else 0
|
|
|
|
base_frames = set()
|
|
# include all blend in / out frames
|
|
if blend_in_frames > 0:
|
|
for i in range(from_frame, from_frame + blend_in_frames - 1 + 1):
|
|
base_frames.add(i)
|
|
if blend_out_frames > 0:
|
|
for i in range(to_frame - blend_out_frames + 1, to_frame + 1):
|
|
base_frames.add(i)
|
|
|
|
root_bone: bpy.types.PoseBone = rig.pose.bones[0]
|
|
|
|
for bone_name in bone_names:
|
|
|
|
is_root = (bone_name == root_bone.name)
|
|
motion_transform_set, motion_has_curves = fetch_action_bone_transform_set(motion_channelbag, bone_name)
|
|
stored_transform_set, stored_has_curves = fetch_action_bone_transform_set(stored_channelbag, bone_name)
|
|
|
|
if not motion_has_curves and not stored_has_curves:
|
|
continue
|
|
|
|
if is_root and relative_root:
|
|
# relative transformation between from_frame of stored motion and start of new motion
|
|
stored_start_loc, stored_start_rot = evaluate_action_bone_transform_set(stored_transform_set, from_frame)
|
|
motion_start_loc, motion_start_rot = evaluate_action_bone_transform_set(motion_transform_set, 1)
|
|
MS = utils.make_transform_matrix(stored_start_loc, stored_start_rot)
|
|
MM = utils.make_transform_matrix(motion_start_loc, motion_start_rot)
|
|
MD = MS @ MM.inverted()
|
|
elif is_root and current_root:
|
|
# relative transformation between current rig root bone position and start of new motion
|
|
motion_start_loc, motion_start_rot = evaluate_action_bone_transform_set(motion_transform_set, 1)
|
|
MS = root_bone.matrix.copy()
|
|
MM = utils.make_transform_matrix(motion_start_loc, motion_start_rot)
|
|
MD = MS @ MM.inverted()
|
|
|
|
|
|
utils.log_detail(f"Blending motion bone: {bone_name} {from_frame}-{to_frame}")
|
|
|
|
# build a list of motion bone transform fcurves
|
|
motion_loc_curves, motion_rot_curves = motion_transform_set
|
|
motion_curves = motion_loc_curves + motion_rot_curves
|
|
# should be 7
|
|
num_curves = len(motion_curves)
|
|
|
|
# build a list of stored bone transform fcurves
|
|
stored_loc_curves, stored_rot_curves = stored_transform_set
|
|
stored_curves = stored_loc_curves + stored_rot_curves
|
|
|
|
frames = base_frames.copy()
|
|
|
|
# include all frames from the motion curve
|
|
fcurve: bpy.types.FCurve = None
|
|
for fcurve in motion_curves:
|
|
if fcurve:
|
|
num_points = len(fcurve.keyframe_points)
|
|
data = [0.0, 0.0] * num_points
|
|
fcurve.keyframe_points.foreach_get('co', data)
|
|
for i in range(0, len(data), 2):
|
|
frame = data[i]
|
|
frames.add(frame)
|
|
|
|
# include all frames from the source curve
|
|
if use_blend or overwrite:
|
|
for fcurve in stored_curves:
|
|
if fcurve:
|
|
num_points = len(fcurve.keyframe_points)
|
|
data = [0.0, 0.0] * num_points
|
|
fcurve.keyframe_points.foreach_get('co', data)
|
|
for i in range(0, len(data), 2):
|
|
frame = data[i]
|
|
frames.add(frame)
|
|
|
|
# sort the frames in order
|
|
num_frames = len(frames)
|
|
sorted_frames = list(frames)
|
|
sorted_frames.sort()
|
|
utils.log_detail(f" - {num_frames} resampled frames")
|
|
|
|
# generate resampled data lists for the curves in the motion bone transform
|
|
resampled_data = []
|
|
for i in range(0, num_curves):
|
|
data = [0.0, 0.0] * num_frames
|
|
resampled_data.append(data)
|
|
|
|
# evaluate the motion root transform at each frame ...
|
|
for f, frame in enumerate(sorted_frames):
|
|
motion_loc, motion_rot = evaluate_action_bone_transform_set(motion_transform_set, frame)
|
|
|
|
# apply relative root:
|
|
if is_root and (relative_root or current_root):
|
|
motion_loc, motion_rot = evaluate_action_bone_transform_set(motion_transform_set, frame)
|
|
MF = utils.make_transform_matrix(motion_loc, motion_rot)
|
|
MR = MD @ MF
|
|
motion_loc = MR.to_translation()
|
|
motion_rot = MR.to_quaternion()
|
|
|
|
# if blending, apply motion blend:
|
|
if use_blend or overwrite:
|
|
blend_strength = overall_strength if use_blend else 1.0
|
|
stored_loc, stored_rot = evaluate_action_bone_transform_set(stored_transform_set, frame)
|
|
if frame < from_frame or frame > to_frame:
|
|
blend_strength = 0
|
|
elif blend_in_frames > 0 and frame < (from_frame + blend_in_frames):
|
|
pos = (frame - from_frame + 1) / (blend_in_frames + 1)
|
|
blend_strength *= ui.eval_curve(blend_in_data, pos)
|
|
elif blend_out_frames > 0 and frame > (to_frame - blend_out_frames):
|
|
pos = (frame - to_frame + blend_out_frames) / (blend_out_frames + 1)
|
|
blend_strength *= ui.eval_curve(blend_out_data, pos)
|
|
if use_mask_bones:
|
|
if bone_name in blend_mask_bones:
|
|
blend_strength *= blend_mask_bones[bone_name]
|
|
else:
|
|
blend_strength = 0.0
|
|
|
|
if blend_strength <= 0:
|
|
blend_loc = stored_loc
|
|
blend_rot = stored_rot
|
|
elif blend_strength >= 1:
|
|
blend_loc = motion_loc
|
|
blend_rot = motion_rot
|
|
else:
|
|
blend_loc = stored_loc.lerp(motion_loc, blend_strength)
|
|
blend_rot = stored_rot.slerp(motion_rot, blend_strength)
|
|
|
|
# if not blending, use the motion transform directly
|
|
else:
|
|
blend_loc = motion_loc
|
|
blend_rot = motion_rot
|
|
|
|
transform_data = [blend_loc.x, blend_loc.y, blend_loc.z,
|
|
blend_rot.w, blend_rot.x, blend_rot.y, blend_rot.z]
|
|
|
|
# write frame to the resampled curve data:
|
|
index = f * 2
|
|
for i in range(0, num_curves):
|
|
resampled_data[i][index] = frame
|
|
resampled_data[i][index+1] = transform_data[i]
|
|
|
|
if overwrite:
|
|
# write the resampled curve data back to the stored transform curves:
|
|
for i, fcurve in enumerate(stored_curves):
|
|
if processed_fcurves:
|
|
if fcurve and fcurve in processed_fcurves: continue
|
|
processed_fcurves.append(fcurve)
|
|
if not fcurve:
|
|
fcurve = clone_fcurve_to_channel(motion_curves[i], stored_channelbag)
|
|
if fcurve:
|
|
utils.log_detail(f"Added bone fcurve {bone_name}[{i}]")
|
|
if fcurve:
|
|
fcurve.keyframe_points.clear()
|
|
fcurve.keyframe_points.add(num_frames)
|
|
fcurve.keyframe_points.foreach_set('co', resampled_data[i])
|
|
reset_fcurve_interpolation(fcurve)
|
|
else:
|
|
# write the resampled curve data back to the motion transform curves:
|
|
for i, fcurve in enumerate(motion_curves):
|
|
if processed_fcurves:
|
|
if fcurve and fcurve in processed_fcurves: continue
|
|
processed_fcurves.append(fcurve)
|
|
if not fcurve:
|
|
fcurve = clone_fcurve_to_channel(stored_curves[i], motion_channelbag)
|
|
if fcurve:
|
|
utils.log_detail(f"Added bone fcurve {bone_name}[{i}]")
|
|
if fcurve:
|
|
fcurve.keyframe_points.clear()
|
|
fcurve.keyframe_points.add(num_frames)
|
|
fcurve.keyframe_points.foreach_set('co', resampled_data[i])
|
|
reset_fcurve_interpolation(fcurve)
|
|
|
|
|
|
def blend_key_curves(motion_channel, stored_channel,
|
|
from_frame, to_frame,
|
|
obj_name, key_names,
|
|
use_blend, overwrite, overall_strength,
|
|
blend_in_frames, blend_out_frames,
|
|
blend_in_data, blend_out_data,
|
|
use_mask_keys, blend_mask_keys,
|
|
processed_fcurves: List[bpy.types.FCurve]=None):
|
|
|
|
motion_action, motion_slot, motion_channelbag = motion_channel
|
|
stored_action, stored_slot, stored_channelbag = stored_channel
|
|
|
|
blend_in_frames = blend_in_frames if use_blend else 0
|
|
blend_out_frames = blend_out_frames if use_blend else 0
|
|
|
|
base_frames = set()
|
|
# include all blend in / out frames
|
|
if blend_in_frames > 0:
|
|
for i in range(from_frame, from_frame + blend_in_frames - 1 + 1):
|
|
base_frames.add(i)
|
|
if blend_out_frames > 0:
|
|
for i in range(to_frame - blend_out_frames + 1, to_frame + 1):
|
|
base_frames.add(i)
|
|
|
|
for key_name in key_names:
|
|
|
|
obj = bpy.data.objects[obj_name]
|
|
if not utils.object_has_shape_key(obj, key_name):
|
|
continue
|
|
|
|
stored_key_curve = fetch_action_key_curve(stored_channelbag, key_name)
|
|
motion_key_curve = fetch_action_key_curve(motion_channelbag, key_name)
|
|
|
|
if processed_fcurves:
|
|
if motion_key_curve and motion_key_curve in processed_fcurves: continue
|
|
processed_fcurves.append(motion_key_curve)
|
|
|
|
if not stored_key_curve and not motion_key_curve:
|
|
continue
|
|
|
|
utils.log_detail(f"Blending motion key: {key_name} {from_frame}-{to_frame}")
|
|
|
|
# go through motion bone transform fcurves and build a list of discrete frames
|
|
frames = base_frames.copy()
|
|
fcurve: bpy.types.FCurve = None
|
|
|
|
# add frames from the motion curve
|
|
if motion_key_curve:
|
|
num_points = len(motion_key_curve.keyframe_points)
|
|
data = [0.0, 0.0] * num_points
|
|
motion_key_curve.keyframe_points.foreach_get('co', data)
|
|
for i in range(0, len(data), 2):
|
|
frame = data[i]
|
|
frames.add(frame)
|
|
|
|
# also include frames from the source curve
|
|
if use_blend or overwrite:
|
|
if stored_key_curve:
|
|
num_points = len(stored_key_curve.keyframe_points)
|
|
data = [0.0, 0.0] * num_points
|
|
stored_key_curve.keyframe_points.foreach_get('co', data)
|
|
for i in range(0, len(data), 2):
|
|
frame = data[i]
|
|
frames.add(frame)
|
|
|
|
# sort the frames in order
|
|
num_frames = len(frames)
|
|
sorted_frames = list(frames)
|
|
sorted_frames.sort()
|
|
utils.log_detail(f" - {num_frames} resampled frames")
|
|
|
|
# generate resampled data lists for the motion curve
|
|
resampled_data = [0.0, 0.0] * num_frames
|
|
|
|
# evaluate the motion curve at each frame ...
|
|
for f, frame in enumerate(sorted_frames):
|
|
motion_value = evaluate_action_curve(motion_key_curve, frame)
|
|
|
|
# if blending, apply motion blend:
|
|
if use_blend or overwrite:
|
|
blend_strength = overall_strength if use_blend else 1.0
|
|
stored_value = evaluate_action_curve(stored_key_curve, frame)
|
|
|
|
if frame < from_frame or frame > to_frame:
|
|
blend_strength = 0
|
|
elif blend_in_frames > 0 and frame < (from_frame + blend_in_frames):
|
|
pos = (frame - from_frame + 1) / (blend_in_frames + 1)
|
|
blend_strength *= ui.eval_curve(blend_in_data, pos)
|
|
elif blend_out_frames > 0 and frame > (to_frame - blend_out_frames):
|
|
pos = (frame - to_frame + blend_out_frames) / (blend_out_frames + 1)
|
|
blend_strength *= ui.eval_curve(blend_out_data, pos)
|
|
if use_mask_keys:
|
|
if key_name in blend_mask_keys:
|
|
blend_strength *= blend_mask_keys[key_name]
|
|
else:
|
|
blend_strength = 0.0
|
|
|
|
if blend_strength <= 0:
|
|
blend_value = stored_value
|
|
elif blend_strength >= 1:
|
|
blend_value = motion_value
|
|
else:
|
|
blend_value = utils.lerp(stored_value, motion_value, blend_strength)
|
|
|
|
# otherwise use motion value directly
|
|
else:
|
|
blend_value = motion_value
|
|
|
|
# write frame to the resampled curve data:
|
|
index = f * 2
|
|
resampled_data[index] = frame
|
|
resampled_data[index+1] = blend_value
|
|
|
|
if overwrite:
|
|
# write the resampled curve data back to the stored curve:
|
|
if not stored_key_curve:
|
|
stored_key_curve = clone_fcurve_to_channel(motion_key_curve, stored_channelbag)
|
|
if stored_key_curve:
|
|
utils.log_detail(f"Added key fcurve {obj_name}/{key_name}")
|
|
if stored_key_curve:
|
|
stored_key_curve.keyframe_points.clear()
|
|
stored_key_curve.keyframe_points.add(num_frames)
|
|
stored_key_curve.keyframe_points.foreach_set('co', resampled_data)
|
|
reset_fcurve_interpolation(stored_key_curve)
|
|
else:
|
|
# write the resampled curve data back to the motion curve:
|
|
if not motion_key_curve:
|
|
motion_key_curve = clone_fcurve_to_channel(stored_key_curve, motion_channelbag)
|
|
if motion_key_curve:
|
|
utils.log_detail(f"Added key fcurve {obj_name}/{key_name}")
|
|
if motion_key_curve:
|
|
motion_key_curve.keyframe_points.clear()
|
|
motion_key_curve.keyframe_points.add(num_frames)
|
|
motion_key_curve.keyframe_points.foreach_set('co', resampled_data)
|
|
reset_fcurve_interpolation(motion_key_curve)
|
|
|
|
|
|
def blend_data_curves(motion_channel, stored_channel,
|
|
from_frame, to_frame,
|
|
use_blend, overwrite, overall_strength,
|
|
blend_in_frames, blend_out_frames,
|
|
blend_in_data, blend_out_data,
|
|
processed_fcurves: List[bpy.types.FCurve]=None):
|
|
|
|
motion_action, motion_slot, motion_channelbag = motion_channel
|
|
stored_action, stored_slot, stored_channelbag = stored_channel
|
|
|
|
blend_in_frames = blend_in_frames if use_blend else 0
|
|
blend_out_frames = blend_out_frames if use_blend else 0
|
|
|
|
base_frames = set()
|
|
# include all blend in / out frames
|
|
if blend_in_frames > 0:
|
|
for i in range(from_frame, from_frame + blend_in_frames - 1 + 1):
|
|
base_frames.add(i)
|
|
if blend_out_frames > 0:
|
|
for i in range(to_frame - blend_out_frames + 1, to_frame + 1):
|
|
base_frames.add(i)
|
|
|
|
paths = {}
|
|
if motion_channelbag:
|
|
for fcurve in motion_channelbag.fcurves:
|
|
if (fcurve.data_path.startswith("pose.bones") or
|
|
fcurve.data_path.startswith("key_blocks")):
|
|
continue
|
|
path_id = (fcurve.data_path, fcurve.array_index)
|
|
paths[path_id] = [fcurve, None]
|
|
if stored_channelbag:
|
|
for fcurve in stored_channelbag.fcurves:
|
|
if (fcurve.data_path.startswith("pose.bones") or
|
|
fcurve.data_path.startswith("key_blocks")):
|
|
continue
|
|
path_id = (fcurve.data_path, fcurve.array_index)
|
|
if path_id in paths:
|
|
paths[path_id][1] = fcurve
|
|
else:
|
|
paths[path_id] = [None, fcurve]
|
|
|
|
for path_id, curves in paths.items():
|
|
data_path, array_index = path_id
|
|
motion_data_curve = curves[0]
|
|
stored_data_curve = curves[1]
|
|
|
|
if processed_fcurves:
|
|
if motion_data_curve and motion_data_curve in processed_fcurves: continue
|
|
processed_fcurves.append(motion_data_curve)
|
|
|
|
utils.log_info(f"Blending motion data: {data_path} {from_frame}-{to_frame}")
|
|
|
|
# go through motion bone transform fcurves and build a list of discrete frames
|
|
frames = base_frames.copy()
|
|
fcurve: bpy.types.FCurve = None
|
|
|
|
# add frames from the motion curve
|
|
if motion_data_curve:
|
|
num_points = len(motion_data_curve.keyframe_points)
|
|
data = [0.0, 0.0] * num_points
|
|
motion_data_curve.keyframe_points.foreach_get('co', data)
|
|
for i in range(0, len(data), 2):
|
|
frame = data[i]
|
|
frames.add(frame)
|
|
|
|
# also include frames from the source curve
|
|
if use_blend or overwrite:
|
|
if stored_data_curve:
|
|
num_points = len(stored_data_curve.keyframe_points)
|
|
data = [0.0, 0.0] * num_points
|
|
stored_data_curve.keyframe_points.foreach_get('co', data)
|
|
for i in range(0, len(data), 2):
|
|
frame = data[i]
|
|
frames.add(frame)
|
|
|
|
# sort the frames in order
|
|
num_frames = len(frames)
|
|
sorted_frames = list(frames)
|
|
sorted_frames.sort()
|
|
utils.log_detail(f" - {num_frames} resampled frames")
|
|
|
|
# generate resampled data lists for the motion curve
|
|
resampled_data = [0.0, 0.0] * num_frames
|
|
|
|
# evaluate the motion curve at each frame ...
|
|
for f, frame in enumerate(sorted_frames):
|
|
motion_value = evaluate_action_curve(motion_data_curve, frame)
|
|
|
|
# if blending, apply motion blend:
|
|
if use_blend or overwrite:
|
|
blend_strength = overall_strength if use_blend else 1.0
|
|
stored_value = evaluate_action_curve(stored_data_curve, frame)
|
|
|
|
if frame < from_frame or frame > to_frame:
|
|
blend_strength = 0
|
|
elif blend_in_frames > 0 and frame < (from_frame + blend_in_frames):
|
|
pos = (frame - from_frame + 1) / (blend_in_frames + 1)
|
|
blend_strength *= ui.eval_curve(blend_in_data, pos)
|
|
elif blend_out_frames > 0 and frame > (to_frame - blend_out_frames):
|
|
pos = (frame - to_frame + blend_out_frames) / (blend_out_frames + 1)
|
|
blend_strength *= ui.eval_curve(blend_out_data, pos)
|
|
#if use_mask_paths:
|
|
# if data_path in blend_mask_paths:
|
|
# blend_strength *= blend_mask_paths[data_path]
|
|
# else:
|
|
# blend_strength = 0.0
|
|
|
|
if blend_strength <= 0:
|
|
blend_value = stored_value
|
|
elif blend_strength >= 1:
|
|
blend_value = motion_value
|
|
else:
|
|
blend_value = utils.lerp(stored_value, motion_value, blend_strength)
|
|
|
|
# otherwise use motion value directly
|
|
else:
|
|
blend_value = motion_value
|
|
|
|
# write frame to the resampled curve data:
|
|
index = f * 2
|
|
resampled_data[index] = frame
|
|
resampled_data[index+1] = blend_value
|
|
|
|
if overwrite:
|
|
# write the resampled curve data back to the stored curve:
|
|
if not stored_data_curve:
|
|
stored_data_curve = clone_fcurve_to_channel(motion_data_curve, stored_channelbag)
|
|
if stored_data_curve:
|
|
utils.log_detail(f"Added data fcurve {path_id}")
|
|
if stored_data_curve:
|
|
stored_data_curve.keyframe_points.clear()
|
|
stored_data_curve.keyframe_points.add(num_frames)
|
|
stored_data_curve.keyframe_points.foreach_set('co', resampled_data)
|
|
reset_fcurve_interpolation(stored_data_curve)
|
|
else:
|
|
# write the resampled curve data back to the motion curve:
|
|
if not motion_data_curve:
|
|
motion_data_curve = clone_fcurve_to_channel(stored_data_curve, motion_channelbag)
|
|
if motion_data_curve:
|
|
utils.log_detail(f"Added data fcurve {path_id}")
|
|
if motion_data_curve:
|
|
motion_data_curve.keyframe_points.clear()
|
|
motion_data_curve.keyframe_points.add(num_frames)
|
|
motion_data_curve.keyframe_points.foreach_set('co', resampled_data)
|
|
reset_fcurve_interpolation(motion_data_curve)
|
|
|
|
|
|
def get_relative_transformation(motion_loc: Vector, motion_rot: Quaternion,
|
|
stored_loc: Vector, stored_rot: Quaternion):
|
|
delta_loc = stored_loc - motion_loc
|
|
M: Matrix = motion_rot.to_matrix().to_4x4()
|
|
S: Matrix = stored_rot.to_matrix().to_4x4()
|
|
D = M.inverted() @ S
|
|
return delta_loc, D
|
|
|
|
|
|
def fetch_action_key_curve(channelbag, key_name: str):
|
|
if channelbag and key_name:
|
|
key_path = f"key_blocks[\"{key_name}\"].value"
|
|
for fcurve in channelbag.fcurves:
|
|
if fcurve.data_path == key_path:
|
|
return fcurve
|
|
return None
|
|
|
|
|
|
def fetch_action_bone_transform_set(channelbag, bone_name: str):
|
|
# TODO instead build a dict in one pass of transforms[bone_name] = (loc[fx,fy,fz], rot[fw,fx,fy,fz])
|
|
fcurve: bpy.types.FCurve = None
|
|
loc_path = f"pose.bones[\"{bone_name}\"].location"
|
|
rot_path = f"pose.bones[\"{bone_name}\"].rotation_quaternion"
|
|
# TODO what if it's not a quaternion?
|
|
loc_curves = [None, None, None]
|
|
rot_curves = [None, None, None, None]
|
|
has_loc_curves = False
|
|
has_rot_curves = False
|
|
if channelbag:
|
|
for fcurve in channelbag.fcurves:
|
|
if fcurve.data_path == loc_path:
|
|
loc_curves[fcurve.array_index] = fcurve
|
|
has_loc_curves = True
|
|
elif fcurve.data_path == rot_path:
|
|
rot_curves[fcurve.array_index] = fcurve
|
|
has_rot_curves = True
|
|
return (loc_curves, rot_curves), has_loc_curves or has_rot_curves
|
|
|
|
|
|
def evaluate_action_bone_transform_set(transform_set: tuple, frame: int):
|
|
loc_curves, rot_curves = transform_set
|
|
try:
|
|
loc = Vector((loc_curves[0].evaluate(frame),
|
|
loc_curves[1].evaluate(frame),
|
|
loc_curves[2].evaluate(frame)))
|
|
except:
|
|
loc = Vector((0,0,0))
|
|
try:
|
|
rot = Quaternion((rot_curves[0].evaluate(frame),
|
|
rot_curves[1].evaluate(frame),
|
|
rot_curves[2].evaluate(frame),
|
|
rot_curves[3].evaluate(frame)))
|
|
except:
|
|
rot = Quaternion((1,0,0,0))
|
|
return loc, rot
|
|
|
|
|
|
def evaluate_action_curve(key_curve: tuple, frame: int):
|
|
try:
|
|
value = key_curve.evaluate(frame)
|
|
except:
|
|
value = 0.0
|
|
return value
|
|
#endregion
|
|
|
|
|
|
#region Operators
|
|
class CCICMotionSetFunctions(bpy.types.Operator):
|
|
bl_idname = "ccic.motion_set_funcs"
|
|
bl_label = "Motion Set Functions"
|
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = ""
|
|
)
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
|
|
props.store_ui_list_indices()
|
|
action = props.action_set_list_action
|
|
#chr_cache = props.get_context_character_cache(context)
|
|
|
|
if self.param == "DELETE":
|
|
delete_motion_set(action)
|
|
|
|
elif self.param == "DUPLICATE":
|
|
duplicate_motion_set(action)
|
|
|
|
return {"FINISHED"}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Show motion set info"
|
|
|
|
|
|
class CCICMotionSetRename(bpy.types.Operator):
|
|
bl_idname = "ccic.motion_set_rename"
|
|
bl_label = "Rename Motion Set"
|
|
|
|
action: bpy.props.StringProperty(name="Action", default="")
|
|
prefix: bpy.props.StringProperty(name="Motion Prefix", default="")
|
|
rig_id: bpy.props.StringProperty(name="Character / Rig ID", default="")
|
|
motion_id: bpy.props.StringProperty(name="Motion Name / ID", default="")
|
|
set_id = bpy.props.StringProperty(name="Set ID", default="")
|
|
action_type = bpy.props.StringProperty(name="Action Type", default="")
|
|
|
|
def execute(self, context):
|
|
rename_motion_set(self.action_type, self.set_id, self.prefix, self.rig_id, self.motion_id)
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
props.store_ui_list_indices()
|
|
action = props.action_set_list_action
|
|
self.action = action.name
|
|
chr_cache = props.get_context_character_cache(context)
|
|
|
|
if not action:
|
|
return {"FINISHED"}
|
|
|
|
set_id, set_generation, action_type_id, key_object = get_motion_set_ids(action)
|
|
self.action_type = action_type_id
|
|
|
|
if not set_id:
|
|
return {"FINISHED"}
|
|
|
|
self.set_id = set_id
|
|
|
|
prefix, rig_id, type_id, obj_id, motion_id = decode_action_name(action)
|
|
if prefix:
|
|
self.prefix = prefix
|
|
else:
|
|
self.prefix = ""
|
|
if rig_id:
|
|
self.rig_id = rig_id
|
|
else:
|
|
self.rig_id = ""
|
|
if motion_id:
|
|
self.motion_id = motion_id
|
|
elif action.name:
|
|
self.motion_id = action.name.split("|")[-1]
|
|
else:
|
|
self.motion_id = "Motion"
|
|
|
|
return context.window_manager.invoke_props_dialog(self, width=400)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
split = layout.split(factor=0.35)
|
|
col_1 = split.column()
|
|
col_2 = split.column()
|
|
|
|
col_1.label(text="Motion Set ID:")
|
|
col_2.label(text=self.set_id)
|
|
|
|
col_1.separator()
|
|
col_2.separator()
|
|
|
|
col_1.label(text="Prefix:")
|
|
col_2.prop(self, "prefix", text="")
|
|
|
|
col_1.separator()
|
|
col_2.separator()
|
|
|
|
col_1.label(text="Character / Rig ID:")
|
|
col_2.prop(self, "rig_id", text="")
|
|
|
|
col_1.separator()
|
|
col_2.separator()
|
|
|
|
col_1.label(text="Motion Name / ID:")
|
|
col_2.prop(self, "motion_id", text="")
|
|
|
|
layout.separator()
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Change the name, prefix, and character/rig id of the motion set"
|
|
|
|
|
|
class CCICMotionSetInfo(bpy.types.Operator):
|
|
bl_idname = "ccic.motion_set_info"
|
|
bl_label = "Motion Set Info"
|
|
|
|
prefix: bpy.props.StringProperty(name="Motion Prefix", default="")
|
|
rig_id: bpy.props.StringProperty(name="Character / Rig ID", default="")
|
|
motion_id: bpy.props.StringProperty(name="Motion Name / ID", default="")
|
|
set_id: bpy.props.StringProperty(name="Set ID", default="")
|
|
|
|
def execute(self, context):
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
props.store_ui_list_indices()
|
|
action = props.action_set_list_action
|
|
chr_cache = props.get_context_character_cache(context)
|
|
|
|
self.delete_me = False
|
|
# TODO make delete an op button?
|
|
# TODO use_fake_user button
|
|
|
|
if not action:
|
|
return {"FINISHED"}
|
|
|
|
set_id, set_generation, action_type_id, key_object = get_motion_set_ids(action)
|
|
|
|
if not set_id:
|
|
return {"FINISHED"}
|
|
|
|
self.set_id = set_id
|
|
|
|
prefix, rig_id, type_id, obj_id, motion_id = decode_action_name(action)
|
|
if prefix:
|
|
self.prefix = prefix
|
|
if rig_id:
|
|
self.rig_id = rig_id
|
|
elif chr_cache:
|
|
self.rig_id = chr_cache.get_name()
|
|
else:
|
|
self.rig_id = "Rig"
|
|
if motion_id:
|
|
self.motion_id = motion_id
|
|
elif action.name:
|
|
self.motion_id = action.name.split("|")[-1]
|
|
else:
|
|
self.motion_id = "Motion"
|
|
|
|
return context.window_manager.invoke_popup(self, width=600)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
|
|
split = layout.split(factor=0.25)
|
|
col_1 = split.column()
|
|
col_2 = split.column()
|
|
|
|
col_1.label(text="Motion Set ID:")
|
|
col_2.label(text=self.set_id)
|
|
|
|
col_1.separator()
|
|
col_2.separator()
|
|
|
|
col_1.label(text="Prefix:")
|
|
col_2.label(text=self.prefix if self.prefix else "(None)")
|
|
col_1.label(text="Character / Rig ID:")
|
|
col_2.label(text=self.rig_id if self.rig_id else "(None)")
|
|
col_1.label(text="Motion Name / ID:")
|
|
col_2.label(text=self.motion_id if self.motion_id else "(None)")
|
|
|
|
layout.separator()
|
|
|
|
layout.label(text="Actions:")
|
|
|
|
split = layout.split(factor=0.25)
|
|
col_1 = split.column()
|
|
col_2 = split.column()
|
|
for action in bpy.data.actions:
|
|
action_set_id = utils.get_prop(action, "rl_set_id")
|
|
action_type = utils.get_prop(action, "rl_action_type")
|
|
if action_set_id == self.set_id:
|
|
if action_type == "ARM":
|
|
col_1.label(text="Armature")
|
|
elif action_type == "KEY":
|
|
obj_id = utils.get_prop(action, "rl_key_object", "(None)")
|
|
col_1.label(text=obj_id)
|
|
else:
|
|
col_1.label(text="?")
|
|
col_2.label(text=action.name)
|
|
col_1.separator()
|
|
col_2.separator()
|
|
col_1.separator()
|
|
row = col_2.split(factor=0.5).column().row()
|
|
row.alert = True
|
|
row.scale_y = 1.5
|
|
row.operator("ccic.rigutils", text="Delete Motion Set", icon="ERROR").param = "DELETE_MOTION_SET"
|
|
layout.separator()
|
|
layout.separator()
|
|
layout.separator()
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Show motion set info"
|
|
|
|
|
|
class CCICRigUtils(bpy.types.Operator):
|
|
"""Rig Utilities"""
|
|
bl_idname = "ccic.rigutils"
|
|
bl_label = "Rig Utils"
|
|
bl_options = {"REGISTER"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = "",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
act_cache = props.get_context_actor_cache(context)
|
|
rlob = act_cache.get_primary_object() if act_cache else None
|
|
|
|
if act_cache:
|
|
|
|
props.store_ui_list_indices()
|
|
|
|
if rlob:
|
|
if rlob.type == "ARMATURE":
|
|
if self.param == "TOGGLE_SHOW_FULL_RIG":
|
|
toggle_show_full_rig(rlob)
|
|
|
|
elif self.param == "TOGGLE_SHOW_BASE_RIG":
|
|
toggle_show_base_rig(rlob)
|
|
|
|
elif self.param == "TOGGLE_SHOW_SPRING_RIG":
|
|
toggle_show_spring_rig(rlob)
|
|
|
|
elif self.param == "TOGGLE_SHOW_RIG_POSE":
|
|
toggle_rig_rest_position(rlob)
|
|
|
|
elif self.param == "TOGGLE_SHOW_SPRING_BONES":
|
|
springbones.toggle_show_spring_bones(act_cache)
|
|
|
|
elif self.param == "TOGGLE_EXPRESSION_RIG_LOCK":
|
|
facerig.toggle_lock_position(act_cache, rlob)
|
|
|
|
elif self.param == "BUTTON_RESET_POSE_SELECTED":
|
|
mode_selection = utils.store_mode_selection_state()
|
|
reset_pose(rlob, use_selected=True)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
elif self.param == "BUTTON_RESET_POSE":
|
|
mode_selection = utils.store_mode_selection_state()
|
|
reset_pose(rlob, use_selected=False)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
elif self.param == "RESET_EXPRESSION_POSE":
|
|
if act_cache.rigified:
|
|
mode_selection = utils.store_mode_selection_state()
|
|
facerig.clear_expression_pose(act_cache, rlob)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
elif self.param == "RESET_EXPRESSION_POSE_SELECTED":
|
|
if act_cache.rigified:
|
|
mode_selection = utils.store_mode_selection_state()
|
|
facerig.clear_expression_pose(act_cache, rlob, selected=True)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
elif self.param == "TOGGLE_SHOW_FACE_RIG":
|
|
toggle_show_only_face_rig(rlob)
|
|
|
|
elif self.param == "SET_LIMB_FK":
|
|
if act_cache.rigified:
|
|
set_rigify_ik_fk_influence(rlob, 1.0)
|
|
poke_rig(rlob)
|
|
|
|
elif self.param == "SET_LIMB_IK":
|
|
if act_cache.rigified:
|
|
set_rigify_ik_fk_influence(rlob, 0.0)
|
|
poke_rig(rlob)
|
|
|
|
elif self.param == "DISABLE_CONSTRAINT_STRETCH":
|
|
mode_selection = utils.store_mode_selection_state()
|
|
rigify_rig = act_cache.get_armature()
|
|
disable_ik_stretch(rigify_rig)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
elif self.param == "ENABLE_CONSTRAINT_STRETCH":
|
|
mode_selection = utils.store_mode_selection_state()
|
|
rigify_rig = act_cache.get_armature()
|
|
restore_ik_stretch(rigify_rig=rigify_rig)
|
|
utils.restore_mode_selection_state(mode_selection)
|
|
|
|
if self.param == "LOAD_MOTION_SET":
|
|
action = props.action_set_list_action
|
|
load_motion_set(rlob, action)
|
|
|
|
elif self.param == "PUSH_ACTION_SET":
|
|
action = props.action_set_list_action
|
|
auto_index = act_cache.get_auto_index()
|
|
push_motion_set(rlob, action, auto_index)
|
|
|
|
elif self.param == "CLEAR_ACTION_SET":
|
|
clear_motion_set(rlob)
|
|
|
|
elif self.param == "NEW_ACTION_SET":
|
|
new_motion_set(rlob)
|
|
|
|
if self.param == "SELECT_SET_STRIPS":
|
|
strip = context.active_nla_strip
|
|
select_strips_by_set(strip)
|
|
|
|
elif self.param == "NLA_ALIGN_LEFT":
|
|
strips = context.selected_nla_strips
|
|
align_strips(strips, left=True)
|
|
|
|
elif self.param == "NLA_ALIGN_TO_LEFT":
|
|
strips = context.selected_nla_strips
|
|
active_strip = context.active_nla_strip
|
|
align_strips(strips, to_strip=active_strip, left=True)
|
|
|
|
elif self.param == "NLA_ALIGN_RIGHT":
|
|
strips = context.selected_nla_strips
|
|
align_strips(strips, left=False)
|
|
|
|
elif self.param == "NLA_ALIGN_TO_RIGHT":
|
|
strips = context.selected_nla_strips
|
|
active_strip = context.active_nla_strip
|
|
align_strips(strips, to_strip=active_strip, left=False)
|
|
|
|
elif self.param == "NLA_SIZE_SHORTEST":
|
|
strips = context.selected_nla_strips
|
|
size_strips(strips, longest=False)
|
|
|
|
elif self.param == "NLA_SIZE_LONGEST":
|
|
strips = context.selected_nla_strips
|
|
size_strips(strips, longest=True)
|
|
|
|
elif self.param == "NLA_SIZE_TO":
|
|
strips = context.selected_nla_strips
|
|
active_strip = context.active_nla_strip
|
|
size_strips(strips, to_strip=active_strip)
|
|
|
|
elif self.param == "NLA_RESET_SIZE":
|
|
strips = context.selected_nla_strips
|
|
size_strips(strips, reset=True)
|
|
|
|
elif self.param == "SET_FAKE_USER_ON":
|
|
action = props.action_set_list_action
|
|
set_action_set_fake_user(action, True)
|
|
|
|
elif self.param == "SET_FAKE_USER_OFF":
|
|
action = props.action_set_list_action
|
|
set_action_set_fake_user(action, False)
|
|
|
|
elif self.param == "DELETE_MOTION_SET":
|
|
action = props.action_set_list_action
|
|
delete_motion_set(action)
|
|
|
|
props.restore_ui_list_indices()
|
|
|
|
if self.param == "CLEAN_ACTIONS":
|
|
clean_actions(rlob)
|
|
|
|
return {"FINISHED"}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
|
|
if properties.param == "TOGGLE_SHOW_SPRING_BONES":
|
|
return "Quick toggle for the armature layers to show just the spring bones or just the body bones"
|
|
|
|
elif properties.param == "TOGGLE_SHOW_FULL_RIG":
|
|
return "Toggles showing all the rig controls"
|
|
|
|
elif properties.param == "TOGGLE_SHOW_BASE_RIG":
|
|
return "Toggles showing the base rig controls"
|
|
|
|
elif properties.param == "TOGGLE_SHOW_SPRING_RIG":
|
|
return "Toggles showing just the spring rig controls"
|
|
|
|
elif properties.param == "TOGGLE_SHOW_RIG_POSE":
|
|
return "Toggles the rig between pose mode and rest pose"
|
|
|
|
elif properties.param == "TOGGLE_SHOW_FACE_RIG":
|
|
return "Toggles showing just the face expression rig controls"
|
|
|
|
elif properties.param == "TOGGLE_EXPRESSION_RIG_LOCK":
|
|
return "Toggle locking the position of the expression rig and making unselectable"
|
|
|
|
elif properties.param == "BUTTON_RESET_POSE":
|
|
return "Clears all pose transforms"
|
|
|
|
elif properties.param == "BUTTON_RESET_POSE_SELECTED":
|
|
return "Clears the pose on all selected bones"
|
|
|
|
elif properties.param == "RESET_EXPRESSION_POSE":
|
|
return "Clears the expression on all expression controls"
|
|
|
|
elif properties.param == "RESET_EXPRESSION_POSE_SELECTED":
|
|
return "Clears the pose on all selected expression rig bones"
|
|
|
|
elif properties.param == "LOAD_MOTION_SET":
|
|
return "Loads the chosen motion set (armature and shape key actions) into the all the character objects"
|
|
|
|
elif properties.param == "PUSH_ACTION_SET":
|
|
return "Pushes the chosen motion set (armature and shape key actions) into the NLA tracks of all the character objects at the current frame. " \
|
|
"A suitable track will be chosen to fit the actions. If there is no room available a new track will be added to contain the actions"
|
|
|
|
elif properties.param == "CLEAR_ACTION_SET":
|
|
return "Removes all actions from the character"
|
|
|
|
elif properties.param == "SELECT_SET_STRIPS":
|
|
return "Selects all the strips belonging to the same motion set and strip index"
|
|
|
|
elif properties.param == "NLA_ALIGN_LEFT":
|
|
return "Aligns all selected strips to the left most of frame of all selected strips"
|
|
|
|
elif properties.param == "NLA_ALIGN_RIGHT":
|
|
return "Aligns all selected strips to right most of frame of all selected strips"
|
|
|
|
elif properties.param == "NLA_ALIGN_TO_LEFT":
|
|
return "Aligns all selected strips to the left hand frame of the active strip"
|
|
|
|
elif properties.param == "NLA_ALIGN_TO_RIGHT":
|
|
return "Aligns all selected strips to the right hand frame of the active strip"
|
|
|
|
elif properties.param == "NLA_SIZE_SHORTEST":
|
|
return "Sets the frame lengths of all selected strips to the length of the shortest strip in the selection"
|
|
|
|
elif properties.param == "NLA_SIZE_TO":
|
|
return "Sets the frame lengths of all selected strips to the length of the active strip"
|
|
|
|
elif properties.param == "NLA_SIZE_LONGEST":
|
|
return "Sets the frame lengths of all selected strips to the length of the longest strip in the selection"
|
|
|
|
elif properties.param == "NLA_RESET_SIZE":
|
|
return "Resets the frame lengths of all selected strips to the length of the underlying action"
|
|
|
|
elif properties.param == "SET_FAKE_USER_ON":
|
|
return "Set fake user on all actions in the motion set"
|
|
|
|
elif properties.param == "SET_FAKE_USER_OFF":
|
|
return "Clear fake user on all actions in the motion set"
|
|
|
|
elif properties.param == "DELETE_MOTION_SET":
|
|
return "Delete all actions in the motion set"
|
|
|
|
elif properties.param == "DISABLE_CONSTRAINT_STRETCH":
|
|
return "Disable stretch in all IK mechanisms on the rig. By default the Blender Rigify rig allows a certain amount of stretch in the bones ease IK alignment.\n" \
|
|
"But in other applications, this bone stretch is not possible, disabling the IK stretch system can aid with animation alignment problems"
|
|
|
|
elif properties.param == "ENABLE_CONSTRAINT_STRETCH":
|
|
return "Re-enable the IK stretch mechanisms in the rig"
|
|
|
|
return ""
|
|
|
|
|
|
class CCIC_IMPORTMIX_UL_List(bpy.types.UIList):
|
|
|
|
visible_bone_indices = []
|
|
visible_key_indices = []
|
|
|
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
|
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
|
layout.label(text=item.name if item else "", translate=False, icon_value=icon)
|
|
elif self.layout_type in {'GRID'}:
|
|
layout.alignment = 'CENTER'
|
|
layout.label(text="", icon_value=icon)
|
|
|
|
def filter_items(self, context, data, propname):
|
|
filtered = []
|
|
ordered = []
|
|
items = getattr(data, propname)
|
|
visible_indices = self.visible_bone_indices if propname == "import_bones" else self.visible_key_indices
|
|
visible_indices.clear()
|
|
filtered = [self.bitflag_filter_item] * len(items)
|
|
LFN = self.filter_name.lower()
|
|
for i, item in enumerate(items):
|
|
allowed = True
|
|
# filter by name
|
|
if LFN and LFN != "*":
|
|
if LFN not in item.name.lower():
|
|
allowed = False
|
|
# block not allowed
|
|
if not allowed:
|
|
filtered[i] &= ~self.bitflag_filter_item
|
|
else:
|
|
visible_indices.append(i)
|
|
visible_indices.sort()
|
|
return filtered, ordered
|
|
|
|
|
|
class CCIC_AVAILABLEMIX_UL_List(bpy.types.UIList):
|
|
|
|
# static
|
|
visible_bone_indices = []
|
|
visible_key_indices = []
|
|
|
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
|
|
if self.layout_type in {'DEFAULT', 'COMPACT'}:
|
|
layout.label(text=item.name if item else "", translate=False, icon_value=icon)
|
|
elif self.layout_type in {'GRID'}:
|
|
layout.alignment = 'CENTER'
|
|
layout.label(text="", icon_value=icon)
|
|
|
|
def is_rigify_bone(bone: bpy.types.Bone):
|
|
return False
|
|
|
|
def filter_items(self, context, data, propname):
|
|
props = vars.props()
|
|
filtered = []
|
|
ordered = []
|
|
items = getattr(data, propname)
|
|
filtered = [self.bitflag_filter_item] * len(items)
|
|
item : bpy.types.Action
|
|
chr_cache = props.get_context_character_cache(context)
|
|
visible_indices = self.visible_bone_indices if propname == "available_bones" else self.visible_key_indices
|
|
visible_indices.clear()
|
|
LFN = self.filter_name.lower()
|
|
if propname == "available_bones":
|
|
selected_items = [ bone_item.name for bone_item in chr_cache.action_options.import_bones ] if chr_cache else []
|
|
elif propname == "available_keys":
|
|
selected_items = [ key_item.name for key_item in chr_cache.action_options.import_keys ] if chr_cache else []
|
|
else:
|
|
selected_items = []
|
|
for i, item in enumerate(items):
|
|
allowed = item.name not in selected_items
|
|
# filter by name
|
|
if LFN and LFN != "*":
|
|
if LFN not in item.name.lower():
|
|
allowed = False
|
|
# block not allowed
|
|
if not allowed:
|
|
filtered[i] &= ~self.bitflag_filter_item
|
|
else:
|
|
visible_indices.append(i)
|
|
visible_indices.sort()
|
|
return filtered, ordered
|
|
|
|
|
|
class CCICActionImportFunctions(bpy.types.Operator):
|
|
"""Action Import Functions"""
|
|
bl_idname = "ccic.action_import_functions"
|
|
bl_label = "Action Import Functions"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = "",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
chr_cache = props.get_context_character_cache(context)
|
|
if chr_cache:
|
|
if self.param == "ADD_BONE":
|
|
self.add_bone(chr_cache)
|
|
elif self.param == "ADD_ALL_BONES":
|
|
self.add_bone(chr_cache, all=True)
|
|
elif self.param == "REMOVE_BONE":
|
|
self.remove_bone(chr_cache)
|
|
elif self.param == "REMOVE_ALL_BONES":
|
|
self.remove_bone(chr_cache, all=True)
|
|
elif self.param == "ADD_KEY":
|
|
self.add_key(chr_cache)
|
|
elif self.param == "ADD_ALL_KEYS":
|
|
self.add_key(chr_cache, all=True)
|
|
elif self.param == "REMOVE_KEY":
|
|
self.remove_key(chr_cache)
|
|
elif self.param == "REMOVE_ALL_KEYS":
|
|
self.remove_key(chr_cache, all=True)
|
|
return {"FINISHED"}
|
|
|
|
def find_remaining_next_index(self, visible: List[int], current, removes=False):
|
|
"""After current index is removed from the visible list"""
|
|
try:
|
|
i = visible.index(current)
|
|
if i == len(visible) - 1:
|
|
if removes:
|
|
return visible[-2]
|
|
else:
|
|
return visible[-2]
|
|
else:
|
|
if removes:
|
|
return visible[i + 1] - 1
|
|
else:
|
|
return visible[i + 1]
|
|
except:
|
|
return -1
|
|
|
|
def has_name(self, collection, name):
|
|
for item in collection:
|
|
if item.name == name:
|
|
return True
|
|
return False
|
|
|
|
def add_bone(self, chr_cache, all=False):
|
|
try:
|
|
props = chr_cache.action_options
|
|
if all:
|
|
visible_bone_indices = CCIC_AVAILABLEMIX_UL_List.visible_bone_indices
|
|
for index in visible_bone_indices:
|
|
try:
|
|
bone_name = props.available_bones[index].name
|
|
if not self.has_name(props.import_bones, bone_name):
|
|
bone_item = props.import_bones.add()
|
|
bone_item.name = bone_name
|
|
bone_item.weight = 1.0
|
|
except: ...
|
|
props.available_bones_index = -1
|
|
else:
|
|
bone_index = props.available_bones_index
|
|
bone_name = props.available_bones[bone_index].name
|
|
props.available_bones_index = self.find_remaining_next_index(CCIC_AVAILABLEMIX_UL_List.visible_bone_indices,
|
|
bone_index)
|
|
for bone_item in props.import_bones:
|
|
if bone_item.name == bone_name:
|
|
return
|
|
bone_item = props.import_bones.add()
|
|
bone_item.name = bone_name
|
|
bone_item.weight = 1.0
|
|
except: ...
|
|
|
|
def remove_bone(self, chr_cache, all=False):
|
|
props = chr_cache.action_options
|
|
if all:
|
|
visible_bone_indices = CCIC_IMPORTMIX_UL_List.visible_bone_indices
|
|
for i in range(len(visible_bone_indices), 0, -1):
|
|
index = visible_bone_indices[i - 1]
|
|
props.import_bones.remove(index)
|
|
props.import_bones_index = -1
|
|
else:
|
|
bone_index = props.import_bones_index
|
|
props.import_bones_index = self.find_remaining_next_index(CCIC_IMPORTMIX_UL_List.visible_bone_indices,
|
|
bone_index,
|
|
removes=True)
|
|
try:
|
|
props.import_bones.remove(bone_index)
|
|
except:
|
|
utils.log_info(f"Unable to remove import bones index: {bone_index}")
|
|
|
|
def add_key(self, chr_cache, all=False):
|
|
try:
|
|
props = chr_cache.action_options
|
|
if all:
|
|
visible_key_indices = CCIC_AVAILABLEMIX_UL_List.visible_key_indices
|
|
for index in visible_key_indices:
|
|
try:
|
|
key_name = props.available_keys[index].name
|
|
if not self.has_name(props.import_keys, key_name):
|
|
key_item = props.import_keys.add()
|
|
key_item.name = key_name
|
|
key_item.weight = 1.0
|
|
except: ...
|
|
props.available_bones_index = -1
|
|
else:
|
|
key_index = props.available_keys_index
|
|
key_name = props.available_keys[key_index].name
|
|
props.available_keys_index = self.find_remaining_next_index(CCIC_AVAILABLEMIX_UL_List.visible_key_indices,
|
|
key_index)
|
|
for key_item in props.import_keys:
|
|
if key_item.name == key_name:
|
|
return
|
|
key_item = props.import_keys.add()
|
|
key_item.name = key_name
|
|
key_item.weight = 1.0
|
|
except: ...
|
|
|
|
def remove_key(self, chr_cache, all=False):
|
|
props = chr_cache.action_options
|
|
if all:
|
|
visible_key_indices = CCIC_IMPORTMIX_UL_List.visible_key_indices
|
|
for i in range(len(visible_key_indices), 0, -1):
|
|
index = visible_key_indices[i - 1]
|
|
props.import_keys.remove(index)
|
|
props.import_keys_index = -1
|
|
else:
|
|
key_index = props.import_keys_index
|
|
props.import_keys_index = self.find_remaining_next_index(CCIC_IMPORTMIX_UL_List.visible_key_indices,
|
|
key_index,
|
|
removes=True)
|
|
try:
|
|
props.import_keys.remove(key_index)
|
|
if props.import_keys_index >= len(props.import_keys):
|
|
props.import_keys_index = len(props.import_keys) - 1
|
|
except:
|
|
utils.log_info(f"Unable to remove import keys index: {key_index}")
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
if properties.param == "ADD_BONE":
|
|
return "Add the selected bone to the bone mask"
|
|
if properties.param == "ADD_ALL_BONES":
|
|
return "Add all availble bones to the bone mask"
|
|
if properties.param == "REMOVE_BONE":
|
|
return "Remove the selected bone from the bone mask"
|
|
if properties.param == "REMOVE_ALL_BONES":
|
|
return "Remove all bones from the bone mask"
|
|
if properties.param == "ADD_KEY":
|
|
return "Add the selected shape key to the key mask"
|
|
if properties.param == "ADD_ALL_KEYS":
|
|
return "Add all available shape keys to the key mask"
|
|
if properties.param == "REMOVE_KEY":
|
|
return "Remove the selected key from the key mask"
|
|
if properties.param == "REMOVE_ALL_KEYS":
|
|
return "Remove all shape keys from the key mask"
|
|
return ""
|
|
|
|
class CCICMotionBlendFunctions(bpy.types.Operator):
|
|
"""Motion Blend Operator Functions"""
|
|
bl_idname = "ccic.motion_blend_funcs"
|
|
bl_label = "Motion Blend Functions"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = "",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
def reset_opts(self, opts):
|
|
opts.frame_mode = "MATCH"
|
|
opts.use_bone_masking = False
|
|
opts.use_key_masking = False
|
|
opts.relative_root = False
|
|
opts.current_root = False
|
|
opts.use_blend = False
|
|
|
|
def get_item(self, collection, name, create=False):
|
|
for item in collection:
|
|
if item.name == name:
|
|
return item
|
|
if create:
|
|
item = collection.add()
|
|
return item
|
|
return None
|
|
|
|
def remove_item(self, collection, name):
|
|
for i, item in enumerate(collection):
|
|
if item.name == name:
|
|
collection.remove(i)
|
|
|
|
def save_preset(self, opts):
|
|
props = vars.props()
|
|
collections = {
|
|
"Bones": opts.import_bones,
|
|
"Keys": opts.import_keys,
|
|
}
|
|
folder = utils.get_resource_folder("Blend Presets")
|
|
file_name = os.path.join(folder, f"{opts.preset_mask_name}.json")
|
|
json_data = {}
|
|
if True: # dev option append preset
|
|
if os.path.exists(file_name):
|
|
file = open(file_name, "rt")
|
|
text_data = file.read()
|
|
file.close()
|
|
json_data = json.loads(text_data)
|
|
for type, collection in collections.items():
|
|
if type not in json_data:
|
|
json_data[type] = {}
|
|
for item in collection:
|
|
json_data[type][item.name] = item.weight
|
|
text_data = json.dumps(json_data, indent = 4)
|
|
with open(file_name, "w") as write_file:
|
|
write_file.write(text_data)
|
|
props.blend_mask_presets = opts.preset_mask_name
|
|
|
|
def delete_preset(self, opts):
|
|
props = vars.props()
|
|
folder = utils.get_resource_folder("Blend Presets")
|
|
file_name = os.path.join(folder, f"{opts.preset_mask_name}.json")
|
|
if os.path.exists(file_name) and os.path.isfile(file_name):
|
|
os.unlink(file_name)
|
|
|
|
def apply_preset(self, opts, remove=False):
|
|
props = vars.props()
|
|
folder = utils.get_resource_folder("Blend Presets")
|
|
preset_name = props.blend_mask_presets
|
|
file_name = os.path.join(folder, f"{preset_name}.json")
|
|
if os.path.exists(file_name):
|
|
file = open(file_name, "rt")
|
|
text_data = file.read()
|
|
file.close()
|
|
json_data = json.loads(text_data)
|
|
for type in json_data:
|
|
if type == "Bones":
|
|
available = opts.available_bones
|
|
collection = opts.import_bones
|
|
else:
|
|
available = opts.available_keys
|
|
collection = opts.import_keys
|
|
if collection is not None:
|
|
for name, weight in json_data[type].items():
|
|
available_item = self.get_item(available, name, create=False)
|
|
if available_item:
|
|
if remove:
|
|
self.remove_item(collection, name)
|
|
else:
|
|
import_item = self.get_item(collection, name, create=True)
|
|
if import_item:
|
|
import_item.name = name
|
|
import_item.weight = weight
|
|
if type == "Bones":
|
|
opts.use_bone_masking = True
|
|
else:
|
|
opts.use_key_masking = True
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
chr_cache = props.get_context_character_cache(context, strict=True)
|
|
|
|
if chr_cache:
|
|
|
|
opts = chr_cache.action_options
|
|
|
|
if self.param == "SAVE_MODE_ON":
|
|
opts.mode_save_mask_preset = True
|
|
opts.preset_mask_name = props.blend_mask_presets
|
|
|
|
if self.param == "SAVE_MODE_OFF":
|
|
opts.mode_save_mask_preset = False
|
|
|
|
if self.param == "SAVE_PRESET":
|
|
opts.mode_save_mask_preset = False
|
|
self.save_preset(opts)
|
|
|
|
if self.param == "DELETE_PRESET":
|
|
opts.mode_save_mask_preset = False
|
|
self.delete_preset(opts)
|
|
|
|
if self.param == "APPLY_PRESET":
|
|
self.apply_preset(opts)
|
|
|
|
if self.param == "REMOVE_PRESET":
|
|
self.apply_preset(opts, remove=True)
|
|
|
|
if self.param == "RESET_OPTS":
|
|
self.reset_opts(opts)
|
|
|
|
return {'FINISHED'}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
if properties.param == "SAVE_MODE_ON":
|
|
return "Enter Preset Save Mode"
|
|
if properties.param == "SAVE_MODE_ON":
|
|
return "Exit Preset Save Mode"
|
|
if properties.param == "SAVE_PRESET":
|
|
return "Save the preset"
|
|
if properties.param == "DELETE_PRESET":
|
|
return "Delete the preset"
|
|
if properties.param == "APPLY_PRESET":
|
|
return "Add the preset to the masks"
|
|
if properties.param == "REMOVE_PRESET":
|
|
return "Remove the preset from the masks"
|
|
return ""
|
|
|
|
|
|
class CCICMotionBlend(bpy.types.Operator):
|
|
"""Motion Import / Blend Options"""
|
|
bl_idname = "ccic.motion_blend"
|
|
bl_label = "Motion Blend Options"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = "",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
# pointer properties don't work in operators
|
|
#source_motion: bpy.props.PointerProperty(type=bpy.types.Action)
|
|
#target_motion: bpy.props.PointerProperty(type=bpy.types.Action)
|
|
|
|
blend_in_data_global = None
|
|
blend_out_data_global = None
|
|
blend_in_data_chr = None
|
|
blend_out_data_chr = None
|
|
|
|
chr_rlx_cache = None
|
|
objects = {}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return True
|
|
#props = vars.props()
|
|
#return props.get_context_character_cache(context) is not None
|
|
|
|
@classmethod
|
|
def label(cls, context, chr_cache=None):
|
|
props = vars.props()
|
|
opts = props.action_options
|
|
if not chr_cache:
|
|
rlx_cache = props.get_context_staging_cache(context)
|
|
chr_cache = props.get_context_character_cache(context, strict=True)
|
|
if rlx_cache:
|
|
opts = rlx_cache.action_options
|
|
elif chr_cache:
|
|
opts = chr_cache.action_options
|
|
frame_text = {
|
|
"MATCH": "Match",
|
|
"CURRENT": "Current",
|
|
"START": "Start",
|
|
}
|
|
mask_text = "Mask|" if opts and (opts.use_bone_masking or opts.use_key_masking) else ""
|
|
blend_text = "Blend|" if opts and opts.use_blend else ""
|
|
label_text = f"{mask_text}{blend_text}{frame_text[opts.get_frame_mode()]}"
|
|
return label_text
|
|
|
|
def preset_group(self, layout, opts):
|
|
props = vars.props()
|
|
row = layout.row()
|
|
c1, c2, c3 = utils.ui_three_column_layout(row, (3,14,3))
|
|
if opts.mode_save_mask_preset:
|
|
c1.label(text="Save Preset:")
|
|
c2.prop(opts, "preset_mask_name", text="")
|
|
r = c3.row(align=False)
|
|
r.operator("ccic.motion_blend_funcs", text="", icon="FILE_TICK", depress=True).param = "SAVE_MODE_OFF"
|
|
r.operator("ccic.motion_blend_funcs", text="", icon="CHECKMARK").param = "SAVE_PRESET"
|
|
r.operator("ccic.motion_blend_funcs", text="", icon="TRASH").param = "DELETE_PRESET"
|
|
else:
|
|
c1.label(text="Load Preset:")
|
|
c2.prop(props, "blend_mask_presets", text="")
|
|
r = c3.row(align=False)
|
|
r.operator("ccic.motion_blend_funcs", text="", icon="FILE_TICK").param = "SAVE_MODE_ON"
|
|
r.operator("ccic.motion_blend_funcs", text="", icon="TRIA_UP").param = "APPLY_PRESET"
|
|
r.operator("ccic.motion_blend_funcs", text="", icon="TRIA_DOWN").param = "REMOVE_PRESET"
|
|
|
|
|
|
def draw(self, context):
|
|
prefs = vars.prefs()
|
|
props = vars.props()
|
|
layout = self.layout
|
|
column = layout.column()
|
|
|
|
if not self.chr_rlx_cache:
|
|
layout.label(text="No Character Selected!")
|
|
return
|
|
|
|
col_1, col_2, col_3 = utils.ui_three_column_layout(column, (2, 5, 3))
|
|
row_3 = col_3.row()
|
|
if self.param == "BLEND":
|
|
col_1.label(text="Blend Options:")
|
|
col_2.label(text=f"{self.chr_rlx_cache.get_name()}")
|
|
row_3.label(text="")
|
|
|
|
else:
|
|
col_1.label(text="Motion Options:")
|
|
col_2.label(text=f"{self.chr_rlx_cache.get_name()}")
|
|
row_3.label(text="")
|
|
|
|
opts = self.chr_rlx_cache.action_options
|
|
|
|
if utils.ui_fake_drop_down(row_3, "", opts, "show_motion_options", align="RIGHT"):
|
|
col_1.label(text="Frame Mode:")
|
|
col_2.prop(opts, "frame_mode", text="")
|
|
col_3.prop(opts, "relative_root")
|
|
if self.param == "BLEND":
|
|
col_1.separator()
|
|
col_2.separator()
|
|
col_3.separator()
|
|
source_range = get_motion_set_frame_range(opts.source_motion)
|
|
target_range = get_motion_set_frame_range(opts.target_motion)
|
|
col_1.label(text="Source Motion:")
|
|
col_2.prop(opts, "source_motion", text="")
|
|
col_3.label(text=f"Frames: {source_range[0]} - {source_range[1]}")
|
|
col_1.label(text="Destination Motion:")
|
|
col_2.prop(opts, "target_motion", text="")
|
|
col_3.label(text=f"Frames: {target_range[0]} - {target_range[1]}")
|
|
|
|
if opts.frame_mode == "START":
|
|
start = 1
|
|
end = source_range[1] - source_range[0] + start
|
|
elif opts.frame_mode == "CURRENT":
|
|
start = bpy.context.scene.frame_current
|
|
end = source_range[1] - source_range[0] + start
|
|
elif opts.frame_mode == "MATCH":
|
|
start = source_range[0]
|
|
end = source_range[1]
|
|
tot_start = min(start, target_range[0])
|
|
tot_end = max(end, target_range[1])
|
|
|
|
col_1.label(text="As New Motion")
|
|
col_2.prop(opts, "motion_prefix", text="")
|
|
col_3.label(text=f"Blend Frames: {start} - {end}")
|
|
col_1.label(text="")
|
|
col_2.prop(opts, "motion_id", text="")
|
|
col_3.label(text=f"Total Frames: {tot_start} - {tot_end}")
|
|
name = self.get_new_motion_name()
|
|
col_1.label(text="Motion Name:")
|
|
col_2.label(text=name)
|
|
col_3.label(text="")
|
|
|
|
row = column.row()
|
|
row.prop(opts, "use_blend")
|
|
if opts.use_blend:
|
|
if utils.ui_fake_drop_down(row, "", opts, "show_blend_options", align="RIGHT"):
|
|
grid = column.grid_flow(row_major=True, columns=2)
|
|
grid.prop(opts, "blend_in_frames")
|
|
grid.prop(opts, "blend_out_frames")
|
|
box_in = grid.box()
|
|
box_out = grid.box()
|
|
box_in.template_curve_mapping(self.blend_in_data_chr, "mapping")
|
|
box_out.template_curve_mapping(self.blend_out_data_chr, "mapping")
|
|
#box_in.enabled = opts.blend_in_frames > 0
|
|
#box_out.enabled = opts.blend_out_frames > 0
|
|
column.prop(opts, "blend_strength", slider=True)
|
|
|
|
if self.chr_rlx_cache.is_avatar() or self.chr_rlx_cache.is_prop():
|
|
# bone masking
|
|
row = column.row()
|
|
row.prop(opts, "use_bone_masking")
|
|
if opts.use_bone_masking:
|
|
if utils.ui_fake_drop_down(row, "", opts, "show_bone_mask_options", align="RIGHT"):
|
|
b_prop = self.get_mix_bone_prop(self.chr_rlx_cache)
|
|
row = column.row()
|
|
c1, c2, c3 = utils.ui_three_column_layout(row, (12,1,12))
|
|
c1.template_list("CCIC_AVAILABLEMIX_UL_List", "available_bones_list",
|
|
opts, "available_bones",
|
|
opts, "available_bones_index",
|
|
rows=5, maxrows=4)
|
|
col = c2.column()
|
|
#col.separator(factor=4.0)
|
|
col.operator("ccic.action_import_functions", text="", icon="TRIA_RIGHT_BAR").param = "ADD_ALL_BONES"
|
|
col.operator("ccic.action_import_functions", text="", icon="TRIA_RIGHT").param = "ADD_BONE"
|
|
col.separator(factor=2.0)
|
|
col.operator("ccic.action_import_functions", text="", icon="TRIA_LEFT").param = "REMOVE_BONE"
|
|
col.operator("ccic.action_import_functions", text="", icon="TRIA_LEFT_BAR").param = "REMOVE_ALL_BONES"
|
|
#col.separator(factor=4.0)
|
|
rows = 4 if b_prop else 5
|
|
c3.template_list("CCIC_IMPORTMIX_UL_List", "import_bones_list",
|
|
opts, "import_bones",
|
|
opts, "import_bones_index",
|
|
rows=rows, maxrows=rows)
|
|
if b_prop:
|
|
c3.prop(b_prop, "weight", text="Blend Weight", slider=True)
|
|
|
|
# key masking
|
|
row = column.row()
|
|
row.prop(opts, "use_key_masking")
|
|
if opts.use_key_masking:
|
|
if utils.ui_fake_drop_down(row, "", opts, "show_key_mask_options", align="RIGHT"):
|
|
k_prop = self.get_mix_key_prop(self.chr_rlx_cache)
|
|
row = column.row()
|
|
c1, c2, c3 = utils.ui_three_column_layout(row, (12,1,12))
|
|
c1.template_list("CCIC_AVAILABLEMIX_UL_List", "available_keys_list",
|
|
opts, "available_keys",
|
|
opts, "available_keys_index",
|
|
rows=5, maxrows=4)
|
|
col = c2.column()
|
|
col.operator("ccic.action_import_functions", text="", icon="TRIA_RIGHT_BAR").param = "ADD_ALL_KEYS"
|
|
col.operator("ccic.action_import_functions", text="", icon="TRIA_RIGHT").param = "ADD_KEY"
|
|
col.separator(factor=2.0)
|
|
col.operator("ccic.action_import_functions", text="", icon="TRIA_LEFT").param = "REMOVE_KEY"
|
|
col.operator("ccic.action_import_functions", text="", icon="TRIA_LEFT_BAR").param = "REMOVE_ALL_KEYS"
|
|
col = row.column()
|
|
rows = 4 if k_prop else 5
|
|
c3.template_list("CCIC_IMPORTMIX_UL_List", "import_keys_list",
|
|
opts, "import_keys",
|
|
opts, "import_keys_index",
|
|
rows=rows, maxrows=rows)
|
|
if k_prop:
|
|
c3.prop(k_prop, "weight", text="Blend Weight", slider=True)
|
|
|
|
# presets
|
|
if opts.use_bone_masking or opts.use_key_masking:
|
|
row = layout.row()
|
|
self.preset_group(row, opts)
|
|
|
|
def get_mix_bone_prop(self, chr_cache):
|
|
bone_item = None
|
|
if chr_cache:
|
|
props = chr_cache.action_options
|
|
index = props.import_bones_index
|
|
try:
|
|
bone_item = props.import_bones[index]
|
|
except: ...
|
|
return bone_item
|
|
|
|
def get_mix_key_prop(self, chr_cache):
|
|
key_item = None
|
|
if chr_cache:
|
|
props = chr_cache.action_options
|
|
index = props.import_keys_index
|
|
try:
|
|
key_item = props.import_keys[index]
|
|
except: ...
|
|
return key_item
|
|
|
|
def is_valid_bone(self, bone: bpy.types.Bone):
|
|
if self.chr_rlx_cache.rigified:
|
|
# also ignore bones with drivers?
|
|
if (bone.name.startswith("facerig")):
|
|
return False
|
|
if (bone.name.startswith("CTRL_") and
|
|
(bone.name.endswith("_line") or bone.name.endswith("_box"))):
|
|
return False
|
|
if ("VIS" in bone.name):
|
|
return False
|
|
collection: bpy.types.BoneCollection
|
|
for collection in bone.collections:
|
|
if ("Hidden" in collection.name or
|
|
"ORG" in collection.name or
|
|
"MCH" in collection.name or
|
|
"DEF" in collection.name):
|
|
return False
|
|
return True
|
|
|
|
def build_available(self):
|
|
if self.chr_rlx_cache:
|
|
props = self.chr_rlx_cache.action_options
|
|
props.available_bones.clear()
|
|
props.available_keys.clear()
|
|
|
|
arm = self.chr_rlx_cache.get_armature()
|
|
if arm:
|
|
for bone in arm.data.bones:
|
|
if self.is_valid_bone(bone):
|
|
bone_item = props.available_bones.add()
|
|
bone_item.name = bone.name
|
|
|
|
keys = []
|
|
for obj in arm.children:
|
|
if utils.object_exists_is_mesh(obj) and utils.object_has_shape_keys(obj):
|
|
for i, key in enumerate(obj.data.shape_keys.key_blocks):
|
|
if i > 0 and key.name not in keys:
|
|
keys.append(key.name)
|
|
keys.sort()
|
|
for key in keys:
|
|
key_item = props.available_keys.add()
|
|
key_item.name = key
|
|
|
|
def get_new_motion_name(self):
|
|
name = ""
|
|
if self.chr_rlx_cache:
|
|
opts = self.chr_rlx_cache.action_options
|
|
actor_obj = self.chr_rlx_cache.get_primary_object()
|
|
if actor_obj:
|
|
target_motion = opts.target_motion
|
|
set_id, set_generation, action_type_id, key_object = get_motion_set_ids(target_motion)
|
|
rig_id = get_rig_id(actor_obj)
|
|
name = get_new_motion_set_action_name(action_type_id, set_id, opts.motion_prefix, rig_id, opts.motion_id, rlob=actor_obj)
|
|
return name
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
|
|
if self.chr_rlx_cache and self.param == "BLEND":
|
|
opts = self.chr_rlx_cache.action_options
|
|
source_motion = opts.source_motion
|
|
target_motion = opts.target_motion
|
|
actor_obj = self.chr_rlx_cache.get_primary_object()
|
|
if actor_obj and source_motion:
|
|
current_motion = utils.safe_get_action(actor_obj)
|
|
duplicate_motion = duplicate_motion_set(source_motion)
|
|
if duplicate_motion:
|
|
set_id, set_generation, action_type_id, key_object = get_motion_set_ids(duplicate_motion)
|
|
rig_id = get_rig_id(actor_obj)
|
|
rename_motion_set(action_type_id, set_id, opts.motion_prefix, rig_id, opts.motion_id)
|
|
# TODO if the target motion is not of this rig id, load_motion_set will make a new motion ...
|
|
if current_motion != target_motion:
|
|
load_motion_set(actor_obj, target_motion)
|
|
# store target motion
|
|
action_store_id = props.store_actions(actor_obj)
|
|
# load the source motion
|
|
# TODO likewise with the duplicate if the source motion is not of this rig id ...
|
|
load_motion_set(actor_obj, duplicate_motion, force=True)
|
|
# shift motion frames based on frame mode option
|
|
if opts.frame_mode == "START":
|
|
shift_motion_frames(duplicate_motion, 1)
|
|
elif opts.frame_mode == "CURRENT":
|
|
shift_motion_frames(duplicate_motion, bpy.context.scene.frame_current)
|
|
finalize_motion_import(self.chr_rlx_cache, actor_obj, duplicate_motion,
|
|
action_store_id)
|
|
else:
|
|
utils.log_error(f"Unable to duplicate motion set: {source_motion.name}")
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
utils.set_mode("OBJECT")
|
|
props.ensure_context_object(context)
|
|
rlx_cache = props.get_context_staging_cache(context)
|
|
chr_cache = props.get_context_character_cache(context, strict=True)
|
|
self.chr_rlx_cache = chr_cache if not rlx_cache else rlx_cache
|
|
self.blend_in_data_global = ui.get_fcurve_data(None, "motion_blend_in", shape="IN")
|
|
self.blend_out_data_global = ui.get_fcurve_data(None, "motion_blend_out", shape="OUT")
|
|
self.blend_in_data_chr = ui.get_fcurve_data(self.chr_rlx_cache, "motion_blend_in", shape="IN")
|
|
self.blend_out_data_chr = ui.get_fcurve_data(self.chr_rlx_cache, "motion_blend_out", shape="OUT")
|
|
if self.chr_rlx_cache:
|
|
actor_obj = self.chr_rlx_cache.get_primary_object()
|
|
if actor_obj:
|
|
opts = self.chr_rlx_cache.action_options
|
|
opts.source_motion = utils.collection_at_index(props.action_set_list_index, bpy.data.actions)
|
|
opts.target_motion = utils.safe_get_action(actor_obj)
|
|
self.build_available()
|
|
|
|
if self.param == "BLEND":
|
|
if self.chr_rlx_cache:
|
|
return context.window_manager.invoke_props_dialog(self, width=600)
|
|
else:
|
|
return context.window_manager.invoke_props_dialog(self, width=600)
|
|
|
|
return {'FINISHED'}
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
if properties.param == "BLEND":
|
|
return "Blend motions using options including: blending, overwriting, frame shifting, bone and shape key masking"
|
|
else:
|
|
return "Options for importing motions, including: blending, overwriting, frame shifting, bone and shape key masking"
|
|
return ""
|
|
# endregion
|
|
# region prefs
|
|
class CCICMotionPrefs(bpy.types.Operator):
|
|
"""Motion Prefs"""
|
|
bl_idname = "ccic.motion_prefs"
|
|
bl_label = "Motion Preferences"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
param: bpy.props.StringProperty(
|
|
name = "param",
|
|
default = "",
|
|
options={"HIDDEN"}
|
|
)
|
|
|
|
def draw(self, context):
|
|
prefs = vars.prefs()
|
|
props = vars.props()
|
|
layout = self.layout
|
|
column = layout.column()
|
|
|
|
# prefs
|
|
column.label(text="Preferences:")
|
|
row = column.row()
|
|
grid = row.grid_flow(row_major=True, columns=1)
|
|
if utils.B440():
|
|
grid.prop(prefs, "action_use_action_slots")
|
|
grid.prop(prefs, "action_add_key_slots_per_obj")
|
|
#grid.prop(prefs, "action_clean_actions")
|
|
grid.prop(prefs, "action_add_empty_key_channels")
|
|
column.separator()
|
|
|
|
def execute(self, context):
|
|
props = vars.props()
|
|
prefs = vars.prefs()
|
|
return {'FINISHED'}
|
|
|
|
def invoke(self, context, event):
|
|
return context.window_manager.invoke_props_dialog(self, width=300)
|
|
|
|
@classmethod
|
|
def description(cls, context, properties):
|
|
return "Motion preferences"
|
|
# endregion
|
|
|