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
This commit is contained in:
Alexander Gavrilov 2019-03-30 22:00:55 +03:00
parent 12af8a28c1
commit 3423174b37
Notes: blender-bot 2023-02-14 19:05:30 +01:00
Referenced by issue #71678, Rigify doesn't generate if advanced option is set to "new"
28 changed files with 4240 additions and 1209 deletions

View File

@ -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
# 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(rig_lists)
importlib.reload(generate)
importlib.reload(ui)
importlib.reload(utils)
importlib.reload(feature_set_list)
importlib.reload(metarig_menu)
importlib.reload(rig_lists)
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:

433
rigify/base_generate.py Normal file
View File

@ -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)

264
rigify/base_rig.py Normal file
View File

@ -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)

File diff suppressed because it is too large Load Diff

View File

@ -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}

View File

@ -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):
""" 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)
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
##############################
## IK/FK snapping functions ##
##############################
def match_pole_target(ik_first, ik_last, pole, match_bone, length):
def correct_rotation(view_layer, bone_ik, target_matrix):
""" Corrects the ik rotation in ik2fk snapping functions
"""
axis = target_matrix.to_3x3().col[1].normalized()
def distance(angle):
# Rotate the bone and return the actual angle between bones
bone_ik.rotation_euler[1] = angle
view_layer.update()
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()
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)

View File

@ -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
from ...base_rig import BaseRig, stage
class Rig:
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.
"""
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.generate_bones
def make_control_chain(self):
if self.make_controls:
for bone in ctrl_chain:
create_bone_widget(self.obj, bone)
super().make_control_chain()
@stage.parent_bones
def parent_control_chain(self):
if self.make_controls:
super().parent_control_chain()
@stage.configure_bones
def configure_control_chain(self):
if self.make_controls:
super().configure_control_chain()
@stage.generate_widgets
def make_control_widgets(self):
if self.make_controls:
super().make_control_widgets()
##############################
# ORG chain
@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()
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")
@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")
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")
@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):

View File

@ -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
self.org_name = strip_org(self.bones.org)
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.make_control = self.params.make_control
self.make_widget = self.params.make_widget
self.make_deform = self.params.make_deform
"""
bpy.ops.object.mode_set(mode='EDIT')
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)
def parent_bones(self):
bones = self.bones
# 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]
self.set_bone_parent(bones.deform, bones.org, use_connect=False)
# Parent
if self.make_deform:
def_bone_e.use_connect = False
def_bone_e.parent = eb[self.org_bone]
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"
)
@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_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"
)
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 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

387
rigify/rigs/chain_rigs.py Normal file
View File

@ -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")

View File

@ -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)

View File

@ -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:

View File

@ -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'
from ...utils.switch_parent import SwitchParentBuilder
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 )
"""
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,31 +559,29 @@ class Rig:
eb[mch_main_parent].roll = 0.0
eb[bones['main_parent']].parent = eb[mch_main_parent]
# 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'], 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=[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 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 arm_parent:
make_constraint( self, ctrl_socket, {
'constraint' : 'COPY_TRANSFORMS',
'subtarget' : ctrl_parent,
})
make_constraint(self, ctrl_pole_socket, {
'constraint': 'COPY_TRANSFORMS',
'subtarget': ctrl_parent,
})
# Constrain mch target bone to the ik control and mch stretch
make_constraint( self, bones['ik']['mch_target'], {
'constraint' : 'COPY_LOCATION',
@ -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"

View File

@ -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'
from ...utils.switch_parent import SwitchParentBuilder
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 )
"""
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]],
}

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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,44 +116,12 @@ 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=''):
""" 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.
"""
#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 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
# Copy edit bone attributes
edit_bone_2.layers = list(edit_bone_1.layers)
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
return bone_name_2
else:
raise MetarigError("Cannot copy bones outside of edit mode")
def copy_bone(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.
Returns the resulting bone's name.
"""
@ -155,9 +138,6 @@ def copy_bone(obj, bone_name, assign_name=''):
bone_name_1 = bone_name
bone_name_2 = edit_bone_2.name
edit_bone_2.parent = edit_bone_1.parent
edit_bone_2.use_connect = edit_bone_1.use_connect
# Copy edit bone attributes
edit_bone_2.layers = list(edit_bone_1.layers)
@ -165,17 +145,36 @@ def copy_bone(obj, bone_name, assign_name=''):
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
if parent:
edit_bone_2.parent = edit_bone_1.parent
edit_bone_2.use_connect = edit_bone_1.use_connect
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
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
bpy.ops.object.mode_set(mode='OBJECT')
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))
# 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
return bone_name_2
else:
raise MetarigError("Cannot copy bones outside of edit mode")
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]
bpy.ops.object.mode_set(mode='EDIT')
return bone_name_2
else:
raise MetarigError("Cannot copy bones outside of edit mode")
raise MetarigError("Cannot copy bone properties in 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

View File

@ -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)

View File

@ -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]

View File

@ -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):

171
rigify/utils/metaclass.py Normal file
View File

@ -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

View File

@ -19,8 +19,12 @@
# <pep8 compliant>
import math
import collections
from itertools import tee, chain, islice, repeat
from mathutils import Vector, Matrix, Color
#=============================================
# Math
#=============================================
@ -81,6 +85,49 @@ def gamma_correct(color):
return corrected_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

View File

@ -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.
"""

View File

@ -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")

View File

@ -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='')
''']

View File

@ -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, [])