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
path: root/rigify
diff options
context:
space:
mode:
authorAlexander Gavrilov <angavrilov@gmail.com>2019-03-30 22:00:55 +0300
committerAlexander Gavrilov <angavrilov@gmail.com>2019-09-14 09:29:26 +0300
commit3423174b37a0784dc12035ff3f2fb536835099e1 (patch)
tree3a54580902cdebdef5ebacd6099e86cc79ba75b3 /rigify
parent12af8a28c14b608e9b9b08568d981273c86590c1 (diff)
Rigify: redesign generate.py and introduce a base rig class.
The main goals are to provide an official way for rigs to interact in a structured way, and to remove mode switching within rigs. This involves introducing a base class for rigs that holds rig-to-rig and rig-to-bone references, converting the main generator into a class and passing it to rigs, and splitting the single generate method into multiple passes. For backward compatibility, old rigs are automatically handled via a wrapper that translates between old and new API. In addition, a way to create objects that receive the generate callbacks that aren't rigs is introduced via the GeneratorPlugin class. The UI script generation code is converted into a plugin. Making generic rig 'template' classes that are intended to be subclassed in specific rigs involves splitting operations done in each stage into multiple methods that can be overridden separately. The main callback thus ends up simply calling a sequence of other methods. To make such code cleaner it's better to allow registering those methods as new callbacks that would be automatically called by the system. This can be done via decorators. A new metaclass used for all rig and generate plugin classes builds and validates a table of all decorated methods, and allows calling them all together with the main callback. A new way to switch parents for IK bones based on the new features is introduced, and used in the existing limb rigs. Reviewers: icappiello campbellbarton Differential Revision: https://developer.blender.org/D4624
Diffstat (limited to 'rigify')
-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, [])