Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rigify/__init__.py34
-rw-r--r--rigify/base_generate.py433
-rw-r--r--rigify/base_rig.py264
-rw-r--r--rigify/generate.py967
-rw-r--r--rigify/rig_lists.py4
-rw-r--r--rigify/rig_ui_template.py565
-rw-r--r--rigify/rigs/basic/copy_chain.py176
-rw-r--r--rigify/rigs/basic/super_copy.py151
-rw-r--r--rigify/rigs/chain_rigs.py387
-rw-r--r--rigify/rigs/experimental/super_chain.py2
-rw-r--r--rigify/rigs/faces/super_face.py2
-rw-r--r--rigify/rigs/limbs/arm.py122
-rw-r--r--rigify/rigs/limbs/leg.py130
-rw-r--r--rigify/rigs/limbs/super_limb.py2
-rw-r--r--rigify/rigs/spines/super_spine.py10
-rw-r--r--rigify/ui.py55
-rw-r--r--rigify/utils/__init__.py9
-rw-r--r--rigify/utils/animation.py818
-rw-r--r--rigify/utils/bones.py332
-rw-r--r--rigify/utils/errors.py11
-rw-r--r--rigify/utils/layers.py9
-rw-r--r--rigify/utils/mechanism.py35
-rw-r--r--rigify/utils/metaclass.py171
-rw-r--r--rigify/utils/misc.py67
-rw-r--r--rigify/utils/naming.py154
-rw-r--r--rigify/utils/rig.py53
-rw-r--r--rigify/utils/switch_parent.py438
-rw-r--r--rigify/utils/widgets_basic.py4
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, [])