Files
2026-03-17 15:34:28 -06:00

4686 lines
181 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
from . import springbones, bones, facerig, modifiers, rigify_mapping_data, lib, 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_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": {},
"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)
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 == "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:
# match actions by name pattern
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
elif type_id == "A":
utils.log_info(f" - Found armature action: {action.name}")
actions["armature"] = action
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):
rig_id = utils.strip_name(rig.name.strip()).replace("|", "_")
return rig_id
def get_armature_id(rig):
return utils.get_prop(rig, "rl_armature_id", None)
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
if "A" in ids or "UA" in ids or "S" in 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):
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 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):
return utils.get_prop(rig, "rl_set_generation", None)
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 get_actions_frame_range(actions):
action: bpy.types.Action = None
frame_start = None
frame_end = None
for action in actions:
start = action.frame_range[0]
end = action.frame_range[1]
if frame_start is None:
frame_start = start
frame_end = end
if start < frame_start:
frame_start = start
if end > frame_end:
frame_end = end
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, obj_id=None, arm_id=None, slotted=False):
action["rl_set_id"] = set_id
action["rl_set_generation"] = set_generation
if slotted:
action["rl_action_type"] = "SLOTTED"
elif obj_id is not None:
action["rl_action_type"] = "KEY"
action["rl_key_object"] = obj_id
else:
action["rl_action_type"] = "ARM"
if arm_id is not None:
action["rl_armature_id"] = arm_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):
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)
else:
arm_action = load_separate_actions(rig, action,
move=move, temp=temp)
if arm_action:
update_motion_set_index(arm_action)
utils.log_recess()
return arm_action
def new_motion_set(rig):
prefs = vars.prefs()
slotted = prefs.use_action_slots()
rig_id = get_rig_id(rig)
rl_arm_id = utils.get_rl_object_id(rig)
set_id = generate_set_id()
set_generation = utils.get_prop(rig, "rl_set_generation")
motion_id = "New"
motion_id = get_unique_set_motion_id(rig_id, motion_id, "", slotted=slotted)
action_name = make_armature_action_name(rig_id, motion_id, "", slotted=slotted)
action = bpy.data.actions.new(action_name)
slot, channel = add_action_ob_slot_channelbag(action, rig)
add_motion_set_data(action, set_id, set_generation, arm_id=rl_arm_id, slotted=slotted)
utils.safe_set_action(rig, action, slot=slot)
utils.safe_set_action(rig.data, None, create=False)
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)
update_motion_set_index(action)
return action
def clear_motion_set(rig):
mode_selection = utils.store_mode_selection_state()
has_actions = utils.safe_get_action(rig)
if not has_actions:
reset_pose(rig)
utils.safe_set_action(rig, None)
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)
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")
set_generation = utils.get_prop(source_action, "rl_set_generation", None)
action_type_id = utils.get_prop(source_action, "rl_action_type", None)
arm_id = utils.get_prop(source_action, "rl_armature_id", None)
new_set_id = generate_set_id()
duplicates = []
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:
set_generation = utils.get_prop(action, "rl_set_generation", set_generation)
action_type_id = utils.get_prop(action, "rl_action_type", action_type_id)
obj_id = utils.get_prop(action, "rl_key_object", None)
arm_id = utils.get_prop(action, "rl_armature_id", arm_id)
is_slotted = action_type_id == "SLOTTED"
duplicate_action = action.copy()
add_motion_set_data(duplicate_action, new_set_id, set_generation, obj_id, arm_id, slotted=is_slotted)
duplicates.append(duplicate_action)
update_motion_set_index(source_action)
utils.update_ui(all=True)
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):
if edit_rig(cc3_rig):
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
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.character_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.character_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)
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):
props = vars.props()
prefix = self.prefix
rig_id = self.rig_id
motion_id = get_unique_set_motion_id(rig_id, self.motion_id, prefix,
exclude_set_id=self.set_id,
slotted=(self.action_type == "SLOTTED"))
for action in bpy.data.actions:
if "rl_set_id" in action:
if action["rl_set_id"] == self.set_id:
set_id, 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
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.character_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"
#region Operators
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()
chr_cache = props.get_context_character_cache(context)
rig = chr_cache.get_armature() if chr_cache else None
if chr_cache:
props.store_ui_list_indices()
if rig:
if self.param == "TOGGLE_SHOW_FULL_RIG":
toggle_show_full_rig(rig)
elif self.param == "TOGGLE_SHOW_BASE_RIG":
toggle_show_base_rig(rig)
elif self.param == "TOGGLE_SHOW_SPRING_RIG":
toggle_show_spring_rig(rig)
elif self.param == "TOGGLE_SHOW_RIG_POSE":
toggle_rig_rest_position(rig)
elif self.param == "TOGGLE_SHOW_SPRING_BONES":
springbones.toggle_show_spring_bones(chr_cache)
elif self.param == "TOGGLE_EXPRESSION_RIG_LOCK":
facerig.toggle_lock_position(chr_cache, rig)
elif self.param == "BUTTON_RESET_POSE_SELECTED":
mode_selection = utils.store_mode_selection_state()
reset_pose(rig, 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(rig, use_selected=False)
utils.restore_mode_selection_state(mode_selection)
elif self.param == "RESET_EXPRESSION_POSE":
if chr_cache.rigified:
mode_selection = utils.store_mode_selection_state()
facerig.clear_expression_pose(chr_cache, rig)
utils.restore_mode_selection_state(mode_selection)
elif self.param == "RESET_EXPRESSION_POSE_SELECTED":
if chr_cache.rigified:
mode_selection = utils.store_mode_selection_state()
facerig.clear_expression_pose(chr_cache, rig, selected=True)
utils.restore_mode_selection_state(mode_selection)
elif self.param == "TOGGLE_SHOW_FACE_RIG":
toggle_show_only_face_rig(rig)
elif self.param == "SET_LIMB_FK":
if chr_cache.rigified:
set_rigify_ik_fk_influence(rig, 1.0)
poke_rig(rig)
elif self.param == "SET_LIMB_IK":
if chr_cache.rigified:
set_rigify_ik_fk_influence(rig, 0.0)
poke_rig(rig)
elif self.param == "LOAD_MOTION_SET":
action = props.action_set_list_action
load_motion_set(rig, action)
elif self.param == "PUSH_ACTION_SET":
action = props.action_set_list_action
auto_index = chr_cache.get_auto_index()
push_motion_set(rig, action, auto_index)
elif self.param == "CLEAR_ACTION_SET":
clear_motion_set(rig)
elif self.param == "NEW_ACTION_SET":
new_motion_set(rig)
elif self.param == "DISABLE_CONSTRAINT_STRETCH":
mode_selection = utils.store_mode_selection_state()
rigify_rig = chr_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 = chr_cache.get_armature()
restore_ik_stretch(rigify_rig=rigify_rig)
utils.restore_mode_selection_state(mode_selection)
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":
if utils.object_exists_is_camera(context.object) or utils.object_exists_is_light(context.object):
clean_actions(context.object)
elif rig:
clean_actions(rig)
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_ImportMixBones_UL_List(bpy.types.UIList):
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)
filtered = [self.bitflag_filter_item] * len(items)
for i, item in enumerate(items):
allowed = True
# filter by name
if self.filter_name and self.filter_name != "*":
if self.filter_name not in item.name:
allowed = False
# block not allowed
if not allowed:
filtered[i] &= ~self.bitflag_filter_item
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 == "REMOVE_BONE":
self.remove_bone(chr_cache)
return {"FINISHED"}
def add_bone(self, chr_cache):
arm = chr_cache.get_armature()
props = chr_cache.action_options
bone_index = props.rig_mix_bones_list_index
bone = arm.data.bones[bone_index]
for bone_item in props.import_mix_bones:
if bone_item.name == bone.name:
return
bone_item = props.import_mix_bones.add()
bone_item.name = bone.name
bone_item.weight = 1.0
def remove_bone(self, chr_cache):
props = chr_cache.action_options
index = props.import_mix_bones_list_index
try:
props.import_mix_bones.remove(index)
except:
print(f"Unable to remove import mix bones index: {index}")
@classmethod
def description(cls, context, properties):
return ""
class CCIC_RigMixBones_UL_List(bpy.types.UIList):
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):
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)
if chr_cache:
arm = chr_cache.get_armature()
for i, item in enumerate(items):
allowed = True
# filter by name
if self.filter_name and self.filter_name != "*":
if self.filter_name not in item.name:
allowed = False
# block not allowed
if not allowed:
filtered[i] &= ~self.bitflag_filter_item
return filtered, ordered
class CCICActionImportOptions(bpy.types.Operator):
"""Action Import Options"""
bl_idname = "ccic.action_import_options"
bl_label = "Action Import Options"
bl_options = {"REGISTER", "UNDO"}
chr_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()
if not chr_cache:
chr_cache = props.get_context_character_cache(context, strict=True)
if not chr_cache:
opts = props.action_options
if chr_cache and chr_cache.action_options:
opts = chr_cache.action_options
action_text = {
"NEW": "New",
"REPLACE": "Replace",
"BLEND": "Overwrite",
}
frame_text = {
"START": "Start",
"CURRENT": "Current",
"MATCH": "Match",
}
mask_text = " (Mask)" if opts.use_masking else ""
return f"{action_text[opts.get_action_mode()]} | {frame_text[opts.get_frame_mode()]}{mask_text}"
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=2)
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")
if True:
opts = props.action_options
column.separator()
column.row().label(text="Global Motion Options:")
column.row().prop(opts, "action_mode")
column.row().prop(opts, "frame_mode")
if self.chr_cache and self.chr_cache.action_options:
arm = self.chr_cache.get_armature()
opts = self.chr_cache.action_options
column.separator()
row = column.row()
row.prop(opts, "override_global")
row.label(text=f"{self.chr_cache.character_name}")
column = column.column()
column.enabled = opts.override_global
column.row().prop(opts, "action_mode")
column.row().prop(opts, "frame_mode")
return
column.separator()
column.row().prop(opts, "use_masking")
if opts.use_masking:
row = column.row()
row.template_list("CCIC_RigMixBones_UL_List", "rig_mix_bones_list",
arm.data, "bones",
opts, "rig_mix_bones_list_index",
rows=8, maxrows=8)
col = row.column()
col.separator(factor=4.0)
col.operator("ccic.action_import_functions", text="", icon="PLAY").param = "ADD_BONE"
col.separator(factor=4.0)
col.operator("ccic.action_import_functions", text="", icon="PLAY_REVERSE").param = "REMOVE_BONE"
col.separator(factor=4.0)
row.template_list("CCIC_ImportMixBones_UL_List", "import_mix_bones_list",
opts, "import_mix_bones",
opts, "import_mix_bones_list_index",
rows=8, maxrows=8)
def execute(self, context):
props = vars.props()
prefs = vars.prefs()
#self.chr_cache = props.get_context_character_cache(context)
return {'FINISHED'}
def invoke(self, context, event):
props = vars.props()
prefs = vars.prefs()
utils.set_mode("OBJECT")
self.chr_cache = props.get_context_character_cache(context, strict=True)
return context.window_manager.invoke_props_dialog(self, width=500)
@classmethod
def description(cls, context, properties):
return "Description"
# endregion
def add_action_slot_channelbag(action, name, slot_type, reuse=False, clear=False):
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 == name:
slot = channelbag.slot
channel = channelbag
if clear:
channel.fcurves.clear()
return slot, channel
slot = action.slots.new(slot_type, 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 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)
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, obj_id=key_object, 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"]
source_arm_actions = [a for a in source_actions["armatures"]]
source_key_actions = [a for a in source_actions["keys"].values()]
arm_actions = [a.copy() for a in source_arm_actions]
key_actions = [a.copy() for a in source_key_actions]
for arm_action in arm_actions:
arm_action.name = "DB_AC_TMP"
for key_action in key_actions:
key_action.name = "DB_KC_TMP"
data_curves = { "OBJECT": {}, "KEY": {} }
database["source_action"] = source_action
database["source_arm_actions"] = source_arm_actions
database["source_key_actions"] = source_key_actions
database["arm_actions"] = arm_actions
database["key_actions"] = key_actions
all_actions = []
database["all_actions"] = all_actions
for a in source_arm_actions + source_key_actions + arm_actions + key_actions:
if a not in all_actions:
all_actions.append(a)
database["curves"] = data_curves
first_frame = None
if utils.B440():
all_actions = arm_actions + key_actions
a: bpy.types.Action
for a in all_actions:
channelbags = utils.get_action_channelbags(a)
# build database of fcurves by slot
for channel in channelbags:
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:
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))
for slot_type, a in all_actions:
# build database of fcurves by slot
if slot_type not in data_curves:
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 load_slotted_action(rig, action: bpy.types.Action, move=False, temp=None):
prefs = vars.prefs()
# determine motion set info
rig_id = get_rig_id(rig)
rl_arm_id = utils.get_rl_object_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_arm_id = utils.get_prop(action, "rl_armature_id")
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 = (rl_arm_id != src_rl_arm_id) or temp or move
if temp:
motion_prefix = "TEMP"
motion_id = temp
if use_new_motion_set:
set_id = generate_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:
motion_id = get_unique_set_motion_id(rig_id, motion_id, motion_prefix, slotted=True)
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
add_motion_set_data(action, set_id, set_generation, arm_id=rl_arm_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 ""
else:
action: bpy.types.Action = database["source_action"]
# build channels for each object in rig
utils.log_info(f"Retargetting 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)
# 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)
if group_name:
if group_name in rig_channel.groups:
group = rig_channel.groups[group_name]
else:
group = rig_channel.groups.new(group_name)
clone.group = group
# 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 bone 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 bone 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:
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)
if group_name:
if group_name in key_channel.groups:
group = key_channel.groups[group_name]
else:
group = key_channel.groups.new(group_name)
clone.group = group
# 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)
if group_name:
if group_name in key_channel.groups:
group = key_channel.groups[group_name]
else:
group = key_channel.groups.new(group_name)
fcurve.group = group
# 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)
# 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"]
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"]
# remove actions, except this action
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):
prefs = vars.prefs()
# determine motion set info
rig_id = get_rig_id(rig)
rl_arm_id = utils.get_rl_object_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_arm_id = utils.get_prop(action, "rl_armature_id")
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 = (rl_arm_id != src_rl_arm_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)
action_name = generate_action_name(rig_id, "A", "", 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_arm_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 ""
# 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)
if group_name:
if group_name in rig_channel.groups:
group = rig_channel.groups[group_name]
else:
group = rig_channel.groups.new(group_name)
clone.group = group
# 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_arm_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:
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, obj_id="ALL", 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, obj_id=obj_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)
if group_name:
if group_name in key_channel.groups:
group = key_channel.groups[group_name]
else:
group = key_channel.groups.new(group_name)
clone.group = group
# 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)
if group_name:
if group_name in key_channel.groups:
group = key_channel.groups[group_name]
else:
group = key_channel.groups.new(group_name)
fcurve.group = group
# 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, obj_id="UNUSED", 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)
# remove database copies ...
remove_actions += database["arm_actions"]
remove_actions += database["key_actions"]
if move or not use_new_motion_set:
remove_actions += database["source_arm_actions"]
remove_actions += database["source_key_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) -> bpy.types.FCurve:
if utils.B500():
to_fcurve: bpy.types.FCurve = to_channel.fcurves.new_from_fcurve(from_fcurve)
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
from_keyframes, num_points = get_fcurve_keyframe_data(from_fcurve)
apply_fcurve_keyframe_data(to_fcurve, from_keyframes)
to_fcurve.update()
return to_fcurve
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_actions(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)
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 mix_motion_set(rig, action_store_id, frame_start, frame_end):
props = vars.props()
stored_main_action = props.fetch_stored_rig_action(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 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)
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, obj_id=obj_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(obj, actions, action_store_id, action_mode, frame_start=None, frame_end=None):
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)
# now decide what to do with the actions based on the action_mode and frame_mode
if action_mode == "NEW":
# add new, do nothing else
utils.log_info(f"Action Mode NEW: Loaded new motion set: {motion_action.name}")
elif not stored_actions:
# no existing actions to replace or blend, do nothing else
utils.log_info(f"No current motion: Loaded new motion set: {motion_action.name}")
elif action_mode == "REPLACE":
# get the stored motion set data (and name) and apply it to the new motion_action
old_action = stored_actions[0]
utils.log_info(f"Action Mode REPLACE: updating motion set to: {old_action.name}")
stored_actions = props.fetch_stored_actions(action_store_id)
utils.delete_actions(stored_actions)
elif 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 finalize_motion_import(rig, motion_action, action_store_id, action_mode, frame_start=None, frame_end=None):
props = vars.props()
motion_start_frame, motion_end_frame = get_motion_set_frame_range(motion_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_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_action}")
props.restore_actions(action_store_id)
props.delete_action_store(action_store_id)
return
if motion_action:
set_id = get_motion_set_ids(motion_action)[0]
stored_actions = props.fetch_stored_actions(action_store_id)
# now decide what to do with the actions based on the action_mode and frame_mode
if action_mode == "NEW":
# already loaded, do nothing else
utils.log_info(f"Action Mode NEW: Loaded new motion set: {motion_action.name} / ({set_id})")
elif not stored_actions:
# already loaded, do nothing else
utils.log_info(f"No current motion: Loaded new motion set: {motion_action.name} / ({set_id})")
elif action_mode == "REPLACE":
# get the stored motion set data (and name) and apply it to the new motion_action
old_action = stored_actions[0]
old_set_id = get_motion_set_ids(old_action)[0]
utils.log_info(f"Action Mode REPLACE: deleting old motion set: {old_action.name} / ({old_set_id})")
#replace_motion_set(set_id, old_set_id)
delete_motion_set(old_set_id)
elif 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_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)