Files
blender-portable-repo/scripts/addons/cc_blender_tools-2_4_2/rigutils.py
T
2026-07-16 12:50:13 -06:00

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