diff options
28 files changed, 4218 insertions, 1187 deletions
diff --git a/rigify/__init__.py b/rigify/__init__.py index 000100f4..0ca663a8 100644 --- a/rigify/__init__.py +++ b/rigify/__init__.py @@ -20,9 +20,9 @@ bl_info = { "name": "Rigify", - "version": (0, 5, 1), - "author": "Nathan Vegdahl, Lucio Rossi, Ivan Cappiello", - "blender": (2, 80, 0), + "version": (0, 6, 0), + "author": "Nathan Vegdahl, Lucio Rossi, Ivan Cappiello, Alexander Gavrilov", + "blender": (2, 81, 0), "description": "Automatic rigging from building-block components", "location": "Armature properties, Bone properties, View3d tools panel, Armature Add menu", "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/" @@ -32,14 +32,17 @@ bl_info = { if "bpy" in locals(): import importlib - importlib.reload(generate) - importlib.reload(ui) + # Don't reload base_rig or base_generate, because it would break issubclass checks, + # unless _all_ modules with classes inheriting from BaseRig are also reloaded. importlib.reload(utils) + importlib.reload(rig_ui_template) importlib.reload(feature_set_list) - importlib.reload(metarig_menu) importlib.reload(rig_lists) + importlib.reload(generate) + importlib.reload(ui) + importlib.reload(metarig_menu) else: - from . import (utils, feature_set_list, rig_lists, generate, ui, metarig_menu) + from . import (utils, base_rig, base_generate, rig_ui_template, feature_set_list, rig_lists, generate, ui, metarig_menu) import bpy import sys @@ -459,12 +462,6 @@ def register(): IDStore.rigify_transfer_only_selected = BoolProperty( name="Transfer Only Selected", description="Transfer selected bones only", default=True) - IDStore.rigify_transfer_start_frame = IntProperty( - name="Start Frame", - description="First Frame to Transfer", default=0, min= 0) - IDStore.rigify_transfer_end_frame = IntProperty( - name="End Frame", - description="Last Frame to Transfer", default=0, min= 0) # Update legacy on restart or reload. if (ui and 'legacy' in str(ui)) or bpy.context.preferences.addons['rigify'].preferences.legacy_mode: @@ -486,11 +483,14 @@ def register_rig_parameters(): pass else: for rig in rig_lists.rigs: - r = rig_lists.rigs[rig]['module'] + rig_module = rig_lists.rigs[rig]['module'] + rig_class = rig_module.Rig + r = rig_class if hasattr(rig_class, 'add_parameters') else rig_module try: r.add_parameters(RigifyParameterValidator(RigifyParameters, rig, RIGIFY_PARAMETER_TABLE)) - except AttributeError: - pass + except Exception: + import traceback + traceback.print_exc() def unregister(): @@ -522,8 +522,6 @@ def unregister(): del IDStore.rigify_rig_ui del IDStore.rigify_rig_basename del IDStore.rigify_transfer_only_selected - del IDStore.rigify_transfer_start_frame - del IDStore.rigify_transfer_end_frame # Classes. for cls in classes: diff --git a/rigify/base_generate.py b/rigify/base_generate.py new file mode 100644 index 00000000..790a0e1e --- /dev/null +++ b/rigify/base_generate.py @@ -0,0 +1,433 @@ +#====================== BEGIN GPL LICENSE BLOCK ====================== +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#======================= END GPL LICENSE BLOCK ======================== + +# <pep8 compliant> + +import bpy +import sys +import traceback + +from .utils.errors import MetarigError, RaiseErrorMixin +from .utils.naming import random_id +from .utils.metaclass import SingletonPluginMetaclass +from .utils.rig import list_bone_names_depth_first_sorted, get_rigify_type +from .utils.misc import assign_parameters + +from . import base_rig + + +#============================================= +# Generator Plugin +#============================================= + + +class GeneratorPlugin(base_rig.GenerateCallbackHost, metaclass=SingletonPluginMetaclass): + """ + Base class for generator plugins. + + Generator plugins are per-Generator singleton utility + classes that receive the same stage callbacks as rigs. + + Useful for building entities shared by multiple rigs + (e.g. the python script), or for making fire-and-forget + utilities that actually require multiple stages to + complete. + + This will create only one instance per set of args: + + instance = PluginClass(generator, ...init args) + """ + + priority = 0 + + def __init__(self, generator): + self.generator = generator + self.obj = generator.obj + + def register_new_bone(self, new_name, old_name=None): + self.generator.bone_owners[new_name] = None + + +#============================================= +# Rig Substitution Mechanism +#============================================= + + +class SubstitutionRig(RaiseErrorMixin): + """A proxy rig that replaces itself with one or more different rigs.""" + + def __init__(self, generator, pose_bone): + self.generator = generator + + self.obj = generator.obj + self.base_bone = pose_bone.name + self.params = pose_bone.rigify_parameters + + def substitute(self): + # return [rig1, rig2...] + raise NotImplementedException() + + # Utility methods + def register_new_bone(self, new_name, old_name=None): + pass + + def get_params(self, bone_name): + return self.obj.pose.bones[bone_name].rigify_parameters + + def assign_params(self, bone_name, param_dict=None, **params): + assign_parameters(self.get_params(bone_name), param_dict, **params) + + def instantiate_rig(self, rig_class, bone_name): + if isinstance(rig_class, str): + rig_class = self.generator.find_rig_class(rig_class) + + return self.generator.instantiate_rig(rig_class, self.obj.pose.bones[bone_name]) + + +#============================================= +# Legacy Rig Wrapper +#============================================= + + +class LegacyRig(base_rig.BaseRig): + """Wrapper around legacy style rigs without a common base class""" + + def __init__(self, generator, pose_bone, wrapped_class): + self.wrapped_rig = None + self.wrapped_class = wrapped_class + + super().__init__(generator, pose_bone) + + def find_org_bones(self, pose_bone): + bone_name = pose_bone.name + + if not self.wrapped_rig: + self.wrapped_rig = self.wrapped_class(self.obj, self.base_bone, self.params) + + # Switch back to OBJECT mode if the rig changed it + if self.obj.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + # Try to extract the main list of bones - old rigs often have it. + # This is not actually strictly necessary, so failing is OK. + if hasattr(self.wrapped_rig, 'org_bones'): + bones = self.wrapped_rig.org_bones + if isinstance(bones, list): + return bones + + return [bone_name] + + def generate_bones(self): + # Inject references into the rig if it won't cause conflict + if not hasattr(self.wrapped_rig, 'rigify_generator'): + self.wrapped_rig.rigify_generator = self.generator + if not hasattr(self.wrapped_rig, 'rigify_wrapper'): + self.wrapped_rig.rigify_wrapper = self + + # Old rigs only have one generate method, so call it from + # generate_bones, which is the only stage allowed to add bones. + scripts = self.wrapped_rig.generate() + + # Switch back to EDIT mode if the rig changed it + if self.obj.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + if isinstance(scripts, dict): + if 'script' in scripts: + self.script.add_panel_code(scripts['script']) + if 'imports' in scripts: + self.script.add_imports(scripts['imports']) + if 'utilities' in scripts: + self.script.add_utilities(scripts['utilities']) + if 'register' in scripts: + self.script.register_classes(scripts['register']) + if 'register_drivers' in scripts: + self.script.register_driver_functions(scripts['register_drivers']) + if 'register_props' in scripts: + for prop, val in scripts['register_props']: + self.script.register_property(prop, val) + if 'noparent_bones' in scripts: + for bone_name in scripts['noparent_bones']: + self.generator.disable_auto_parent(bone_name) + elif scripts is not None: + self.script.add_panel_code([scripts[0]]) + + def finalize(self): + if hasattr(self.wrapped_rig, 'glue'): + self.wrapped_rig.glue() + + # Switch back to OBJECT mode if the rig changed it + if self.obj.mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + +#============================================= +# Base Generate Engine +#============================================= + + +class BaseGenerator: + """Base class for the main generator object. Contains rig and plugin management code.""" + + def __init__(self, context, metarig): + self.context = context + self.scene = context.scene + self.view_layer = context.view_layer + self.layer_collection = context.layer_collection + self.collection = self.layer_collection.collection + self.metarig = metarig + self.obj = None + + # List of all rig instances + self.rig_list = [] + # List of rigs that don't have a parent + self.root_rigs = [] + # Map from bone names to their rigs + self.bone_owners = {} + + # Set of plugins + self.plugin_list = [] + self.plugin_map = {} + + # Current execution stage so plugins could check they are used correctly + self.stage = None + + # Set of bones that should be left without parent + self.noparent_bones = set() + + # Random string with time appended so that + # different rigs don't collide id's + self.rig_id = random_id(16) + + + def disable_auto_parent(self, bone_name): + """Prevent automatically parenting the bone to root if parentless.""" + self.noparent_bones.add(bone_name) + + + def __run_object_stage(self, method_name): + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'OBJECT') + num_bones = len(self.obj.data.bones) + + self.stage = method_name + + for rig in [*self.rig_list, *self.plugin_list]: + rig.rigify_invoke_stage(method_name) + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'OBJECT') + assert(num_bones == len(self.obj.data.bones)) + + + def __run_edit_stage(self, method_name): + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + num_bones = len(self.obj.data.edit_bones) + + self.stage = method_name + + for rig in [*self.rig_list, *self.plugin_list]: + rig.rigify_invoke_stage(method_name) + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + assert(num_bones == len(self.obj.data.edit_bones)) + + + def invoke_initialize(self): + self.__run_object_stage('initialize') + + + def invoke_prepare_bones(self): + self.__run_edit_stage('prepare_bones') + + + def __auto_register_bones(self, bones, rig): + """Find bones just added and not registered by this rig.""" + for bone in bones: + name = bone.name + if name not in self.bone_owners: + self.bone_owners[name] = rig + if rig: + rig.rigify_new_bones[name] = None + + if not isinstance(rig, LegacyRig): + print("WARNING: rig %s didn't register bone %s\n" % (self.describe_rig(rig), name)) + + + def invoke_generate_bones(self): + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + + self.stage = 'generate_bones' + + for rig in self.rig_list: + rig.rigify_invoke_stage('generate_bones') + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + + self.__auto_register_bones(self.obj.data.edit_bones, rig) + + for plugin in self.plugin_list: + plugin.rigify_invoke_stage('generate_bones') + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + + self.__auto_register_bones(self.obj.data.edit_bones, None) + + + def invoke_parent_bones(self): + self.__run_edit_stage('parent_bones') + + + def invoke_configure_bones(self): + self.__run_object_stage('configure_bones') + + + def invoke_apply_bones(self): + self.__run_edit_stage('apply_bones') + + + def invoke_rig_bones(self): + self.__run_object_stage('rig_bones') + + + def invoke_generate_widgets(self): + self.__run_object_stage('generate_widgets') + + + def invoke_finalize(self): + self.__run_object_stage('finalize') + + + def instantiate_rig(self, rig_class, pose_bone): + assert not issubclass(rig_class, SubstitutionRig) + + if issubclass(rig_class, base_rig.BaseRig): + return rig_class(self, pose_bone) + else: + return LegacyRig(self, pose_bone, rig_class) + + + def instantiate_rig_by_type(self, rig_type, pose_bone): + return self.instantiate_rig(self.find_rig_class(rig_type), pose_bone) + + + def describe_rig(self, rig): + base_bone = rig.base_bone + + if isinstance(rig, LegacyRig): + rig = rig.wrapped_rig + + return "%s (%s)" % (rig.__class__, base_bone) + + + def __create_rigs(self, bone_name, halt_on_missing): + """Recursively walk bones and create rig instances.""" + + pose_bone = self.obj.pose.bones[bone_name] + + rig_type = get_rigify_type(pose_bone) + + if rig_type != "": + try: + rig_class = self.find_rig_class(rig_type) + + if issubclass(rig_class, SubstitutionRig): + rigs = rig_class(self, pose_bone).substitute() + else: + rigs = [self.instantiate_rig(rig_class, pose_bone)] + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'OBJECT') + + for rig in rigs: + self.rig_list.append(rig) + + for org_name in rig.rigify_org_bones: + if org_name in self.bone_owners: + old_rig = self.describe_rig(self.bone_owners[org_name]) + new_rig = self.describe_rig(rig) + print("CONFLICT: bone %s is claimed by rigs %s and %s\n" % (org_name, old_rig, new_rig)) + + self.bone_owners[org_name] = rig + + except ImportError: + message = "Rig Type Missing: python module for type '%s' not found (bone: %s)" % (rig_type, bone_name) + if halt_on_missing: + raise MetarigError(message) + else: + print(message) + print('print_exc():') + traceback.print_exc(file=sys.stdout) + + + def __build_rig_tree_rec(self, bone, current_rig, handled): + """Recursively walk bones and connect rig instances into a tree.""" + + rig = self.bone_owners.get(bone.name) + + if rig: + if rig is current_rig: + pass + + elif rig not in handled: + rig.rigify_parent = current_rig + + if current_rig: + current_rig.rigify_children.append(rig) + else: + self.root_rigs.append(rig) + + handled[rig] = bone.name + + elif rig.rigify_parent is not current_rig: + raise MetarigError("CONFLICT: bone %s owned by rig %s has different parent rig from %s\n" % + (bone.name, rig.base_bone, handled[rig])) + + current_rig = rig + else: + if current_rig: + current_rig.rigify_child_bones.add(bone.name) + + self.bone_owners[bone.name] = current_rig + + for child in bone.children: + self.__build_rig_tree_rec(child, current_rig, handled) + + + def instantiate_rig_tree(self, halt_on_missing=False): + """Create rig instances and connect them into a tree.""" + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'OBJECT') + + # Construct the rig instances + for name in list_bone_names_depth_first_sorted(self.obj): + self.__create_rigs(name, halt_on_missing) + + # Connect rigs and bones into a tree + handled = {} + + for bone in self.obj.data.bones: + if bone.parent is None: + self.__build_rig_tree_rec(bone, None, handled) + diff --git a/rigify/base_rig.py b/rigify/base_rig.py new file mode 100644 index 00000000..cae0e569 --- /dev/null +++ b/rigify/base_rig.py @@ -0,0 +1,264 @@ +#====================== BEGIN GPL LICENSE BLOCK ====================== +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#======================= END GPL LICENSE BLOCK ======================== + +# <pep8 compliant> + +import bpy +import sys +import traceback + +from .utils.errors import RaiseErrorMixin +from .utils.bones import BoneDict, BoneUtilityMixin +from .utils.mechanism import MechanismUtilityMixin +from .utils.metaclass import BaseStagedClass + +# Only export certain symbols via 'from base_rig import *' +__all__ = ['BaseRig', 'stage'] + +#============================================= +# Base Rig +#============================================= + +class GenerateCallbackHost(BaseStagedClass, define_stages=True): + """ + Standard set of callback methods to redefine. + Shared between BaseRig and GeneratorPlugin. + + These callbacks are called in this order; every one is + called for all rigs before proceeding to the next stage. + + Switching modes is not allowed in rigs for performance + reasons. Place code in the appropriate callbacks to use + the mode set by the main engine. + + After each callback, all other methods decorated with + @stage.<method_name> are called, for instance: + + def generate_bones(self): + print('first') + + @stage.generate_bones + def foo(self): + print('second') + + Will print 'first', then 'second'. Multiple methods in the + same stage are called in the order they are first defined; + in case of inheritance, the class bodies are scanned in + reverse MRO order. E.g.: + + class Base(...): + @stage.generate_bones + def first(self):... + + @stage.generate_bones + def second(self):... + + class Derived(Base): + @stage.generate_bones + def third(self):... + + # Was first defined in Base so still first: + @stage.generate_bones + def first(self):... + + @stage.generate_bones + def fourth(self):... + + Multiple inheritance can make this ordering confusing, so it + is best to avoid it. + + When overriding such methods in a subclass the appropriate + decorator should be repeated for code clarity reasons; + a warning is printed if this is not done. + """ + def initialize(self): + """ + Initialize processing after all rig classes are constructed. + Called in Object mode. May not change the armature. + """ + pass + + def prepare_bones(self): + """ + Prepare ORG bones for generation, e.g. align them. + Called in Edit mode. May not add bones. + """ + pass + + def generate_bones(self): + """ + Create all bones. + Called in Edit mode. + """ + pass + + def parent_bones(self): + """ + Parent all bones and set other edit mode properties. + Called in Edit mode. May not add bones. + """ + pass + + def configure_bones(self): + """ + Configure bone properties, e.g. transform locks, layers etc. + Called in Object mode. May not do Edit mode operations. + """ + pass + + def apply_bones(self): + """ + Can be used to apply some constraints to rest pose, and for final parenting. + Called in Edit mode. May not add bones. + """ + pass + + def rig_bones(self): + """ + Create and configure all constraints, drivers etc. + Called in Object mode. May not do Edit mode operations. + """ + pass + + def generate_widgets(self): + """ + Create all widget objects. + Called in Object mode. May not do Edit mode operations. + """ + pass + + def finalize(self): + """ + Finishing touches to the construction of the rig. + Called in Object mode. May not do Edit mode operations. + """ + pass + + +class BaseRig(GenerateCallbackHost, RaiseErrorMixin, BoneUtilityMixin, MechanismUtilityMixin): + """ + Base class for all rigs. + + The main weak areas in the legacy Rigify rig class structure + was that there were no provisions for intelligent interactions + between rigs, and all processing was done via one generate + method, necessitating frequent expensive mode switches. + + This structure fixes those problems by providing a mandatory + base class that hold documented connections between rigs, bones, + and the common generator object. The generation process is also + split into multiple stages. + """ + def __init__(self, generator, pose_bone): + self.generator = generator + + self.obj = generator.obj + self.script = generator.script + self.base_bone = pose_bone.name + self.params = pose_bone.rigify_parameters + + # Collection of bone names for use in implementing the rig + self.bones = BoneDict( + # ORG bone names + org = self.find_org_bones(pose_bone), + # Control bone names + ctrl = BoneDict(), + # MCH bone names + mch = BoneDict(), + # DEF bone names + deform = BoneDict(), + ) + + # Data useful for complex rig interaction: + # Parent-child links between rigs. + self.rigify_parent = None + self.rigify_children = [] + # ORG bones directly owned by the rig. + self.rigify_org_bones = set(self.bones.flatten('org')) + # Children of bones owned by the rig. + self.rigify_child_bones = set() + # Bones created by the rig (mapped to original names) + self.rigify_new_bones = dict() + + def register_new_bone(self, new_name, old_name=None): + """Registers this rig as the owner of this new bone.""" + self.rigify_new_bones[new_name] = old_name + self.generator.bone_owners[new_name] = self + + ########################################################### + # Bone ownership + + def find_org_bones(self, pose_bone): + """ + Select bones directly owned by the rig. Returning the + same bone from multiple rigs is an error. + + May return a single name, a list, or a BoneDict. + + Called in Object mode, may not change the armature. + """ + return [pose_bone.name] + + ########################################################### + # Parameters and UI + + @classmethod + def add_parameters(cls, params): + """ + This method add more parameters to params + :param params: rigify_parameters of a pose_bone + :return: + """ + pass + + @classmethod + def parameters_ui(cls, layout, params): + """ + This method draws the UI of the rigify_parameters defined on the pose_bone + :param layout: + :param params: + :return: + """ + layout.label(text="No options") + + +#============================================= +# Rig Utility +#============================================= + + +class RigUtility(BoneUtilityMixin, MechanismUtilityMixin): + """Base class for utility classes that generate part of a rig.""" + def __init__(self, owner): + self.owner = owner + self.obj = owner.obj + + def register_new_bone(self, new_name, old_name=None): + self.owner.register_new_bone(new_name, old_name) + + +#============================================= +# Rig Stage Decorators +#============================================= + +class stage: + pass + +# Generate @stage.<...> decorators for all valid stages +for name, decorator in GenerateCallbackHost.make_stage_decorators(): + setattr(stage, name, decorator) diff --git a/rigify/generate.py b/rigify/generate.py index 22769a41..bebe4fc3 100644 --- a/rigify/generate.py +++ b/rigify/generate.py @@ -24,24 +24,21 @@ import time import traceback import sys from rna_prop_ui import rna_idprop_ui_prop_get -from collections import OrderedDict -from .utils import MetarigError, new_bone -from .utils import MCH_PREFIX, DEF_PREFIX, WGT_PREFIX, ROOT_NAME, make_original_name -from .utils import create_root_widget +from .utils.errors import MetarigError +from .utils.bones import new_bone +from .utils.layers import ORG_LAYER, MCH_LAYER, DEF_LAYER, ROOT_LAYER +from .utils.naming import ORG_PREFIX, MCH_PREFIX, DEF_PREFIX, ROOT_NAME, make_original_name +from .utils.widgets import WGT_PREFIX +from .utils.widgets_special import create_root_widget +from .utils.misc import copy_attributes, gamma_correct, select_object from .utils.collections import ensure_widget_collection, list_layer_collections, filter_layer_collections_by_object -from .utils import random_id -from .utils import copy_attributes -from .utils import gamma_correct -from . import rig_lists + +from . import base_generate from . import rig_ui_template +from . import rig_lists RIG_MODULE = "rigs" -ORG_LAYER = [n == 31 for n in range(0, 32)] # Armature layer that original bones should be moved to. -MCH_LAYER = [n == 30 for n in range(0, 32)] # Armature layer that mechanism bones should be moved to. -DEF_LAYER = [n == 29 for n in range(0, 32)] # Armature layer that deformation bones should be moved to. -ROOT_LAYER = [n == 28 for n in range(0, 32)] # Armature layer that root bone should be moved to. - class Timer: def __init__(self): @@ -53,512 +50,462 @@ class Timer: self.timez = t -# TODO: generalize to take a group as input instead of an armature. -def generate_rig(context, metarig): - """ Generates a rig from a metarig. - - """ - t = Timer() +class Generator(base_generate.BaseGenerator): + def __init__(self, context, metarig): + super().__init__(context, metarig) - # Random string with time appended so that - # different rigs don't collide id's - rig_id = random_id(16) + self.id_store = context.window_manager - # Initial configuration - # mode_orig = context.mode # UNUSED - rest_backup = metarig.data.pose_position - metarig.data.pose_position = 'REST' + self.rig_new_name = "" + self.rig_old_name = "" - bpy.ops.object.mode_set(mode='OBJECT') - scene = context.scene - view_layer = context.view_layer - layer_collection = context.layer_collection - id_store = context.window_manager + def find_rig_class(self, rig_type): + rig_module = rig_lists.rigs[rig_type]["module"] - usable_collections = list_layer_collections(view_layer.layer_collection, selectable=True) + return rig_module.Rig - if layer_collection not in usable_collections: - metarig_collections = filter_layer_collections_by_object(usable_collections, metarig) - layer_collection = (metarig_collections + [view_layer.layer_collection])[0] - collection = layer_collection.collection + def __create_rig_object(self): + scene = self.scene + id_store = self.id_store - #------------------------------------------ - # Create/find the rig object and set it up + # Check if the generated rig already exists, so we can + # regenerate in the same object. If not, create a new + # object to generate the rig in. + print("Fetch rig.") - # Check if the generated rig already exists, so we can - # regenerate in the same object. If not, create a new - # object to generate the rig in. - print("Fetch rig.") + if id_store.rigify_generate_mode == 'overwrite': + name = id_store.rigify_target_rig or "rig" + try: + obj = scene.objects[name] + self.rig_old_name = name + obj.name = self.rig_new_name or name - rig_new_name = "" - rig_old_name = "" - if id_store.rigify_rig_basename: - rig_new_name = id_store.rigify_rig_basename + "_rig" + rig_collections = filter_layer_collections_by_object(self.usable_collections, obj) + self.layer_collection = (rig_collections + [self.layer_collection])[0] + self.collection = self.layer_collection.collection - if id_store.rigify_generate_mode == 'overwrite': - name = id_store.rigify_target_rig or "rig" - try: - obj = scene.objects[name] - rig_old_name = name - obj.name = rig_new_name or name - - rig_collections = filter_layer_collections_by_object(usable_collections, obj) - layer_collection = (rig_collections + [layer_collection])[0] - collection = layer_collection.collection - - except KeyError: - rig_old_name = name - name = rig_new_name or name - obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) + except KeyError: + self.rig_old_name = name + name = self.rig_new_name or name + obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) + obj.display_type = 'WIRE' + self.collection.objects.link(obj) + else: + name = self.rig_new_name or "rig" + obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) # in case name 'rig' exists it will be rig.001 obj.display_type = 'WIRE' - collection.objects.link(obj) - else: - name = rig_new_name or "rig" - obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) # in case name 'rig' exists it will be rig.001 - obj.display_type = 'WIRE' - collection.objects.link(obj) + self.collection.objects.link(obj) - id_store.rigify_target_rig = obj.name - obj.data.pose_position = 'POSE' + id_store.rigify_target_rig = obj.name + obj.data.pose_position = 'POSE' - # Get rid of anim data in case the rig already existed - print("Clear rig animation data.") - obj.animation_data_clear() - obj.data.animation_data_clear() + self.obj = obj + return obj - # Select generated rig object - metarig.select_set(False) - obj.select_set(True) - view_layer.objects.active = obj - # Remove wgts if force update is set - wgts_group_name = "WGTS_" + (rig_old_name or obj.name) - if wgts_group_name in scene.objects and id_store.rigify_force_widget_update: + def __create_widget_group(self, new_group_name): + context = self.context + scene = self.scene + id_store = self.id_store + + # Create/find widge collection + self.widget_collection = ensure_widget_collection(context) + + # Remove wgts if force update is set + wgts_group_name = "WGTS_" + (self.rig_old_name or obj.name) + if wgts_group_name in scene.objects and id_store.rigify_force_widget_update: + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + for wgt in bpy.data.objects[wgts_group_name].children: + wgt.select_set(True) + bpy.ops.object.delete(use_global=False) + if self.rig_old_name: + bpy.data.objects[wgts_group_name].name = new_group_name + + # Create Group widget + wgts_group_name = new_group_name + if wgts_group_name not in scene.objects: + if wgts_group_name in bpy.data.objects: + bpy.data.objects[wgts_group_name].user_clear() + bpy.data.objects.remove(bpy.data.objects[wgts_group_name]) + mesh = bpy.data.meshes.new(wgts_group_name) + wgts_obj = bpy.data.objects.new(wgts_group_name, mesh) + self.widget_collection.objects.link(wgts_obj) + + self.wgts_group_name = new_group_name + + + def __duplicate_rig(self): + obj = self.obj + metarig = self.metarig + context = self.context + + # Remove all bones from the generated rig armature. + bpy.ops.object.mode_set(mode='EDIT') + for bone in obj.data.edit_bones: + obj.data.edit_bones.remove(bone) bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.select_all(action='DESELECT') - for wgt in bpy.data.objects[wgts_group_name].children: - wgt.select_set(True) - bpy.ops.object.delete(use_global=False) - if rig_old_name: - bpy.data.objects[wgts_group_name].name = "WGTS_" + obj.name - - wgts_group_name = "WGTS_" + obj.name - - # Get parented objects to restore later - childs = {} # {object: bone} - for child in obj.children: - childs[child] = child.parent_bone - - # Remove all bones from the generated rig armature. - bpy.ops.object.mode_set(mode='EDIT') - for bone in obj.data.edit_bones: - obj.data.edit_bones.remove(bone) - bpy.ops.object.mode_set(mode='OBJECT') - # Create temporary duplicates for merging - temp_rig_1 = metarig.copy() - temp_rig_1.data = metarig.data.copy() - collection.objects.link(temp_rig_1) + # Select and duplicate metarig + select_object(context, metarig, deselect_all=True) - temp_rig_2 = metarig.copy() - temp_rig_2.data = obj.data - collection.objects.link(temp_rig_2) + bpy.ops.object.duplicate() - # Select the temp rigs for merging - for objt in view_layer.objects: - objt.select_set(False) # deselect all objects - temp_rig_1.select_set(True) - temp_rig_2.select_set(True) - view_layer.objects.active = temp_rig_2 + # Select the target rig and join + select_object(context, obj) - # Merge the temporary rigs - bpy.ops.object.join() + bpy.ops.object.join() - # Delete the second temp rig - bpy.ops.object.delete() + # Select the generated rig + select_object(context, obj, deselect_all=True) + + # Clean up animation data + if obj.animation_data: + obj.animation_data.action = None + + for track in obj.animation_data.nla_tracks: + obj.animation_data.nla_tracks.remove(track) + + # Freeze drivers referring to custom properties + for d in obj.animation_data.drivers: + for var in d.driver.variables: + for tar in var.targets: + # If a custom property + if var.type == 'SINGLE_PROP' \ + and re.match('^pose.bones\["[^"\]]*"\]\["[^"\]]*"\]$', tar.data_path): + tar.data_path = "RIGIFY-" + tar.data_path + + + def __rename_org_bones(self): + obj = self.obj + + #---------------------------------- + # Make a list of the original bones so we can keep track of them. + original_bones = [bone.name for bone in obj.data.bones] + + # Add the ORG_PREFIX to the original bones. + for i in range(0, len(original_bones)): + new_name = make_original_name(original_bones[i]) + obj.data.bones[original_bones[i]].name = new_name + original_bones[i] = new_name + + self.original_bones = original_bones + + + def __create_root_bone(self): + obj = self.obj + metarig = self.metarig + + #---------------------------------- + # Create the root bone. + root_bone = new_bone(obj, ROOT_NAME) + spread = get_xy_spread(metarig.data.bones) or metarig.data.bones[0].length + spread = float('%.3g' % spread) + scale = spread/0.589 + obj.data.edit_bones[root_bone].head = (0, 0, 0) + obj.data.edit_bones[root_bone].tail = (0, scale, 0) + obj.data.edit_bones[root_bone].roll = 0 + self.root_bone = root_bone + self.bone_owners[root_bone] = None + + + def __parent_bones_to_root(self): + eb = self.obj.data.edit_bones + + # Parent loose bones to root + for bone in eb: + if bone.name in self.noparent_bones: + continue + elif bone.parent is None: + bone.use_connect = False + bone.parent = eb[self.root_bone] + + + def __lock_transforms(self): + # Lock transforms on all non-control bones + r = re.compile("[A-Z][A-Z][A-Z]-") + for pb in self.obj.pose.bones: + if r.match(pb.name): + pb.lock_location = (True, True, True) + pb.lock_rotation = (True, True, True) + pb.lock_rotation_w = True + pb.lock_scale = (True, True, True) + + + def __assign_layers(self): + bones = self.obj.data.bones + + bones[self.root_bone].layers = ROOT_LAYER + + # Every bone that has a name starting with "DEF-" make deforming. All the + # others make non-deforming. + for bone in bones: + name = bone.name + + bone.use_deform = name.startswith(DEF_PREFIX) + + # Move all the original bones to their layer. + if name.startswith(ORG_PREFIX): + bone.layers = ORG_LAYER + # Move all the bones with names starting with "MCH-" to their layer. + elif name.startswith(MCH_PREFIX): + bone.layers = MCH_LAYER + # Move all the bones with names starting with "DEF-" to their layer. + elif name.startswith(DEF_PREFIX): + bone.layers = DEF_LAYER + + + def __restore_driver_vars(self): + obj = self.obj + + # Alter marked driver targets + if obj.animation_data: + for d in obj.animation_data.drivers: + for v in d.driver.variables: + for tar in v.targets: + if tar.data_path.startswith("RIGIFY-"): + temp, bone, prop = tuple([x.strip('"]') for x in tar.data_path.split('["')]) + if bone in obj.data.bones \ + and prop in obj.pose.bones[bone].keys(): + tar.data_path = tar.data_path[7:] + else: + tar.data_path = 'pose.bones["%s"]["%s"]' % (make_original_name(bone), prop) + + + def __assign_widgets(self): + obj_table = {obj.name: obj for obj in self.scene.objects} + + # Assign shapes to bones + # Object's with name WGT-<bone_name> get used as that bone's shape. + for bone in self.obj.pose.bones: + # Object names are limited to 63 characters... arg + wgt_name = (WGT_PREFIX + self.obj.name + '_' + bone.name)[:63] + + if wgt_name in obj_table: + bone.custom_shape = obj_table[wgt_name] + + + def __compute_visible_layers(self): + # Reveal all the layers with control bones on them + vis_layers = [False for n in range(0, 32)] + + for bone in self.obj.data.bones: + for i in range(0, 32): + vis_layers[i] = vis_layers[i] or bone.layers[i] + + for i in range(0, 32): + vis_layers[i] = vis_layers[i] and not (ORG_LAYER[i] or MCH_LAYER[i] or DEF_LAYER[i]) + + self.obj.data.layers = vis_layers + + + def generate(self): + context = self.context + metarig = self.metarig + scene = self.scene + id_store = self.id_store + view_layer = self.view_layer + t = Timer() + + self.usable_collections = list_layer_collections(view_layer.layer_collection, selectable=True) + + if self.layer_collection not in self.usable_collections: + metarig_collections = filter_layer_collections_by_object(self.usable_collections, self.metarig) + self.layer_collection = (metarig_collections + [view_layer.layer_collection])[0] + self.collection = self.layer_collection.collection + + bpy.ops.object.mode_set(mode='OBJECT') + + #------------------------------------------ + # Create/find the rig object and set it up + if id_store.rigify_rig_basename: + self.rig_new_name = id_store.rigify_rig_basename + "_rig" + + obj = self.__create_rig_object() + + # Get rid of anim data in case the rig already existed + print("Clear rig animation data.") + + obj.animation_data_clear() + obj.data.animation_data_clear() + + select_object(context, obj, deselect_all=True) + + #------------------------------------------ + # Create Group widget + self.__create_widget_group("WGTS_" + obj.name) - # Select the generated rig - for objt in view_layer.objects: - objt.select_set(False) # deselect all objects - obj.select_set(True) - view_layer.objects.active = obj - - # Copy over bone properties - for bone in metarig.data.bones: - bone_gen = obj.data.bones[bone.name] - - # B-bone stuff - bone_gen.bbone_segments = bone.bbone_segments - bone_gen.bbone_easein = bone.bbone_easein - bone_gen.bbone_easeout = bone.bbone_easeout - - # Copy over the pose_bone properties - for bone in metarig.pose.bones: - bone_gen = obj.pose.bones[bone.name] - - # Rotation mode and transform locks - bone_gen.rotation_mode = bone.rotation_mode - bone_gen.lock_rotation = tuple(bone.lock_rotation) - bone_gen.lock_rotation_w = bone.lock_rotation_w - bone_gen.lock_rotations_4d = bone.lock_rotations_4d - bone_gen.lock_location = tuple(bone.lock_location) - bone_gen.lock_scale = tuple(bone.lock_scale) - - # rigify_type and rigify_parameters - bone_gen.rigify_type = bone.rigify_type - for prop in dir(bone_gen.rigify_parameters): - if (not prop.startswith("_")) \ - and (not prop.startswith("bl_")) \ - and (prop != "rna_type"): - try: - setattr(bone_gen.rigify_parameters, prop, \ - getattr(bone.rigify_parameters, prop)) - except AttributeError: - print("FAILED TO COPY PARAMETER: " + str(prop)) - - # Custom properties - for prop in bone.keys(): - try: - bone_gen[prop] = bone[prop] - except KeyError: - pass - - # Constraints - for con1 in bone.constraints: - con2 = bone_gen.constraints.new(type=con1.type) - copy_attributes(con1, con2) - - # Set metarig target to rig target - if "target" in dir(con2): - if con2.target == metarig: - con2.target = obj - - # Copy drivers - if metarig.animation_data: - for d1 in metarig.animation_data.drivers: - d2 = obj.driver_add(d1.data_path) - copy_attributes(d1, d2) - copy_attributes(d1.driver, d2.driver) - - # Remove default modifiers, variables, etc. - for m in d2.modifiers: - d2.modifiers.remove(m) - for v in d2.driver.variables: - d2.driver.variables.remove(v) - - # Copy modifiers - for m1 in d1.modifiers: - m2 = d2.modifiers.new(type=m1.type) - copy_attributes(m1, m2) - - # Copy variables - for v1 in d1.driver.variables: - v2 = d2.driver.variables.new() - copy_attributes(v1, v2) - for i in range(len(v1.targets)): - copy_attributes(v1.targets[i], v2.targets[i]) - # Switch metarig targets to rig targets - if v2.targets[i].id == metarig: - v2.targets[i].id = obj - - # Mark targets that may need to be altered after rig generation - tar = v2.targets[i] - # If a custom property - if v2.type == 'SINGLE_PROP' \ - and re.match('^pose.bones\["[^"\]]*"\]\["[^"\]]*"\]$', tar.data_path): - tar.data_path = "RIGIFY-" + tar.data_path - - # Copy key frames - for i in range(len(d1.keyframe_points)): - d2.keyframe_points.add() - k1 = d1.keyframe_points[i] - k2 = d2.keyframe_points[i] - copy_attributes(k1, k2) - - t.tick("Duplicate rig: ") - #---------------------------------- - # Make a list of the original bones so we can keep track of them. - original_bones = [bone.name for bone in obj.data.bones] - - # Add the ORG_PREFIX to the original bones. - bpy.ops.object.mode_set(mode='OBJECT') - for i in range(0, len(original_bones)): - obj.data.bones[original_bones[i]].name = make_original_name(original_bones[i]) - original_bones[i] = make_original_name(original_bones[i]) - - # Create a sorted list of the original bones, sorted in the order we're - # going to traverse them for rigging. - # (root-most -> leaf-most, alphabetical) - bones_sorted = [] - for name in original_bones: - bones_sorted += [name] - bones_sorted.sort() # first sort by names - bones_sorted.sort(key=lambda bone: len(obj.pose.bones[bone].parent_recursive)) # then parents before children - - t.tick("Make list of org bones: ") - #---------------------------------- - # Create the root bone. - bpy.ops.object.mode_set(mode='EDIT') - root_bone = new_bone(obj, ROOT_NAME) - spread = get_xy_spread(metarig.data.bones) or metarig.data.bones[0].length - spread = float('%.3g' % spread) - scale = spread/0.589 - obj.data.edit_bones[root_bone].head = (0, 0, 0) - obj.data.edit_bones[root_bone].tail = (0, scale, 0) - obj.data.edit_bones[root_bone].roll = 0 - bpy.ops.object.mode_set(mode='OBJECT') - obj.data.bones[root_bone].layers = ROOT_LAYER - - # Put the rig_name in the armature custom properties - rna_idprop_ui_prop_get(obj.data, "rig_id", create=True) - obj.data["rig_id"] = rig_id - - t.tick("Create root bone: ") - - # Create/find widge collection - widget_collection = ensure_widget_collection(context) - - # Create Group widget - # wgts_group_name = "WGTS" - if wgts_group_name not in scene.objects: - if wgts_group_name in bpy.data.objects: - bpy.data.objects[wgts_group_name].user_clear() - bpy.data.objects.remove(bpy.data.objects[wgts_group_name]) - mesh = bpy.data.meshes.new(wgts_group_name) - wgts_obj = bpy.data.objects.new(wgts_group_name, mesh) - widget_collection.objects.link(wgts_obj) t.tick("Create main WGTS: ") - # - # if id_store.rigify_generate_mode == 'new': - # bpy.ops.object.select_all(action='DESELECT') - # for wgt in bpy.data.objects[wgts_group_name].children: - # wgt.select_set(True) - # bpy.ops.object.make_single_user(obdata=True) - - #---------------------------------- - try: - # Collect/initialize all the rigs. - rigs = [] - for bone in bones_sorted: - bpy.ops.object.mode_set(mode='EDIT') - rigs += get_bone_rigs(obj, bone) + + #------------------------------------------ + # Get parented objects to restore later + childs = {} # {object: bone} + for child in obj.children: + childs[child] = child.parent_bone + + #------------------------------------------ + # Copy bones from metarig to obj + self.__duplicate_rig() + + t.tick("Duplicate rig: ") + + #------------------------------------------ + # Add the ORG_PREFIX to the original bones. + bpy.ops.object.mode_set(mode='OBJECT') + + self.__rename_org_bones() + + t.tick("Make list of org bones: ") + + #------------------------------------------ + # Put the rig_name in the armature custom properties + rna_idprop_ui_prop_get(obj.data, "rig_id", create=True) + obj.data["rig_id"] = self.rig_id + + self.script = rig_ui_template.ScriptGenerator(self) + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.instantiate_rig_tree() + + t.tick("Instantiate rigs: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.invoke_initialize() + t.tick("Initialize rigs: ") - # Generate all the rigs. - ui_scripts = [] - ui_imports = rig_ui_template.UI_IMPORTS.copy() - ui_utilities = rig_ui_template.UI_UTILITIES.copy() - ui_register = rig_ui_template.UI_REGISTER.copy() - noparent_bones = [] - for rig in rigs: - # Go into editmode in the rig armature - bpy.ops.object.mode_set(mode='OBJECT') - context.view_layer.objects.active = obj - obj.select_set(True) - bpy.ops.object.mode_set(mode='EDIT') - scripts = rig.generate() - if isinstance(scripts, dict): - if 'script' in scripts: - ui_scripts += scripts['script'] - if 'imports' in scripts: - ui_imports += scripts['imports'] - if 'utilities' in scripts: - ui_utilities += scripts['utilities'] - if 'register' in scripts: - ui_register += scripts['register'] - if 'noparent_bones' in scripts: - noparent_bones += scripts['noparent_bones'] - elif scripts is not None: - ui_scripts += [scripts[0]] - t.tick("Generate rigs: ") + #------------------------------------------ + bpy.ops.object.mode_set(mode='EDIT') - except Exception as e: - # Cleanup if something goes wrong - print("Rigify: failed to generate rig.") - metarig.data.pose_position = rest_backup - obj.data.pose_position = 'POSE' + self.invoke_prepare_bones() + + t.tick("Prepare bones: ") + + #------------------------------------------ bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') - # Continue the exception - raise e + self.__create_root_bone() - #---------------------------------- - bpy.ops.object.mode_set(mode='OBJECT') + self.invoke_generate_bones() - # Get a list of all the bones in the armature - bones = [bone.name for bone in obj.data.bones] + t.tick("Generate bones: ") - # Parent any free-floating bones to the root excluding noparent_bones - noparent_bones = dict.fromkeys(noparent_bones) + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.mode_set(mode='EDIT') - for bone in bones: - if bone in noparent_bones: - continue - elif obj.data.edit_bones[bone].parent is None: - obj.data.edit_bones[bone].use_connect = False - obj.data.edit_bones[bone].parent = obj.data.edit_bones[root_bone] + self.invoke_parent_bones() - bpy.ops.object.mode_set(mode='OBJECT') + self.__parent_bones_to_root() - # Lock transforms on all non-control bones - r = re.compile("[A-Z][A-Z][A-Z]-") - for bone in bones: - if r.match(bone): - pb = obj.pose.bones[bone] - pb.lock_location = (True, True, True) - pb.lock_rotation = (True, True, True) - pb.lock_rotation_w = True - pb.lock_scale = (True, True, True) - - # Every bone that has a name starting with "DEF-" make deforming. All the - # others make non-deforming. - for bone in bones: - if obj.data.bones[bone].name.startswith(DEF_PREFIX): - obj.data.bones[bone].use_deform = True - else: - obj.data.bones[bone].use_deform = False - - # Alter marked driver targets - if obj.animation_data: - for d in obj.animation_data.drivers: - for v in d.driver.variables: - for tar in v.targets: - if tar.data_path.startswith("RIGIFY-"): - temp, bone, prop = tuple([x.strip('"]') for x in tar.data_path.split('["')]) - if bone in obj.data.bones \ - and prop in obj.pose.bones[bone].keys(): - tar.data_path = tar.data_path[7:] - else: - tar.data_path = 'pose.bones["%s"]["%s"]' % (make_original_name(bone), prop) - - # Move all the original bones to their layer. - for bone in original_bones: - obj.data.bones[bone].layers = ORG_LAYER - - # Move all the bones with names starting with "MCH-" to their layer. - for bone in bones: - if obj.data.bones[bone].name.startswith(MCH_PREFIX): - obj.data.bones[bone].layers = MCH_LAYER - - # Move all the bones with names starting with "DEF-" to their layer. - for bone in bones: - if obj.data.bones[bone].name.startswith(DEF_PREFIX): - obj.data.bones[bone].layers = DEF_LAYER - - # Create root bone widget - create_root_widget(obj, "root") - - # Assign shapes to bones - # Object's with name WGT-<bone_name> get used as that bone's shape. - for bone in bones: - wgt_name = (WGT_PREFIX + obj.name + '_' + obj.data.bones[bone].name)[:63] # Object names are limited to 63 characters... arg - if wgt_name in context.scene.objects: - # Weird temp thing because it won't let me index by object name - for ob in context.scene.objects: - if ob.name == wgt_name: - obj.pose.bones[bone].custom_shape = ob - break - # This is what it should do: - # obj.pose.bones[bone].custom_shape = context.scene.objects[wgt_name] - # Reveal all the layers with control bones on them - vis_layers = [False for n in range(0, 32)] - for bone in bones: - for i in range(0, 32): - vis_layers[i] = vis_layers[i] or obj.data.bones[bone].layers[i] - for i in range(0, 32): - vis_layers[i] = vis_layers[i] and not (ORG_LAYER[i] or MCH_LAYER[i] or DEF_LAYER[i]) - obj.data.layers = vis_layers - - # Ensure the collection of layer names exists - for i in range(1 + len(metarig.data.rigify_layers), 29): - metarig.data.rigify_layers.add() - - # Create list of layer name/row pairs - layer_layout = [] - for l in metarig.data.rigify_layers: - print(l.name) - layer_layout += [(l.name, l.row)] - - # Generate the UI script - if id_store.rigify_generate_mode == 'overwrite': - rig_ui_name = id_store.rigify_rig_ui or 'rig_ui.py' - else: - rig_ui_name = 'rig_ui.py' + t.tick("Parent bones: ") - if id_store.rigify_generate_mode == 'overwrite' and rig_ui_name in bpy.data.texts.keys(): - script = bpy.data.texts[rig_ui_name] - script.clear() - else: - script = bpy.data.texts.new("rig_ui.py") - - rig_ui_old_name = "" - if id_store.rigify_rig_basename: - rig_ui_old_name = script.name - script.name = id_store.rigify_rig_basename + "_rig_ui.py" - - id_store.rigify_rig_ui = script.name - - for s in OrderedDict.fromkeys(ui_imports): - script.write(s + "\n") - script.write(rig_ui_template.UI_BASE_UTILITIES % rig_id) - for s in OrderedDict.fromkeys(ui_utilities): - script.write(s + "\n") - script.write(rig_ui_template.UI_SLIDERS) - for s in ui_scripts: - script.write("\n " + s.replace("\n", "\n ") + "\n") - script.write(rig_ui_template.layers_ui(vis_layers, layer_layout)) - script.write("\ndef register():\n") - ui_register = OrderedDict.fromkeys(ui_register) - for s in ui_register: - script.write(" bpy.utils.register_class("+s+");\n") - script.write("\ndef unregister():\n") - for s in ui_register: - script.write(" bpy.utils.unregister_class("+s+");\n") - script.write("\nregister()\n") - script.use_module = True - - # Run UI script - exec(script.as_string(), {}) - - # Create Selection Sets - create_selection_sets(obj, metarig) - - # Create Bone Groups - create_bone_groups(obj, metarig) - - # Add rig_ui to logic - create_persistent_rig_ui(obj, script) - - # Do final gluing - for rig in rigs: - if hasattr(rig, "glue"): - # update glue_bone rigs - bpy.ops.object.mode_set(mode='EDIT') - rig = rig.__class__(rig.obj, rig.base_bone, rig.params) - - rig.glue() - t.tick("Glue pass") - - t.tick("The rest: ") - #---------------------------------- - # Deconfigure - bpy.ops.object.mode_set(mode='OBJECT') - metarig.data.pose_position = rest_backup - obj.data.pose_position = 'POSE' + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.invoke_configure_bones() + + t.tick("Configure bones: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='EDIT') + + self.invoke_apply_bones() + + t.tick("Apply bones: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.invoke_rig_bones() + + t.tick("Rig bones: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + create_root_widget(obj, "root") + + self.invoke_generate_widgets() + + t.tick("Generate widgets: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.__lock_transforms() + self.__assign_layers() + self.__compute_visible_layers() + self.__restore_driver_vars() + + t.tick("Assign layers: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.invoke_finalize() + + t.tick("Finalize: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') - # Restore parent to bones - for child, sub_parent in childs.items(): - if sub_parent in obj.pose.bones: - mat = child.matrix_world.copy() - child.parent_bone = sub_parent - child.matrix_world = mat + self.__assign_widgets() - #---------------------------------- - # Restore active collection - view_layer.active_layer_collection = layer_collection + # Create Selection Sets + create_selection_sets(obj, metarig) + + # Create Bone Groups + create_bone_groups(obj, metarig) + + t.tick("The rest: ") + + #---------------------------------- + # Deconfigure + bpy.ops.object.mode_set(mode='OBJECT') + obj.data.pose_position = 'POSE' + + # Restore parent to bones + for child, sub_parent in childs.items(): + if sub_parent in obj.pose.bones: + mat = child.matrix_world.copy() + child.parent_bone = sub_parent + child.matrix_world = mat + + #---------------------------------- + # Restore active collection + view_layer.active_layer_collection = self.layer_collection + + +def generate_rig(context, metarig): + """ Generates a rig from a metarig. + + """ + # Initial configuration + rest_backup = metarig.data.pose_position + metarig.data.pose_position = 'REST' + + try: + Generator(context, metarig).generate() + + metarig.data.pose_position = rest_backup + + except Exception as e: + # Cleanup if something goes wrong + print("Rigify: failed to generate rig.") + + bpy.ops.object.mode_set(mode='OBJECT') + metarig.data.pose_position = rest_backup + + # Continue the exception + raise e def create_selection_sets(obj, metarig): @@ -630,64 +577,6 @@ def create_bone_groups(obj, metarig): b.bone_group = obj.pose.bone_groups[name] -def create_persistent_rig_ui(obj, script): - """Make sure the ui script always follows the rig around""" - skip = False - driver = None - - if not obj.animation_data: - obj.animation_data_create() - - for fcurve in obj.animation_data.drivers: - if fcurve.data_path == 'pass_index': - driver = fcurve.driver - for variable in driver.variables: - if variable.name == script.name: - skip = True - break - break - - if not skip: - if not driver: - fcurve = obj.driver_add("pass_index") - driver = fcurve.driver - - variable = driver.variables.new() - variable.name = script.name - variable.targets[0].id_type = 'TEXT' - variable.targets[0].id = script - - -def get_bone_rigs(obj, bone_name, halt_on_missing=False): - """ Fetch all the rigs specified on a bone. - """ - rigs = [] - rig_type = obj.pose.bones[bone_name].rigify_type - rig_type = rig_type.replace(" ", "") - - if rig_type == "": - pass - else: - # Gather parameters - params = obj.pose.bones[bone_name].rigify_parameters - - # Get the rig - try: - rig = rig_lists.rigs[rig_type]["module"] - rig = rig.Rig(obj, bone_name, params) - except (KeyError, ImportError): - message = "Rig Type Missing: python module for type '%s' not found (bone: %s)" % (rig_type, bone_name) - if halt_on_missing: - raise MetarigError(message) - else: - print(message) - print('print_exc():') - traceback.print_exc(file=sys.stdout) - else: - rigs += [rig] - return rigs - - def get_xy_spread(bones): x_max = 0 y_max = 0 diff --git a/rigify/rig_lists.py b/rigify/rig_lists.py index 0045b185..018bbbac 100644 --- a/rigify/rig_lists.py +++ b/rigify/rig_lists.py @@ -18,6 +18,7 @@ import os import traceback +import importlib from . import utils from . import feature_set_list @@ -63,7 +64,8 @@ def get_rigs(base_dir, base_path, *, path=[], feature_set=feature_set_list.DEFAU # Check straight-up python files subpath = [*path, f[:-3]] key = '.'.join(subpath) - rig_module = utils.get_resource('.'.join(base_path + subpath)) + # Don't reload rig modules - it breaks isinstance + rig_module = importlib.import_module('.'.join(base_path + subpath)) if hasattr(rig_module, "Rig"): rigs[key] = {"module": rig_module, "feature_set": feature_set} diff --git a/rigify/rig_ui_template.py b/rigify/rig_ui_template.py index 6180ad03..8c66b8b3 100644 --- a/rigify/rig_ui_template.py +++ b/rigify/rig_ui_template.py @@ -18,12 +18,28 @@ # <pep8 compliant> +import bpy + +from collections import OrderedDict + +from .utils.animation import SCRIPT_REGISTER_BAKE, SCRIPT_UTILITIES_BAKE +from .utils.layers import get_layers +from .utils.rig import attach_persistent_script + +from . import base_generate + +from rna_prop_ui import rna_idprop_quote_path + + UI_IMPORTS = [ 'import bpy', - 'from bpy.props import StringProperty', 'import math', + 'import json', + 'import collections', 'from math import pi', + 'from bpy.props import StringProperty', 'from mathutils import Euler, Matrix, Quaternion, Vector', + 'from rna_prop_ui import rna_idprop_quote_path', ] UI_BASE_UTILITIES = ''' @@ -63,42 +79,21 @@ def rotation_difference(mat1, mat2): angle = -angle + (2*pi) return angle -def tail_distance(angle,bone_ik,bone_fk): - """ Returns the distance between the tails of two bones - after rotating bone_ik in AXIS_ANGLE mode. - """ - rot_mod=bone_ik.rotation_mode - if rot_mod != 'AXIS_ANGLE': - bone_ik.rotation_mode = 'AXIS_ANGLE' - bone_ik.rotation_axis_angle[0] = angle - bpy.context.view_layer.update() - - dv = (bone_fk.tail - bone_ik.tail).length - - bone_ik.rotation_mode = rot_mod - return dv - -def find_min_range(bone_ik,bone_fk,f=tail_distance,delta=pi/8): +def find_min_range(f,start_angle,delta=pi/8): """ finds the range where lies the minimum of function f applied on bone_ik and bone_fk at a certain angle. """ - rot_mod=bone_ik.rotation_mode - if rot_mod != 'AXIS_ANGLE': - bone_ik.rotation_mode = 'AXIS_ANGLE' - - start_angle = bone_ik.rotation_axis_angle[0] angle = start_angle while (angle > (start_angle - 2*pi)) and (angle < (start_angle + 2*pi)): - l_dist = f(angle-delta,bone_ik,bone_fk) - c_dist = f(angle,bone_ik,bone_fk) - r_dist = f(angle+delta,bone_ik,bone_fk) + l_dist = f(angle-delta) + c_dist = f(angle) + r_dist = f(angle+delta) if min((l_dist,c_dist,r_dist)) == c_dist: - bone_ik.rotation_mode = rot_mod return (angle-delta,angle+delta) else: angle=angle+delta -def ternarySearch(f, left, right, bone_ik, bone_fk, absolutePrecision): +def ternarySearch(f, left, right, absolutePrecision): """ Find minimum of unimodal function f() within [left, right] To find the maximum, revert the if/else statement or revert the comparison. @@ -111,11 +106,13 @@ def ternarySearch(f, left, right, bone_ik, bone_fk, absolutePrecision): leftThird = left + (right - left)/3 rightThird = right - (right - left)/3 - if f(leftThird, bone_ik, bone_fk) > f(rightThird, bone_ik, bone_fk): + if f(leftThird) > f(rightThird): left = leftThird else: right = rightThird +''' +UTILITIES_FUNC_COMMON_IKFK = [''' ######################################### ## "Visual Transform" helper functions ## ######################################### @@ -125,28 +122,8 @@ def get_pose_matrix_in_other_space(mat, pose_bone): transform space. In other words, presuming that mat is in armature space, slapping the returned matrix onto pose_bone should give it the armature-space transforms of mat. - TODO: try to handle cases with axis-scaled parents better. """ - rest = pose_bone.bone.matrix_local.copy() - rest_inv = rest.inverted() - if pose_bone.parent: - par_mat = pose_bone.parent.matrix.copy() - par_inv = par_mat.inverted() - par_rest = pose_bone.parent.bone.matrix_local.copy() - else: - par_mat = Matrix() - par_inv = Matrix() - par_rest = Matrix() - - # Get matrix in bone's current transform space - smat = rest_inv @ (par_rest @ (par_inv @ mat)) - - # Compensate for non-local location - #if not pose_bone.bone.use_local_location: - # loc = smat.to_translation() @ (par_rest.inverted() @ rest).to_quaternion() - # smat.translation = loc - - return smat + return pose_bone.id_data.convert_space(matrix=mat, pose_bone=pose_bone, from_space='POSE', to_space='LOCAL') def get_local_pose_matrix(pose_bone): @@ -159,19 +136,7 @@ def set_pose_translation(pose_bone, mat): """ Sets the pose bone's translation to the same translation as the given matrix. Matrix should be given in bone's local space. """ - if pose_bone.bone.use_local_location == True: - pose_bone.location = mat.to_translation() - else: - loc = mat.to_translation() - - rest = pose_bone.bone.matrix_local.copy() - if pose_bone.bone.parent: - par_rest = pose_bone.bone.parent.matrix_local.copy() - else: - par_rest = Matrix() - - q = (par_rest.inverted() @ rest).to_quaternion() - pose_bone.location = q @ loc + pose_bone.location = mat.to_translation() def set_pose_rotation(pose_bone, mat): @@ -205,8 +170,6 @@ def match_pose_translation(pose_bone, target_bone): """ mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone) set_pose_translation(pose_bone, mat) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='POSE') def match_pose_rotation(pose_bone, target_bone): @@ -216,8 +179,6 @@ def match_pose_rotation(pose_bone, target_bone): """ mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone) set_pose_rotation(pose_bone, mat) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='POSE') def match_pose_scale(pose_bone, target_bone): @@ -227,27 +188,55 @@ def match_pose_scale(pose_bone, target_bone): """ mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone) set_pose_scale(pose_bone, mat) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='POSE') -def correct_rotation(bone_ik, bone_fk): + +############################## +## IK/FK snapping functions ## +############################## + +def correct_rotation(view_layer, bone_ik, target_matrix): """ Corrects the ik rotation in ik2fk snapping functions """ - alfarange = find_min_range(bone_ik,bone_fk) - alfamin = ternarySearch(tail_distance,alfarange[0],alfarange[1],bone_ik,bone_fk,0.1) + axis = target_matrix.to_3x3().col[1].normalized() - rot_mod = bone_ik.rotation_mode - if rot_mod != 'AXIS_ANGLE': - bone_ik.rotation_mode = 'AXIS_ANGLE' - bone_ik.rotation_axis_angle[0] = alfamin - bone_ik.rotation_mode = rot_mod + def distance(angle): + # Rotate the bone and return the actual angle between bones + bone_ik.rotation_euler[1] = angle + view_layer.update() -############################## -## IK/FK snapping functions ## -############################## + return -(bone_ik.vector.normalized().dot(axis)) + + if bone_ik.rotation_mode in {'QUATERNION', 'AXIS_ANGLE'}: + bone_ik.rotation_mode = 'ZXY' + + start_angle = bone_ik.rotation_euler[1] + + alfarange = find_min_range(distance, start_angle) + alfamin = ternarySearch(distance, alfarange[0], alfarange[1], pi / 180) + + bone_ik.rotation_euler[1] = alfamin + view_layer.update() + + +def correct_scale(view_layer, bone_ik, target_matrix): + """ Correct the scale of the base IK bone. """ + input_scale = target_matrix.to_scale() -def match_pole_target(ik_first, ik_last, pole, match_bone, length): + for i in range(3): + cur_scale = bone_ik.matrix.to_scale() + + bone_ik.scale = [ + v * i / c for v, i, c in zip(bone_ik.scale, input_scale, cur_scale) + ] + + view_layer.update() + + if all(abs((c - i)/i) < 0.01 for i, c in zip(input_scale, cur_scale)): + break + + +def match_pole_target(view_layer, ik_first, ik_last, pole, match_bone_matrix, length): """ Places an IK chain's pole target to match ik_first's transforms to match_bone. All bones should be given as pose bones. You need to be in pose mode on the relevant armature object. @@ -278,22 +267,21 @@ def match_pole_target(ik_first, ik_last, pole, match_bone, length): mat = get_pose_matrix_in_other_space(Matrix.Translation(ploc), pole) set_pose_translation(pole, mat) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='POSE') + view_layer.update() set_pole(pv) # Get the rotation difference between ik_first and match_bone - angle = rotation_difference(ik_first.matrix, match_bone.matrix) + angle = rotation_difference(ik_first.matrix, match_bone_matrix) # Try compensating for the rotation difference in both directions pv1 = Matrix.Rotation(angle, 4, ikv) @ pv set_pole(pv1) - ang1 = rotation_difference(ik_first.matrix, match_bone.matrix) + ang1 = rotation_difference(ik_first.matrix, match_bone_matrix) pv2 = Matrix.Rotation(-angle, 4, ikv) @ pv set_pole(pv2) - ang2 = rotation_difference(ik_first.matrix, match_bone.matrix) + ang2 = rotation_difference(ik_first.matrix, match_bone_matrix) # Do the one with the smaller angle if ang1 < ang2: @@ -309,7 +297,7 @@ def parse_bone_names(names_string): else: return names_string -''' +'''] UTILITIES_FUNC_ARM_FKIK = [''' ###################### @@ -322,6 +310,7 @@ def fk2ik_arm(obj, fk, ik): fk: list of fk bone names ik: list of ik bone names """ + view_layer = bpy.context.view_layer uarm = obj.pose.bones[fk[0]] farm = obj.pose.bones[fk[1]] hand = obj.pose.bones[fk[2]] @@ -341,29 +330,35 @@ def fk2ik_arm(obj, fk, ik): # Upper arm position match_pose_rotation(uarm, uarmi) match_pose_scale(uarm, uarmi) + view_layer.update() # Forearm position match_pose_rotation(farm, farmi) match_pose_scale(farm, farmi) + view_layer.update() # Hand position match_pose_rotation(hand, handi) match_pose_scale(hand, handi) + view_layer.update() else: # Upper arm position match_pose_translation(uarm, uarmi) match_pose_rotation(uarm, uarmi) match_pose_scale(uarm, uarmi) + view_layer.update() # Forearm position #match_pose_translation(hand, handi) match_pose_rotation(farm, farmi) match_pose_scale(farm, farmi) + view_layer.update() # Hand position match_pose_translation(hand, handi) match_pose_rotation(hand, handi) match_pose_scale(hand, handi) + view_layer.update() def ik2fk_arm(obj, fk, ik): @@ -372,6 +367,7 @@ def ik2fk_arm(obj, fk, ik): fk: list of fk bone names ik: list of ik bone names """ + view_layer = bpy.context.view_layer uarm = obj.pose.bones[fk[0]] farm = obj.pose.bones[fk[1]] hand = obj.pose.bones[fk[2]] @@ -395,21 +391,29 @@ def ik2fk_arm(obj, fk, ik): match_pose_translation(handi, hand) match_pose_rotation(handi, hand) match_pose_scale(handi, hand) + view_layer.update() + # Pole target position - match_pole_target(uarmi, farmi, pole, uarm, (uarmi.length + farmi.length)) + match_pole_target(view_layer, uarmi, farmi, pole, uarm.matrix, (uarmi.length + farmi.length)) else: # Hand position match_pose_translation(handi, hand) match_pose_rotation(handi, hand) match_pose_scale(handi, hand) + view_layer.update() # Upper Arm position match_pose_translation(uarmi, uarm) - match_pose_rotation(uarmi, uarm) + #match_pose_rotation(uarmi, uarm) + set_pose_rotation(uarmi, Matrix()) match_pose_scale(uarmi, uarm) + view_layer.update() + # Rotation Correction - correct_rotation(uarmi, uarm) + correct_rotation(view_layer, uarmi, uarm.matrix) + + correct_scale(view_layer, uarmi, uarm.matrix) '''] UTILITIES_FUNC_LEG_FKIK = [''' @@ -423,6 +427,7 @@ def fk2ik_leg(obj, fk, ik): fk: list of fk bone names ik: list of ik bone names """ + view_layer = bpy.context.view_layer thigh = obj.pose.bones[fk[0]] shin = obj.pose.bones[fk[1]] foot = obj.pose.bones[fk[2]] @@ -444,36 +449,38 @@ def fk2ik_leg(obj, fk, ik): # Thigh position match_pose_rotation(thigh, thighi) match_pose_scale(thigh, thighi) + view_layer.update() # Shin position match_pose_rotation(shin, shini) match_pose_scale(shin, shini) + view_layer.update() # Foot position mat = mfoot.bone.matrix_local.inverted() @ foot.bone.matrix_local footmat = get_pose_matrix_in_other_space(mfooti.matrix, foot) @ mat set_pose_rotation(foot, footmat) set_pose_scale(foot, footmat) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='POSE') + view_layer.update() else: # Thigh position match_pose_translation(thigh, thighi) match_pose_rotation(thigh, thighi) match_pose_scale(thigh, thighi) + view_layer.update() # Shin position match_pose_rotation(shin, shini) match_pose_scale(shin, shini) + view_layer.update() # Foot position mat = mfoot.bone.matrix_local.inverted() @ foot.bone.matrix_local footmat = get_pose_matrix_in_other_space(mfooti.matrix, foot) @ mat set_pose_rotation(foot, footmat) set_pose_scale(foot, footmat) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='POSE') + view_layer.update() def ik2fk_leg(obj, fk, ik): @@ -482,6 +489,7 @@ def ik2fk_leg(obj, fk, ik): fk: list of fk bone names ik: list of ik bone names """ + view_layer = bpy.context.view_layer thigh = obj.pose.bones[fk[0]] shin = obj.pose.bones[fk[1]] mfoot = obj.pose.bones[fk[2]] @@ -506,6 +514,7 @@ def ik2fk_leg(obj, fk, ik): # Clear footroll set_pose_rotation(footroll, Matrix()) + view_layer.update() # Foot position mat = mfooti.bone.matrix_local.inverted() @ footi.bone.matrix_local @@ -513,16 +522,17 @@ def ik2fk_leg(obj, fk, ik): set_pose_translation(footi, footmat) set_pose_rotation(footi, footmat) set_pose_scale(footi, footmat) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='POSE') + view_layer.update() # Thigh position match_pose_translation(thighi, thigh) - match_pose_rotation(thighi, thigh) + #match_pose_rotation(thighi, thigh) + set_pose_rotation(thighi, Matrix()) match_pose_scale(thighi, thigh) + view_layer.update() # Rotation Correction - correct_rotation(thighi,thigh) + correct_rotation(view_layer, thighi, thigh.matrix) else: # Stretch @@ -532,6 +542,7 @@ def ik2fk_leg(obj, fk, ik): # Clear footroll set_pose_rotation(footroll, Matrix()) + view_layer.update() # Foot position mat = mfooti.bone.matrix_local.inverted() @ footi.bone.matrix_local @@ -539,11 +550,12 @@ def ik2fk_leg(obj, fk, ik): set_pose_translation(footi, footmat) set_pose_rotation(footi, footmat) set_pose_scale(footi, footmat) - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='POSE') + view_layer.update() # Pole target position - match_pole_target(thighi, shini, pole, thigh, (thighi.length + shini.length)) + match_pole_target(view_layer, thighi, shini, pole, thigh.matrix, (thighi.length + shini.length)) + + correct_scale(view_layer, thighi, thigh.matrix) '''] UTILITIES_FUNC_POLE = [''' @@ -765,6 +777,7 @@ class Rigify_Rot2PoleSwitch(bpy.types.Operator): REGISTER_RIG_ARM = REGISTER_OP_ARM_FKIK + REGISTER_OP_POLE UTILITIES_RIG_ARM = [ + *UTILITIES_FUNC_COMMON_IKFK, *UTILITIES_FUNC_ARM_FKIK, *UTILITIES_FUNC_POLE, *UTILITIES_OP_ARM_FKIK, @@ -774,6 +787,7 @@ UTILITIES_RIG_ARM = [ REGISTER_RIG_LEG = REGISTER_OP_LEG_FKIK + REGISTER_OP_POLE UTILITIES_RIG_LEG = [ + *UTILITIES_FUNC_COMMON_IKFK, *UTILITIES_FUNC_LEG_FKIK, *UTILITIES_FUNC_POLE, *UTILITIES_OP_LEG_FKIK, @@ -794,6 +808,7 @@ UI_REGISTER = [ # Include arm and leg utilities for now in case somebody wants to use # legacy limb rigs, which expect these to be available by default. UI_UTILITIES = [ + *UTILITIES_FUNC_COMMON_IKFK, *UTILITIES_FUNC_ARM_FKIK, *UTILITIES_FUNC_LEG_FKIK, *UTILITIES_OP_ARM_FKIK, @@ -825,24 +840,44 @@ class RigUI(bpy.types.Panel): layout = self.layout pose_bones = context.active_object.pose.bones try: - selected_bones = [bone.name for bone in context.selected_pose_bones] - selected_bones += [context.active_pose_bone.name] + selected_bones = set(bone.name for bone in context.selected_pose_bones) + selected_bones.add(context.active_pose_bone.name) except (AttributeError, TypeError): return def is_selected(names): # Returns whether any of the named bones are selected. - if type(names) == list: - for name in names: - if name in selected_bones: - return True + if isinstance(names, list) or isinstance(names, set): + return not selected_bones.isdisjoint(names) elif names in selected_bones: return True return False + num_rig_separators = [-1] + def emit_rig_separator(): + if num_rig_separators[0] >= 0: + layout.separator() + num_rig_separators[0] += 1 ''' +UI_REGISTER_BAKE_SETTINGS = ['RigBakeSettings'] + +UI_BAKE_SETTINGS = ''' +class RigBakeSettings(bpy.types.Panel): + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + bl_label = "Rig Bake Settings" + bl_idname = "VIEW3D_PT_rig_bake_settings_" + rig_id + bl_category = 'Item' + + @classmethod + def poll(self, context): + return RigUI.poll(context) and find_action(context.active_object) is not None + + def draw(self, context): + RigifyBakeKeyframesMixin.draw_common_bake_ui(context, self.layout) +''' def layers_ui(layers, layout): """ Turn a list of booleans + a list of names into a layer UI. @@ -896,3 +931,321 @@ class RigLayers(bpy.types.Panel): code += " row.prop(context.active_object.data, 'layers', index=28, toggle=True, text='Root')\n" return code + + +def quote_parameters(positional, named): + """Quote the given positional and named parameters as a code string.""" + positional_list = [ repr(v) for v in positional ] + named_list = [ "%s=%r" % (k, v) for k, v in named.items() ] + return ', '.join(positional_list + named_list) + +def indent_lines(lines, indent=4): + if indent > 0: + prefix = ' ' * indent + return [ prefix + line for line in lines ] + else: + return lines + + +class PanelLayout(object): + """Utility class that builds code for creating a layout.""" + + def __init__(self, parent, index=0): + if isinstance(parent, PanelLayout): + self.parent = parent + self.script = parent.script + else: + self.parent = None + self.script = parent + + self.header = [] + self.items = [] + self.indent = 0 + self.index = index + self.layout = self._get_layout_var(index) + self.is_empty = True + + @staticmethod + def _get_layout_var(index): + return 'layout' if index == 0 else 'group' + str(index) + + def clear_empty(self): + self.is_empty = False + + if self.parent: + self.parent.clear_empty() + + def get_lines(self): + lines = [] + + for item in self.items: + if isinstance(item, PanelLayout): + lines += item.get_lines() + else: + lines.append(item) + + if len(lines) > 0: + return self.wrap_lines(lines) + else: + return [] + + def wrap_lines(self, lines): + return self.header + indent_lines(lines, self.indent) + + def add_line(self, line): + assert isinstance(line, str) + + self.items.append(line) + + if self.is_empty: + self.clear_empty() + + def use_bake_settings(self): + """This panel contains operators that need the common Bake settings.""" + self.parent.use_bake_settings() + + def custom_prop(self, bone_name, prop_name, **params): + """Add a custom property input field to the panel.""" + param_str = quote_parameters([ rna_idprop_quote_path(prop_name) ], params) + self.add_line( + "%s.prop(pose_bones[%r], %s)" % (self.layout, bone_name, param_str) + ) + + def operator(self, operator_name, *, properties=None, **params): + """Add an operator call button to the panel.""" + name = operator_name.format_map(self.script.format_args) + param_str = quote_parameters([ name ], params) + call_str = "%s.operator(%s)" % (self.layout, param_str) + if properties: + self.add_line("props = " + call_str) + for k, v in properties.items(): + self.add_line("props.%s = %r" % (k,v)) + else: + self.add_line(call_str) + + def add_nested_layout(self, name, params): + param_str = quote_parameters([], params) + sub_panel = PanelLayout(self, self.index + 1) + sub_panel.header.append('%s = %s.%s(%s)' % (sub_panel.layout, self.layout, name, param_str)) + self.items.append(sub_panel) + return sub_panel + + def row(self, **params): + """Add a nested row layout to the panel.""" + return self.add_nested_layout('row', params) + + def column(self, **params): + """Add a nested column layout to the panel.""" + return self.add_nested_layout('column', params) + + def split(self, **params): + """Add a split layout to the panel.""" + return self.add_nested_layout('split', params) + + +class BoneSetPanelLayout(PanelLayout): + """Panel restricted to a certain set of bones.""" + + def __init__(self, rig_panel, bones): + assert isinstance(bones, frozenset) + super().__init__(rig_panel) + self.bones = bones + self.show_bake_settings = False + + def clear_empty(self): + self.parent.bones |= self.bones + + super().clear_empty() + + def wrap_lines(self, lines): + if self.bones != self.parent.bones: + header = ["if is_selected(%r):" % (set(self.bones))] + return header + indent_lines(lines) + else: + return lines + + def use_bake_settings(self): + self.show_bake_settings = True + if not self.script.use_bake_settings: + self.script.use_bake_settings = True + self.script.add_utilities(SCRIPT_UTILITIES_BAKE) + self.script.register_classes(SCRIPT_REGISTER_BAKE) + + +class RigPanelLayout(PanelLayout): + """Panel owned by a certain rig.""" + + def __init__(self, script, rig): + super().__init__(script) + self.bones = set() + self.subpanels = OrderedDict() + + def wrap_lines(self, lines): + header = [ "if is_selected(%r):" % (set(self.bones)) ] + prefix = [ "emit_rig_separator()" ] + return header + indent_lines(prefix + lines) + + def panel_with_selected_check(self, control_names): + selected_set = frozenset(control_names) + + if selected_set in self.subpanels: + return self.subpanels[selected_set] + else: + panel = BoneSetPanelLayout(self, selected_set) + self.subpanels[selected_set] = panel + self.items.append(panel) + return panel + + +class ScriptGenerator(base_generate.GeneratorPlugin): + """Generator plugin that builds the python script attached to the rig.""" + + priority = -100 + + def __init__(self, generator): + super().__init__(generator) + + self.ui_scripts = [] + self.ui_imports = UI_IMPORTS.copy() + self.ui_utilities = UI_UTILITIES.copy() + self.ui_register = UI_REGISTER.copy() + self.ui_register_drivers = [] + self.ui_register_props = [] + + self.ui_rig_panels = OrderedDict() + + self.use_bake_settings = False + + # Structured panel code generation + def panel_with_selected_check(self, rig, control_names): + """Add a panel section with restricted selection.""" + rig_key = id(rig) + + if rig_key in self.ui_rig_panels: + panel = self.ui_rig_panels[rig_key] + else: + panel = RigPanelLayout(self, rig) + self.ui_rig_panels[rig_key] = panel + + return panel.panel_with_selected_check(control_names) + + # Raw output + def add_panel_code(self, str_list): + """Add raw code to the panel.""" + self.ui_scripts += str_list + + def add_imports(self, str_list): + self.ui_imports += str_list + + def add_utilities(self, str_list): + self.ui_utilities += str_list + + def register_classes(self, str_list): + self.ui_register += str_list + + def register_driver_functions(self, str_list): + self.ui_register_drivers += str_list + + def register_property(self, name, definition): + self.ui_register_props.append((name, definition)) + + def initialize(self): + self.format_args = { + 'rig_id': self.generator.rig_id, + } + + def finalize(self): + metarig = self.generator.metarig + id_store = self.generator.id_store + rig_id = self.generator.rig_id + + vis_layers = self.obj.data.layers + + # Ensure the collection of layer names exists + for i in range(1 + len(metarig.data.rigify_layers), 29): + metarig.data.rigify_layers.add() + + # Create list of layer name/row pairs + layer_layout = [] + for l in metarig.data.rigify_layers: + layer_layout += [(l.name, l.row)] + + # Generate the UI script + if id_store.rigify_generate_mode == 'overwrite': + rig_ui_name = id_store.rigify_rig_ui or 'rig_ui.py' + else: + rig_ui_name = 'rig_ui.py' + + if id_store.rigify_generate_mode == 'overwrite' and rig_ui_name in bpy.data.texts.keys(): + script = bpy.data.texts[rig_ui_name] + script.clear() + else: + script = bpy.data.texts.new("rig_ui.py") + + rig_ui_old_name = "" + if id_store.rigify_rig_basename: + rig_ui_old_name = script.name + script.name = id_store.rigify_rig_basename + "_rig_ui.py" + + id_store.rigify_rig_ui = script.name + + for s in OrderedDict.fromkeys(self.ui_imports): + script.write(s + "\n") + + script.write(UI_BASE_UTILITIES % rig_id) + + for s in OrderedDict.fromkeys(self.ui_utilities): + script.write(s + "\n") + + script.write(UI_SLIDERS) + + for s in self.ui_scripts: + script.write("\n " + s.replace("\n", "\n ") + "\n") + + if len(self.ui_scripts) > 0: + script.write("\n num_rig_separators[0] = 0\n") + + for panel in self.ui_rig_panels.values(): + lines = panel.get_lines() + if len(lines) > 1: + script.write("\n ".join([''] + lines) + "\n") + + if self.use_bake_settings: + self.ui_register = UI_REGISTER_BAKE_SETTINGS + self.ui_register + script.write(UI_BAKE_SETTINGS) + + script.write(layers_ui(vis_layers, layer_layout)) + + script.write("\ndef register():\n") + + ui_register = OrderedDict.fromkeys(self.ui_register) + for s in ui_register: + script.write(" bpy.utils.register_class("+s+")\n") + + ui_register_drivers = OrderedDict.fromkeys(self.ui_register_drivers) + for s in ui_register_drivers: + script.write(" bpy.app.driver_namespace['"+s+"'] = "+s+"\n") + + ui_register_props = OrderedDict.fromkeys(self.ui_register_props) + for s in ui_register_props: + script.write(" bpy.types.%s = %s\n " % (*s,)) + + script.write("\ndef unregister():\n") + + for s in ui_register_props: + script.write(" del bpy.types.%s\n" % s[0]) + + for s in ui_register: + script.write(" bpy.utils.unregister_class("+s+")\n") + + for s in ui_register_drivers: + script.write(" del bpy.app.driver_namespace['"+s+"']\n") + + script.write("\nregister()\n") + script.use_module = True + + # Run UI script + exec(script.as_string(), {}) + + # Attach the script to the rig + attach_persistent_script(self.obj, script) diff --git a/rigify/rigs/basic/copy_chain.py b/rigify/rigs/basic/copy_chain.py index 4e426284..5145d735 100644 --- a/rigify/rigs/basic/copy_chain.py +++ b/rigify/rigs/basic/copy_chain.py @@ -20,124 +20,96 @@ import bpy -from ...utils import MetarigError -from ...utils import copy_bone -from ...utils import connected_children_names -from ...utils import strip_org, make_deformer_name -from ...utils import create_bone_widget +from ..chain_rigs import SimpleChainRig +from ...utils.errors import MetarigError +from ...utils.rig import connected_children_names +from ...utils.naming import strip_org, make_deformer_name +from ...utils.widgets_basic import create_bone_widget -class Rig: +from ...base_rig import BaseRig, stage + + +class Rig(SimpleChainRig): """ A "copy_chain" rig. All it does is duplicate the original bone chain and constrain it. This is a control and deformation rig. - """ - def __init__(self, obj, bone_name, params): + def initialize(self): + super().initialize() + """ Gather and validate data about the rig. """ - self.obj = obj - self.org_bones = [bone_name] + connected_children_names(obj, bone_name) - self.params = params - self.make_controls = params.make_controls - self.make_deforms = params.make_deforms + self.make_controls = self.params.make_controls + self.make_deforms = self.params.make_deforms - if len(self.org_bones) <= 1: - raise MetarigError("RIGIFY ERROR: Bone '%s': input to rig type must be a chain of 2 or more bones" % (strip_org(bone_name))) + ############################## + # Control chain - def generate(self): - """ Generate the rig. - Do NOT modify any of the original bones, except for adding constraints. - The main armature should be selected and active before this is called. + @stage.generate_bones + def make_control_chain(self): + if self.make_controls: + super().make_control_chain() - """ - bpy.ops.object.mode_set(mode='EDIT') - - # Create the deformation and control bone chains. - # Just copies of the original chain. - def_chain = [] - ctrl_chain = [] - for i in range(len(self.org_bones)): - name = self.org_bones[i] - - # Control bone - if self.make_controls: - # Copy - ctrl_bone = copy_bone(self.obj, name) - eb = self.obj.data.edit_bones - ctrl_bone_e = eb[ctrl_bone] - # Name - ctrl_bone_e.name = strip_org(name) - # Parenting - if i == 0: - # First bone - ctrl_bone_e.parent = eb[self.org_bones[0]].parent - else: - # The rest - ctrl_bone_e.parent = eb[ctrl_chain[-1]] - # Add to list - ctrl_chain += [ctrl_bone_e.name] - else: - ctrl_chain += [None] - - # Deformation bone - if self.make_deforms: - # Copy - def_bone = copy_bone(self.obj, name) - eb = self.obj.data.edit_bones - def_bone_e = eb[def_bone] - # Name - def_bone_e.name = make_deformer_name(strip_org(name)) - # Parenting - if i == 0: - # First bone - def_bone_e.parent = eb[self.org_bones[0]].parent - else: - # The rest - def_bone_e.parent = eb[def_chain[-1]] - # Add to list - def_chain += [def_bone_e.name] - else: - def_chain += [None] - - bpy.ops.object.mode_set(mode='OBJECT') - pb = self.obj.pose.bones - - # Constraints for org and def - for org, ctrl, defrm in zip(self.org_bones, ctrl_chain, def_chain): - if self.make_controls: - con = pb[org].constraints.new('COPY_TRANSFORMS') - con.name = "copy_transforms" - con.target = self.obj - con.subtarget = ctrl - - if self.make_deforms: - con = pb[defrm].constraints.new('COPY_TRANSFORMS') - con.name = "copy_transforms" - con.target = self.obj - con.subtarget = org - - # Create control widgets + @stage.parent_bones + def parent_control_chain(self): if self.make_controls: - for bone in ctrl_chain: - create_bone_widget(self.obj, bone) + super().parent_control_chain() + @stage.configure_bones + def configure_control_chain(self): + if self.make_controls: + super().configure_control_chain() -def add_parameters(params): - """ Add the parameters of this rig type to the - RigifyParameters PropertyGroup - """ - params.make_controls = bpy.props.BoolProperty(name="Controls", default=True, description="Create control bones for the copy") - params.make_deforms = bpy.props.BoolProperty(name="Deform", default=True, description="Create deform bones for the copy") + @stage.generate_widgets + def make_control_widgets(self): + if self.make_controls: + super().make_control_widgets() + ############################## + # ORG chain -def parameters_ui(layout, params): - """ Create the ui for the rig parameters. - """ - r = layout.row() - r.prop(params, "make_controls") - r = layout.row() - r.prop(params, "make_deforms") + @stage.rig_bones + def rig_org_chain(self): + if self.make_controls: + super().rig_org_chain() + + ############################## + # Deform chain + + @stage.generate_bones + def make_deform_chain(self): + if self.make_deforms: + super().make_deform_chain() + + @stage.parent_bones + def parent_deform_chain(self): + if self.make_deforms: + super().parent_deform_chain() + + @stage.rig_bones + def rig_deform_chain(self): + if self.make_deforms: + super().rig_deform_chain() + + + @classmethod + def add_parameters(self, params): + """ Add the parameters of this rig type to the + RigifyParameters PropertyGroup + """ + params.make_controls = bpy.props.BoolProperty(name="Controls", default=True, description="Create control bones for the copy") + params.make_deforms = bpy.props.BoolProperty(name="Deform", default=True, description="Create deform bones for the copy") + + + @classmethod + def parameters_ui(self, layout, params): + """ Create the ui for the rig parameters. + """ + r = layout.row() + r.prop(params, "make_controls") + r = layout.row() + r.prop(params, "make_deforms") def create_sample(obj): diff --git a/rigify/rigs/basic/super_copy.py b/rigify/rigs/basic/super_copy.py index b2045346..5abbf22e 100644 --- a/rigify/rigs/basic/super_copy.py +++ b/rigify/rigs/basic/super_copy.py @@ -20,107 +20,112 @@ import bpy -from ...utils import copy_bone -from ...utils import strip_org, make_deformer_name -from ...utils import create_bone_widget, create_circle_widget +from ...base_rig import BaseRig +from ...utils.naming import strip_org, make_deformer_name +from ...utils.widgets_basic import create_bone_widget, create_circle_widget -class Rig: + +class Rig(BaseRig): """ A "copy" rig. All it does is duplicate the original bone and constrain it. This is a control and deformation rig. """ - def __init__(self, obj, bone, params): + def find_org_bones(self, pose_bone): + return pose_bone.name + + + def initialize(self): """ Gather and validate data about the rig. """ - self.obj = obj - self.org_bone = bone - self.org_name = strip_org(bone) - self.params = params - self.make_control = params.make_control - self.make_widget = params.make_widget - self.make_deform = params.make_deform - - def generate(self): - """ Generate the rig. - Do NOT modify any of the original bones, except for adding constraints. - The main armature should be selected and active before this is called. + self.org_name = strip_org(self.bones.org) - """ - bpy.ops.object.mode_set(mode='EDIT') + self.make_control = self.params.make_control + self.make_widget = self.params.make_widget + self.make_deform = self.params.make_deform + + + def generate_bones(self): + bones = self.bones # Make a control bone (copy of original). if self.make_control: - bone = copy_bone(self.obj, self.org_bone, self.org_name) + bones.ctrl = self.copy_bone(bones.org, self.org_name, parent=True) # Make a deformation bone (copy of original, child of original). if self.make_deform: - def_bone = copy_bone(self.obj, self.org_bone, make_deformer_name(self.org_name)) + bones.deform = self.copy_bone(bones.org, make_deformer_name(self.org_name), bbone=True) - # Get edit bones - eb = self.obj.data.edit_bones - # UNUSED - # if self.make_control: - # bone_e = eb[bone] - if self.make_deform: - def_bone_e = eb[def_bone] - # Parent + def parent_bones(self): + bones = self.bones + if self.make_deform: - def_bone_e.use_connect = False - def_bone_e.parent = eb[self.org_bone] + self.set_bone_parent(bones.deform, bones.org, use_connect=False) + - bpy.ops.object.mode_set(mode='OBJECT') - pb = self.obj.pose.bones + def configure_bones(self): + bones = self.bones + + if self.make_control: + self.copy_bone_properties(bones.org, bones.ctrl) + + + def rig_bones(self): + bones = self.bones if self.make_control: # Constrain the original bone. - con = pb[self.org_bone].constraints.new('COPY_TRANSFORMS') - con.name = "copy_transforms" - con.target = self.obj - con.subtarget = bone + self.make_constraint(bones.org, 'COPY_TRANSFORMS', bones.ctrl) + + def generate_widgets(self): + bones = self.bones + + if self.make_control: # Create control widget if self.make_widget: - create_circle_widget(self.obj, bone, radius=0.5) + create_circle_widget(self.obj, bones.ctrl, radius=0.5) else: - create_bone_widget(self.obj, bone) + create_bone_widget(self.obj, bones.ctrl) -def add_parameters(params): - """ Add the parameters of this rig type to the - RigifyParameters PropertyGroup - """ - params.make_control = bpy.props.BoolProperty( - name = "Control", - default = True, - description = "Create a control bone for the copy" - ) - - params.make_widget = bpy.props.BoolProperty( - name = "Widget", - default = True, - description = "Choose a widget for the bone control" - ) - - params.make_deform = bpy.props.BoolProperty( - name = "Deform", - default = True, - description = "Create a deform bone for the copy" - ) - - -def parameters_ui(layout, params): - """ Create the ui for the rig parameters. - """ - r = layout.row() - r.prop(params, "make_control") - r = layout.row() - r.prop(params, "make_widget") - r.enabled = params.make_control - r = layout.row() - r.prop(params, "make_deform") + @classmethod + def add_parameters(self, params): + """ Add the parameters of this rig type to the + RigifyParameters PropertyGroup + """ + params.make_control = bpy.props.BoolProperty( + name = "Control", + default = True, + description = "Create a control bone for the copy" + ) + + params.make_widget = bpy.props.BoolProperty( + name = "Widget", + default = True, + description = "Choose a widget for the bone control" + ) + + params.make_deform = bpy.props.BoolProperty( + name = "Deform", + default = True, + description = "Create a deform bone for the copy" + ) + + + @classmethod + def parameters_ui(self, layout, params): + """ Create the ui for the rig parameters. + """ + r = layout.row() + r.prop(params, "make_control") + r = layout.row() + r.prop(params, "make_widget") + r.enabled = params.make_control + r = layout.row() + r.prop(params, "make_deform") def create_sample(obj): @@ -159,3 +164,5 @@ def create_sample(obj): bone.select_head = True bone.select_tail = True arm.edit_bones.active = bone + + return bones diff --git a/rigify/rigs/chain_rigs.py b/rigify/rigs/chain_rigs.py new file mode 100644 index 00000000..3f53cd69 --- /dev/null +++ b/rigify/rigs/chain_rigs.py @@ -0,0 +1,387 @@ +#====================== BEGIN GPL LICENSE BLOCK ====================== +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#======================= END GPL LICENSE BLOCK ======================== + +# <pep8 compliant> + +import bpy +from itertools import count + +from ..utils.rig import connected_children_names +from ..utils.naming import strip_org, make_derived_name +from ..utils.bones import put_bone, flip_bone, flip_bone_chain, is_same_position, is_connected_position +from ..utils.bones import copy_bone_position, connect_bbone_chain_handles +from ..utils.widgets_basic import create_bone_widget, create_sphere_widget +from ..utils.misc import map_list + +from ..base_rig import BaseRig, stage + + +class SimpleChainRig(BaseRig): + """A rig that consists of 3 connected chains of control, org and deform bones.""" + def find_org_bones(self, bone): + return [bone.name] + connected_children_names(self.obj, bone.name) + + def initialize(self): + if len(self.bones.org) <= 1: + self.raise_error("Input to rig type must be a chain of 2 or more bones.") + + def parent_bones(self): + self.rig_parent_bone = self.get_bone_parent(self.bones.org[0]) + + bbone_segments = None + + ############################## + # BONES + # + # org[]: + # ORG bones + # ctrl: + # fk[]: + # FK control chain. + # deform[]: + # DEF bones + # + ############################## + + ############################## + # Control chain + + @stage.generate_bones + def make_control_chain(self): + self.bones.ctrl.fk = map_list(self.make_control_bone, count(0), self.bones.org) + + def make_control_bone(self, i, org): + return self.copy_bone(org, make_derived_name(org, 'ctrl'), parent=True) + + @stage.parent_bones + def parent_control_chain(self): + self.parent_bone_chain(self.bones.ctrl.fk, use_connect=True) + + @stage.configure_bones + def configure_control_chain(self): + for args in zip(count(0), self.bones.ctrl.fk, self.bones.org): + self.configure_control_bone(*args) + + def configure_control_bone(self, i, ctrl, org): + self.copy_bone_properties(org, ctrl) + + @stage.generate_widgets + def make_control_widgets(self): + for ctrl in self.bones.ctrl.fk: + self.make_control_widget(ctrl) + + def make_control_widget(self, ctrl): + create_bone_widget(self.obj, ctrl) + + ############################## + # ORG chain + + @stage.parent_bones + def parent_org_chain(self): + pass + + @stage.rig_bones + def rig_org_chain(self): + for args in zip(count(0), self.bones.org, self.bones.ctrl.fk): + self.rig_org_bone(*args) + + def rig_org_bone(self, i, org, ctrl): + self.make_constraint(org, 'COPY_TRANSFORMS', ctrl) + + ############################## + # Deform chain + + @stage.generate_bones + def make_deform_chain(self): + self.bones.deform = map_list(self.make_deform_bone, count(0), self.bones.org) + + def make_deform_bone(self, i, org): + name = self.copy_bone(org, make_derived_name(org, 'def'), parent=True, bbone=True) + if self.bbone_segments: + self.get_bone(name).bbone_segments = self.bbone_segments + return name + + @stage.parent_bones + def parent_deform_chain(self): + self.parent_bone_chain(self.bones.deform, use_connect=True) + + @stage.rig_bones + def rig_deform_chain(self): + for args in zip(count(0), self.bones.deform, self.bones.org): + self.rig_deform_bone(*args) + + def rig_deform_bone(self, i, deform, org): + self.make_constraint(deform, 'COPY_TRANSFORMS', org) + + +class TweakChainRig(SimpleChainRig): + """A rig that adds tweak controls to the triple chain.""" + + ############################## + # BONES + # + # org[]: + # ORG bones + # ctrl: + # fk[]: + # FK control chain. + # tweak[]: + # Tweak control chain. + # deform[]: + # DEF bones + # + ############################## + + ############################## + # Tweak chain + + @stage.generate_bones + def make_tweak_chain(self): + orgs = self.bones.org + self.bones.ctrl.tweak = map_list(self.make_tweak_bone, count(0), orgs + orgs[-1:]) + + def make_tweak_bone(self, i, org): + name = self.copy_bone(org, 'tweak_' + strip_org(org), parent=False, scale=0.5) + + if i == len(self.bones.org): + put_bone(self.obj, name, self.get_bone(org).tail) + + return name + + @stage.parent_bones + def parent_tweak_chain(self): + ctrl = self.bones.ctrl + for tweak, main in zip(ctrl.tweak, ctrl.fk + ctrl.fk[-1:]): + self.set_bone_parent(tweak, main) + + @stage.configure_bones + def configure_tweak_chain(self): + for args in zip(count(0), self.bones.ctrl.tweak): + self.configure_tweak_bone(*args) + + def configure_tweak_bone(self, i, tweak): + tweak_pb = self.get_bone(tweak) + tweak_pb.rotation_mode = 'ZXY' + + if i == len(self.bones.org): + tweak_pb.lock_rotation_w = True + tweak_pb.lock_rotation = (True, True, True) + tweak_pb.lock_scale = (True, True, True) + else: + tweak_pb.lock_rotation_w = False + tweak_pb.lock_rotation = (True, False, True) + tweak_pb.lock_scale = (False, True, False) + + @stage.generate_widgets + def make_tweak_widgets(self): + for tweak in self.bones.ctrl.tweak: + self.make_tweak_widget(tweak) + + def make_tweak_widget(self, tweak): + create_sphere_widget(self.obj, tweak) + + ############################## + # ORG chain + + @stage.rig_bones + def rig_org_chain(self): + tweaks = self.bones.ctrl.tweak + for args in zip(count(0), self.bones.org, tweaks, tweaks[1:]): + self.rig_org_bone(*args) + + def rig_org_bone(self, i, org, tweak, next_tweak): + self.make_constraint(org, 'COPY_TRANSFORMS', tweak) + if next_tweak: + self.make_constraint(org, 'DAMPED_TRACK', next_tweak) + self.make_constraint(org, 'STRETCH_TO', next_tweak) + + +class ConnectingChainRig(TweakChainRig): + """Chain rig that can attach to an end of the parent, merging bbone chains.""" + + bbone_segments = 8 + use_connect_reverse = None + + def initialize(self): + super().initialize() + + self.use_connect_chain = self.params.connect_chain + self.connected_tweak = None + + if self.use_connect_chain: + first_org = self.bones.org[0] + parent = self.rigify_parent + parent_orgs = parent.bones.org + + if not isinstance(parent, SimpleChainRig): + self.raise_error("Cannot connect to non-chain parent rig.") + + ok_reverse = is_same_position(self.obj, parent_orgs[0], first_org) + ok_direct = is_connected_position(self.obj, parent_orgs[-1], first_org) + + if self.use_connect_reverse is None: + self.use_connect_reverse = ok_reverse and not ok_direct + + if not (ok_reverse if self.use_connect_reverse else ok_direct): + self.raise_error("Cannot connect chain - bone position is disjoint.") + + if isinstance(parent, ConnectingChainRig) and parent.use_connect_reverse: + self.raise_error("Cannot connect chain - parent is reversed.") + + def prepare_bones(self): + # Exactly match bone position to parent + if self.use_connect_chain: + first_bone = self.get_bone(self.bones.org[0]) + parent_orgs = self.rigify_parent.bones.org + + if self.use_connect_reverse: + first_bone.head = self.get_bone(parent_orgs[0]).head + else: + first_bone.head = self.get_bone(parent_orgs[-1]).tail + + def parent_bones(self): + # Use the parent of the shared tweak as the rig parent + root = self.connected_tweak or self.bones.org[0] + + self.rig_parent_bone = self.get_bone_parent(root) + + ############################## + # Control chain + + @stage.parent_bones + def parent_control_chain(self): + super().parent_control_chain() + + self.set_bone_parent(self.bones.ctrl.fk[0], self.rig_parent_bone) + + ############################## + # Tweak chain + + def check_connect_tweak(self, org): + """ Check if it is possible to share the last parent tweak control. """ + + assert self.connected_tweak is None + + if self.use_connect_chain and isinstance(self.rigify_parent, TweakChainRig): + # Share the last tweak bone of the parent rig + parent_tweaks = self.rigify_parent.bones.ctrl.tweak + index = 0 if self.use_connect_reverse else -1 + name = parent_tweaks[index] + + if not is_same_position(self.obj, name, org): + self.raise_error("Cannot connect tweaks - position mismatch.") + + if not self.use_connect_reverse: + copy_bone_position(self.obj, org, name, scale=0.5) + + name = self.rename_bone(name, 'tweak_' + strip_org(org)) + + self.connected_tweak = parent_tweaks[index] = name + + return name + else: + return None + + def make_tweak_bone(self, i, org): + if i == 0 and self.check_connect_tweak(org): + return self.connected_tweak + else: + return super().make_tweak_bone(i, org) + + @stage.parent_bones + def parent_tweak_chain(self): + ctrl = self.bones.ctrl + for i, tweak, main in zip(count(0), ctrl.tweak, ctrl.fk + ctrl.fk[-1:]): + if i > 0 or not (self.connected_tweak and self.use_connect_reverse): + self.set_bone_parent(tweak, main) + + def configure_tweak_bone(self, i, tweak): + super().configure_tweak_bone(i, tweak) + + if self.use_connect_chain and self.use_connect_reverse and i == len(self.bones.org): + tweak_pb = self.get_bone(tweak) + tweak_pb.lock_rotation_w = False + tweak_pb.lock_rotation = (True, False, True) + tweak_pb.lock_scale = (False, True, False) + + ############################## + # ORG chain + + @stage.parent_bones + def parent_org_chain(self): + if self.use_connect_chain and self.use_connect_reverse: + flip_bone_chain(self.obj, self.bones.org) + + for org, tweak in zip(self.bones.org, self.bones.ctrl.tweak[1:]): + self.set_bone_parent(org, tweak) + + else: + self.set_bone_parent(self.bones.org[0], self.rig_parent_bone) + + def rig_org_bone(self, i, org, tweak, next_tweak): + if self.use_connect_chain and self.use_connect_reverse: + self.make_constraint(org, 'DAMPED_TRACK', tweak) + self.make_constraint(org, 'STRETCH_TO', tweak) + else: + super().rig_org_bone(i, org, tweak, next_tweak) + + ############################## + # Deform chain + + def make_deform_bone(self, i, org): + name = super().make_deform_bone(i, org) + + if self.use_connect_chain and self.use_connect_reverse: + self.set_bone_parent(name, None) + flip_bone(self.obj, name) + + return name + + @stage.parent_bones + def parent_deform_chain(self): + if self.use_connect_chain: + deform = self.bones.deform + parent_deform = self.rigify_parent.bones.deform + + if self.use_connect_reverse: + self.set_bone_parent(deform[-1], self.bones.org[-1]) + self.parent_bone_chain(reversed(deform), use_connect=True) + + connect_bbone_chain_handles(self.obj, [ deform[0], parent_deform[0] ]) + return + + else: + self.set_bone_parent(deform[0], parent_deform[-1], use_connect=True) + + super().parent_deform_chain() + + ############################## + # Settings + + @classmethod + def add_parameters(self, params): + params.connect_chain = bpy.props.BoolProperty( + name='Connect chain', + default=False, + description='Connect the B-Bone chain to the parent rig' + ) + + @classmethod + def parameters_ui(self, layout, params): + r = layout.row() + r.prop(params, "connect_chain") diff --git a/rigify/rigs/experimental/super_chain.py b/rigify/rigs/experimental/super_chain.py index f3d0e182..592441fe 100644 --- a/rigify/rigs/experimental/super_chain.py +++ b/rigify/rigs/experimental/super_chain.py @@ -14,7 +14,7 @@ class Rig: def __init__(self, obj, bone_name, params): """ Chain with pivot Rig """ - eb = obj.data.edit_bones + eb = obj.data.bones self.obj = obj self.org_bones = [bone_name] + connected_children_names(obj, bone_name) diff --git a/rigify/rigs/faces/super_face.py b/rigify/rigs/faces/super_face.py index 7e36ce68..4eda647d 100644 --- a/rigify/rigs/faces/super_face.py +++ b/rigify/rigs/faces/super_face.py @@ -44,7 +44,7 @@ class Rig: grand_children += connected_children_names( self.obj, child ) self.org_bones = [bone_name] + children + grand_children - self.face_length = obj.data.edit_bones[ self.org_bones[0] ].length + self.face_length = obj.data.bones[ self.org_bones[0] ].length self.params = params if params.primary_layers_extra: diff --git a/rigify/rigs/limbs/arm.py b/rigify/rigs/limbs/arm.py index 3b2f3658..aacc1e86 100644 --- a/rigify/rigs/limbs/arm.py +++ b/rigify/rigs/limbs/arm.py @@ -15,17 +15,8 @@ from ...utils.mechanism import make_property, make_driver from ..widgets import create_ikarrow_widget from math import trunc, pi -extra_script = """ -controls = [%s] -ctrl = '%s' - -if is_selected( controls ): - layout.prop( pose_bones[ ctrl ], '["%s"]') - if '%s' in pose_bones[ctrl].keys(): - layout.prop( pose_bones[ ctrl ], '["%s"]', slider = True ) - if '%s' in pose_bones[ctrl].keys(): - layout.prop( pose_bones[ ctrl ], '["%s"]', slider = True ) -""" +from ...utils.switch_parent import SwitchParentBuilder + IMPLEMENTATION = True # Include and set True if Rig is just an implementation for a wrapper class # add_parameters and parameters_ui are unused for implementation classes @@ -561,35 +552,6 @@ class Rig: eb[ bones['ik']['mch_target'] ].parent = eb[ ctrl ] eb[ bones['ik']['mch_target'] ].use_connect = False - # MCH for ik control - ctrl_socket = copy_bone(self.obj, org_bones[2], get_bone_name( org_bones[2], 'mch', 'ik_socket')) - eb[ctrl_socket].tail = eb[ctrl_socket].head + 0.8*(eb[ctrl_socket].tail-eb[ctrl_socket].head) - eb[ctrl_socket].parent = None - eb[ctrl].parent = eb[ctrl_socket] - - # MCH for pole ik control - ctrl_pole_socket = copy_bone(self.obj, org_bones[2], get_bone_name(org_bones[2], 'mch', 'pole_ik_socket')) - eb[ctrl_pole_socket].tail = eb[ctrl_pole_socket].head + 0.8 * (eb[ctrl_pole_socket].tail - eb[ctrl_pole_socket].head) - eb[ctrl_pole_socket].parent = None - eb[pole_target].parent = eb[ctrl_pole_socket] - - ctrl_root = copy_bone(self.obj, org_bones[2], get_bone_name( org_bones[2], 'mch', 'ik_root')) - eb[ctrl_root].tail = eb[ctrl_root].head + 0.7*(eb[ctrl_root].tail-eb[ctrl_root].head) - eb[ctrl_root].use_connect = False - eb[ctrl_root].parent = eb['root'] - - if eb[org_bones[0]].parent: - arm_parent = eb[org_bones[0]].parent - ctrl_parent = copy_bone(self.obj, org_bones[2], get_bone_name( org_bones[2], 'mch', 'ik_parent')) - eb[ctrl_parent].tail = eb[ctrl_parent].head + 0.6*(eb[ctrl_parent].tail-eb[ctrl_parent].head) - eb[ctrl_parent].use_connect = False - if eb[org_bones[0]].parent_recursive: - eb[ctrl_parent].parent = eb[org_bones[0]].parent_recursive[-1] - else: - eb[ctrl_parent].parent = eb[org_bones[0]].parent - else: - arm_parent = None - mch_name = get_bone_name(strip_org(org_bones[0]), 'mch', 'parent_socket') mch_main_parent = copy_bone(self.obj, org_bones[0], mch_name) eb[mch_main_parent].length = eb[org_bones[0]].length / 12 @@ -597,30 +559,28 @@ class Rig: eb[mch_main_parent].roll = 0.0 eb[bones['main_parent']].parent = eb[mch_main_parent] - # Set up constraints + # Switchable parent + pbuilder = SwitchParentBuilder(self.rigify_generator) - # Constrain ik ctrl to root / parent + if eb[org_bones[0]].parent: + pbuilder.register_parent(self.rigify_wrapper, eb[org_bones[0]].parent.name) - make_constraint( self, ctrl_socket, { - 'constraint' : 'COPY_TRANSFORMS', - 'subtarget' : ctrl_root, - }) + pbuilder.register_parent(self.rigify_wrapper, org_bones[2], exclude_self=True) - make_constraint(self, ctrl_pole_socket, { - 'constraint': 'COPY_TRANSFORMS', - 'subtarget': ctrl_root, - }) + pcontrols = [ bones['main_parent'], bones['ik']['ctrl']['limb'], ctrl, pole_target ] - if arm_parent: - make_constraint( self, ctrl_socket, { - 'constraint' : 'COPY_TRANSFORMS', - 'subtarget' : ctrl_parent, - }) + pbuilder.build_child( + self.rigify_wrapper, ctrl, + prop_bone=bones['main_parent'], prop_id='IK_parent', prop_name='IK Parent', controls=pcontrols, + ) - make_constraint(self, ctrl_pole_socket, { - 'constraint': 'COPY_TRANSFORMS', - 'subtarget': ctrl_parent, - }) + pbuilder.build_child( + self.rigify_wrapper, pole_target, extra_parents=[ctrl], + prop_bone=bones['main_parent'], prop_id='pole_parent', prop_name='Pole Parent', controls=pcontrols, + no_fix_rotation=True, no_fix_scale=True + ) + + # Set up constraints # Constrain mch target bone to the ik control and mch stretch make_constraint( self, bones['ik']['mch_target'], { @@ -675,10 +635,6 @@ class Rig: create_hand_widget(self.obj, ctrl, bone_transform_name=None) bones['ik']['ctrl']['terminal'] = [ctrl] - if arm_parent: - bones['ik']['mch_hand'] = [ctrl_socket, ctrl_pole_socket, ctrl_root, ctrl_parent] - else: - bones['ik']['mch_hand'] = [ctrl_socket, ctrl_pole_socket, ctrl_root] return bones @@ -687,13 +643,10 @@ class Rig: bpy.ops.object.mode_set(mode='OBJECT') pb = self.obj.pose.bones - ctrl = pb[bones['ik']['mch_hand'][0]] - ctrl_pole = pb[bones['ik']['mch_hand'][1]] - #owner = pb[bones['ik']['ctrl']['limb']] owner = pb[bones['main_parent']] - props = ["IK_follow", "root/parent", "pole_vector", "pole_follow"] + props = ["pole_vector"] for prop in props: @@ -723,30 +676,6 @@ class Rig: else: make_driver(cns, "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - elif prop == 'IK_follow': - make_property(owner, prop, True) - - make_driver(ctrl.constraints[0], "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - - if len(ctrl.constraints) > 1: - make_driver(ctrl.constraints[1], "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - - make_driver(ctrl_pole.constraints[0], "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - - if len(ctrl_pole.constraints) > 1: - make_driver(ctrl_pole.constraints[1], "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - - elif prop == 'root/parent': - if len(ctrl.constraints) > 1: - make_property(owner, prop, 0.0) - - make_driver(ctrl.constraints[1], "influence", variables=[(self.obj, owner, prop)]) - - elif prop == 'pole_follow': - if len(ctrl_pole.constraints) > 1: - make_property(owner, prop, 0.0) - - make_driver(ctrl_pole.constraints[1], "influence", variables=[(self.obj, owner, prop)]) @staticmethod def get_future_names(bones): @@ -822,22 +751,13 @@ class Rig: bones = self.create_arm(bones) self.create_drivers(bones) - controls = [bones['ik']['ctrl']['limb'], bones['ik']['ctrl']['terminal'][0]] - - controls.append(bones['main_parent']) - # Create UI - controls_string = ", ".join(["'" + x + "'" for x in controls]) - script = create_script(bones, 'arm') - script += extra_script % (controls_string, bones['main_parent'], 'IK_follow', - 'pole_follow', 'pole_follow', 'root/parent', 'root/parent') return { 'script': [script], 'utilities': UTILITIES_RIG_ARM, 'register': REGISTER_RIG_ARM, - 'noparent_bones': [bones['ik']['mch_hand'][i] for i in [0,1]], } @@ -858,7 +778,7 @@ def add_parameters(params): default = 'automatic' ) - params.auto_align_extremity = bpy.BoolProperty( + params.auto_align_extremity = bpy.props.BoolProperty( name='auto_align_extremity', default=False, description="Auto Align Extremity Bone" diff --git a/rigify/rigs/limbs/leg.py b/rigify/rigs/limbs/leg.py index 2b846eca..59e6f799 100644 --- a/rigify/rigs/limbs/leg.py +++ b/rigify/rigs/limbs/leg.py @@ -17,17 +17,8 @@ from ...utils.mechanism import make_property, make_driver from ..widgets import create_ikarrow_widget from math import trunc, pi -extra_script = """ -controls = [%s] -ctrl = '%s' - -if is_selected( controls ): - layout.prop( pose_bones[ ctrl ], '["%s"]') - if '%s' in pose_bones[ctrl].keys(): - layout.prop( pose_bones[ ctrl ], '["%s"]', slider = True ) - if '%s' in pose_bones[ctrl].keys(): - layout.prop( pose_bones[ ctrl ], '["%s"]', slider = True ) -""" +from ...utils.switch_parent import SwitchParentBuilder + IMPLEMENTATION = True # Include and set True if Rig is just an implementation for a wrapper class # add_parameters and parameters_ui are unused for implementation classes @@ -599,35 +590,6 @@ class Rig: eb[ctrl].parent = None eb[ctrl].use_connect = False - # MCH for ik control - ctrl_socket = copy_bone(self.obj, org_bones[2], get_bone_name( org_bones[2], 'mch', 'ik_socket')) - eb[ctrl_socket].tail = eb[ctrl_socket].head + 0.8*(eb[ctrl_socket].tail-eb[ctrl_socket].head) - eb[ctrl_socket].parent = None - eb[ctrl].parent = eb[ctrl_socket] - - # MCH for pole ik control - ctrl_pole_socket = copy_bone(self.obj, org_bones[2], get_bone_name(org_bones[2], 'mch', 'pole_ik_socket')) - eb[ctrl_pole_socket].tail = eb[ctrl_pole_socket].head + 0.8 * (eb[ctrl_pole_socket].tail - eb[ctrl_pole_socket].head) - eb[ctrl_pole_socket].parent = None - eb[pole_target].parent = eb[ctrl_pole_socket] - - ctrl_root = copy_bone(self.obj, org_bones[2], get_bone_name( org_bones[2], 'mch', 'ik_root')) - eb[ctrl_root].tail = eb[ctrl_root].head + 0.7*(eb[ctrl_root].tail-eb[ctrl_root].head) - eb[ctrl_root].use_connect = False - eb[ctrl_root].parent = eb['root'] - - if eb[org_bones[0]].parent: - leg_parent = eb[org_bones[0]].parent - ctrl_parent = copy_bone(self.obj, org_bones[2], get_bone_name( org_bones[2], 'mch', 'ik_parent')) - eb[ctrl_parent].tail = eb[ctrl_parent].head + 0.6*(eb[ctrl_parent].tail-eb[ctrl_parent].head) - eb[ctrl_parent].use_connect = False - if eb[org_bones[0]].parent_recursive: - eb[ctrl_parent].parent = eb[org_bones[0]].parent_recursive[-1] - else: - eb[ctrl_parent].parent = eb[org_bones[0]].parent - else: - leg_parent = None - mch_name = get_bone_name(strip_org(org_bones[0]), 'mch', 'parent_socket') mch_main_parent = copy_bone(self.obj, org_bones[0], mch_name) eb[mch_main_parent].length = eb[org_bones[0]].length / 12 @@ -658,6 +620,26 @@ class Rig: eb[ctrl].tail[2] = eb[ctrl].head[2] eb[ctrl].roll = 0 + # Switchable parent + pbuilder = SwitchParentBuilder(self.rigify_generator) + + if eb[org_bones[0]].parent: + pbuilder.register_parent(self.rigify_wrapper, eb[org_bones[0]].parent.name) + + pbuilder.register_parent(self.rigify_wrapper, org_bones[2], exclude_self=True) + + pcontrols = [ bones['main_parent'], bones['ik']['ctrl']['limb'], heel, ctrl, pole_target ] + + pbuilder.build_child( + self.rigify_wrapper, ctrl, + prop_bone=bones['main_parent'], prop_id='IK_parent', prop_name='IK Parent', controls=pcontrols, + ) + + pbuilder.build_child( + self.rigify_wrapper, pole_target, extra_parents=[(bones['ik']['mch_target'], ctrl)], + prop_bone=bones['main_parent'], prop_id='pole_parent', prop_name='Pole Parent', controls=pcontrols, + no_fix_rotation=True, no_fix_scale=True + ) # Parent eb[ heel ].use_connect = False @@ -847,30 +829,6 @@ class Rig: # Set up constraints - # Constrain ik ctrl to root / parent - - make_constraint( self, ctrl_socket, { - 'constraint' : 'COPY_TRANSFORMS', - 'subtarget' : ctrl_root, - }) - - make_constraint(self, ctrl_pole_socket, { - 'constraint': 'COPY_TRANSFORMS', - 'subtarget': ctrl_root, - }) - - if leg_parent: - make_constraint( self, ctrl_socket, { - 'constraint' : 'COPY_TRANSFORMS', - 'subtarget' : ctrl_parent, - 'influence' : 0.0, - }) - - make_constraint(self, ctrl_pole_socket, { - 'constraint': 'COPY_TRANSFORMS', - 'subtarget': bones['ik']['mch_target'], - }) - # Constrain mch target bone to the ik control and mch stretch make_constraint( self, bones['ik']['mch_target'], { 'constraint' : 'COPY_LOCATION', @@ -982,11 +940,6 @@ class Rig: bones['ik']['ctrl']['terminal'] += [ heel, ctrl ] - if leg_parent: - bones['ik']['mch_foot'] = [ctrl_socket, ctrl_pole_socket, ctrl_root, ctrl_parent] - else: - bones['ik']['mch_foot'] = [ctrl_socket, ctrl_pole_socket, ctrl_root] - return bones def create_drivers(self, bones): @@ -994,13 +947,10 @@ class Rig: bpy.ops.object.mode_set(mode='OBJECT') pb = self.obj.pose.bones - ctrl = pb[bones['ik']['mch_foot'][0]] - ctrl_pole = pb[bones['ik']['mch_foot'][1]] - #owner = pb[bones['ik']['ctrl']['limb']] owner = pb[bones['main_parent']] - props = ["IK_follow", "root/parent", "pole_vector", "pole_follow"] + props = ["pole_vector"] for prop in props: @@ -1031,31 +981,6 @@ class Rig: make_driver(cns, "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - elif prop == 'IK_follow': - make_property(owner, prop, True) - - make_driver(ctrl.constraints[0], "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - - if len(ctrl.constraints) > 1: - make_driver(ctrl.constraints[1], "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - - make_driver(ctrl_pole.constraints[0], "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - - if len(ctrl_pole.constraints) > 1: - make_driver(ctrl_pole.constraints[1], "mute", variables=[(self.obj, owner, prop)], polynomial=[1.0, -1.0]) - - elif prop == 'root/parent': - if len(ctrl.constraints) > 1: - make_property(owner, prop, 0.0) - - make_driver(ctrl.constraints[1], "influence", variables=[(self.obj, owner, prop)]) - - elif prop == 'pole_follow': - if len(ctrl_pole.constraints) > 1: - make_property(owner, prop, 0.0) - - make_driver(ctrl_pole.constraints[1], "influence", variables=[(self.obj, owner, prop)]) - @staticmethod def get_future_names(bones): @@ -1133,22 +1058,13 @@ class Rig: bones = self.create_leg(bones) self.create_drivers(bones) - controls = [bones['ik']['ctrl']['limb'], bones['ik']['ctrl']['terminal'][-1], bones['ik']['ctrl']['terminal'][-2] ] - - controls.append(bones['main_parent']) - # Create UI - controls_string = ", ".join(["'" + x + "'" for x in controls]) - script = create_script(bones, 'leg') - script += extra_script % (controls_string, bones['main_parent'], 'IK_follow', - 'pole_follow', 'pole_follow', 'root/parent', 'root/parent') return { 'script': [script], 'utilities': UTILITIES_RIG_LEG, 'register': REGISTER_RIG_LEG, - 'noparent_bones': [bones['ik']['mch_foot'][i] for i in [0,1]], } diff --git a/rigify/rigs/limbs/super_limb.py b/rigify/rigs/limbs/super_limb.py index 3d2bb8e2..0d557bb7 100644 --- a/rigify/rigs/limbs/super_limb.py +++ b/rigify/rigs/limbs/super_limb.py @@ -21,6 +21,8 @@ class Rig: self.limb = pawRig(obj, bone_name, params) def generate(self): + self.limb.rigify_generator = self.rigify_generator + self.limb.rigify_wrapper = self.rigify_wrapper return self.limb.generate() diff --git a/rigify/rigs/spines/super_spine.py b/rigify/rigs/spines/super_spine.py index 5afe15b0..ebc76fcb 100644 --- a/rigify/rigs/spines/super_spine.py +++ b/rigify/rigs/spines/super_spine.py @@ -8,6 +8,8 @@ from ...utils import MetarigError, make_mechanism_name, create_cube_widget from ...utils import ControlLayersOption from ...utils.mechanism import make_property, make_driver +from ...utils.switch_parent import SwitchParentBuilder + script = """ controls = [%s] torso = '%s' @@ -951,6 +953,14 @@ class Rig: bones['chest'] = self.create_chest(upper_torso_bones) bones['hips'] = self.create_hips(lower_torso_bones) + # Register viable parent bones + pbuilder = SwitchParentBuilder(self.rigify_generator) + pbuilder.register_parent(self.rigify_wrapper, bones['pivot']['ctrl'], name='Torso') + pbuilder.register_parent(self.rigify_wrapper, bone_chains['lower'][0], name='Hips') + pbuilder.register_parent(self.rigify_wrapper, bone_chains['upper'][-1], name='Chest') + if self.use_head: + pbuilder.register_parent(self.rigify_wrapper, bone_chains['neck'][-1], name='Head') + # TODO: Add create tail if tail_bones: bones['tail'] = self.create_tail(tail_bones) diff --git a/rigify/ui.py b/rigify/ui.py index 933aec71..c0b827b1 100644 --- a/rigify/ui.py +++ b/rigify/ui.py @@ -32,9 +32,15 @@ from .utils import MetarigError from .utils import write_metarig, write_widget from .utils import unique_name from .utils import upgradeMetarigTypes, outdated_types -from .utils import get_keyed_frames, bones_in_frame -from .utils import overwrite_prop_animation from .rigs.utils import get_limb_generated_names + +from .utils.animation import get_keyed_frames_in_range, bones_in_frame, overwrite_prop_animation +from .utils.animation import RIGIFY_OT_get_frame_range + +from .utils.animation import register as animation_register +from .utils.animation import unregister as animation_unregister + +from . import base_rig from . import rig_lists from . import generate from . import rot_mode @@ -613,6 +619,8 @@ class BONE_PT_rigify_buttons(bpy.types.Panel): box = row.box() box.label(text="ALERT: type \"%s\" does not exist!" % rig_name) else: + if hasattr(rig.Rig, 'parameters_ui'): + rig = rig.Rig try: rig.parameters_ui except AttributeError: @@ -699,20 +707,19 @@ class VIEW3D_PT_rigify_animation_tools(bpy.types.Panel): op.value = False op.toggle = False op.bake = True - row = self.layout.row(align=True) - row.prop(id_store, 'rigify_transfer_start_frame') - row.prop(id_store, 'rigify_transfer_end_frame') - row.operator("rigify.get_frame_range", icon='TIME', text='') + RIGIFY_OT_get_frame_range.draw_range_ui(context, self.layout) def rigify_report_exception(operator, exception): import traceback import sys import os - # find the module name where the error happened + # find the non-utils module name where the error happened # hint, this is the metarig type! exceptionType, exceptionValue, exceptionTraceback = sys.exc_info() - fn = traceback.extract_tb(exceptionTraceback)[-1][0] + fns = [ item.filename for item in traceback.extract_tb(exceptionTraceback) ] + fns_rig = [ fn for fn in fns if os.path.basename(os.path.dirname(fn)) != 'utils' ] + fn = fns_rig[-1] fn = os.path.basename(fn) fn = os.path.splitext(fn)[0] message = [] @@ -759,6 +766,9 @@ class Generate(bpy.types.Operator): try: generate.generate_rig(context, context.object) except MetarigError as rig_exception: + import traceback + traceback.print_exc() + rigify_report_exception(self, rig_exception) return {'FINISHED'} @@ -905,21 +915,6 @@ class EncodeWidget(bpy.types.Operator): return {'FINISHED'} -class OBJECT_OT_GetFrameRange(bpy.types.Operator): - """Get start and end frame range""" - bl_idname = "rigify.get_frame_range" - bl_label = "Get Frame Range" - - def execute(self, context): - scn = context.scene - id_store = context.window_manager - - id_store.rigify_transfer_start_frame = scn.frame_start - id_store.rigify_transfer_end_frame = scn.frame_end - - return {'FINISHED'} - - def FktoIk(rig, window='ALL'): scn = bpy.context.scene @@ -931,8 +926,7 @@ def FktoIk(rig, window='ALL'): limb_generated_names = get_limb_generated_names(rig) if window == 'ALL': - frames = get_keyed_frames(rig) - frames = [f for f in frames if f in range(id_store.rigify_transfer_start_frame, id_store.rigify_transfer_end_frame+1)] + frames = get_keyed_frames_in_range(bpy.context, rig) elif window == 'CURRENT': frames = [scn.frame_current] else: @@ -1009,8 +1003,7 @@ def IktoFk(rig, window='ALL'): limb_generated_names = get_limb_generated_names(rig) if window == 'ALL': - frames = get_keyed_frames(rig) - frames = [f for f in frames if f in range(id_store.rigify_transfer_start_frame, id_store.rigify_transfer_end_frame+1)] + frames = get_keyed_frames_in_range(bpy.context, rig) elif window == 'CURRENT': frames = [scn.frame_current] else: @@ -1122,8 +1115,7 @@ def rotPoleToggle(rig, window='ALL', value=False, toggle=False, bake=False): limb_generated_names = get_limb_generated_names(rig) if window == 'ALL': - frames = get_keyed_frames(rig) - frames = [f for f in frames if f in range(id_store.rigify_transfer_start_frame, id_store.rigify_transfer_end_frame+1)] + frames = get_keyed_frames_in_range(bpy.context, rig) elif window == 'CURRENT': frames = [scn.frame_current] else: @@ -1340,7 +1332,6 @@ classes = ( EncodeMetarig, EncodeMetarigSample, EncodeWidget, - OBJECT_OT_GetFrameRange, OBJECT_OT_FK2IK, OBJECT_OT_IK2FK, OBJECT_OT_TransferFKtoIK, @@ -1353,6 +1344,8 @@ classes = ( def register(): from bpy.utils import register_class + animation_register() + # Classes. for cls in classes: register_class(cls) @@ -1370,3 +1363,5 @@ def unregister(): # Classes. for cls in classes: unregister_class(cls) + + animation_unregister() diff --git a/rigify/utils/__init__.py b/rigify/utils/__init__.py index 9fa6a3d2..f45acded 100644 --- a/rigify/utils/__init__.py +++ b/rigify/utils/__init__.py @@ -11,7 +11,7 @@ from .naming import strip_trailing_number, unique_name, org_name, strip_org, str from .naming import org, make_original_name, mch, make_mechanism_name, deformer, make_deformer_name from .naming import insert_before_lr, random_id -from .bones import new_bone, copy_bone_simple, copy_bone, flip_bone, put_bone, make_nonscaling_child +from .bones import new_bone, flip_bone, put_bone from .bones import align_bone_roll, align_bone_x_axis, align_bone_z_axis, align_bone_y_axis from .widgets import WGT_PREFIX, obj_to_bone, create_widget, write_widget, create_circle_polygon @@ -22,10 +22,13 @@ from .widgets_basic import create_sphere_widget, create_limb_widget, create_bone from .widgets_special import create_compass_widget, create_root_widget from .widgets_special import create_neck_bend_widget, create_neck_tweak_widget -from .animation import get_keyed_frames, bones_in_frame, overwrite_prop_animation - from .rig import RIG_DIR, METARIG_DIR, TEMPLATE_DIR, outdated_types, upgradeMetarigTypes from .rig import write_metarig, get_resource from .rig import connected_children_names, has_connected_children from .layers import get_layers, ControlLayersOption + +# Definitions so bad as to make them strictly compatibility only +from .bones import copy_bone as copy_bone_simple +from .bones import _legacy_copy_bone as copy_bone +from .bones import _legacy_make_nonscaling_child as make_nonscaling_child diff --git a/rigify/utils/animation.py b/rigify/utils/animation.py index ab99282f..62042923 100644 --- a/rigify/utils/animation.py +++ b/rigify/utils/animation.py @@ -18,25 +18,28 @@ # <pep8 compliant> +import bpy + +import math +import json + +from mathutils import Matrix, Vector + +rig_id = None #============================================= # Keyframing functions #============================================= -def get_keyed_frames(rig): - frames = [] - if rig.animation_data: - if rig.animation_data.action: - fcus = rig.animation_data.action.fcurves - for fc in fcus: - for kp in fc.keyframe_points: - if kp.co[0] not in frames: - frames.append(kp.co[0]) +def get_keyed_frames_in_range(context, rig): + action = find_action(rig) + if action: + frame_range = RIGIFY_OT_get_frame_range.get_range(context) - frames.sort() - - return frames + return sorted(get_curve_frame_set(action.fcurves, frame_range)) + else: + return [] def bones_in_frame(f, rig, *args): @@ -82,3 +85,794 @@ def overwrite_prop_animation(rig, bone, prop_name, value, frames): for kp in curve.keyframe_points: if kp.co[0] in frames: kp.co[1] = value + +################################################################ +# Utilities for inserting keyframes and/or setting transforms ## +################################################################ + +SCRIPT_UTILITIES_KEYING = [''' +###################### +## Keyframing tools ## +###################### + +def get_keying_flags(context): + "Retrieve the general keyframing flags from user preferences." + prefs = context.preferences + ts = context.scene.tool_settings + flags = set() + # Not adding INSERTKEY_VISUAL + if prefs.edit.use_keyframe_insert_needed: + flags.add('INSERTKEY_NEEDED') + if prefs.edit.use_insertkey_xyz_to_rgb: + flags.add('INSERTKEY_XYZ_TO_RGB') + if ts.use_keyframe_cycle_aware: + flags.add('INSERTKEY_CYCLE_AWARE') + return flags + +def get_autokey_flags(context, ignore_keyset=False): + "Retrieve the Auto Keyframe flags, or None if disabled." + ts = context.scene.tool_settings + if ts.use_keyframe_insert_auto and (ignore_keyset or not ts.use_keyframe_insert_keyingset): + flags = get_keying_flags(context) + if context.preferences.edit.use_keyframe_insert_available: + flags.add('INSERTKEY_AVAILABLE') + if ts.auto_keying_mode == 'REPLACE_KEYS': + flags.add('INSERTKEY_REPLACE') + return flags + else: + return None + +def add_flags_if_set(base, new_flags): + "Add more flags if base is not None." + if base is None: + return None + else: + return base | new_flags + +def get_4d_rotlock(bone): + "Retrieve the lock status for 4D rotation." + if bone.lock_rotations_4d: + return [bone.lock_rotation_w, *bone.lock_rotation] + else: + return [all(bone.lock_rotation)] * 4 + +def keyframe_transform_properties(obj, bone_name, keyflags, *, ignore_locks=False, no_loc=False, no_rot=False, no_scale=False): + "Keyframe transformation properties, taking flags and mode into account, and avoiding keying locked channels." + bone = obj.pose.bones[bone_name] + + def keyframe_channels(prop, locks): + if ignore_locks or not all(locks): + if ignore_locks or not any(locks): + bone.keyframe_insert(prop, group=bone_name, options=keyflags) + else: + for i, lock in enumerate(locks): + if not lock: + bone.keyframe_insert(prop, index=i, group=bone_name, options=keyflags) + + if not (no_loc or bone.bone.use_connect): + keyframe_channels('location', bone.lock_location) + + if not no_rot: + if bone.rotation_mode == 'QUATERNION': + keyframe_channels('rotation_quaternion', get_4d_rotlock(bone)) + elif bone.rotation_mode == 'AXIS_ANGLE': + keyframe_channels('rotation_axis_angle', get_4d_rotlock(bone)) + else: + keyframe_channels('rotation_euler', bone.lock_rotation) + + if not no_scale: + keyframe_channels('scale', bone.lock_scale) + +###################### +## Constraint tools ## +###################### + +def get_constraint_target_matrix(con): + target = con.target + if target: + if target.type == 'ARMATURE' and con.subtarget: + if con.subtarget in target.pose.bones: + bone = target.pose.bones[con.subtarget] + return target.convert_space(pose_bone=bone, matrix=bone.matrix, from_space='POSE', to_space=con.target_space) + else: + return target.convert_space(matrix=target.matrix_world, from_space='WORLD', to_space=con.target_space) + return Matrix.Identity(4) + +def undo_copy_scale_with_offset(obj, bone, con, old_matrix): + "Undo the effects of Copy Scale with Offset constraint on a bone matrix." + inf = con.influence + + if con.mute or inf == 0 or not con.is_valid or not con.use_offset or con.use_add or con.use_make_uniform: + return old_matrix + + scale_delta = [ + 1 / (1 + (math.pow(x, con.power) - 1) * inf) + for x in get_constraint_target_matrix(con).to_scale() + ] + + for i, use in enumerate([con.use_x, con.use_y, con.use_z]): + if not use: + scale_delta[i] = 1 + + return old_matrix @ Matrix.Diagonal([*scale_delta, 1]) + +def undo_copy_scale_constraints(obj, bone, matrix): + "Undo the effects of all Copy Scale with Offset constraints on a bone matrix." + for con in reversed(bone.constraints): + if con.type == 'COPY_SCALE': + matrix = undo_copy_scale_with_offset(obj, bone, con, matrix) + return matrix + +############################### +## Assign and keyframe tools ## +############################### + +def set_custom_property_value(obj, bone_name, prop, value, *, keyflags=None): + "Assign the value of a custom property, and optionally keyframe it." + from rna_prop_ui import rna_idprop_ui_prop_update + bone = obj.pose.bones[bone_name] + bone[prop] = value + rna_idprop_ui_prop_update(bone, prop) + if keyflags is not None: + bone.keyframe_insert(rna_idprop_quote_path(prop), group=bone.name, options=keyflags) + +def get_transform_matrix(obj, bone_name, *, space='POSE', with_constraints=True): + "Retrieve the matrix of the bone before or after constraints in the given space." + bone = obj.pose.bones[bone_name] + if with_constraints: + return obj.convert_space(pose_bone=bone, matrix=bone.matrix, from_space='POSE', to_space=space) + else: + return obj.convert_space(pose_bone=bone, matrix=bone.matrix_basis, from_space='LOCAL', to_space=space) + +def get_chain_transform_matrices(obj, bone_names, **options): + return [get_transform_matrix(obj, name, **options) for name in bone_names] + +def set_transform_from_matrix(obj, bone_name, matrix, *, space='POSE', undo_copy_scale=False, ignore_locks=False, no_loc=False, no_rot=False, no_scale=False, keyflags=None): + "Apply the matrix to the transformation of the bone, taking locked channels, mode and certain constraints into account, and optionally keyframe it." + bone = obj.pose.bones[bone_name] + + def restore_channels(prop, old_vec, locks, extra_lock): + if extra_lock or (not ignore_locks and all(locks)): + setattr(bone, prop, old_vec) + else: + if not ignore_locks and any(locks): + new_vec = Vector(getattr(bone, prop)) + + for i, lock in enumerate(locks): + if lock: + new_vec[i] = old_vec[i] + + setattr(bone, prop, new_vec) + + # Save the old values of the properties + old_loc = Vector(bone.location) + old_rot_euler = Vector(bone.rotation_euler) + old_rot_quat = Vector(bone.rotation_quaternion) + old_rot_axis = Vector(bone.rotation_axis_angle) + old_scale = Vector(bone.scale) + + # Compute and assign the local matrix + if space != 'LOCAL': + matrix = obj.convert_space(pose_bone=bone, matrix=matrix, from_space=space, to_space='LOCAL') + + if undo_copy_scale: + matrix = undo_copy_scale_constraints(obj, bone, matrix) + + bone.matrix_basis = matrix + + # Restore locked properties + restore_channels('location', old_loc, bone.lock_location, no_loc or bone.bone.use_connect) + + if bone.rotation_mode == 'QUATERNION': + restore_channels('rotation_quaternion', old_rot_quat, get_4d_rotlock(bone), no_rot) + bone.rotation_axis_angle = old_rot_axis + bone.rotation_euler = old_rot_euler + elif bone.rotation_mode == 'AXIS_ANGLE': + bone.rotation_quaternion = old_rot_quat + restore_channels('rotation_axis_angle', old_rot_axis, get_4d_rotlock(bone), no_rot) + bone.rotation_euler = old_rot_euler + else: + bone.rotation_quaternion = old_rot_quat + bone.rotation_axis_angle = old_rot_axis + restore_channels('rotation_euler', old_rot_euler, bone.lock_rotation, no_rot) + + restore_channels('scale', old_scale, bone.lock_scale, no_scale) + + # Keyframe properties + if keyflags is not None: + keyframe_transform_properties( + obj, bone_name, keyflags, ignore_locks=ignore_locks, + no_loc=no_loc, no_rot=no_rot, no_scale=no_scale + ) + +def set_chain_transforms_from_matrices(context, obj, bone_names, matrices, **options): + for bone, matrix in zip(bone_names, matrices): + set_transform_from_matrix(obj, bone, matrix, **options) + context.view_layer.update() +'''] + +exec(SCRIPT_UTILITIES_KEYING[-1]) + +############################################ +# Utilities for managing animation curves ## +############################################ + +SCRIPT_UTILITIES_CURVES = [''' +########################### +## Animation curve tools ## +########################### + +def flatten_curve_set(curves): + "Iterate over all FCurves inside a set of nested lists and dictionaries." + if curves is None: + pass + elif isinstance(curves, bpy.types.FCurve): + yield curves + elif isinstance(curves, dict): + for sub in curves.values(): + yield from flatten_curve_set(sub) + else: + for sub in curves: + yield from flatten_curve_set(sub) + +def flatten_curve_key_set(curves, key_range=None): + "Iterate over all keys of the given fcurves in the specified range." + for curve in flatten_curve_set(curves): + for key in curve.keyframe_points: + if key_range is None or key_range[0] <= key.co[0] <= key_range[1]: + yield key + +def get_curve_frame_set(curves, key_range=None): + "Compute a set of all time values with existing keys in the given curves and range." + return set(key.co[0] for key in flatten_curve_key_set(curves, key_range)) + +def set_curve_key_interpolation(curves, ipo, key_range=None): + "Assign the given interpolation value to all curve keys in range." + for key in flatten_curve_key_set(curves, key_range): + key.interpolation = ipo + +def delete_curve_keys_in_range(curves, key_range=None): + "Delete all keys of the given curves within the given range." + for curve in flatten_curve_set(curves): + points = curve.keyframe_points + for i in range(len(points), 0, -1): + key = points[i - 1] + if key_range is None or key_range[0] <= key.co[0] <= key_range[1]: + points.remove(key, fast=True) + curve.update() + +def nla_tweak_to_scene(anim_data, frames, invert=False): + "Convert a frame value or list between scene and tweaked NLA strip time." + if frames is None: + return None + elif anim_data is None or not anim_data.use_tweak_mode: + return frames + elif isinstance(frames, (int, float)): + return anim_data.nla_tweak_strip_time_to_scene(frames, invert=invert) + else: + return type(frames)( + anim_data.nla_tweak_strip_time_to_scene(v, invert=invert) for v in frames + ) + +def find_action(action): + if isinstance(action, bpy.types.Object): + action = action.animation_data + if isinstance(action, bpy.types.AnimData): + action = action.action + if isinstance(action, bpy.types.Action): + return action + else: + return None + +def clean_action_empty_curves(action): + "Delete completely empty curves from the given action." + action = find_action(action) + for curve in list(action.fcurves): + if curve.is_empty: + action.fcurves.remove(curve) + action.update_tag() + +TRANSFORM_PROPS_LOCATION = frozenset(['location']) +TRANSFORM_PROPS_ROTATION = frozenset(['rotation_euler', 'rotation_quaternion', 'rotation_axis_angle']) +TRANSFORM_PROPS_SCALE = frozenset(['scale']) +TRANSFORM_PROPS_ALL = frozenset(TRANSFORM_PROPS_LOCATION | TRANSFORM_PROPS_ROTATION | TRANSFORM_PROPS_SCALE) + +class ActionCurveTable(object): + "Table for efficient lookup of FCurves by properties." + + def __init__(self, action): + from collections import defaultdict + self.action = find_action(action) + self.curve_map = defaultdict(dict) + self.index_action() + + def index_action(self): + if not self.action: + return + + for curve in self.action.fcurves: + index = curve.array_index + if index < 0: + index = 0 + self.curve_map[curve.data_path][index] = curve + + def get_prop_curves(self, ptr, prop_path): + "Returns a dictionary from array index to curve for the given property, or Null." + return self.curve_map.get(ptr.path_from_id(prop_path)) + + def list_all_prop_curves(self, ptr_set, path_set): + "Iterates over all FCurves matching the given object(s) and properti(es)." + if isinstance(ptr_set, bpy.types.bpy_struct): + ptr_set = [ptr_set] + for ptr in ptr_set: + for path in path_set: + curves = self.get_prop_curves(ptr, path) + if curves: + yield from curves.values() + + def get_custom_prop_curves(self, ptr, prop): + return self.get_prop_curves(ptr, rna_idprop_quote_path(prop)) +'''] + +exec(SCRIPT_UTILITIES_CURVES[-1]) + +################################################ +# Utilities for operators that bake keyframes ## +################################################ + +_SCRIPT_REGISTER_WM_PROPS = ''' +bpy.types.WindowManager.rigify_transfer_use_all_keys = bpy.props.BoolProperty( + name="Bake All Keyed Frames", description="Bake on every frame that has a key for any of the bones, as opposed to just the relevant ones", default=False +) +bpy.types.WindowManager.rigify_transfer_use_frame_range = bpy.props.BoolProperty( + name="Limit Frame Range", description="Only bake keyframes in a certain frame range", default=False +) +bpy.types.WindowManager.rigify_transfer_start_frame = bpy.props.IntProperty( + name="Start", description="First frame to transfer", default=0, min=0 +) +bpy.types.WindowManager.rigify_transfer_end_frame = bpy.props.IntProperty( + name="End", description="Last frame to transfer", default=0, min=0 +) +''' + +_SCRIPT_UNREGISTER_WM_PROPS = ''' +del bpy.types.WindowManager.rigify_transfer_use_all_keys +del bpy.types.WindowManager.rigify_transfer_use_frame_range +del bpy.types.WindowManager.rigify_transfer_start_frame +del bpy.types.WindowManager.rigify_transfer_end_frame +''' + +_SCRIPT_UTILITIES_BAKE_OPS = ''' +class RIGIFY_OT_get_frame_range(bpy.types.Operator): + bl_idname = "rigify.get_frame_range" + ('_'+rig_id if rig_id else '') + bl_label = "Get Frame Range" + bl_description = "Set start and end frame from scene" + bl_options = {'INTERNAL'} + + def execute(self, context): + scn = context.scene + id_store = context.window_manager + id_store.rigify_transfer_start_frame = scn.frame_start + id_store.rigify_transfer_end_frame = scn.frame_end + return {'FINISHED'} + + @staticmethod + def get_range(context): + id_store = context.window_manager + if not id_store.rigify_transfer_use_frame_range: + return None + else: + return (id_store.rigify_transfer_start_frame, id_store.rigify_transfer_end_frame) + + @classmethod + def draw_range_ui(self, context, layout): + id_store = context.window_manager + + row = layout.row(align=True) + row.prop(id_store, 'rigify_transfer_use_frame_range', icon='PREVIEW_RANGE', text='') + + row = row.row(align=True) + row.active = id_store.rigify_transfer_use_frame_range + row.prop(id_store, 'rigify_transfer_start_frame') + row.prop(id_store, 'rigify_transfer_end_frame') + row.operator(self.bl_idname, icon='TIME', text='') +''' + +exec(_SCRIPT_UTILITIES_BAKE_OPS) + +################################################ +# Framework for operators that bake keyframes ## +################################################ + +SCRIPT_REGISTER_BAKE = ['RIGIFY_OT_get_frame_range'] + +SCRIPT_UTILITIES_BAKE = SCRIPT_UTILITIES_KEYING + SCRIPT_UTILITIES_CURVES + [''' +################################## +# Common bake operator settings ## +################################## +''' + _SCRIPT_REGISTER_WM_PROPS + _SCRIPT_UTILITIES_BAKE_OPS + ''' +####################################### +# Keyframe baking operator framework ## +####################################### + +class RigifyBakeKeyframesMixin: + """Basic framework for an operator that updates a set of keyed frames.""" + + # Utilities + def nla_from_raw(self, frames): + "Convert frame(s) from inner action time to scene time." + return nla_tweak_to_scene(self.bake_anim, frames) + + def nla_to_raw(self, frames): + "Convert frame(s) from scene time to inner action time." + return nla_tweak_to_scene(self.bake_anim, frames, invert=True) + + def bake_get_bone(self, bone_name): + "Get pose bone by name." + return self.bake_rig.pose.bones[bone_name] + + def bake_get_bones(self, bone_names): + "Get multiple pose bones by name." + if isinstance(bone_names, (list, set)): + return [self.bake_get_bone(name) for name in bone_names] + else: + return self.bake_get_bone(bone_names) + + def bake_get_all_bone_curves(self, bone_names, props): + "Get a list of all curves for the specified properties of the specified bones." + return list(self.bake_curve_table.list_all_prop_curves(self.bake_get_bones(bone_names), props)) + + def bake_get_all_bone_custom_prop_curves(self, bone_names, props): + "Get a list of all curves for the specified custom properties of the specified bones." + return self.bake_get_all_bone_curves(bone_names, [rna_idprop_quote_path(p) for p in props]) + + def bake_get_bone_prop_curves(self, bone_name, prop): + "Get an index to curve dict for the specified property of the specified bone." + return self.bake_curve_table.get_prop_curves(self.bake_get_bone(bone_name), prop) + + def bake_get_bone_custom_prop_curves(self, bone_name, prop): + "Get an index to curve dict for the specified custom property of the specified bone." + return self.bake_curve_table.get_custom_prop_curves(self.bake_get_bone(bone_name), prop) + + def bake_add_curve_frames(self, curves): + "Register frames keyed in the specified curves for baking." + self.bake_frames_raw |= get_curve_frame_set(curves, self.bake_frame_range_raw) + + def bake_add_bone_frames(self, bone_names, props): + "Register frames keyed for the specified properties of the specified bones for baking." + curves = self.bake_get_all_bone_curves(bone_names, props) + self.bake_add_curve_frames(curves) + return curves + + def bake_replace_custom_prop_keys_constant(self, bone, prop, new_value): + "If the property is keyframed, delete keys in bake range and re-key as Constant." + prop_curves = self.bake_get_bone_custom_prop_curves(bone, prop) + + if prop_curves and 0 in prop_curves: + range_raw = self.nla_to_raw(self.get_bake_range()) + delete_curve_keys_in_range(prop_curves, range_raw) + set_custom_property_value(self.bake_rig, bone, prop, new_value, keyflags={'INSERTKEY_AVAILABLE'}) + set_curve_key_interpolation(prop_curves, 'CONSTANT', range_raw) + + # Default behavior implementation + def bake_init(self, context): + self.bake_rig = context.active_object + self.bake_anim = self.bake_rig.animation_data + self.bake_frame_range = RIGIFY_OT_get_frame_range.get_range(context) + self.bake_frame_range_raw = self.nla_to_raw(self.bake_frame_range) + self.bake_curve_table = ActionCurveTable(self.bake_rig) + self.bake_current_frame = context.scene.frame_current + self.bake_frames_raw = set() + self.bake_state = dict() + + self.keyflags = get_keying_flags(context) + + if context.window_manager.rigify_transfer_use_all_keys: + self.bake_add_curve_frames(self.bake_curve_table.curve_map) + + def bake_add_frames_done(self): + "Computes and sets the final set of frames to bake." + frames = self.nla_from_raw(self.bake_frames_raw) + self.bake_frames = sorted(set(map(round, frames))) + + def is_bake_empty(self): + return len(self.bake_frames_raw) == 0 + + def report_bake_empty(self): + self.bake_add_frames_done() + if self.is_bake_empty(): + self.report({'WARNING'}, 'No keys to bake.') + return True + return False + + def get_bake_range(self): + "Returns the frame range that is being baked." + if self.bake_frame_range: + return self.bake_frame_range + else: + frames = self.bake_frames + return (frames[0], frames[-1]) + + def get_bake_range_pair(self): + "Returns the frame range that is being baked, both in scene and action time." + range = self.get_bake_range() + return range, self.nla_to_raw(range) + + def bake_save_state(self, context): + "Scans frames and collects data for baking before changing anything." + rig = self.bake_rig + scene = context.scene + saved_state = self.bake_state + + for frame in self.bake_frames: + scene.frame_set(frame) + saved_state[frame] = self.save_frame_state(context, rig) + + def bake_clean_curves_in_range(self, context, curves): + "Deletes all keys from the given curves in the bake range." + range, range_raw = self.get_bake_range_pair() + + context.scene.frame_set(range[0]) + delete_curve_keys_in_range(curves, range_raw) + + return range, range_raw + + def bake_apply_state(self, context): + "Scans frames and applies the baking operation." + rig = self.bake_rig + scene = context.scene + saved_state = self.bake_state + + for frame in self.bake_frames: + scene.frame_set(frame) + self.apply_frame_state(context, rig, saved_state.get(frame)) + + clean_action_empty_curves(self.bake_rig) + scene.frame_set(self.bake_current_frame) + + @staticmethod + def draw_common_bake_ui(context, layout): + layout.prop(context.window_manager, 'rigify_transfer_use_all_keys') + + RIGIFY_OT_get_frame_range.draw_range_ui(context, layout) + + @classmethod + def poll(cls, context): + return find_action(context.active_object) is not None + + def execute_scan_curves(self, context, obj): + "Override to register frames to be baked, and return curves that should be cleared." + raise NotImplementedError() + + def execute_before_apply(self, context, obj, range, range_raw): + "Override to execute code one time before the bake apply frame scan." + pass + + def init_execute(self, context): + "Override to initialize the operator." + pass + + def execute(self, context): + self.init_execute(context) + self.bake_init(context) + + curves = self.execute_scan_curves(context, self.bake_rig) + + if self.report_bake_empty(): + return {'CANCELLED'} + + self.bake_save_state(context) + + range, range_raw = self.bake_clean_curves_in_range(context, curves) + + self.execute_before_apply(context, self.bake_rig, range, range_raw) + + self.bake_apply_state(context) + return {'FINISHED'} + + def init_invoke(self, context): + "Override to initialize the operator." + pass + + def invoke(self, context, event): + self.init_invoke(context) + + if hasattr(self, 'draw'): + return context.window_manager.invoke_props_dialog(self) + else: + return context.window_manager.invoke_confirm(self, event) + + +class RigifySingleUpdateMixin: + """Basic framework for an operator that updates only the current frame.""" + + def init_execute(self, context): + pass + + def execute(self, context): + self.init_execute(context) + obj = context.active_object + self.keyflags = get_autokey_flags(context, ignore_keyset=True) + self.keyflags_switch = add_flags_if_set(self.keyflags, {'INSERTKEY_AVAILABLE'}) + self.apply_frame_state(context, obj, self.save_frame_state(context, obj)) + return {'FINISHED'} + + def init_invoke(self, context): + pass + + def invoke(self, context, event): + self.init_invoke(context) + + if hasattr(self, 'draw'): + return context.window_manager.invoke_props_popup(self, event) + else: + return self.execute(context) +'''] + +exec(SCRIPT_UTILITIES_BAKE[-1]) + +##################################### +# Generic Clear Keyframes operator ## +##################################### + +SCRIPT_REGISTER_OP_CLEAR_KEYS = ['POSE_OT_rigify_clear_keyframes'] + +SCRIPT_UTILITIES_OP_CLEAR_KEYS = [''' +############################# +## Generic Clear Keyframes ## +############################# + +class POSE_OT_rigify_clear_keyframes(bpy.types.Operator): + bl_idname = "pose.rigify_clear_keyframes_" + rig_id + bl_label = "Clear Keyframes And Transformation" + bl_options = {'UNDO', 'INTERNAL'} + bl_description = "Remove all keyframes for the relevant bones and reset transformation" + + bones: StringProperty(name="Bone List") + + @classmethod + def poll(cls, context): + return find_action(context.active_object) is not None + + def invoke(self, context, event): + return context.window_manager.invoke_confirm(self, event) + + def execute(self, context): + obj = context.active_object + bone_list = [ obj.pose.bones[name] for name in json.loads(self.bones) ] + + curve_table = ActionCurveTable(context.active_object) + curves = list(curve_table.list_all_prop_curves(bone_list, TRANSFORM_PROPS_ALL)) + + key_range = RIGIFY_OT_get_frame_range.get_range(context) + range_raw = nla_tweak_to_scene(obj.animation_data, key_range, invert=True) + delete_curve_keys_in_range(curves, range_raw) + + for bone in bone_list: + bone.location = bone.rotation_euler = (0,0,0) + bone.rotation_quaternion = (1,0,0,0) + bone.rotation_axis_angle = (0,0,1,0) + bone.scale = (1,1,1) + + clean_action_empty_curves(obj) + obj.update_tag(refresh={'TIME'}) + return {'FINISHED'} +'''] + +def add_clear_keyframes_button(panel, *, bones=[], label='', text=''): + panel.use_bake_settings() + panel.script.add_utilities(SCRIPT_UTILITIES_OP_CLEAR_KEYS) + panel.script.register_classes(SCRIPT_REGISTER_OP_CLEAR_KEYS) + + op_props = { 'bones': json.dumps(bones) } + + panel.operator('pose.rigify_clear_keyframes_{rig_id}', text=text, icon='CANCEL', properties=op_props) + + +################################### +# Generic Snap FK to IK operator ## +################################### + +SCRIPT_REGISTER_OP_SNAP_FK_IK = ['POSE_OT_rigify_generic_fk2ik', 'POSE_OT_rigify_generic_fk2ik_bake'] + +SCRIPT_UTILITIES_OP_SNAP_FK_IK = [''' +########################### +## Generic Snap FK to IK ## +########################### + +class RigifyGenericFk2IkBase: + fk_bones: StringProperty(name="FK Bone Chain") + ik_bones: StringProperty(name="IK Result Bone Chain") + ctrl_bones: StringProperty(name="IK Controls") + + undo_copy_scale: bpy.props.BoolProperty(name="Undo Copy Scale", default=False) + + keyflags = None + + def init_execute(self, context): + self.fk_bone_list = json.loads(self.fk_bones) + self.ik_bone_list = json.loads(self.ik_bones) + self.ctrl_bone_list = json.loads(self.ctrl_bones) + + def save_frame_state(self, context, obj): + return get_chain_transform_matrices(obj, self.ik_bone_list) + + def apply_frame_state(self, context, obj, matrices): + set_chain_transforms_from_matrices( + context, obj, self.fk_bone_list, matrices, + undo_copy_scale=self.undo_copy_scale, keyflags=self.keyflags + ) + +class POSE_OT_rigify_generic_fk2ik(RigifyGenericFk2IkBase, RigifySingleUpdateMixin, bpy.types.Operator): + bl_idname = "pose.rigify_generic_fk2ik_" + rig_id + bl_label = "Snap FK->IK" + bl_options = {'UNDO', 'INTERNAL'} + bl_description = "Snap the FK chain to IK result" + +class POSE_OT_rigify_generic_fk2ik_bake(RigifyGenericFk2IkBase, RigifyBakeKeyframesMixin, bpy.types.Operator): + bl_idname = "pose.rigify_generic_fk2ik_bake_" + rig_id + bl_label = "Apply Snap FK->IK To Keyframes" + bl_options = {'UNDO', 'INTERNAL'} + bl_description = "Snap the FK chain keyframes to IK result" + + def execute_scan_curves(self, context, obj): + self.bake_add_bone_frames(self.ctrl_bone_list, TRANSFORM_PROPS_ALL) + return self.bake_get_all_bone_curves(self.fk_bone_list, TRANSFORM_PROPS_ALL) +'''] + +def add_fk_ik_snap_buttons(panel, op_single, op_bake, *, label=None, rig_name='', properties=None, clear_bones=None, compact=None): + assert label and properties + + if rig_name: + label += ' (%s)' % (rig_name) + + if compact or not clear_bones: + row = panel.row(align=True) + row.operator(op_single, text=label, icon='SNAP_ON', properties=properties) + row.operator(op_bake, text='', icon='ACTION_TWEAK', properties=properties) + + if clear_bones: + add_clear_keyframes_button(row, bones=clear_bones) + else: + col = panel.column(align=True) + col.operator(op_single, text=label, icon='SNAP_ON', properties=properties) + row = col.row(align=True) + row.operator(op_bake, text='Action', icon='ACTION_TWEAK', properties=properties) + add_clear_keyframes_button(row, bones=clear_bones, text='Clear') + +def add_generic_snap_fk_to_ik(panel, *, fk_bones=[], ik_bones=[], ik_ctrl_bones=[], label='FK->IK', rig_name='', undo_copy_scale=False, compact=None, clear=True): + panel.use_bake_settings() + panel.script.add_utilities(SCRIPT_UTILITIES_OP_SNAP_FK_IK) + panel.script.register_classes(SCRIPT_REGISTER_OP_SNAP_FK_IK) + + op_props = { + 'fk_bones': json.dumps(fk_bones), + 'ik_bones': json.dumps(ik_bones), + 'ctrl_bones': json.dumps(ik_ctrl_bones), + 'undo_copy_scale': undo_copy_scale, + } + + clear_bones = fk_bones if clear else None + + add_fk_ik_snap_buttons( + panel, 'pose.rigify_generic_fk2ik_{rig_id}', 'pose.rigify_generic_fk2ik_bake_{rig_id}', + label=label, rig_name=rig_name, properties=op_props, clear_bones=clear_bones, compact=compact, + ) + +############################### +# Module register/unregister ## +############################### + +def register(): + from bpy.utils import register_class + + exec(_SCRIPT_REGISTER_WM_PROPS) + + register_class(RIGIFY_OT_get_frame_range) + +def unregister(): + from bpy.utils import unregister_class + + exec(_SCRIPT_UNREGISTER_WM_PROPS) + + unregister_class(RIGIFY_OT_get_frame_range) diff --git a/rigify/utils/bones.py b/rigify/utils/bones.py index 136ece7d..b5559a76 100644 --- a/rigify/utils/bones.py +++ b/rigify/utils/bones.py @@ -24,7 +24,8 @@ from mathutils import Vector, Matrix, Color from rna_prop_ui import rna_idprop_ui_prop_get from .errors import MetarigError -from .naming import make_derived_name +from .naming import get_name, make_derived_name +from .misc import pairwise #======================= # Bone collection @@ -55,7 +56,7 @@ class BoneDict(dict): raise ValueError("Invalid BoneDict value: %r" % (value)) def __init__(self, *args, **kwargs): - super(BoneDict, self).__init__() + super().__init__() for key, value in dict(*args, **kwargs).items(): dict.__setitem__(self, key, BoneDict.__sanitize_attr(key, value)) @@ -72,12 +73,14 @@ class BoneDict(dict): for key, value in dict(*args, **kwargs).items(): dict.__setitem__(self, key, BoneDict.__sanitize_attr(key, value)) - def flatten(self): - """Return all contained bones as a list.""" + def flatten(self, key=None): + """Return all contained bones or a single key as a list.""" + + items = [self[key]] if key is not None else self.values() all_bones = [] - for item in self.values(): + for item in items: if isinstance(item, BoneDict): all_bones.extend(item.flatten()) elif isinstance(item, list): @@ -90,6 +93,18 @@ class BoneDict(dict): #======================= # Bone manipulation #======================= +# +# NOTE: PREFER USING BoneUtilityMixin IN NEW STYLE RIGS! + +def get_bone(obj, bone_name): + """Get EditBone or PoseBone by name, depending on the current mode.""" + if not bone_name: + return None + bones = obj.data.edit_bones if obj.mode == 'EDIT' else obj.pose.bones + if bone_name not in bones: + raise MetarigError("bone '%s' not found" % bone_name) + return bones[bone_name] + def new_bone(obj, bone_name): """ Adds a new bone to the given armature object. @@ -101,17 +116,14 @@ def new_bone(obj, bone_name): edit_bone.head = (0, 0, 0) edit_bone.tail = (0, 1, 0) edit_bone.roll = 0 - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='EDIT') return name else: raise MetarigError("Can't add new bone '%s' outside of edit mode" % bone_name) -def copy_bone_simple(obj, bone_name, assign_name=''): +def copy_bone(obj, bone_name, assign_name='', *, parent=False, bbone=False, length=None, scale=None): """ Makes a copy of the given bone in the given armature object. - but only copies head, tail positions and roll. Does not - address parenting either. + Returns the resulting bone's name. """ #if bone_name not in obj.data.bones: if bone_name not in obj.data.edit_bones: @@ -133,49 +145,36 @@ def copy_bone_simple(obj, bone_name, assign_name=''): edit_bone_2.tail = Vector(edit_bone_1.tail) edit_bone_2.roll = edit_bone_1.roll - return bone_name_2 - else: - raise MetarigError("Cannot copy bones outside of edit mode") - - -def copy_bone(obj, bone_name, assign_name=''): - """ Makes a copy of the given bone in the given armature object. - Returns the resulting bone's name. - """ - #if bone_name not in obj.data.bones: - if bone_name not in obj.data.edit_bones: - raise MetarigError("copy_bone(): bone '%s' not found, cannot copy it" % bone_name) + if parent: + edit_bone_2.parent = edit_bone_1.parent + edit_bone_2.use_connect = edit_bone_1.use_connect - if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE': - if assign_name == '': - assign_name = bone_name - # Copy the edit bone - edit_bone_1 = obj.data.edit_bones[bone_name] - edit_bone_2 = obj.data.edit_bones.new(assign_name) - bone_name_1 = bone_name - bone_name_2 = edit_bone_2.name + edit_bone_2.use_inherit_rotation = edit_bone_1.use_inherit_rotation + edit_bone_2.use_inherit_scale = edit_bone_1.use_inherit_scale + edit_bone_2.use_local_location = edit_bone_1.use_local_location - edit_bone_2.parent = edit_bone_1.parent - edit_bone_2.use_connect = edit_bone_1.use_connect + if bbone: + for name in ['bbone_segments', + 'bbone_easein', 'bbone_easeout', + 'bbone_rollin', 'bbone_rollout', + 'bbone_curveinx', 'bbone_curveiny', 'bbone_curveoutx', 'bbone_curveouty', + 'bbone_scaleinx', 'bbone_scaleiny', 'bbone_scaleoutx', 'bbone_scaleouty']: + setattr(edit_bone_2, name, getattr(edit_bone_1, name)) - # Copy edit bone attributes - edit_bone_2.layers = list(edit_bone_1.layers) + # Resize the bone after copy if requested + if length is not None: + edit_bone_2.length = length + elif scale is not None: + edit_bone_2.length *= scale - edit_bone_2.head = Vector(edit_bone_1.head) - edit_bone_2.tail = Vector(edit_bone_1.tail) - edit_bone_2.roll = edit_bone_1.roll - - edit_bone_2.use_inherit_rotation = edit_bone_1.use_inherit_rotation - edit_bone_2.use_inherit_scale = edit_bone_1.use_inherit_scale - edit_bone_2.use_local_location = edit_bone_1.use_local_location - - edit_bone_2.use_deform = edit_bone_1.use_deform - edit_bone_2.bbone_segments = edit_bone_1.bbone_segments - edit_bone_2.bbone_easein = edit_bone_1.bbone_easein - edit_bone_2.bbone_easeout = edit_bone_1.bbone_easeout + return bone_name_2 + else: + raise MetarigError("Cannot copy bones outside of edit mode") - bpy.ops.object.mode_set(mode='OBJECT') +def copy_bone_properties(obj, bone_name_1, bone_name_2): + """ Copy transform and custom properties from bone 1 to bone 2. """ + if obj.mode in {'OBJECT','POSE'}: # Get the pose bones pose_bone_1 = obj.pose.bones[bone_name_1] pose_bone_2 = obj.pose.bones[bone_name_2] @@ -203,18 +202,24 @@ def copy_bone(obj, bone_name, assign_name=''): prop2 = rna_idprop_ui_prop_get(pose_bone_2, key, create=True) for key in prop1.keys(): prop2[key] = prop1[key] + else: + raise MetarigError("Cannot copy bone properties in edit mode") - bpy.ops.object.mode_set(mode='EDIT') - return bone_name_2 - else: - raise MetarigError("Cannot copy bones outside of edit mode") +def _legacy_copy_bone(obj, bone_name, assign_name=''): + """LEGACY ONLY, DON'T USE""" + new_name = copy_bone(obj, bone_name, assign_name, parent=True, bbone=True) + # Mode switch PER BONE CREATION?! + bpy.ops.object.mode_set(mode='OBJECT') + copy_bone_properties(obj, bone_name, new_name) + bpy.ops.object.mode_set(mode='EDIT') + return new_name def flip_bone(obj, bone_name): """ Flips an edit bone. """ - if bone_name not in obj.data.bones: + if bone_name not in obj.data.edit_bones: raise MetarigError("flip_bone(): bone '%s' not found, cannot copy it" % bone_name) if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE': @@ -228,10 +233,38 @@ def flip_bone(obj, bone_name): raise MetarigError("Cannot flip bones outside of edit mode") +def flip_bone_chain(obj, bone_names): + """Flips a connected bone chain.""" + assert obj.mode == 'EDIT' + + bones = [ obj.data.edit_bones[name] for name in bone_names ] + + # Verify chain and unparent + for prev_bone, bone in pairwise(bones): + assert bone.parent == prev_bone and bone.use_connect + + for bone in bones: + bone.parent = None + bone.use_connect = False + for child in bone.children: + child.use_connect = False + + # Flip bones + for bone in bones: + head, tail = Vector(bone.head), Vector(bone.tail) + bone.tail = head + tail + bone.head, bone.tail = tail, head + + # Re-parent + for bone, next_bone in pairwise(bones): + bone.parent = next_bone + bone.use_connect = True + + def put_bone(obj, bone_name, pos): """ Places a bone at the given position. """ - if bone_name not in obj.data.bones: + if bone_name not in obj.data.edit_bones: raise MetarigError("put_bone(): bone '%s' not found, cannot move it" % bone_name) if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE': @@ -243,7 +276,14 @@ def put_bone(obj, bone_name, pos): raise MetarigError("Cannot 'put' bones outside of edit mode") -def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""): +def disable_bbones(obj, bone_names): + """Disables B-Bone segments on the specified bones.""" + assert(obj.mode != 'EDIT') + for bone in bone_names: + obj.data.bones[bone].bbone_segments = 1 + + +def _legacy_make_nonscaling_child(obj, bone_name, location, child_name_postfix=""): """ Takes the named bone and creates a non-scaling child of it at the given location. The returned bone (returned by name) is not a true child, but behaves like one sans inheriting scaling. @@ -251,8 +291,10 @@ def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""): It is intended as an intermediate construction to prevent rig types from scaling with their parents. The named bone is assumed to be an ORG bone. + + LEGACY ONLY, DON'T USE """ - if bone_name not in obj.data.bones: + if bone_name not in obj.data.edit_bones: raise MetarigError("make_nonscaling_child(): bone '%s' not found, cannot copy it" % bone_name) if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE': @@ -305,11 +347,129 @@ def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""): raise MetarigError("Cannot make nonscaling child outside of edit mode") +#=================================== +# Bone manipulation as rig methods +#=================================== + + +class BoneUtilityMixin(object): + """ + Provides methods for more convenient creation of bones. + + Requires self.obj to be the armature object being worked on. + """ + def register_new_bone(self, new_name, old_name=None): + """Registers creation or renaming of a bone based on old_name""" + pass + + def new_bone(self, new_name): + """Create a new bone with the specified name.""" + name = new_bone(self.obj, bone_name) + self.register_new_bone(self, name) + return name + + def copy_bone(self, bone_name, new_name='', *, parent=False, bbone=False, length=None, scale=None): + """Copy the bone with the given name, returning the new name.""" + name = copy_bone(self.obj, bone_name, new_name, parent=parent, bbone=bbone, length=length, scale=scale) + self.register_new_bone(name, bone_name) + return name + + def copy_bone_properties(self, src_name, tgt_name): + """Copy pose-mode properties of the bone.""" + copy_bone_properties(self.obj, src_name, tgt_name) + + def rename_bone(self, old_name, new_name): + """Rename the bone, returning the actual new name.""" + bone = self.get_bone(old_name) + bone.name = new_name + if bone.name != old_name: + self.register_new_bone(bone.name, old_name) + return bone.name + + def get_bone(self, bone_name): + """Get EditBone or PoseBone by name, depending on the current mode.""" + return get_bone(self.obj, bone_name) + + def get_bone_parent(self, bone_name): + """Get the name of the parent bone, or None.""" + return get_name(self.get_bone(bone_name).parent) + + def set_bone_parent(self, bone_name, parent_name, use_connect=False): + """Set the parent of the bone.""" + eb = self.obj.data.edit_bones + bone = eb[bone_name] + if use_connect is not None: + bone.use_connect = use_connect + bone.parent = (eb[parent_name] if parent_name else None) + + def parent_bone_chain(self, bone_names, use_connect=None): + """Link bones into a chain with parenting. First bone may be None.""" + for parent, child in pairwise(bone_names): + self.set_bone_parent(child, parent, use_connect=use_connect) + +#============================================= +# B-Bones +#============================================= + +def connect_bbone_chain_handles(obj, bone_names): + assert obj.mode == 'EDIT' + + for prev_name, next_name in pairwise(bone_names): + prev_bone = get_bone(obj, prev_name) + next_bone = get_bone(obj, next_name) + + prev_bone.bbone_handle_type_end = 'ABSOLUTE' + prev_bone.bbone_custom_handle_end = next_bone + + next_bone.bbone_handle_type_start = 'ABSOLUTE' + next_bone.bbone_custom_handle_start = prev_bone + #============================================= # Math #============================================= +def is_same_position(obj, bone_name1, bone_name2): + head1 = get_bone(obj, bone_name1).head + head2 = get_bone(obj, bone_name2).head + + return (head1 - head2).length < 1e-5 + + +def is_connected_position(obj, bone_name1, bone_name2): + tail1 = get_bone(obj, bone_name1).tail + head2 = get_bone(obj, bone_name2).head + + return (tail1 - head2).length < 1e-5 + + +def copy_bone_position(obj, bone_name, target_bone_name, *, length=None, scale=None): + """ Completely copies the position and orientation of the bone. """ + bone1_e = obj.data.edit_bones[bone_name] + bone2_e = obj.data.edit_bones[target_bone_name] + + bone2_e.head = bone1_e.head + bone2_e.tail = bone1_e.tail + bone2_e.roll = bone1_e.roll + + # Resize the bone after copy if requested + if length is not None: + bone2_e.length = length + elif scale is not None: + bone2_e.length *= scale + + +def align_bone_orientation(obj, bone_name, target_bone_name): + """ Aligns the orientation of bone to target bone. """ + bone1_e = obj.data.edit_bones[bone_name] + bone2_e = obj.data.edit_bones[target_bone_name] + + axis = bone2_e.y_axis.normalized() * bone1_e.length + + bone1_e.tail = bone1_e.head + axis + bone1_e.roll = bone2_e.roll + + def align_bone_roll(obj, bone1, bone2): """ Aligns the roll of two bones. """ @@ -416,3 +576,65 @@ def align_bone_y_axis(obj, bone, vec): vec = vec * bone_e.length bone_e.tail = bone_e.head + vec + + +def compute_chain_x_axis(obj, bone_names): + """ + Compute the x axis of all bones to be perpendicular + to the primary plane in which the bones lie. + """ + eb = obj.data.edit_bones + + assert(len(bone_names) > 1) + first_bone = eb[bone_names[0]] + last_bone = eb[bone_names[-1]] + + # Compute normal to the plane defined by the first bone, + # and the end of the last bone in the chain + chain_y_axis = last_bone.tail - first_bone.head + chain_rot_axis = first_bone.y_axis.cross(chain_y_axis) + + if chain_rot_axis.length < first_bone.length/100: + return first_bone.x_axis.normalized() + else: + return chain_rot_axis.normalized() + + +def align_chain_x_axis(obj, bone_names): + """ + Aligns the x axis of all bones to be perpendicular + to the primary plane in which the bones lie. + """ + chain_rot_axis = compute_chain_x_axis(obj, bone_names) + + for name in bone_names: + align_bone_x_axis(obj, name, chain_rot_axis) + + +def align_bone_to_axis(obj, bone_name, axis, *, length=None, roll=0, flip=False): + """ + Aligns the Y axis of the bone to the global axis (x,y,z,-x,-y,-z), + optionally adjusting length and initially flipping the bone. + """ + bone_e = obj.data.edit_bones[bone_name] + + if length is None: + length = bone_e.length + if roll is None: + roll = bone_e.roll + + if axis[0] == '-': + length = -length + axis = axis[1:] + + vec = Vector((0,0,0)) + setattr(vec, axis, length) + + if flip: + base = Vector(bone_e.tail) + bone_e.tail = base + vec + bone_e.head = base + else: + bone_e.tail = bone_e.head + vec + + bone_e.roll = roll diff --git a/rigify/utils/errors.py b/rigify/utils/errors.py index 71295057..0fc81ccb 100644 --- a/rigify/utils/errors.py +++ b/rigify/utils/errors.py @@ -32,3 +32,14 @@ class MetarigError(Exception): def __str__(self): return repr(self.message) + +class RaiseErrorMixin(object): + def raise_error(self, message, *args, **kwargs): + from .naming import strip_org + + message = message.format(*args, **kwargs) + + if hasattr(self, 'base_bone'): + message = "Bone '%s': %s" % (strip_org(self.base_bone), message) + + raise MetarigError("RIGIFY ERROR: " + message) diff --git a/rigify/utils/layers.py b/rigify/utils/layers.py index 1045e493..b624e9ac 100644 --- a/rigify/utils/layers.py +++ b/rigify/utils/layers.py @@ -21,6 +21,12 @@ import bpy +ORG_LAYER = [n == 31 for n in range(0, 32)] # Armature layer that original bones should be moved to. +MCH_LAYER = [n == 30 for n in range(0, 32)] # Armature layer that mechanism bones should be moved to. +DEF_LAYER = [n == 29 for n in range(0, 32)] # Armature layer that deformation bones should be moved to. +ROOT_LAYER = [n == 28 for n in range(0, 32)] # Armature layer that root bone should be moved to. + + def get_layers(layers): """ Does its best to extract a set of layers from any data thrown at it. """ @@ -69,6 +75,9 @@ class ControlLayersOption: def assign(self, params, bone_set, bone_list): layers = self.get(params) + if isinstance(bone_set, bpy.types.Object): + bone_set = bone_set.data.bones + if layers: for name in bone_list: bone = bone_set[name] diff --git a/rigify/utils/mechanism.py b/rigify/utils/mechanism.py index ee1e3dfc..937e07e9 100644 --- a/rigify/utils/mechanism.py +++ b/rigify/utils/mechanism.py @@ -32,9 +32,13 @@ _TRACK_AXIS_MAP = { 'Z': 'TRACK_Z', '-Z': 'TRACK_NEGATIVE_Z', } +def _set_default_attr(obj, options, attr, value): + if hasattr(obj, attr): + options.setdefault(attr, value) + def make_constraint( owner, type, target=None, subtarget=None, *, - space=None, track_axis=None, use_xyz=None, + space=None, track_axis=None, use_xyz=None, use_limit_xyz=None, **options): """ Creates and initializes constraint of the specified type for the owner bone. @@ -45,7 +49,8 @@ def make_constraint( space : assigned to both owner_space and target_space track_axis : allows shorter X, Y, Z, -X, -Y, -Z notation use_xyz : list of 3 items is assigned to use_x, use_y and use_z options - min/max_x/y/z : a corresponding use_min/max_x/y/z option is set to True + use_limit_xyz : list of 3 items is assigned to use_limit_x/y/z options + min/max_x/y/z : a corresponding use_(min/max/limit)_(x/y/z) option is set to True Other keyword arguments are directly assigned to the constraint options. Returns the newly created constraint. @@ -59,7 +64,8 @@ def make_constraint( con.subtarget = subtarget if space is not None: - con.owner_space = con.target_space = space + _set_default_attr(con, options, 'owner_space', space) + _set_default_attr(con, options, 'target_space', space) if track_axis is not None: con.track_axis = _TRACK_AXIS_MAP.get(track_axis, track_axis) @@ -67,9 +73,13 @@ def make_constraint( if use_xyz is not None: con.use_x, con.use_y, con.use_z = use_xyz[0:3] + if use_limit_xyz is not None: + con.use_limit_x, con.use_limit_y, con.use_limit_z = use_limit_xyz[0:3] + for key in ['min_x', 'max_x', 'min_y', 'max_y', 'min_z', 'max_z']: - if key in options and 'use_'+key not in options: - options['use_'+key] = True + if key in options: + _set_default_attr(con, options, 'use_'+key, True) + _set_default_attr(con, options, 'use_limit_'+key[-1], True) for p, v in options.items(): setattr(con, p, v) @@ -125,7 +135,10 @@ def _init_driver_target(drv_target, var_info, target_id): # Use ".foo" type path items verbatim, otherwise quote path = subtarget.path_from_id() for item in refs: - path += item if item[0] == '.' else '["'+item+'"]' + if isinstance(item, str): + path += item if item[0] == '.' else '["'+item+'"]' + else: + path += '[%r]' % (item) drv_target.id = target_id drv_target.data_path = path @@ -223,6 +236,14 @@ def make_driver(owner, prop, *, index=-1, type='SUM', expression=None, variables else: drv.type = type + # In case the driver already existed, remove contents + for var in list(drv.variables): + drv.variables.remove(var) + + for mod in list(fcu.modifiers): + fcu.modifiers.remove(mod) + + # Fill in new data if isinstance(variables, list): # variables = [ info, ... ] for i, var_info in enumerate(variables): @@ -234,7 +255,7 @@ def make_driver(owner, prop, *, index=-1, type='SUM', expression=None, variables _add_driver_variable(drv, var_name, var_info, target_id) if polynomial is not None: - drv_modifier = fcu.modifiers[0] + drv_modifier = fcu.modifiers.new('GENERATOR') drv_modifier.mode = 'POLYNOMIAL' drv_modifier.poly_order = len(polynomial)-1 for i,v in enumerate(polynomial): diff --git a/rigify/utils/metaclass.py b/rigify/utils/metaclass.py new file mode 100644 index 00000000..77ce4b6b --- /dev/null +++ b/rigify/utils/metaclass.py @@ -0,0 +1,171 @@ +#====================== BEGIN GPL LICENSE BLOCK ====================== +# +# This program 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 2 +# of the License, or (at your option) any later version. +# +# This program 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 this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +#======================= END GPL LICENSE BLOCK ======================== + +# <pep8 compliant> + +import collections + +from types import FunctionType +from itertools import chain + + +#============================================= +# Class With Stages +#============================================= + + +def rigify_stage(stage): + """Decorates the method with the specified stage.""" + def process(method): + if not isinstance(method, FunctionType): + raise ValueError("Stage decorator must be applied to a method definition") + method._rigify_stage = stage + return method + return process + + +class StagedMetaclass(type): + """ + Metaclass for rigs that manages assignment of methods to stages via @stage.* decorators. + + Using define_stages=True in the class definition will register all non-system + method names from that definition as valid stages. After that, subclasses can + register methods to those stages, to be called via rigify_invoke_stage. + """ + def __new__(metacls, class_name, bases, namespace, define_stages=None, **kwds): + # suppress keyword args to avoid issues with __init_subclass__ + return super().__new__(metacls, class_name, bases, namespace, **kwds) + + def __init__(self, class_name, bases, namespace, define_stages=None, **kwds): + super().__init__(class_name, bases, namespace, **kwds) + + # Compute the set of stages defined by this class + if not define_stages: + define_stages = [] + + elif define_stages is True: + define_stages = [ + name for name, item in namespace.items() + if name[0] != '_' and isinstance(item, FunctionType) + ] + + self.rigify_own_stages = frozenset(define_stages) + + # Compute complete set of inherited stages + staged_bases = [ cls for cls in reversed(self.__mro__) if isinstance(cls, StagedMetaclass) ] + + self.rigify_stages = stages = frozenset(chain.from_iterable( + cls.rigify_own_stages for cls in staged_bases + )) + + # Compute the inherited stage to method mapping + stage_map = collections.defaultdict(collections.OrderedDict) + own_stage_map = collections.defaultdict(collections.OrderedDict) + method_map = {} + + self.rigify_own_stage_map = own_stage_map + + for base in staged_bases: + for stage_name, methods in base.rigify_own_stage_map.items(): + for method_name, method_class in methods.items(): + if method_name in stages: + raise ValueError("Stage method '%s' inherited @stage.%s in class %s (%s)" % + (method_name, stage_name, class_name, self.__module__)) + + # Check consistency of inherited stage assignment to methods + if method_name in method_map: + if method_map[method_name] != stage_name: + print("RIGIFY CLASS %s (%s): method '%s' has inherited both @stage.%s and @stage.%s\n" % + (class_name, self.__module__, method_name, method_map[method_name], stage_name)) + else: + method_map[method_name] = stage_name + + stage_map[stage_name][method_name] = method_class + + # Scan newly defined methods for stage decorations + for method_name, item in namespace.items(): + if isinstance(item, FunctionType): + stage = getattr(item, '_rigify_stage', None) + + if stage and method_name in stages: + print("RIGIFY CLASS %s (%s): cannot use stage decorator on the stage method '%s' (@stage.%s ignored)" % + (class_name, self.__module__, method_name, stage)) + continue + + # Ensure that decorators aren't lost when redefining methods + if method_name in method_map: + if not stage: + stage = method_map[method_name] + print("RIGIFY CLASS %s (%s): missing stage decorator on method '%s' (should be @stage.%s)" % + (class_name, self.__module__, method_name, stage)) + # Check that the method is assigned to only one stage + elif stage != method_map[method_name]: + print("RIGIFY CLASS %s (%s): method '%s' has decorator @stage.%s, but inherited base has @stage.%s" % + (class_name, self.__module__, method_name, stage, method_map[method_name])) + + # Assign the method to the stage, verifying that it's valid + if stage: + if stage not in stages: + raise ValueError("Invalid stage name '%s' for method '%s' in class %s (%s)" % + (stage, method_name, class_name, self.__module__)) + else: + stage_map[stage][method_name] = self + own_stage_map[stage][method_name] = self + + self.rigify_stage_map = stage_map + + def make_stage_decorators(self): + return [(name, rigify_stage(name)) for name in self.rigify_stages] + + +class BaseStagedClass(object, metaclass=StagedMetaclass): + rigify_sub_objects = tuple() + + def rigify_invoke_stage(self, stage): + """Call all methods decorated with the given stage, followed by the callback.""" + cls = self.__class__ + assert isinstance(cls, StagedMetaclass) + assert stage in cls.rigify_stages + + getattr(self, stage)() + + for sub in self.rigify_sub_objects: + sub.rigify_invoke_stage(stage) + + for method_name in cls.rigify_stage_map[stage]: + getattr(self, method_name)() + + +#============================================= +# Per-owner singleton class +#============================================= + + +class SingletonPluginMetaclass(StagedMetaclass): + """Metaclass for maintaining one instance per owner object per constructor arg set.""" + def __call__(cls, owner, *constructor_args): + key = (cls, *constructor_args) + try: + return owner.plugin_map[key] + except KeyError: + new_obj = super().__call__(owner, *constructor_args) + owner.plugin_map[key] = new_obj + owner.plugin_list.append(new_obj) + owner.plugin_list.sort(key=lambda obj: obj.priority, reverse=True) + return new_obj + diff --git a/rigify/utils/misc.py b/rigify/utils/misc.py index 2ca7b016..b0f79ea7 100644 --- a/rigify/utils/misc.py +++ b/rigify/utils/misc.py @@ -19,8 +19,12 @@ # <pep8 compliant> import math +import collections + +from itertools import tee, chain, islice, repeat from mathutils import Vector, Matrix, Color + #============================================= # Math #============================================= @@ -82,6 +86,49 @@ def gamma_correct(color): #============================================= +# Iterators +#============================================= + + +def padnone(iterable, pad=None): + return chain(iterable, repeat(pad)) + + +def pairwise_nozip(iterable): + "s -> (s0,s1), (s1,s2), (s2,s3), ..." + a, b = tee(iterable) + next(b, None) + return a, b + + +def pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2,s3), ..." + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +def map_list(func, *inputs): + "[func(a0,b0...), func(a1,b1...), ...]" + return list(map(func, *inputs)) + + +def skip(n, iterable): + "Returns an iterator skipping first n elements of an iterable." + iterator = iter(iterable) + if n == 1: + next(iterator, None) + else: + next(islice(iterator, n, n), None) + return iterator + + +def map_apply(func, *inputs): + "Apply the function to inputs like map for side effects, discarding results." + collections.deque(map(func, *inputs), maxlen=0) + + +#============================================= # Misc #============================================= @@ -98,3 +145,23 @@ def copy_attributes(a, b): setattr(b, key, getattr(a, key)) except AttributeError: pass + + +def assign_parameters(target, val_dict=None, **params): + data = { **val_dict, **params } if val_dict else params + for key, value in data.items(): + try: + target[key] = value + except Exception as e: + raise Exception("Couldn't set {} to {}: {}".format(key,value,e)) + + +def select_object(context, object, deselect_all=False): + view_layer = context.view_layer + + if deselect_all: + for objt in view_layer.objects: + objt.select_set(False) # deselect all objects + + object.select_set(True) + view_layer.objects.active = object diff --git a/rigify/utils/naming.py b/rigify/utils/naming.py index 3983704a..d2fa90a3 100644 --- a/rigify/utils/naming.py +++ b/rigify/utils/naming.py @@ -21,34 +21,150 @@ import random import time import re +import collections +import enum ORG_PREFIX = "ORG-" # Prefix of original bones. MCH_PREFIX = "MCH-" # Prefix of mechanism bones. DEF_PREFIX = "DEF-" # Prefix of deformation bones. ROOT_NAME = "root" # Name of the root bone. +_PREFIX_TABLE = { 'org': "ORG", 'mch': "MCH", 'def': "DEF", 'ctrl': '' } + +#======================================================================= +# Name structure +#======================================================================= + +NameParts = collections.namedtuple('NameParts', ['prefix', 'base', 'side', 'number']) + + +def split_name(name): + name_parts = re.match(r'^(?:(ORG|MCH|DEF)-)?(.*?)([._-][lLrR])?(?:\.(\d+))?$', name) + return NameParts(*name_parts.groups()) + + +def combine_name(parts, *, prefix=None, base=None, side=None, number=None): + eff_prefix = prefix if prefix is not None else parts.prefix + eff_number = number if number is not None else parts.number + if isinstance(eff_number, int): + eff_number = '%03d' % (eff_number) + + return ''.join([ + eff_prefix+'-' if eff_prefix else '', + base if base is not None else parts.base, + side if side is not None else parts.side or '', + '.'+eff_number if eff_number else '', + ]) + + +def insert_before_lr(name, text): + parts = split_name(name) + + if parts.side: + return combine_name(parts, base=parts.base + text) + else: + return name + text + + +def make_derived_name(name, subtype, suffix=None): + """ Replaces the name prefix, and optionally adds the suffix (before .LR if found). + """ + assert(subtype in _PREFIX_TABLE) + + parts = split_name(name) + new_base = parts.base + (suffix or '') + + return combine_name(parts, prefix=_PREFIX_TABLE[subtype], base=new_base) + + +#======================================================================= +# Name mirroring +#======================================================================= + +class Side(enum.IntEnum): + LEFT = -1 + MIDDLE = 0 + RIGHT = 1 + + @staticmethod + def from_parts(parts): + if parts.side: + if parts.side[1].lower() == 'l': + return Side.LEFT + else: + return Side.RIGHT + else: + return Side.MIDDLE + + @staticmethod + def to_string(parts, side): + if side != Side.MIDDLE: + side_char = 'L' if side == Side.LEFT else 'R' + + if parts.side: + sep, schar = parts.side[0:2] + if schar.lower() == schar: + side_char = side_char.lower() + else: + sep = '.' + + return sep + side_char + else: + return '' + + @staticmethod + def to_name(parts, side): + new_side = Side.to_string(parts, side) + return combine_name(parts, side=new_side) + + +def get_name_side(name): + return Side.from_parts(split_name(name)) + + +def get_name_side_and_base(name): + parts = split_name(name) + return Side.from_parts(parts), Side.to_name(parts, side=Side.MIDDLE) + + +def change_name_side(name, side): + return Side.to_name(split_name(name), side) + + +def mirror_name(name): + parts = split_name(name) + side = Side.from_parts(parts) + + if side != Side.MIDDLE: + return Side.to_name(parts, -side) + else: + return name + #======================================================================= # Name manipulation #======================================================================= +def get_name(bone): + return bone.name if bone else None + -def strip_trailing_number(s): - m = re.search(r'\.(\d{3})$', s) - return s[0:-4] if m else s +def strip_trailing_number(name): + return combine_name(split_name(name), number='') def strip_prefix(name): - return re.sub(r'^(?:ORG|MCH|DEF)-', '', name) + return combine_name(split_name(name), prefix='') def unique_name(collection, base_name): - base_name = strip_trailing_number(base_name) + parts = split_name(base_name) + name = combine_name(parts, number='') count = 1 - name = base_name - while collection.get(name): - name = "%s.%03d" % (base_name, count) + while name in collection: + name = combine_name(parts, number=count) count += 1 + return name @@ -120,28 +236,6 @@ def deformer(name): make_deformer_name = deformer -_prefix_functions = { 'org': org, 'mch': mch, 'def': deformer, 'ctrl': lambda x: x } - - -def insert_before_lr(name, text): - name_parts = re.match(r'^(.*?)((?:[._-][lLrR](?:\.\d+)?)?)$', name) - name_base, name_suffix = name_parts.groups() - return name_base + text + name_suffix - - -def make_derived_name(name, subtype, suffix=None): - """ Replaces the name prefix, and optionally adds the suffix (before .LR if found). - """ - assert(subtype in _prefix_functions) - - name = strip_prefix(name) - - if suffix: - name = insert_before_lr(name, suffix) - - return _prefix_functions[subtype](name) - - def random_id(length=8): """ Generates a random alphanumeric id string. """ diff --git a/rigify/utils/rig.py b/rigify/utils/rig.py index 414ea133..41027c69 100644 --- a/rigify/utils/rig.py +++ b/rigify/utils/rig.py @@ -50,6 +50,12 @@ outdated_types = {"pitchipoy.limbs.super_limb": "limbs.super_limb", "spine": "" } +def get_rigify_type(pose_bone): + return pose_bone.rigify_type.replace(" ", "") + +def is_rig_base_bone(obj, name): + return bool(get_rigify_type(obj.pose.bones[name])) + def upgradeMetarigTypes(metarig, revert=False): """Replaces rigify_type properties from old versions with their current names @@ -89,6 +95,34 @@ def get_resource(resource_name): return module +def attach_persistent_script(obj, script): + """Make sure the ui script always follows the rig around""" + skip = False + driver = None + + if not obj.animation_data: + obj.animation_data_create() + + for fcurve in obj.animation_data.drivers: + if fcurve.data_path == 'pass_index': + driver = fcurve.driver + for variable in driver.variables: + if variable.name == script.name: + skip = True + break + break + + if not skip: + if not driver: + fcurve = obj.driver_add("pass_index") + driver = fcurve.driver + + variable = driver.variables.new() + variable.name = script.name + variable.targets[0].id_type = 'TEXT' + variable.targets[0].id = script + + def connected_children_names(obj, bone_name): """ Returns a list of bone names (in order) of the bones that form a single connected chain starting with the given bone as a parent. @@ -124,6 +158,23 @@ def has_connected_children(bone): return t +def _list_bone_names_depth_first_sorted_rec(result_list, bone): + result_list.append(bone.name) + + for child in sorted(list(bone.children), key=lambda b: b.name): + _list_bone_names_depth_first_sorted_rec(result_list, child) + +def list_bone_names_depth_first_sorted(obj): + """Returns a list of bone names in depth first name sorted order.""" + result_list = [] + + for bone in sorted(list(obj.data.bones), key=lambda b: b.name): + if bone.parent is None: + _list_bone_names_depth_first_sorted_rec(result_list, bone) + + return result_list + + def write_metarig(obj, layers=False, func_name="create", groups=False): """ Write a metarig as a python script, this rig is to have all info needed for @@ -248,6 +299,8 @@ def write_metarig(obj, layers=False, func_name="create", groups=False): code.append("\n arm.layers = [(x in " + str(active_layers) + ") for x in range(" + str(len(arm.layers)) + ")]") + code.append("\n return bones") + code.append('\nif __name__ == "__main__":') code.append(" " + func_name + "(bpy.context.active_object)\n") diff --git a/rigify/utils/switch_parent.py b/rigify/utils/switch_parent.py new file mode 100644 index 00000000..5daa6a6c --- /dev/null +++ b/rigify/utils/switch_parent.py @@ -0,0 +1,438 @@ +import bpy + +import re +import itertools +import bisect +import json + +from .errors import MetarigError +from .naming import strip_prefix, make_derived_name +from .mechanism import MechanismUtilityMixin +from .misc import map_list, map_apply + +from ..base_rig import * +from ..base_generate import GeneratorPlugin + +from itertools import count, repeat + +def _auto_call(value): + if callable(value): + return value() + else: + return value + +def _rig_is_child(rig, parent): + if parent is None: + return True + + while rig: + if rig is parent: + return True + + rig = rig.rigify_parent + + return False + + +class SwitchParentBuilder(GeneratorPlugin, MechanismUtilityMixin): + """ + Implements centralized generation of switchable parent mechanisms. + Allows all rigs to register their bones as possible parents for other rigs. + """ + + def __init__(self, generator): + super().__init__(generator) + + self.child_list = [] + self.global_parents = [] + self.local_parents = [] + self.child_map = {} + self.frozen = False + + self.register_parent(None, 'root', name='Root', is_global=True) + + + ############################## + # API + + def register_parent(self, rig, bone, *, name=None, is_global=False, exclude_self=False): + """ + Registers a bone of the specified rig as a possible parent. + + Parameters: + rig Owner of the bone. + bone Actual name of the parent bone. + name Name of the parent for mouse-over hint. + is_global The parent is accessible to all rigs, instead of just children of owner. + exclude_self The parent is invisible to the owner rig itself. + + Lazy creation: + The bone parameter may be a function creating the bone on demand and + returning its name. It is guaranteed to be called at most once. + """ + + assert not self.frozen + assert isinstance(bone, str) or callable(bone) + + entry = { + 'rig': rig, 'bone': bone, 'name': name, + 'is_global': is_global, 'exclude_self': exclude_self, 'used': False, + } + + if is_global: + self.global_parents.append(entry) + else: + self.local_parents.append(entry) + + + def build_child(self, rig, bone, *, use_parent_mch=True, **options): + """ + Build a switchable parent mechanism for the specified bone. + + Parameters: + rig Owner of the child bone. + bone Name of the child bone. + extra_parents List of bone names or (name, user_name) pairs to use as additional parents. + use_parent_mch Create an intermediate MCH bone for the constraints and parent the child to it. + select_parent Select the specified bone instead of the last one. + ignore_global Ignore the is_global flag of potential parents. + context_rig Rig to use for selecting parents. + + prop_bone Name of the bone to add the property to. + prop_id Actual name of the control property. + prop_name Name of the property to use in the UI script. + controls Collection of controls to bind property UI to. + + ctrl_bone User visible control bone that depends on this parent (for switch & keep transform) + no_fix_* Disable "Switch and Keep Transform" correction for specific channels. + copy_* Override the specified components by copying from another bone. + + Lazy parameters: + 'extra_parents', 'select_parent', 'prop_bone', 'controls', 'copy_*' + may be a function returning the value. They are called in the configure_bones stage. + """ + assert self.generator.stage == 'generate_bones' and not self.frozen + assert rig is not None + assert isinstance(bone, str) + assert bone not in self.child_map + + # Create MCH proxy + if use_parent_mch: + mch_bone = rig.copy_bone(bone, make_derived_name(bone, 'mch', '.parent'), scale=1/3) + else: + mch_bone = bone + + child = { + **self.child_option_table, + 'rig':rig, 'bone': bone, 'mch_bone': mch_bone, + 'is_done': False, 'is_configured': False, + } + self.assign_child_options(child, options) + self.child_list.append(child) + self.child_map[bone] = child + + + def amend_child(self, rig, bone, **options): + """ + Change parameters assigned in a previous build_child call. + + Provided to make it more convenient to change rig behavior by subclassing. + """ + assert self.generator.stage == 'generate_bones' and not self.frozen + child = self.child_map[bone] + assert child['rig'] == rig + self.assign_child_options(child, options) + + + def rig_child_now(self, bone): + """Create the constraints immediately.""" + assert self.generator.stage == 'rig_bones' + child = self.child_map[bone] + assert not child['is_done'] + self.__rig_child(child) + + ############################## + # Implementation + + child_option_table = { + 'extra_parents': None, + 'prop_bone': None, 'prop_id': None, 'prop_name': None, 'controls': None, + 'select_parent': None, 'ignore_global': False, 'context_rig': None, + 'ctrl_bone': None, + 'no_fix_location': False, 'no_fix_rotation': False, 'no_fix_scale': False, + 'copy_location': None, 'copy_rotation': None, 'copy_scale': None, + } + + def assign_child_options(self, child, options): + if 'context_rig' in options: + assert _rig_is_child(child['rig'], options['context_rig']) + + for name, value in options.items(): + if name not in self.child_option_table: + raise AttributeError('invalid child option: '+name) + + child[name] = value + + def generate_bones(self): + self.frozen = True + self.parent_list = self.global_parents + self.local_parents + + # Link children to parents + for child in self.child_list: + child_rig = child['context_rig'] or child['rig'] + parents = [] + + for parent in self.parent_list: + if parent['rig'] is child_rig: + if parent['exclude_self']: + continue + elif parent['is_global'] and not child['ignore_global']: + # Can't use parents from own children, even if global (cycle risk) + if _rig_is_child(parent['rig'], child_rig): + continue + else: + # Required to be a child of the parent's rig + if not _rig_is_child(child_rig, parent['rig']): + continue + + parent['used'] = True + parents.append(parent) + + child['parents'] = parents + + # Call lazy creation for parents + for parent in self.parent_list: + if parent['used']: + parent['bone'] = _auto_call(parent['bone']) + + def parent_bones(self): + for child in self.child_list: + rig = child['rig'] + mch = child['mch_bone'] + + # Remove real parent from the child + rig.set_bone_parent(mch, None) + self.generator.disable_auto_parent(mch) + + # Parent child to the MCH proxy + if mch != child['bone']: + rig.set_bone_parent(child['bone'], mch) + + def configure_bones(self): + for child in self.child_list: + self.__configure_child(child) + + def __configure_child(self, child): + if child['is_configured']: + return + + child['is_configured'] = True + + bone = child['bone'] + + # Build the final list of parent bone names + parent_map = dict() + + for parent in child['parents']: + if parent['bone'] not in parent_map: + parent_map[parent['bone']] = parent['name'] + + last_main_parent_bone = child['parents'][-1]['bone'] + num_main_parents = len(parent_map.items()) + + for parent in _auto_call(child['extra_parents'] or []): + if not isinstance(parent, tuple): + parent = (parent, None) + if parent[0] not in parent_map: + parent_map[parent[0]] = parent[1] + + parent_bones = list(parent_map.items()) + child['parent_bones'] = parent_bones + + # Find which bone to select + select_bone = _auto_call(child['select_parent']) or last_main_parent_bone + select_index = num_main_parents + + try: + select_index = 1 + next(i for i, (bone, _) in enumerate(parent_bones) if bone == select_bone) + except StopIteration: + print("RIGIFY ERROR: Can't find bone '%s' to select as default parent of '%s'\n" % (select_bone, bone)) + + # Create the controlling property + prop_bone = child['prop_bone'] = _auto_call(child['prop_bone']) or bone + prop_name = child['prop_name'] or child['prop_id'] or 'Parent Switch' + prop_id = child['prop_id'] = child['prop_id'] or 'parent_switch' + + parent_names = [ parent[1] or strip_prefix(parent[0]) for parent in [(None, 'None'), *parent_bones] ] + parent_str = ', '.join([ '%s (%d)' % (name, i) for i, name in enumerate(parent_names) ]) + + ctrl_bone = child['ctrl_bone'] or bone + + self.make_property( + prop_bone, prop_id, select_index, + min=0, max=len(parent_bones), + description='Switch parent of %s: %s' % (ctrl_bone, parent_str) + ) + + # Find which channels don't depend on the parent + + no_fix = [ child[n] for n in ['no_fix_location', 'no_fix_rotation', 'no_fix_scale'] ] + + child['copy'] = [ _auto_call(child[n]) for n in ['copy_location', 'copy_rotation', 'copy_scale'] ] + + locks = tuple(bool(nofix or copy) for nofix, copy in zip(no_fix, child['copy'])) + + # Create the script for the property + controls = _auto_call(child['controls']) or set([prop_bone, bone]) + + script = self.generator.script + panel = script.panel_with_selected_check(child['rig'], controls) + + panel.use_bake_settings() + script.add_utilities(SCRIPT_UTILITIES_OP_SWITCH_PARENT) + script.register_classes(SCRIPT_REGISTER_OP_SWITCH_PARENT) + + op_props = { + 'bone': ctrl_bone, 'prop_bone': prop_bone, 'prop_id': prop_id, + 'parent_names': json.dumps(parent_names), 'locks': locks, + } + + row = panel.row(align=True) + lsplit = row.split(factor=0.75, align=True) + lsplit.operator('pose.rigify_switch_parent_{rig_id}', text=prop_name, icon='DOWNARROW_HLT', properties=op_props) + lsplit.custom_prop(prop_bone, prop_id, text='') + row.operator('pose.rigify_switch_parent_bake_{rig_id}', text='', icon='ACTION_TWEAK', properties=op_props) + + def rig_bones(self): + for child in self.child_list: + self.__rig_child(child) + + def __rig_child(self, child): + if child['is_done']: + return + + child['is_done'] = True + + # Implement via an Armature constraint + mch = child['mch_bone'] + con = self.make_constraint(mch, 'ARMATURE', name='SWITCH_PARENT') + + prop_var = [(child['prop_bone'], child['prop_id'])] + + for i, (parent, parent_name) in enumerate(child['parent_bones']): + tgt = con.targets.new() + + tgt.target = self.obj + tgt.subtarget = parent + tgt.weight = 0.0 + + expr = 'var == %d' % (i+1) + self.make_driver(tgt, 'weight', expression=expr, variables=prop_var) + + # Add copy constraints + copy = child['copy'] + + if copy[0]: + self.make_constraint(mch, 'COPY_LOCATION', copy[0]) + if copy[1]: + self.make_constraint(mch, 'COPY_ROTATION', copy[1]) + if copy[2]: + self.make_constraint(mch, 'COPY_SCALE', copy[2]) + + +SCRIPT_REGISTER_OP_SWITCH_PARENT = ['POSE_OT_rigify_switch_parent', 'POSE_OT_rigify_switch_parent_bake'] + +SCRIPT_UTILITIES_OP_SWITCH_PARENT = [''' +################################ +## Switchable Parent operator ## +################################ + +class RigifySwitchParentBase: + bone: StringProperty(name="Control Bone") + prop_bone: StringProperty(name="Property Bone") + prop_id: StringProperty(name="Property") + parent_names: StringProperty(name="Parent Names") + locks: bpy.props.BoolVectorProperty(name="Locked", size=3, default=[False,False,False]) + + parent_items = [('0','None','None')] + + selected: bpy.props.EnumProperty( + name='Selected Parent', + items=lambda s,c: RigifySwitchParentBase.parent_items + ) + + keyflags = None + keyflags_switch = None + + def save_frame_state(self, context, obj): + return get_transform_matrix(obj, self.bone, with_constraints=False) + + def apply_frame_state(self, context, obj, old_matrix): + # Change the parent + set_custom_property_value( + obj, self.prop_bone, self.prop_id, int(self.selected), + keyflags=self.keyflags_switch + ) + + context.view_layer.update() + + # Set the transforms to restore position + set_transform_from_matrix( + obj, self.bone, old_matrix, keyflags=self.keyflags, + no_loc=self.locks[0], no_rot=self.locks[1], no_scale=self.locks[2] + ) + + def get_bone_props(self): + props = set() + if not self.locks[0]: + props |= TRANSFORM_PROPS_LOCATION + if not self.locks[1]: + props |= TRANSFORM_PROPS_ROTATION + if not self.locks[2]: + props |= TRANSFORM_PROPS_SCALE + return props + + def init_invoke(self, context): + pose = context.active_object.pose + + if (not pose or not self.parent_names + or self.bone not in pose.bones + or self.prop_bone not in pose.bones + or self.prop_id not in pose.bones[self.prop_bone]): + self.report({'ERROR'}, "Invalid parameters") + return {'CANCELLED'} + + parents = json.loads(self.parent_names) + pitems = [(str(i), name, name) for i, name in enumerate(parents)] + + RigifySwitchParentBase.parent_items = pitems + + self.selected = str(pose.bones[self.prop_bone][self.prop_id]) + + +class POSE_OT_rigify_switch_parent(RigifySwitchParentBase, RigifySingleUpdateMixin, bpy.types.Operator): + bl_idname = "pose.rigify_switch_parent_" + rig_id + bl_label = "Switch Parent (Keep Transform)" + bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} + bl_description = "Switch parent, preserving the bone position and orientation" + + def draw(self, _context): + col = self.layout.column() + col.prop(self, 'selected', expand=True) + + +class POSE_OT_rigify_switch_parent_bake(RigifySwitchParentBase, RigifyBakeKeyframesMixin, bpy.types.Operator): + bl_idname = "pose.rigify_switch_parent_bake_" + rig_id + bl_label = "Apply Switch Parent To Keyframes" + bl_options = {'UNDO', 'INTERNAL'} + bl_description = "Switch parent over a frame range, adjusting keys to preserve the bone position and orientation" + + def execute_scan_curves(self, context, obj): + return self.bake_add_bone_frames(self.bone, self.get_bone_props()) + + def execute_before_apply(self, context, obj, range, range_raw): + self.bake_replace_custom_prop_keys_constant(self.prop_bone, self.prop_id, int(self.selected)) + + def draw(self, context): + self.layout.prop(self, 'selected', text='') +'''] diff --git a/rigify/utils/widgets_basic.py b/rigify/utils/widgets_basic.py index aae8f6bb..de04aecc 100644 --- a/rigify/utils/widgets_basic.py +++ b/rigify/utils/widgets_basic.py @@ -109,12 +109,12 @@ def create_limb_widget(rig, bone_name, bone_transform_name=None): mesh.update() -def create_bone_widget(rig, bone_name, bone_transform_name=None): +def create_bone_widget(rig, bone_name, r1=0.1, l1=0.0, r2=0.04, l2=1.0, bone_transform_name=None): """ Creates a basic bone widget, a simple obolisk-esk shape. """ obj = create_widget(rig, bone_name, bone_transform_name) if obj != None: - verts = [(0.04, 1.0, -0.04), (0.1, 0.0, -0.1), (-0.1, 0.0, -0.1), (-0.04, 1.0, -0.04), (0.04, 1.0, 0.04), (0.1, 0.0, 0.1), (-0.1, 0.0, 0.1), (-0.04, 1.0, 0.04)] + verts = [(r2, l2, -r2), (r1, l1, -r1), (-r1, l1, -r1), (-r2, l2, -r2), (r2, l2, r2), (r1, l1, r1), (-r1, l1, r1), (-r2, l2, r2)] edges = [(1, 2), (0, 1), (0, 3), (2, 3), (4, 5), (5, 6), (6, 7), (4, 7), (1, 5), (0, 4), (2, 6), (3, 7)] mesh = obj.data mesh.from_pydata(verts, edges, []) |