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

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Gavrilov <angavrilov@gmail.com>2021-08-10 21:19:11 +0300
committerAlexander Gavrilov <angavrilov@gmail.com>2021-08-10 21:19:24 +0300
commit2acf22b5932c57f7d29b578fc74542f81d4fb087 (patch)
treef98d1929d023ab45abc9291ce6273bff51dcfb9a
parentbb587e5bf80ea81d03f4ad7a22f209d687dbf818 (diff)
Rigify: move new face rig components from the experimental feature set.
Apart from imports the files are identical to the latest version. Ref T89808
-rw-r--r--rigify/rigs/face/basic_tongue.py206
-rw-r--r--rigify/rigs/face/skin_eye.py825
-rw-r--r--rigify/rigs/face/skin_jaw.py862
-rw-r--r--rigify/rigs/skin/anchor.py142
-rw-r--r--rigify/rigs/skin/basic_chain.py520
-rw-r--r--rigify/rigs/skin/glue.py321
-rw-r--r--rigify/rigs/skin/skin_nodes.py520
-rw-r--r--rigify/rigs/skin/skin_parents.py395
-rw-r--r--rigify/rigs/skin/skin_rigs.py241
-rw-r--r--rigify/rigs/skin/stretchy_chain.py422
-rw-r--r--rigify/rigs/skin/transform/basic.py148
-rw-r--r--rigify/utils/layers.py13
12 files changed, 4615 insertions, 0 deletions
diff --git a/rigify/rigs/face/basic_tongue.py b/rigify/rigs/face/basic_tongue.py
new file mode 100644
index 00000000..380e14df
--- /dev/null
+++ b/rigify/rigs/face/basic_tongue.py
@@ -0,0 +1,206 @@
+# ====================== 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 math
+
+from itertools import count
+
+from ...utils.naming import make_derived_name
+from ...utils.bones import flip_bone, copy_bone_position
+from ...utils.layers import ControlLayersOption
+from ...utils.misc import map_list
+
+from ...base_rig import stage
+
+from ..chain_rigs import TweakChainRig
+from ..widgets import create_jaw_widget
+
+
+class Rig(TweakChainRig):
+ """Basic tongue from the original PitchiPoy face rig."""
+
+ min_chain_length = 3
+
+ def initialize(self):
+ super().initialize()
+
+ self.bbone_segments = self.params.bbones
+
+ ####################################################
+ # BONES
+ #
+ # ctrl:
+ # master:
+ # Master control.
+ # mch:
+ # follow[]:
+ # Partial follow master bones.
+ #
+ ####################################################
+
+ ####################################################
+ # Control chain
+
+ @stage.generate_bones
+ def make_control_chain(self):
+ org = self.bones.org[0]
+ name = self.copy_bone(org, make_derived_name(org, 'ctrl'), parent=True)
+ flip_bone(self.obj, name)
+ self.bones.ctrl.master = name
+
+ @stage.parent_bones
+ def parent_control_chain(self):
+ pass
+
+ @stage.configure_bones
+ def configure_control_chain(self):
+ master = self.bones.ctrl.master
+
+ self.copy_bone_properties(self.bones.org[0], master)
+
+ ControlLayersOption.SKIN_PRIMARY.assign(self.params, self.obj, [master])
+
+ @stage.generate_widgets
+ def make_control_widgets(self):
+ create_jaw_widget(self.obj, self.bones.ctrl.master)
+
+ ####################################################
+ # Mechanism chain
+
+ @stage.generate_bones
+ def make_follow_chain(self):
+ self.bones.mch.follow = map_list(self.make_mch_follow_bone, count(1), self.bones.org[1:])
+
+ def make_mch_follow_bone(self, i, org):
+ name = self.copy_bone(org, make_derived_name(org, 'mch'))
+ copy_bone_position(self.obj, self.base_bone, name)
+ flip_bone(self.obj, name)
+ return name
+
+ @stage.parent_bones
+ def parent_follow_chain(self):
+ for mch in self.bones.mch.follow:
+ self.set_bone_parent(mch, self.rig_parent_bone)
+
+ @stage.rig_bones
+ def rig_follow_chain(self):
+ master = self.bones.ctrl.master
+ num_orgs = len(self.bones.org)
+
+ for i, mch in enumerate(self.bones.mch.follow):
+ self.make_constraint(mch, 'COPY_TRANSFORMS', master, influence=1-(1+i)/num_orgs)
+
+ ####################################################
+ # Tweak chain
+
+ @stage.parent_bones
+ def parent_tweak_chain(self):
+ ctrl = self.bones.ctrl
+ parents = [ctrl.master, *self.bones.mch.follow, self.rig_parent_bone]
+ for tweak, main in zip(ctrl.tweak, parents):
+ self.set_bone_parent(tweak, main)
+
+ ####################################################
+ # SETTINGS
+
+ @classmethod
+ def add_parameters(self, params):
+ params.bbones = bpy.props.IntProperty(
+ name='B-Bone Segments',
+ default=10,
+ min=1,
+ description='Number of B-Bone segments'
+ )
+
+ ControlLayersOption.SKIN_PRIMARY.add_parameters(params)
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ layout.prop(params, 'bbones')
+
+ ControlLayersOption.SKIN_PRIMARY.parameters_ui(layout, params)
+
+
+def create_sample(obj):
+ # generated by rigify.utils.write_metarig
+ bpy.ops.object.mode_set(mode='EDIT')
+ arm = obj.data
+
+ bones = {}
+
+ bone = arm.edit_bones.new('tongue')
+ bone.head = 0.0000, 0.0000, 0.0000
+ bone.tail = 0.0000, 0.0161, 0.0074
+ bone.roll = 0.0000
+ bone.use_connect = False
+ bones['tongue'] = bone.name
+ bone = arm.edit_bones.new('tongue.001')
+ bone.head = 0.0000, 0.0161, 0.0074
+ bone.tail = 0.0000, 0.0375, 0.0091
+ bone.roll = 0.0000
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['tongue']]
+ bones['tongue.001'] = bone.name
+ bone = arm.edit_bones.new('tongue.002')
+ bone.head = 0.0000, 0.0375, 0.0091
+ bone.tail = 0.0000, 0.0605, -0.0029
+ bone.roll = 0.0000
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['tongue.001']]
+ bones['tongue.002'] = bone.name
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+ pbone = obj.pose.bones[bones['tongue']]
+ pbone.rigify_type = 'face.basic_tongue'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['tongue.001']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['tongue.002']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+
+ bpy.ops.object.mode_set(mode='EDIT')
+ for bone in arm.edit_bones:
+ bone.select = False
+ bone.select_head = False
+ bone.select_tail = False
+ for b in bones:
+ bone = arm.edit_bones[bones[b]]
+ bone.select = True
+ bone.select_head = True
+ bone.select_tail = True
+ bone.bbone_x = bone.bbone_z = bone.length * 0.05
+ arm.edit_bones.active = bone
+
+ return bones
diff --git a/rigify/rigs/face/skin_eye.py b/rigify/rigs/face/skin_eye.py
new file mode 100644
index 00000000..498a90c4
--- /dev/null
+++ b/rigify/rigs/face/skin_eye.py
@@ -0,0 +1,825 @@
+# ====================== 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 math
+import functools
+import mathutils
+
+from itertools import count
+from mathutils import Vector, Matrix
+
+from ...utils.naming import make_derived_name, mirror_name, change_name_side, Side, SideZ
+from ...utils.bones import align_bone_z_axis, put_bone
+from ...utils.widgets import (widget_generator, generate_circle_geometry,
+ generate_circle_hull_geometry)
+from ...utils.widgets_basic import create_circle_widget
+from ...utils.switch_parent import SwitchParentBuilder
+from ...utils.misc import map_list, matrix_from_axis_pair, LazyRef
+
+from ...base_rig import stage, RigComponent
+
+from ..skin.skin_nodes import ControlBoneNode
+from ..skin.skin_parents import ControlBoneParentOffset
+from ..skin.skin_rigs import BaseSkinRig
+
+from ..skin.basic_chain import Rig as BasicChainRig
+
+
+class Rig(BaseSkinRig):
+ """
+ Eye rig that manages two child eyelid chains. The chains must
+ connect at their ends using T/B symmetry.
+ """
+
+ def find_org_bones(self, bone):
+ return bone.name
+
+ cluster_control = None
+
+ def initialize(self):
+ super().initialize()
+
+ bone = self.get_bone(self.base_bone)
+ self.center = bone.head
+ self.axis = bone.vector
+
+ self.eye_corner_nodes = []
+ self.eye_corner_matrix = None
+
+ # Create the cluster control (it will assign self.cluster_control)
+ if not self.cluster_control:
+ self.create_cluster_control()
+
+ self.init_child_chains()
+
+ def create_cluster_control(self):
+ return EyeClusterControl(self)
+
+ ####################################################
+ # UTILITIES
+
+ def is_eye_control_node(self, node):
+ return node.rig in self.child_chains and node.is_master_node
+
+ def is_eye_corner_node(self, node):
+ # Corners are nodes where the two T and B chains merge
+ sides = set(n.name_split.side_z for n in node.get_merged_siblings())
+ return {SideZ.BOTTOM, SideZ.TOP}.issubset(sides)
+
+ def init_eye_corner_space(self):
+ """Initialize the coordinate space of the eye based on two corners."""
+ if self.eye_corner_matrix:
+ return
+
+ if len(self.eye_corner_nodes) != 2:
+ self.raise_error('Expected 2 eye corners, but found {}', len(self.eye_corner_nodes))
+
+ # Build a coordinate space with XY plane based on center and two corners,
+ # and Y axis oriented as close to the eye axis as possible.
+ vecs = [(node.point - self.center).normalized() for node in self.eye_corner_nodes]
+ normal = vecs[0].cross(vecs[1])
+ space_axis = self.axis - self.axis.project(normal)
+
+ matrix = matrix_from_axis_pair(space_axis, normal, 'z').to_4x4()
+ matrix.translation = self.center
+ self.eye_corner_matrix = matrix.inverted()
+
+ # Compute signed angles from space_axis to the eye corners
+ amin, amax = self.eye_corner_range = list(
+ sorted(map(self.get_eye_corner_angle, self.eye_corner_nodes)))
+
+ if not (amin <= 0 <= amax):
+ self.raise_error('Bad relative angles of eye corners: {}..{}',
+ math.degrees(amin), math.degrees(amax))
+
+ def get_eye_corner_angle(self, node):
+ """Compute a signed Z rotation angle from the eye axis to the node."""
+ pt = self.eye_corner_matrix @ node.point
+ return math.atan2(pt.x, pt.y)
+
+ def get_master_control_position(self):
+ """Compute suitable position for the master control."""
+ self.init_eye_corner_space()
+
+ # Place the control between the two corners on the eye axis
+ pcorners = [node.point for node in self.eye_corner_nodes]
+
+ point, _ = mathutils.geometry.intersect_line_line(
+ self.center, self.center + self.axis, pcorners[0], pcorners[1]
+ )
+ return point
+
+ def get_lid_follow_influence(self, node):
+ """Compute the influence factor of the eye movement on this eyelid control node."""
+ self.init_eye_corner_space()
+
+ # Interpolate from axis to corners based on Z angle
+ angle = self.get_eye_corner_angle(node)
+ amin, amax = self.eye_corner_range
+
+ if amin < angle < 0:
+ return 1 - min(1, angle/amin) ** 2
+ elif 0 < angle < amax:
+ return 1 - min(1, angle/amax) ** 2
+ else:
+ return 0
+
+ ####################################################
+ # BONES
+ #
+ # ctrl:
+ # master:
+ # Parent control for moving the whole eye.
+ # target:
+ # Individual target this eye aims for.
+ # mch:
+ # master:
+ # Bone that rotates to track ctrl.target.
+ # track:
+ # Bone that translates to follow mch.master tail.
+ # deform:
+ # master:
+ # Deform mirror of ctrl.master.
+ # eye:
+ # Deform bone that rotates with mch.master.
+ # iris:
+ # Iris deform bone at master tail that scales with ctrl.target
+ #
+ ####################################################
+
+ ####################################################
+ # CHILD CHAINS
+
+ def init_child_chains(self):
+ self.child_chains = [rig for rig in self.rigify_children if isinstance(rig, BasicChainRig)]
+
+ # Inject a component twisting handles to the eye radius
+ for child in self.child_chains:
+ self.patch_chain(child)
+
+ def patch_chain(self, child):
+ return EyelidChainPatch(child, self)
+
+ ####################################################
+ # CONTROL NODES
+
+ def extend_control_node_parent(self, parent, node):
+ if self.is_eye_control_node(node):
+ if self.is_eye_corner_node(node):
+ # Remember corners for later computations
+ assert not self.eye_corner_matrix
+ self.eye_corner_nodes.append(node)
+ else:
+ # Non-corners get extra motion applied to them
+ return self.extend_mid_node_parent(parent, node)
+
+ return parent
+
+ def extend_mid_node_parent(self, parent, node):
+ parent = ControlBoneParentOffset(self, node, parent)
+
+ # Add movement of the eye to the eyelid controls
+ parent.add_copy_local_location(
+ LazyRef(self.bones.mch, 'track'),
+ influence=LazyRef(self.get_lid_follow_influence, node)
+ )
+
+ # If Limit Distance on the control can be disabled, add another one to the mch
+ if self.params.eyelid_detach_option:
+ parent.add_limit_distance(
+ self.bones.org,
+ distance=(node.point - self.center).length,
+ limit_mode='LIMITDIST_ONSURFACE', use_transform_limit=True,
+ # Use custom space to accomodate scaling
+ space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
+ # Don't allow reordering this limit and subsequent offsets
+ ensure_order=True,
+ )
+
+ return parent
+
+ def extend_control_node_rig(self, node):
+ if self.is_eye_control_node(node):
+ # Add Limit Distance to enforce following the surface of the eye to the control
+ con = self.make_constraint(
+ node.control_bone, 'LIMIT_DISTANCE', self.bones.org,
+ distance=(node.point - self.center).length,
+ limit_mode='LIMITDIST_ONSURFACE', use_transform_limit=True,
+ # Use custom space to accomodate scaling
+ space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
+ )
+
+ if self.params.eyelid_detach_option:
+ self.make_driver(con, 'influence',
+ variables=[(self.bones.ctrl.target, 'lid_attach')])
+
+ ####################################################
+ # SCRIPT
+
+ @stage.configure_bones
+ def configure_script_panels(self):
+ ctrl = self.bones.ctrl
+
+ controls = sum((chain.get_all_controls() for chain in self.child_chains), ctrl.flatten())
+ panel = self.script.panel_with_selected_check(self, controls)
+
+ self.add_custom_properties()
+ self.add_ui_sliders(panel)
+
+ def add_custom_properties(self):
+ target = self.bones.ctrl.target
+
+ if self.params.eyelid_follow_split:
+ self.make_property(
+ target, 'lid_follow', list(self.params.eyelid_follow_default),
+ description='Eylids follow eye movement (X and Z)'
+ )
+ else:
+ self.make_property(target, 'lid_follow', 1.0,
+ description='Eylids follow eye movement')
+
+ if self.params.eyelid_detach_option:
+ self.make_property(target, 'lid_attach', 1.0,
+ description='Eylids follow eye surface')
+
+ def add_ui_sliders(self, panel, *, add_name=False):
+ target = self.bones.ctrl.target
+
+ name_tail = f' ({target})' if add_name else ''
+ follow_text = f'Eyelids Follow{name_tail}'
+
+ if self.params.eyelid_follow_split:
+ row = panel.split(factor=0.66, align=True)
+ row.custom_prop(target, 'lid_follow', index=0, text=follow_text, slider=True)
+ row.custom_prop(target, 'lid_follow', index=1, text='', slider=True)
+ else:
+ panel.custom_prop(target, 'lid_follow', text=follow_text, slider=True)
+
+ if self.params.eyelid_detach_option:
+ panel.custom_prop(
+ target, 'lid_attach', text=f'Eyelids Attached{name_tail}', slider=True)
+
+ ####################################################
+ # Master control
+
+ @stage.generate_bones
+ def make_master_control(self):
+ org = self.bones.org
+ name = self.copy_bone(org, make_derived_name(org, 'ctrl', '_master'), parent=True)
+ put_bone(self.obj, name, self.get_master_control_position())
+ self.bones.ctrl.master = name
+
+ @stage.configure_bones
+ def configure_master_control(self):
+ self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
+
+ @stage.generate_widgets
+ def make_master_control_widget(self):
+ ctrl = self.bones.ctrl.master
+ create_circle_widget(self.obj, ctrl, radius=1, head_tail=0.25)
+
+ ####################################################
+ # Tracking MCH
+
+ @stage.generate_bones
+ def make_mch_track_bones(self):
+ org = self.bones.org
+ mch = self.bones.mch
+
+ mch.master = self.copy_bone(org, make_derived_name(org, 'mch'))
+ mch.track = self.copy_bone(org, make_derived_name(org, 'mch', '_track'), scale=1/4)
+
+ put_bone(self.obj, mch.track, self.get_bone(org).tail)
+
+ @stage.parent_bones
+ def parent_mch_track_bones(self):
+ mch = self.bones.mch
+ ctrl = self.bones.ctrl
+ self.set_bone_parent(mch.master, ctrl.master)
+ self.set_bone_parent(mch.track, ctrl.master)
+
+ @stage.rig_bones
+ def rig_mch_track_bones(self):
+ mch = self.bones.mch
+ ctrl = self.bones.ctrl
+
+ # Rotationally track the target bone in mch.master
+ self.make_constraint(mch.master, 'DAMPED_TRACK', ctrl.target)
+
+ # Translate to track the tail of mch.master in mch.track. Its local
+ # location is then copied to the control nodes.
+ # Two constraints are used to provide different X and Z influence values.
+ con_x = self.make_constraint(
+ mch.track, 'COPY_LOCATION', mch.master, head_tail=1, name='lid_follow_x',
+ use_xyz=(True, False, False),
+ space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
+ )
+
+ con_z = self.make_constraint(
+ mch.track, 'COPY_LOCATION', mch.master, head_tail=1, name='lid_follow_z',
+ use_xyz=(False, False, True),
+ space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
+ )
+
+ # Apply follow slider influence(s)
+ if self.params.eyelid_follow_split:
+ self.make_driver(con_x, 'influence', variables=[(ctrl.target, 'lid_follow', 0)])
+ self.make_driver(con_z, 'influence', variables=[(ctrl.target, 'lid_follow', 1)])
+ else:
+ factor = self.params.eyelid_follow_default
+
+ self.make_driver(
+ con_x, 'influence', expression=f'var*{factor[0]}',
+ variables=[(ctrl.target, 'lid_follow')]
+ )
+ self.make_driver(
+ con_z, 'influence', expression=f'var*{factor[1]}',
+ variables=[(ctrl.target, 'lid_follow')]
+ )
+
+ ####################################################
+ # ORG bone
+
+ @stage.parent_bones
+ def parent_org_chain(self):
+ self.set_bone_parent(self.bones.org, self.bones.ctrl.master, inherit_scale='FULL')
+
+ ####################################################
+ # Deform bones
+
+ @stage.generate_bones
+ def make_deform_bone(self):
+ org = self.bones.org
+ deform = self.bones.deform
+ deform.master = self.copy_bone(org, make_derived_name(org, 'def', '_master'), scale=3/2)
+
+ if self.params.make_deform:
+ deform.eye = self.copy_bone(org, make_derived_name(org, 'def'))
+ deform.iris = self.copy_bone(org, make_derived_name(org, 'def', '_iris'), scale=1/2)
+ put_bone(self.obj, deform.iris, self.get_bone(org).tail)
+
+ @stage.parent_bones
+ def parent_deform_chain(self):
+ deform = self.bones.deform
+ self.set_bone_parent(deform.master, self.bones.org)
+
+ if self.params.make_deform:
+ self.set_bone_parent(deform.eye, self.bones.mch.master)
+ self.set_bone_parent(deform.iris, deform.eye)
+
+ @stage.rig_bones
+ def rig_deform_chain(self):
+ if self.params.make_deform:
+ # Copy XZ local scale from the eye target control
+ self.make_constraint(
+ self.bones.deform.iris, 'COPY_SCALE', self.bones.ctrl.target,
+ owner_space='LOCAL', target_space='LOCAL_OWNER_ORIENT', use_y=False,
+ )
+
+ ####################################################
+ # SETTINGS
+
+ @classmethod
+ def add_parameters(self, params):
+ params.make_deform = bpy.props.BoolProperty(
+ name="Deform",
+ default=True,
+ description="Create a deform bone for the copy"
+ )
+
+ params.eyelid_detach_option = bpy.props.BoolProperty(
+ name="Eyelid Detach Option",
+ default=False,
+ description="Create an option to detach eyelids from the eye surface"
+ )
+
+ params.eyelid_follow_split = bpy.props.BoolProperty(
+ name="Split Eyelid Follow Slider",
+ default=False,
+ description="Create separate eyelid follow influence sliders for X and Z"
+ )
+
+ params.eyelid_follow_default = bpy.props.FloatVectorProperty(
+ size=2,
+ name="Eyelids Follow Default",
+ default=(0.2, 0.7), min=0, max=1,
+ description="Default setting for the Eyelids Follow sliders (X and Z)",
+ )
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ col = layout.column()
+ col.prop(params, "make_deform", text="Eyball And Iris Deforms")
+ col.prop(params, "eyelid_detach_option")
+
+ col.prop(params, "eyelid_follow_split")
+
+ row = col.row(align=True)
+ row.prop(params, "eyelid_follow_default", index=0, text="Follow X", slider=True)
+ row.prop(params, "eyelid_follow_default", index=1, text="Follow Z", slider=True)
+
+
+class EyelidChainPatch(RigComponent):
+ """Component injected into child chains to twist handles aiming Z axis at the eye center."""
+
+ rigify_sub_object_run_late = True
+
+ def __init__(self, owner, eye):
+ super().__init__(owner)
+
+ self.eye = eye
+ self.owner.use_pre_handles = True
+
+ def align_bone(self, name):
+ """Align bone rest orientation to aim Z axis at the eye center."""
+ align_bone_z_axis(self.obj, name, self.eye.center - self.get_bone(name).head)
+
+ def prepare_bones(self):
+ for org in self.owner.bones.org:
+ self.align_bone(org)
+
+ def generate_bones(self):
+ if self.owner.use_bbones:
+ mch = self.owner.bones.mch
+ for pre in [*mch.handles_pre, *mch.handles]:
+ self.align_bone(pre)
+
+ def rig_bones(self):
+ if self.owner.use_bbones:
+ for pre, node in zip(self.owner.bones.mch.handles_pre, self.owner.control_nodes):
+ self.make_constraint(pre, 'COPY_LOCATION', node.control_bone, name='locate_cur')
+ self.make_constraint(
+ pre, 'LOCKED_TRACK', self.eye.bones.org, name='track_center',
+ track_axis='TRACK_Z', lock_axis='LOCK_Y',
+ )
+
+
+class EyeClusterControl(RigComponent):
+ """Component generating a common control for an eye cluster."""
+
+ def __init__(self, owner):
+ super().__init__(owner)
+
+ self.find_cluster_rigs()
+
+ def find_cluster_rigs(self):
+ """Find and register all other eyes that belong to this cluster."""
+ owner = self.owner
+
+ owner.cluster_control = self
+ self.rig_list = [owner]
+
+ # Collect all sibling eye rigs
+ parent_rig = owner.rigify_parent
+ if parent_rig:
+ for rig in parent_rig.rigify_children:
+ if isinstance(rig, Rig) and rig != owner:
+ rig.cluster_control = self
+ self.rig_list.append(rig)
+
+ self.rig_count = len(self.rig_list)
+
+ ####################################################
+ # UTILITIES
+
+ def find_cluster_position(self):
+ """Compute the eye cluster control position and orientation."""
+
+ # Average location and Y axis of all the eyes
+ axis = Vector((0, 0, 0))
+ center = Vector((0, 0, 0))
+ length = 0
+
+ for rig in self.rig_list:
+ bone = self.get_bone(rig.base_bone)
+ axis += bone.y_axis
+ center += bone.head
+ length += bone.length
+
+ axis /= self.rig_count
+ center /= self.rig_count
+ length /= self.rig_count
+
+ # Create the matrix from the average Y and world Z
+ matrix = matrix_from_axis_pair((0, 0, 1), axis, 'z').to_4x4()
+ matrix.translation = center + axis * length * 5
+
+ self.size = length * 3 / 4
+ self.matrix = matrix
+ self.inv_matrix = matrix.inverted()
+
+ def project_rig_control(self, rig):
+ """Intersect the given eye Y axis with the cluster plane, returns (x,y,0)."""
+ bone = self.get_bone(rig.base_bone)
+
+ head = self.inv_matrix @ bone.head
+ tail = self.inv_matrix @ bone.tail
+ axis = tail - head
+
+ return head + axis * (-head.z / axis.z)
+
+ def get_common_rig_name(self):
+ """Choose a name for the cluster control based on the members."""
+ names = set(rig.base_bone for rig in self.rig_list)
+ name = min(names)
+
+ if mirror_name(name) in names:
+ return change_name_side(name, side=Side.MIDDLE)
+
+ return name
+
+ def get_rig_control_matrix(self, rig):
+ """Compute a matrix for an individual eye sub-control."""
+ matrix = self.matrix.copy()
+ matrix.translation = self.matrix @ self.rig_points[rig]
+ return matrix
+
+ def get_master_control_layers(self):
+ """Combine layers of all eyes for the cluster control."""
+ all_layers = [list(self.get_bone(rig.base_bone).layers) for rig in self.rig_list]
+ return [any(items) for items in zip(*all_layers)]
+
+ def get_all_rig_control_bones(self):
+ """Make a list of all control bones of all clustered eyes."""
+ return list(set(sum((rig.bones.ctrl.flatten() for rig in self.rig_list), [self.master_bone])))
+
+ ####################################################
+ # STAGES
+
+ def initialize(self):
+ self.find_cluster_position()
+ self.rig_points = {rig: self.project_rig_control(rig) for rig in self.rig_list}
+
+ def generate_bones(self):
+ if self.rig_count > 1:
+ self.master_bone = self.make_master_control()
+ self.child_bones = []
+
+ for rig in self.rig_list:
+ rig.bones.ctrl.target = child = self.make_child_control(rig)
+ self.child_bones.append(child)
+ else:
+ self.master_bone = self.make_child_control(self.rig_list[0])
+ self.child_bones = [self.master_bone]
+ self.owner.bones.ctrl.target = self.master_bone
+
+ self.build_parent_switch()
+
+ def make_master_control(self):
+ name = self.new_bone(make_derived_name(self.get_common_rig_name(), 'ctrl', '_common'))
+ bone = self.get_bone(name)
+ bone.matrix = self.matrix
+ bone.length = self.size
+ bone.layers = self.get_master_control_layers()
+ return name
+
+ def make_child_control(self, rig):
+ name = rig.copy_bone(
+ rig.base_bone, make_derived_name(rig.base_bone, 'ctrl'), length=self.size)
+ self.get_bone(name).matrix = self.get_rig_control_matrix(rig)
+ return name
+
+ def build_parent_switch(self):
+ pbuilder = SwitchParentBuilder(self.owner.generator)
+
+ org_parent = self.owner.rig_parent_bone
+ parents = [org_parent] if org_parent else []
+
+ pbuilder.build_child(
+ self.owner, self.master_bone,
+ prop_name=f'Parent ({self.master_bone})',
+ extra_parents=parents, select_parent=org_parent,
+ controls=self.get_all_rig_control_bones
+ )
+
+ def parent_bones(self):
+ if self.rig_count > 1:
+ for child in self.child_bones:
+ self.set_bone_parent(child, self.master_bone)
+
+ def configure_bones(self):
+ for child in self.child_bones:
+ bone = self.get_bone(child)
+ bone.lock_rotation = (True, True, True)
+ bone.lock_rotation_w = True
+
+ # When the cluster master control is selected, show sliders for all eyes
+ if self.rig_count > 1:
+ panel = self.owner.script.panel_with_selected_check(self.owner, [self.master_bone])
+
+ for rig in self.rig_list:
+ rig.add_ui_sliders(panel, add_name=True)
+
+ def generate_widgets(self):
+ for child in self.child_bones:
+ create_eye_widget(self.obj, child)
+
+ if self.rig_count > 1:
+ pt2d = [p.to_2d() / self.size for p in self.rig_points.values()]
+ create_eye_cluster_widget(self.obj, self.master_bone, points=pt2d)
+
+
+@widget_generator
+def create_eye_widget(geom, *, size=1):
+ generate_circle_geometry(geom, Vector((0, 0, 0)), size/2)
+
+
+@widget_generator
+def create_eye_cluster_widget(geom, *, size=1, points):
+ hpoints = [points[i] for i in mathutils.geometry.convex_hull_2d(points)]
+
+ generate_circle_hull_geometry(geom, hpoints, size*0.75, size*0.6)
+ generate_circle_hull_geometry(geom, hpoints, size, size*0.85)
+
+
+def create_sample(obj):
+ # generated by rigify.utils.write_metarig
+ bpy.ops.object.mode_set(mode='EDIT')
+ arm = obj.data
+
+ bones = {}
+
+ bone = arm.edit_bones.new('eye.L')
+ bone.head = 0.0000, 0.0000, 0.0000
+ bone.tail = 0.0000, -0.0125, 0.0000
+ bone.roll = 0.0000
+ bone.use_connect = False
+ bones['eye.L'] = bone.name
+ bone = arm.edit_bones.new('lid1.T.L')
+ bone.head = 0.0155, -0.0006, -0.0003
+ bone.tail = 0.0114, -0.0099, 0.0029
+ bone.roll = 2.9453
+ bone.use_connect = False
+ bone.parent = arm.edit_bones[bones['eye.L']]
+ bones['lid1.T.L'] = bone.name
+ bone = arm.edit_bones.new('lid1.B.L')
+ bone.head = 0.0155, -0.0006, -0.0003
+ bone.tail = 0.0112, -0.0095, -0.0039
+ bone.roll = -0.0621
+ bone.use_connect = False
+ bone.parent = arm.edit_bones[bones['eye.L']]
+ bones['lid1.B.L'] = bone.name
+ bone = arm.edit_bones.new('lid2.T.L')
+ bone.head = 0.0114, -0.0099, 0.0029
+ bone.tail = 0.0034, -0.0149, 0.0040
+ bone.roll = 2.1070
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lid1.T.L']]
+ bones['lid2.T.L'] = bone.name
+ bone = arm.edit_bones.new('lid2.B.L')
+ bone.head = 0.0112, -0.0095, -0.0039
+ bone.tail = 0.0029, -0.0140, -0.0057
+ bone.roll = 0.8337
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lid1.B.L']]
+ bones['lid2.B.L'] = bone.name
+ bone = arm.edit_bones.new('lid3.T.L')
+ bone.head = 0.0034, -0.0149, 0.0040
+ bone.tail = -0.0046, -0.0157, 0.0026
+ bone.roll = 1.7002
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lid2.T.L']]
+ bones['lid3.T.L'] = bone.name
+ bone = arm.edit_bones.new('lid3.B.L')
+ bone.head = 0.0029, -0.0140, -0.0057
+ bone.tail = -0.0041, -0.0145, -0.0057
+ bone.roll = 1.0671
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lid2.B.L']]
+ bones['lid3.B.L'] = bone.name
+ bone = arm.edit_bones.new('lid4.T.L')
+ bone.head = -0.0046, -0.0157, 0.0026
+ bone.tail = -0.0123, -0.0140, -0.0049
+ bone.roll = 1.0850
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lid3.T.L']]
+ bones['lid4.T.L'] = bone.name
+ bone = arm.edit_bones.new('lid4.B.L')
+ bone.head = -0.0041, -0.0145, -0.0057
+ bone.tail = -0.0123, -0.0140, -0.0049
+ bone.roll = 1.1667
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lid3.B.L']]
+ bones['lid4.B.L'] = bone.name
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+ pbone = obj.pose.bones[bones['eye.L']]
+ pbone.rigify_type = 'face.skin_eye'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lid1.T.L']]
+ pbone.rigify_type = 'skin.stretchy_chain'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ try:
+ pbone.rigify_parameters.skin_chain_pivot_pos = 2
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.bbones = 5
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_connect_mirror = [False, False]
+ except AttributeError:
+ pass
+ pbone = obj.pose.bones[bones['lid1.B.L']]
+ pbone.rigify_type = 'skin.stretchy_chain'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ try:
+ pbone.rigify_parameters.skin_chain_pivot_pos = 2
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.bbones = 5
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_connect_mirror = [False, False]
+ except AttributeError:
+ pass
+ pbone = obj.pose.bones[bones['lid2.T.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lid2.B.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lid3.T.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lid3.B.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lid4.T.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lid4.B.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+
+ bpy.ops.object.mode_set(mode='EDIT')
+ for bone in arm.edit_bones:
+ bone.select = False
+ bone.select_head = False
+ bone.select_tail = False
+ for b in bones:
+ bone = arm.edit_bones[bones[b]]
+ bone.select = True
+ bone.select_head = True
+ bone.select_tail = True
+ bone.bbone_x = bone.bbone_z = bone.length * 0.05
+ arm.edit_bones.active = bone
+
+ return bones
diff --git a/rigify/rigs/face/skin_jaw.py b/rigify/rigs/face/skin_jaw.py
new file mode 100644
index 00000000..6829818c
--- /dev/null
+++ b/rigify/rigs/face/skin_jaw.py
@@ -0,0 +1,862 @@
+# ====================== 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 math
+
+from itertools import count, repeat
+from mathutils import Vector, Matrix
+from bl_math import clamp
+
+from ...utils.naming import make_derived_name, Side, SideZ, get_name_side_z
+from ...utils.bones import align_bone_z_axis, put_bone
+from ...utils.misc import map_list, matrix_from_axis_pair, LazyRef
+from ...utils.widgets_basic import create_circle_widget
+
+from ...base_rig import stage, RigComponent
+
+from ..skin.skin_nodes import ControlBoneNode
+from ..skin.skin_parents import ControlBoneParentOrg, ControlBoneParentArmature
+from ..skin.skin_rigs import BaseSkinRig
+
+from ..skin.basic_chain import Rig as BasicChainRig
+
+from ..widgets import create_jaw_widget
+
+
+class Rig(BaseSkinRig):
+ """
+ Jaw rig that manages loops of four mouth chains each. The chains
+ must connect together at their ends using L/R and T/B symmetry.
+ """
+
+ def find_org_bones(self, bone):
+ return bone.name
+
+ def initialize(self):
+ super().initialize()
+
+ self.mouth_orientation = self.get_mouth_orientation()
+ self.chain_to_layer = None
+
+ self.init_child_chains()
+
+ ####################################################
+ # UTILITIES
+
+ def get_mouth_orientation(self):
+ jaw_axis = self.get_bone(self.base_bone).y_axis.copy()
+ jaw_axis[2] = 0
+
+ return matrix_from_axis_pair(jaw_axis, (0, 0, 1), 'z').to_quaternion()
+
+ def is_corner_node(self, node):
+ # Corners are nodes where two T/B or L/R chains meet.
+ siblings = [n for n in node.get_merged_siblings() if n.rig in self.child_chains]
+
+ sides_x = set(n.name_split.side for n in siblings)
+ sides_z = set(n.name_split.side_z for n in siblings)
+
+ if {SideZ.BOTTOM, SideZ.TOP}.issubset(sides_z):
+ if Side.LEFT in sides_x:
+ return Side.LEFT
+ else:
+ return Side.RIGHT
+
+ if {Side.LEFT, Side.RIGHT}.issubset(sides_x):
+ if SideZ.TOP in sides_z:
+ return SideZ.TOP
+ else:
+ return SideZ.BOTTOM
+
+ return None
+
+ ####################################################
+ # BONES
+ #
+ # ctrl:
+ # master:
+ # Main jaw open control.
+ # mouth:
+ # Main control for adjusting mouth position and scale.
+ # mch:
+ # lock:
+ # Jaw master mirror for the locked mouth.
+ # top[]:
+ # Jaw master mirrors for the loop top.
+ # bottom[]:
+ # Jaw master mirrors for the loop bottom.
+ # middle[]:
+ # Middle position between top[] and bottom[].
+ # mouth_parent = middle[0]:
+ # Parent for ctrl.mouth, mouth_layers and *_in
+ # mouth_layers[]:
+ # Apply fade out of ctrl.mouth motion for outer loops.
+ # top_out[], bottom_out[], middle_out[]:
+ # Combine mouth and jaw motions via Copy Custom to Local.
+ # deform:
+ # master:
+ # Deform mirror of ctrl.master.
+ #
+ ####################################################
+
+ ####################################################
+ # CHILD CHAINS
+
+ def init_child_chains(self):
+ self.child_chains = [
+ rig
+ for rig in self.rigify_children
+ if isinstance(rig, BasicChainRig) and get_name_side_z(rig.base_bone) != SideZ.MIDDLE
+ ]
+
+ self.corners = {Side.LEFT: [], Side.RIGHT: [], SideZ.TOP: [], SideZ.BOTTOM: []}
+
+ def arrange_child_chains(self):
+ """Sort child chains into their corresponding mouth loops."""
+ if self.chain_to_layer is not None:
+ return
+
+ # Index child node corners
+ for child in self.child_chains:
+ for node in child.control_nodes:
+ corner = self.is_corner_node(node)
+ if corner:
+ if node.merged_master not in self.corners[corner]:
+ self.corners[corner].append(node.merged_master)
+
+ self.num_layers = len(self.corners[SideZ.TOP])
+
+ for k, v in self.corners.items():
+ if len(v) == 0:
+ self.raise_error("Could not find all mouth corners")
+ if len(v) != self.num_layers:
+ self.raise_error(
+ "Mouth corner counts differ: {} vs {}",
+ [n.name for n in v], [n.name for n in self.corners[SideZ.TOP]]
+ )
+
+ # Find inner top/bottom corners
+ anchor = self.corners[SideZ.BOTTOM][0].point
+ inner_top = min(self.corners[SideZ.TOP], key=lambda p: (p.point - anchor).length)
+
+ anchor = inner_top.point
+ inner_bottom = min(self.corners[SideZ.BOTTOM], key=lambda p: (p.point - anchor).length)
+
+ # Compute the mouth space
+ self.mouth_center = center = (inner_top.point + inner_bottom.point) / 2
+
+ matrix = self.mouth_orientation.to_matrix().to_4x4()
+ matrix.translation = center
+ self.mouth_space = matrix
+ self.to_mouth_space = matrix.inverted()
+
+ # Build a mapping of child chain to layer (i.e. sort multiple mouth loops)
+ self.chain_to_layer = {}
+ self.chains_by_side = {}
+
+ for k, v in list(self.corners.items()):
+ self.corners[k] = ordered = sorted(v, key=lambda p: (p.point - center).length)
+
+ chain_set = set()
+
+ for i, node in enumerate(ordered):
+ for sibling in node.get_merged_siblings():
+ if sibling.rig in self.child_chains:
+ cur_layer = self.chain_to_layer.get(sibling.rig)
+
+ if cur_layer is not None and cur_layer != i:
+ self.raise_error(
+ "Conflicting mouth chain layer on {}: {} and {}", sibling.rig.base_bone, i, cur_layer)
+
+ self.chain_to_layer[sibling.rig] = i
+ chain_set.add(sibling.rig)
+
+ self.chains_by_side[k] = chain_set
+
+ for child in self.child_chains:
+ if child not in self.chain_to_layer:
+ self.raise_error("Could not determine chain layer on {}", child.base_bone)
+
+ if not self.chains_by_side[Side.LEFT].isdisjoint(self.chains_by_side[Side.RIGHT]):
+ self.raise_error("Left/right conflict in mouth")
+ if not self.chains_by_side[SideZ.TOP].isdisjoint(self.chains_by_side[SideZ.BOTTOM]):
+ self.raise_error("Top/bottom conflict in mouth")
+
+ # Find left/right direction
+ pt = self.to_mouth_space @ self.corners[Side.LEFT][0].point
+
+ self.left_sign = 1 if pt.x > 0 else -1
+
+ for node in self.corners[Side.LEFT]:
+ if (self.to_mouth_space @ node.point).x * self.left_sign <= 0:
+ self.raise_error("Bad left corner location: {}", node.name)
+
+ for node in self.corners[Side.RIGHT]:
+ if (self.to_mouth_space @ node.point).x * self.left_sign >= 0:
+ self.raise_error("Bad right corner location: {}", node.name)
+
+ # Find layer loop widths
+ self.layer_width = [
+ (self.corners[Side.LEFT][i].point - self.corners[Side.RIGHT][i].point).length
+ for i in range(self.num_layers)
+ ]
+
+ def position_mouth_bone(self, name, scale):
+ self.arrange_child_chains()
+
+ bone = self.get_bone(name)
+ bone.matrix = self.mouth_space
+ bone.length = self.layer_width[0] * scale
+
+ ####################################################
+ # CONTROL NODES
+
+ def get_node_parent_bones(self, node):
+ """Get parent bones and their armature weights for the given control node."""
+ self.arrange_child_chains()
+
+ # Choose correct layer bones
+ layer = self.chain_to_layer[node.rig]
+
+ top_mch = LazyRef(self.bones.mch, 'top_out', layer)
+ bottom_mch = LazyRef(self.bones.mch, 'bottom_out', layer)
+ middle_mch = LazyRef(self.bones.mch, 'middle_out', layer)
+
+ # Corners have one input
+ corner = self.is_corner_node(node)
+ if corner:
+ if corner == SideZ.TOP:
+ return [top_mch]
+ elif corner == SideZ.BOTTOM:
+ return [bottom_mch]
+ else:
+ return [middle_mch]
+
+ # Otherwise blend two
+ if node.rig in self.chains_by_side[SideZ.TOP]:
+ side_mch = top_mch
+ else:
+ side_mch = bottom_mch
+
+ pt_x = (self.to_mouth_space @ node.point).x
+ side = Side.LEFT if pt_x * self.left_sign >= 0 else Side.RIGHT
+
+ corner_x = (self.to_mouth_space @ self.corners[side][layer].point).x
+ factor = math.sqrt(1 - clamp(pt_x / corner_x) ** 2)
+
+ return [(side_mch, factor), (middle_mch, 1-factor)]
+
+ def get_parent_for_name(self, name, parent_bone):
+ """Get single replacement parent for the given child bone."""
+ if parent_bone == self.base_bone:
+ side = get_name_side_z(name)
+ if side == SideZ.TOP:
+ return LazyRef(self.bones.mch, 'top', -1)
+ if side == SideZ.BOTTOM:
+ return LazyRef(self.bones.mch, 'bottom', -1)
+
+ return parent_bone
+
+ def get_child_chain_parent(self, rig, parent_bone):
+ return self.get_parent_for_name(rig.base_bone, parent_bone)
+
+ def build_control_node_parent(self, node, parent_bone):
+ if node.rig in self.child_chains:
+ return ControlBoneParentArmature(
+ self, node,
+ bones=self.get_node_parent_bones(node),
+ orientation=self.mouth_orientation,
+ copy_scale=LazyRef(self.bones.mch, 'mouth_parent'),
+ )
+
+ return ControlBoneParentOrg(self.get_parent_for_name(node.name, parent_bone))
+
+ ####################################################
+ # Master control
+
+ @stage.generate_bones
+ def make_master_control(self):
+ org = self.bones.org
+ name = self.copy_bone(org, make_derived_name(org, 'ctrl'), parent=True)
+ self.bones.ctrl.master = name
+
+ @stage.configure_bones
+ def configure_master_control(self):
+ self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
+
+ self.get_bone(self.bones.ctrl.master).lock_scale = (True, True, True)
+
+ @stage.generate_widgets
+ def make_master_control_widget(self):
+ ctrl = self.bones.ctrl.master
+ create_jaw_widget(self.obj, ctrl)
+
+ ####################################################
+ # Mouth control
+
+ @stage.generate_bones
+ def make_mouth_control(self):
+ org = self.bones.org
+ name = self.copy_bone(org, make_derived_name(org, 'ctrl', '_mouth'))
+ self.position_mouth_bone(name, 1)
+ self.bones.ctrl.mouth = name
+
+ @stage.parent_bones
+ def parent_mouth_control(self):
+ self.set_bone_parent(self.bones.ctrl.mouth, self.bones.mch.mouth_parent)
+
+ @stage.configure_bones
+ def configure_mouth_control(self):
+ pass
+
+ @stage.generate_widgets
+ def make_mouth_control_widget(self):
+ ctrl = self.bones.ctrl.mouth
+
+ width = (self.corners[Side.LEFT][0].point - self.corners[Side.RIGHT][0].point).length
+ height = (self.corners[SideZ.TOP][0].point - self.corners[SideZ.BOTTOM][0].point).length
+ back = (self.corners[Side.LEFT][0].point + self.corners[Side.RIGHT][0].point) / 2
+ front = (self.corners[SideZ.TOP][0].point + self.corners[SideZ.BOTTOM][0].point) / 2
+ depth = (front - back).length
+
+ create_circle_widget(
+ self.obj, ctrl,
+ radius=0.2 + 0.5 * (height / width), radius_x=0.7,
+ head_tail=0.2, head_tail_x=0.2 - (depth / width)
+ )
+
+ ####################################################
+ # Jaw Motion MCH
+
+ @stage.generate_bones
+ def make_mch_lock_bones(self):
+ org = self.bones.org
+ mch = self.bones.mch
+
+ self.arrange_child_chains()
+
+ mch.lock = self.copy_bone(
+ org, make_derived_name(org, 'mch', '_lock'), scale=1/2, parent=True)
+
+ mch.top = map_list(self.make_mch_top_bone, range(self.num_layers), repeat(org))
+ mch.bottom = map_list(self.make_mch_bottom_bone, range(self.num_layers), repeat(org))
+ mch.middle = map_list(self.make_mch_middle_bone, range(self.num_layers), repeat(org))
+
+ mch.mouth_parent = mch.middle[0]
+
+ def make_mch_top_bone(self, i, org):
+ return self.copy_bone(org, make_derived_name(org, 'mch', '_top'), scale=1/4, parent=True)
+
+ def make_mch_bottom_bone(self, i, org):
+ return self.copy_bone(org, make_derived_name(org, 'mch', '_bottom'), scale=1/3, parent=True)
+
+ def make_mch_middle_bone(self, i, org):
+ return self.copy_bone(org, make_derived_name(org, 'mch', '_middle'), scale=2/3, parent=True)
+
+ @stage.parent_bones
+ def parent_mch_lock_bones(self):
+ mch = self.bones.mch
+ ctrl = self.bones.ctrl
+
+ for mid, top in zip(mch.middle, mch.top):
+ self.set_bone_parent(mid, top)
+
+ for bottom in mch.bottom[1:]:
+ self.set_bone_parent(bottom, ctrl.master)
+
+ @stage.configure_bones
+ def configure_mch_lock_bones(self):
+ ctrl = self.bones.ctrl
+
+ panel = self.script.panel_with_selected_check(self, [ctrl.master, ctrl.mouth])
+
+ self.make_property(ctrl.master, 'mouth_lock', 0.0, description='Mouth is locked closed')
+ panel.custom_prop(ctrl.master, 'mouth_lock', text='Mouth Lock', slider=True)
+
+ @stage.rig_bones
+ def rig_mch_track_bones(self):
+ mch = self.bones.mch
+ ctrl = self.bones.ctrl
+
+ # Lock position follows jaw master with configured influence
+ self.make_constraint(
+ mch.lock, 'COPY_TRANSFORMS', ctrl.master,
+ influence=self.params.jaw_locked_influence,
+ )
+
+ # Innermost top bone follows lock position according to slider
+ con = self.make_constraint(mch.top[0], 'COPY_TRANSFORMS', mch.lock)
+ self.make_driver(con, 'influence', variables=[(ctrl.master, 'mouth_lock')])
+
+ # Innermost bottom bone follows jaw master with configured influence, and then lock
+ self.make_constraint(
+ mch.bottom[0], 'COPY_TRANSFORMS', ctrl.master,
+ influence=self.params.jaw_mouth_influence,
+ )
+
+ con = self.make_constraint(mch.bottom[0], 'COPY_TRANSFORMS', mch.lock)
+ self.make_driver(con, 'influence', variables=[(ctrl.master, 'mouth_lock')])
+
+ # Outer layer bones interpolate toward innermost based on influence decay
+ coeff = self.params.jaw_secondary_influence
+
+ for i, name in enumerate(mch.top[1:]):
+ self.make_constraint(name, 'COPY_TRANSFORMS', mch.top[0], influence=coeff ** (1+i))
+
+ for i, name in enumerate(mch.bottom[1:]):
+ self.make_constraint(name, 'COPY_TRANSFORMS', mch.bottom[0], influence=coeff ** (1+i))
+
+ # Middle bones interpolate the middle between top and bottom
+ for mid, bottom in zip(mch.middle, mch.bottom):
+ self.make_constraint(mid, 'COPY_TRANSFORMS', bottom, influence=0.5)
+
+ ####################################################
+ # Mouth MCH
+
+ @stage.generate_bones
+ def make_mch_mouth_bones(self):
+ mch = self.bones.mch
+
+ mch.mouth_layers = map_list(self.make_mch_mouth_bone,
+ range(1, self.num_layers), repeat('_mouth_layer'), repeat(0.6))
+
+ mch.top_out = map_list(self.make_mch_mouth_inout_bone,
+ range(self.num_layers), repeat('_top_out'), repeat(0.4))
+ mch.bottom_out = map_list(self.make_mch_mouth_inout_bone,
+ range(self.num_layers), repeat('_bottom_out'), repeat(0.35))
+ mch.middle_out = map_list(self.make_mch_mouth_inout_bone,
+ range(self.num_layers), repeat('_middle_out'), repeat(0.3))
+
+ def make_mch_mouth_bone(self, i, suffix, size):
+ name = self.copy_bone(self.bones.org, make_derived_name(self.bones.org, 'mch', suffix))
+ self.position_mouth_bone(name, size)
+ return name
+
+ def make_mch_mouth_inout_bone(self, i, suffix, size):
+ return self.copy_bone(self.bones.org, make_derived_name(self.bones.org, 'mch', suffix), scale=size)
+
+ @stage.parent_bones
+ def parent_mch_mouth_bones(self):
+ mch = self.bones.mch
+ layers = [self.bones.ctrl.mouth, *mch.mouth_layers]
+
+ for name in mch.mouth_layers:
+ self.set_bone_parent(name, mch.mouth_parent)
+
+ for name_list in [mch.top_out, mch.bottom_out, mch.middle_out]:
+ for name, parent in zip(name_list, layers):
+ self.set_bone_parent(name, parent)
+
+ @stage.rig_bones
+ def rig_mch_mouth_bones(self):
+ mch = self.bones.mch
+ ctrl = self.bones.ctrl.mouth
+
+ # Mouth influence fade out
+ for i, name in enumerate(mch.mouth_layers):
+ self.rig_mch_mouth_layer_bone(i+1, name, ctrl)
+
+ # Transfer and combine jaw motion with mouth
+ all_jaw = mch.top + mch.bottom + mch.middle
+ all_out = mch.top_out + mch.bottom_out + mch.middle_out
+
+ for dest, src in zip(all_out, all_jaw):
+ self.make_constraint(
+ dest, 'COPY_TRANSFORMS', src,
+ owner_space='LOCAL', target_space='CUSTOM',
+ space_object=self.obj, space_subtarget=mch.mouth_parent,
+ )
+
+ def rig_mch_mouth_layer_bone(self, i, mch, ctrl):
+ # Fade location and rotation based on influence decay
+ inf = self.params.jaw_secondary_influence ** i
+
+ self.make_constraint(mch, 'COPY_LOCATION', ctrl, influence=inf)
+ self.make_constraint(mch, 'COPY_ROTATION', ctrl, influence=inf)
+
+ # For scale, additionally take radius into account
+ inf_scale = inf * self.layer_width[0] / self.layer_width[i]
+
+ self.make_constraint(mch, 'COPY_SCALE', ctrl, influence=inf_scale)
+
+ ####################################################
+ # ORG bone
+
+ @stage.parent_bones
+ def parent_org_chain(self):
+ self.set_bone_parent(self.bones.org, self.bones.ctrl.master, inherit_scale='FULL')
+
+ ####################################################
+ # Deform bones
+
+ @stage.generate_bones
+ def make_deform_bone(self):
+ org = self.bones.org
+ deform = self.bones.deform
+ self.bones.deform.master = self.copy_bone(org, make_derived_name(org, 'def'))
+
+ @stage.parent_bones
+ def parent_deform_chain(self):
+ deform = self.bones.deform
+ self.set_bone_parent(deform.master, self.bones.org)
+
+ ####################################################
+ # SETTINGS
+
+ @classmethod
+ def add_parameters(self, params):
+ params.jaw_mouth_influence = bpy.props.FloatProperty(
+ name="Bottom Lip Influence",
+ default=0.5, min=0, max=1,
+ description="Influence of the jaw on the bottom lip chains"
+ )
+
+ params.jaw_locked_influence = bpy.props.FloatProperty(
+ name="Locked Influence",
+ default=0.2, min=0, max=1,
+ description="Influence of the jaw on the locked mouth"
+ )
+
+ params.jaw_secondary_influence = bpy.props.FloatProperty(
+ name="Secondary Influence Falloff",
+ default=0.5, min=0, max=1,
+ description="Reduction factor for each level of secondary mouth loops"
+ )
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ layout.prop(params, "jaw_mouth_influence", slider=True)
+ layout.prop(params, "jaw_locked_influence", slider=True)
+ layout.prop(params, "jaw_secondary_influence", slider=True)
+
+
+def create_sample(obj):
+ # generated by rigify.utils.write_metarig
+ bpy.ops.object.mode_set(mode='EDIT')
+ arm = obj.data
+
+ bones = {}
+
+ bone = arm.edit_bones.new('jaw')
+ bone.head = 0.0000, 0.0000, 0.0000
+ bone.tail = 0.0000, -0.0585, -0.0489
+ bone.roll = 0.0000
+ bone.use_connect = False
+ bones['jaw'] = bone.name
+ bone = arm.edit_bones.new('teeth.T')
+ bone.head = 0.0000, -0.0589, 0.0080
+ bone.tail = 0.0000, -0.0283, 0.0080
+ bone.roll = 0.0000
+ bone.use_connect = False
+ bones['teeth.T'] = bone.name
+ bone = arm.edit_bones.new('lip.T.L')
+ bone.head = -0.0000, -0.0684, 0.0030
+ bone.tail = 0.0105, -0.0655, 0.0033
+ bone.roll = -0.0000
+ bone.use_connect = False
+ bone.parent = arm.edit_bones[bones['jaw']]
+ bones['lip.T.L'] = bone.name
+ bone = arm.edit_bones.new('lip.B.L')
+ bone.head = -0.0000, -0.0655, -0.0078
+ bone.tail = 0.0107, -0.0625, -0.0053
+ bone.roll = -0.0551
+ bone.use_connect = False
+ bone.parent = arm.edit_bones[bones['jaw']]
+ bones['lip.B.L'] = bone.name
+ bone = arm.edit_bones.new('lip.T.R')
+ bone.head = 0.0000, -0.0684, 0.0030
+ bone.tail = -0.0105, -0.0655, 0.0033
+ bone.roll = 0.0000
+ bone.use_connect = False
+ bone.parent = arm.edit_bones[bones['jaw']]
+ bones['lip.T.R'] = bone.name
+ bone = arm.edit_bones.new('lip.B.R')
+ bone.head = 0.0000, -0.0655, -0.0078
+ bone.tail = -0.0107, -0.0625, -0.0053
+ bone.roll = 0.0551
+ bone.use_connect = False
+ bone.parent = arm.edit_bones[bones['jaw']]
+ bones['lip.B.R'] = bone.name
+ bone = arm.edit_bones.new('teeth.B')
+ bone.head = 0.0000, -0.0543, -0.0136
+ bone.tail = 0.0000, -0.0237, -0.0136
+ bone.roll = 0.0000
+ bone.use_connect = False
+ bone.parent = arm.edit_bones[bones['jaw']]
+ bones['teeth.B'] = bone.name
+ bone = arm.edit_bones.new('lip1.T.L')
+ bone.head = 0.0105, -0.0655, 0.0033
+ bone.tail = 0.0193, -0.0586, 0.0007
+ bone.roll = -0.0257
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lip.T.L']]
+ bones['lip1.T.L'] = bone.name
+ bone = arm.edit_bones.new('lip1.B.L')
+ bone.head = 0.0107, -0.0625, -0.0053
+ bone.tail = 0.0194, -0.0573, -0.0029
+ bone.roll = 0.0716
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lip.B.L']]
+ bones['lip1.B.L'] = bone.name
+ bone = arm.edit_bones.new('lip1.T.R')
+ bone.head = -0.0105, -0.0655, 0.0033
+ bone.tail = -0.0193, -0.0586, 0.0007
+ bone.roll = 0.0257
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lip.T.R']]
+ bones['lip1.T.R'] = bone.name
+ bone = arm.edit_bones.new('lip1.B.R')
+ bone.head = -0.0107, -0.0625, -0.0053
+ bone.tail = -0.0194, -0.0573, -0.0029
+ bone.roll = -0.0716
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lip.B.R']]
+ bones['lip1.B.R'] = bone.name
+ bone = arm.edit_bones.new('lip2.T.L')
+ bone.head = 0.0193, -0.0586, 0.0007
+ bone.tail = 0.0236, -0.0539, -0.0014
+ bone.roll = 0.0324
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lip1.T.L']]
+ bones['lip2.T.L'] = bone.name
+ bone = arm.edit_bones.new('lip2.B.L')
+ bone.head = 0.0194, -0.0573, -0.0029
+ bone.tail = 0.0236, -0.0539, -0.0014
+ bone.roll = 0.0467
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lip1.B.L']]
+ bones['lip2.B.L'] = bone.name
+ bone = arm.edit_bones.new('lip2.T.R')
+ bone.head = -0.0193, -0.0586, 0.0007
+ bone.tail = -0.0236, -0.0539, -0.0014
+ bone.roll = -0.0324
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lip1.T.R']]
+ bones['lip2.T.R'] = bone.name
+ bone = arm.edit_bones.new('lip2.B.R')
+ bone.head = -0.0194, -0.0573, -0.0029
+ bone.tail = -0.0236, -0.0539, -0.0014
+ bone.roll = -0.0467
+ bone.use_connect = True
+ bone.parent = arm.edit_bones[bones['lip1.B.R']]
+ bones['lip2.B.R'] = bone.name
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+ pbone = obj.pose.bones[bones['jaw']]
+ pbone.rigify_type = 'face.skin_jaw'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['teeth.T']]
+ pbone.rigify_type = 'basic.super_copy'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ try:
+ pbone.rigify_parameters.make_deform = False
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.super_copy_widget_type = "teeth"
+ except AttributeError:
+ pass
+ pbone = obj.pose.bones[bones['lip.T.L']]
+ pbone.rigify_type = 'skin.stretchy_chain'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ try:
+ pbone.rigify_parameters.bbones = 3
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
+ except AttributeError:
+ pass
+ pbone = obj.pose.bones[bones['lip.B.L']]
+ pbone.rigify_type = 'skin.stretchy_chain'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ try:
+ pbone.rigify_parameters.bbones = 3
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
+ except AttributeError:
+ pass
+ pbone = obj.pose.bones[bones['lip.T.R']]
+ pbone.rigify_type = 'skin.stretchy_chain'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ try:
+ pbone.rigify_parameters.bbones = 3
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
+ except AttributeError:
+ pass
+ pbone = obj.pose.bones[bones['lip.B.R']]
+ pbone.rigify_type = 'skin.stretchy_chain'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ try:
+ pbone.rigify_parameters.bbones = 3
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
+ except AttributeError:
+ pass
+ pbone = obj.pose.bones[bones['teeth.B']]
+ pbone.rigify_type = 'basic.super_copy'
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ try:
+ pbone.rigify_parameters.super_copy_widget_type = "teeth"
+ except AttributeError:
+ pass
+ try:
+ pbone.rigify_parameters.make_deform = False
+ except AttributeError:
+ pass
+ pbone = obj.pose.bones[bones['lip1.T.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lip1.B.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lip1.T.R']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lip1.B.R']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lip2.T.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lip2.B.L']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lip2.T.R']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+ pbone = obj.pose.bones[bones['lip2.B.R']]
+ pbone.rigify_type = ''
+ pbone.lock_location = (False, False, False)
+ pbone.lock_rotation = (False, False, False)
+ pbone.lock_rotation_w = False
+ pbone.lock_scale = (False, False, False)
+ pbone.rotation_mode = 'QUATERNION'
+
+ bpy.ops.object.mode_set(mode='EDIT')
+ for bone in arm.edit_bones:
+ bone.select = False
+ bone.select_head = False
+ bone.select_tail = False
+ for b in bones:
+ bone = arm.edit_bones[bones[b]]
+ bone.select = True
+ bone.select_head = True
+ bone.select_tail = True
+ bone.bbone_x = bone.bbone_z = bone.length * 0.05
+ arm.edit_bones.active = bone
+
+ return bones
diff --git a/rigify/rigs/skin/anchor.py b/rigify/rigs/skin/anchor.py
new file mode 100644
index 00000000..0392761f
--- /dev/null
+++ b/rigify/rigs/skin/anchor.py
@@ -0,0 +1,142 @@
+# ====================== 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 ...utils.naming import make_derived_name
+from ...utils.widgets import layout_widget_dropdown, create_registered_widget
+from ...utils.mechanism import move_all_constraints
+
+from ...base_rig import stage
+
+from .skin_nodes import ControlBoneNode, ControlNodeIcon, ControlNodeEnd
+from .skin_rigs import BaseSkinChainRigWithRotationOption
+
+from ..basic.raw_copy import RelinkConstraintsMixin
+
+
+class Rig(BaseSkinChainRigWithRotationOption, RelinkConstraintsMixin):
+ """Custom skin control node."""
+
+ chain_priority = 20
+
+ def find_org_bones(self, bone):
+ return bone.name
+
+ def initialize(self):
+ super().initialize()
+
+ self.make_deform = self.params.make_extra_deform
+
+ ####################################################
+ # CONTROL NODES
+
+ @stage.initialize
+ def init_control_nodes(self):
+ org = self.bones.org
+ name = make_derived_name(org, 'ctrl')
+
+ self.control_node = node = ControlBoneNode(
+ self, org, name, icon=ControlNodeIcon.CUSTOM, chain_end=ControlNodeEnd.START)
+
+ node.hide_control = self.params.skin_anchor_hide
+
+ def make_control_node_widget(self, node):
+ create_registered_widget(self.obj, node.control_bone,
+ self.params.pivot_master_widget_type or 'cube')
+
+ def extend_control_node_rig(self, node):
+ if node.rig == self:
+ org = self.bones.org
+
+ self.copy_bone_properties(org, node.control_bone)
+
+ self.relink_bone_constraints(org)
+
+ move_all_constraints(self.obj, org, node.control_bone)
+
+ ##############################
+ # ORG chain
+
+ @stage.parent_bones
+ def parent_org_chain(self):
+ self.set_bone_parent(self.bones.org, self.control_node.control_bone)
+
+ ##############################
+ # Deform bone
+
+ @stage.generate_bones
+ def make_deform_bone(self):
+ if self.make_deform:
+ self.bones.deform = self.copy_bone(
+ self.bones.org, make_derived_name(self.bones.org, 'def'))
+
+ @stage.parent_bones
+ def parent_deform_chain(self):
+ if self.make_deform:
+ self.set_bone_parent(self.bones.deform, self.bones.org)
+
+ ####################################################
+ # SETTINGS
+
+ @classmethod
+ def add_parameters(self, params):
+ params.make_extra_deform = bpy.props.BoolProperty(
+ name="Extra Deform",
+ default=False,
+ description="Create an optional deform bone"
+ )
+
+ params.skin_anchor_hide = bpy.props.BoolProperty(
+ name='Suppress Control',
+ default=False,
+ description='Make the control bone a mechanism bone invisible to the user and only affected by constraints'
+ )
+
+ params.pivot_master_widget_type = bpy.props.StringProperty(
+ name="Widget Type",
+ default='cube',
+ description="Choose the type of the widget to create"
+ )
+
+ self.add_relink_constraints_params(params)
+
+ super().add_parameters(params)
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ col = layout.column()
+ col.prop(params, "make_extra_deform", text='Generate Deform Bone')
+ col.prop(params, "skin_anchor_hide")
+
+ row = layout.row()
+ row.active = not params.skin_anchor_hide
+ layout_widget_dropdown(row, params, "pivot_master_widget_type")
+
+ layout.prop(params, "relink_constraints")
+
+ layout.label(text="All constraints are moved to the control bone.", icon='INFO')
+
+ super().parameters_ui(layout, params)
+
+
+def create_sample(obj):
+ from rigify.rigs.basic.super_copy import create_sample as inner
+ obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.anchor'
diff --git a/rigify/rigs/skin/basic_chain.py b/rigify/rigs/skin/basic_chain.py
new file mode 100644
index 00000000..b2cac8a6
--- /dev/null
+++ b/rigify/rigs/skin/basic_chain.py
@@ -0,0 +1,520 @@
+# ====================== 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 math
+
+from itertools import count, repeat
+from mathutils import Vector, Matrix, Quaternion
+
+from math import acos
+from bl_math import smoothstep
+
+from ...utils.rig import connected_children_names, rig_is_child
+from ...utils.layers import ControlLayersOption
+from ...utils.naming import make_derived_name
+from ...utils.bones import align_bone_orientation, align_bone_to_axis, align_bone_roll
+from ...utils.mechanism import driver_var_distance
+from ...utils.widgets_basic import create_cube_widget, create_sphere_widget
+from ...utils.misc import map_list, matrix_from_axis_roll
+
+from ...base_rig import stage
+
+from .skin_nodes import ControlBoneNode, ControlNodeEnd
+from .skin_rigs import BaseSkinChainRigWithRotationOption, get_bone_quaternion
+
+
+class Rig(BaseSkinChainRigWithRotationOption):
+ """
+ Base deform rig of the skin system, implementing a B-Bone chain without
+ any automation on the control nodes.
+ """
+
+ chain_priority = None
+
+ def find_org_bones(self, bone):
+ return [bone.name] + connected_children_names(self.obj, bone.name)
+
+ def initialize(self):
+ super().initialize()
+
+ self.bbone_segments = self.params.bbones
+ self.use_bbones = self.bbone_segments > 1
+ self.use_connect_mirror = self.params.skin_chain_connect_mirror
+ self.use_connect_ends = self.params.skin_chain_connect_ends
+ self.use_scale = any(self.params.skin_chain_use_scale)
+ self.use_reparent_handles = self.params.skin_chain_use_reparent
+
+ orgs = self.bones.org
+
+ self.num_orgs = len(orgs)
+ self.length = sum([self.get_bone(b).length for b in orgs]) / len(orgs)
+
+ ####################################################
+ # OVERRIDES
+
+ def get_control_node_rotation(self, node):
+ """Compute the chain-aligned control orientation."""
+ orgs = self.bones.org
+
+ # Average the adjoining org bone orientations
+ bones = orgs[max(0, node.index-1):node.index+1]
+ quats = [get_bone_quaternion(self.obj, name) for name in bones]
+ result = sum(quats, Quaternion((0, 0, 0, 0))).normalized()
+
+ # For end bones, align to the connected chain tangent
+ if node.index in (0, self.num_orgs):
+ chain = self.get_node_chain_with_mirror()
+ nprev = chain[node.index]
+ nnext = chain[node.index+2]
+
+ if nprev and nnext:
+ # Apply only swing to preserve roll; tgt roll thus doesn't matter
+ tgt = matrix_from_axis_roll(nnext.point - nprev.point, 0).to_quaternion()
+ swing, _ = (result.inverted() @ tgt).to_swing_twist('Y')
+ result = result @ swing
+
+ return result
+
+ def get_all_controls(self):
+ return [node.control_bone for node in self.control_nodes]
+
+ ####################################################
+ # BONES
+ #
+ # mch:
+ # handles[]
+ # Final B-Bone handles.
+ # handles_pre[] (optional, may be copy of handles[])
+ # Mechanism bones that emulate Auto handle behavior.
+ # deform[]:
+ # Deformation B-Bones.
+ #
+ ####################################################
+
+ ####################################################
+ # CONTROL NODES
+
+ @stage.initialize
+ def init_control_nodes(self):
+ orgs = self.bones.org
+
+ self.control_nodes = nodes = [
+ # Bone head nodes
+ *map_list(self.make_control_node, count(0), orgs, repeat(False)),
+ # Tail of the final bone
+ self.make_control_node(len(orgs), orgs[-1], True),
+ ]
+
+ self.control_node_chain = None
+
+ nodes[0].chain_end_neighbor = nodes[1]
+ nodes[-1].chain_end_neighbor = nodes[-2]
+
+ def make_control_node(self, i, org, is_end):
+ bone = self.get_bone(org)
+ name = make_derived_name(org, 'ctrl', '_end' if is_end else '')
+ pos = bone.tail if is_end else bone.head
+
+ if i == 0:
+ chain_end = ControlNodeEnd.START
+ elif is_end:
+ chain_end = ControlNodeEnd.END
+ else:
+ chain_end = ControlNodeEnd.MIDDLE
+
+ return ControlBoneNode(
+ self, org, name, point=pos, size=self.length/3, index=i,
+ allow_scale=self.use_scale, needs_reparent=self.use_reparent_handles,
+ chain_end=chain_end,
+ )
+
+ def make_control_node_widget(self, node):
+ create_sphere_widget(self.obj, node.control_bone)
+
+ ####################################################
+ # B-Bone handle MCH
+
+ # Generate two layers of handle bones, 'pre' for the auto handle mechanism,
+ # and final handles combining that with user transformation. This flag may
+ # be enabled by parent controller rigs when needed in order to be able to
+ # inject more automatic handle positioning mechanisms.
+ use_pre_handles = False
+
+ def get_connected_node(self, node):
+ """Find which other chain to connect this chain to at this node."""
+ is_end = 1 if node.index != 0 else 0
+ corner = self.params.skin_chain_connect_sharp_angle[is_end]
+
+ # First try merge through mirror
+ if self.use_connect_mirror[is_end]:
+ mirror = node.get_best_mirror()
+
+ if mirror and mirror.chain_end_neighbor and isinstance(mirror.rig, Rig):
+ # Connect the same chain end
+ s_is_end = 1 if mirror.index != 0 else 0
+
+ if is_end == s_is_end and mirror.rig.use_connect_mirror[is_end]:
+ mirror_corner = mirror.rig.params.skin_chain_connect_sharp_angle[is_end]
+
+ return mirror, mirror.chain_end_neighbor, (corner + mirror_corner)/2
+
+ # Then try connecting ends
+ if self.use_connect_ends[is_end]:
+ # Find chains that want to connect ends at this node group
+ groups = ([], [])
+
+ for sibling in node.get_merged_siblings():
+ if isinstance(sibling.rig, Rig) and sibling.chain_end_neighbor:
+ s_is_end = 1 if sibling.index != 0 else 0
+
+ if sibling.rig.use_connect_ends[s_is_end]:
+ groups[s_is_end].append(sibling)
+
+ # Only connect if the pairing is unambiguous
+ if len(groups[0]) == 1 and len(groups[1]) == 1:
+ assert node == groups[is_end][0]
+
+ link = groups[1 - is_end][0]
+ link_corner = link.rig.params.skin_chain_connect_sharp_angle[1 - is_end]
+
+ return link, link.chain_end_neighbor, (corner + link_corner)/2
+
+ return None, None, 0
+
+ def get_node_chain_with_mirror(self):
+ """Get node chain with connected node extensions at the ends."""
+ if self.control_node_chain is not None:
+ return self.control_node_chain
+
+ nodes = self.control_nodes
+ prev_link, self.prev_node, self.prev_corner = self.get_connected_node(nodes[0])
+ next_link, self.next_node, self.next_corner = self.get_connected_node(nodes[-1])
+
+ self.control_node_chain = [self.prev_node, *nodes, self.next_node]
+
+ # Optimize connect next by sharing last handle mch
+ if next_link and next_link.index == 0:
+ self.next_chain_rig = next_link.rig
+ else:
+ self.next_chain_rig = None
+
+ return self.control_node_chain
+
+ def get_all_mch_handles(self):
+ if self.next_chain_rig:
+ return self.bones.mch.handles + [self.next_chain_rig.bones.mch.handles[0]]
+ else:
+ return self.bones.mch.handles
+
+ def get_all_mch_handles_pre(self):
+ if self.next_chain_rig:
+ return self.bones.mch.handles_pre + [self.next_chain_rig.bones.mch.handles_pre[0]]
+ else:
+ return self.bones.mch.handles_pre
+
+ @stage.generate_bones
+ def make_mch_handle_bones(self):
+ if self.use_bbones:
+ mch = self.bones.mch
+ chain = self.get_node_chain_with_mirror()
+
+ # If the last handle mch will be shared, drop it from chain
+ if self.next_chain_rig:
+ chain = chain[0:-1]
+
+ mch.handles = map_list(self.make_mch_handle_bone, count(0),
+ chain, chain[1:], chain[2:])
+
+ if self.use_pre_handles:
+ mch.handles_pre = map_list(self.make_mch_pre_handle_bone, count(0), mch.handles)
+ else:
+ mch.handles_pre = mch.handles
+
+ def make_mch_handle_bone(self, i, prev_node, node, next_node):
+ name = self.copy_bone(node.org, make_derived_name(node.name, 'mch', '_handle'))
+
+ hstart = prev_node or node
+ hend = next_node or node
+ haxis = (hend.point - hstart.point).normalized()
+
+ bone = self.get_bone(name)
+ bone.tail = bone.head + haxis * self.length * 3/4
+
+ align_bone_roll(self.obj, name, node.org)
+ return name
+
+ def make_mch_pre_handle_bone(self, i, handle):
+ return self.copy_bone(handle, make_derived_name(handle, 'mch', '_pre'))
+
+ @stage.parent_bones
+ def parent_mch_handle_bones(self):
+ if self.use_bbones:
+ mch = self.bones.mch
+
+ if self.use_pre_handles:
+ for pre in mch.handles_pre:
+ self.set_bone_parent(pre, self.rig_parent_bone, inherit_scale='AVERAGE')
+
+ for handle in mch.handles:
+ self.set_bone_parent(handle, self.rig_parent_bone, inherit_scale='AVERAGE')
+
+ @stage.rig_bones
+ def rig_mch_handle_bones(self):
+ if self.use_bbones:
+ mch = self.bones.mch
+ chain = self.get_node_chain_with_mirror()
+
+ # Rig Auto-handle emulation (on pre handles)
+ for args in zip(count(0), mch.handles_pre, chain, chain[1:], chain[2:]):
+ self.rig_mch_handle_auto(*args)
+
+ # Apply user transformation to the final handles
+ for args in zip(count(0), mch.handles, chain, chain[1:], chain[2:], mch.handles_pre):
+ self.rig_mch_handle_user(*args)
+
+ def rig_mch_handle_auto(self, i, mch, prev_node, node, next_node):
+ hstart = prev_node or node
+ hend = next_node or node
+
+ # Emulate auto handle
+ self.make_constraint(mch, 'COPY_LOCATION', hstart.control_bone, name='locate_prev')
+ self.make_constraint(mch, 'DAMPED_TRACK', hend.control_bone, name='track_next')
+
+ def rig_mch_handle_user(self, i, mch, prev_node, node, next_node, pre):
+ # Copy from the pre handle if used. Before Full is used to allow
+ # drivers on local transform channels to still work.
+ if pre != mch:
+ self.make_constraint(
+ mch, 'COPY_TRANSFORMS', pre, name='copy_pre',
+ space='LOCAL', mix_mode='BEFORE_FULL',
+ )
+
+ # Apply user rotation and scale.
+ # If the node belongs to a parent of this rig, there is a good chance this
+ # may cause weird double transformation, so skip it in that case.
+ if not rig_is_child(self, node.merged_master.rig, strict=True):
+ input_bone = node.reparent_bone if self.use_reparent_handles else node.control_bone
+
+ self.make_constraint(
+ mch, 'COPY_TRANSFORMS', input_bone, name='copy_user',
+ target_space='LOCAL_OWNER_ORIENT', owner_space='LOCAL',
+ mix_mode='BEFORE_FULL',
+ )
+
+ # Remove any shear created by the previous steps
+ self.make_constraint(mch, 'LIMIT_ROTATION', name='remove_shear')
+
+ ##############################
+ # ORG chain
+
+ @stage.parent_bones
+ def parent_org_chain(self):
+ orgs = self.bones.org
+ self.set_bone_parent(orgs[0], self.rig_parent_bone, inherit_scale='AVERAGE')
+ self.parent_bone_chain(orgs, use_connect=True, inherit_scale='AVERAGE')
+
+ @stage.rig_bones
+ def rig_org_chain(self):
+ for args in zip(count(0), self.bones.org, self.control_nodes, self.control_nodes[1:]):
+ self.rig_org_bone(*args)
+
+ def rig_org_bone(self, i, org, node, next_node):
+ if i == 0:
+ self.make_constraint(org, 'COPY_LOCATION', node.control_bone)
+
+ self.make_constraint(org, 'STRETCH_TO', next_node.control_bone, keep_axis='SWING_Y')
+
+ ##############################
+ # 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'), bbone=True)
+ self.get_bone(name).bbone_segments = self.bbone_segments
+ return name
+
+ @stage.parent_bones
+ def parent_deform_chain(self):
+ deform = self.bones.deform
+
+ self.set_bone_parent(deform[0], self.rig_parent_bone, inherit_scale='AVERAGE')
+ self.parent_bone_chain(deform, use_connect=True, inherit_scale='AVERAGE')
+
+ if self.use_bbones:
+ handles = self.get_all_mch_handles()
+
+ for name, start_handle, end_handle in zip(deform, handles, handles[1:]):
+ bone = self.get_bone(name)
+ bone.bbone_handle_type_start = 'TANGENT'
+ bone.bbone_custom_handle_start = self.get_bone(start_handle)
+ bone.bbone_handle_type_end = 'TANGENT'
+ bone.bbone_custom_handle_end = self.get_bone(end_handle)
+
+ if self.use_scale:
+ bone.bbone_handle_use_scale_start = self.params.skin_chain_use_scale[0:3]
+ bone.bbone_handle_use_scale_end = self.params.skin_chain_use_scale[0:3]
+
+ bone.bbone_handle_use_ease_start = self.params.skin_chain_use_scale[3]
+ bone.bbone_handle_use_ease_end = self.params.skin_chain_use_scale[3]
+
+ @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)
+
+ if self.use_bbones:
+ if i == 0 and self.prev_corner > 1e-3:
+ self.make_corner_driver(
+ deform, 'bbone_easein', self.control_nodes[0], self.control_nodes[1], self.prev_node, self.prev_corner)
+
+ elif i == self.num_orgs-1 and self.next_corner > 1e-3:
+ self.make_corner_driver(
+ deform, 'bbone_easeout', self.control_nodes[-1], self.control_nodes[-2], self.next_node, self.next_corner)
+
+ def make_corner_driver(self, bbone, field, corner_node, next_node1, next_node2, angle):
+ """
+ Create a driver adjusting B-Bone Ease based on the angle between controls,
+ gradually making the corner sharper when the angle drops below the threshold.
+ """
+ pbone = self.get_bone(bbone)
+
+ a = (corner_node.point - next_node1.point).length
+ b = (corner_node.point - next_node2.point).length
+ c = (next_node1.point - next_node2.point).length
+
+ varmap = {
+ 'a': driver_var_distance(self.obj, bone1=corner_node.control_bone, bone2=next_node1.control_bone),
+ 'b': driver_var_distance(self.obj, bone1=corner_node.control_bone, bone2=next_node2.control_bone),
+ 'c': driver_var_distance(self.obj, bone1=next_node1.control_bone, bone2=next_node2.control_bone),
+ }
+
+ # Compute and set the ease in rest pose
+ initval = -1+2*smoothstep(-1, 1, acos((a*a+b*b-c*c)/max(2*a*b, 1e-10))/angle)
+
+ setattr(pbone.bone, field, initval)
+
+ # Create the actual driver
+ self.make_driver(
+ pbone, field,
+ expression='%f+2*smoothstep(-1,1,acos((a*a+b*b-c*c)/max(2*a*b,1e-10))/%f)' % (-1-initval, angle),
+ variables=varmap
+ )
+
+ ####################################################
+ # SETTINGS
+
+ @classmethod
+ def add_parameters(self, params):
+ params.bbones = bpy.props.IntProperty(
+ name='B-Bone Segments',
+ default=10,
+ min=1,
+ description='Number of B-Bone segments'
+ )
+
+ params.skin_chain_use_reparent = bpy.props.BoolProperty(
+ name='Merge Parent Rotation And Scale',
+ default=False,
+ description='When controls are merged into ones owned by other chains, include ' +
+ 'parent-induced rotation/scale difference into handle motion. Otherwise ' +
+ 'only local motion of the control bone is used',
+ )
+
+ params.skin_chain_use_scale = bpy.props.BoolVectorProperty(
+ size=4,
+ name='Use Handle Scale',
+ default=(False, False, False, False),
+ description='Use control scaling to scale the B-Bone'
+ )
+
+ params.skin_chain_connect_mirror = bpy.props.BoolVectorProperty(
+ size=2,
+ name='Connect With Mirror',
+ default=(True, True),
+ description='Create a smooth B-Bone transition if an end of the chain meets its mirror'
+ )
+
+ params.skin_chain_connect_sharp_angle = bpy.props.FloatVectorProperty(
+ size=2,
+ name='Sharpen Corner',
+ default=(0, 0),
+ min=0,
+ max=math.pi,
+ description='Create a mechanism to sharpen a connected corner when the angle is below this value',
+ unit='ROTATION',
+ )
+
+ params.skin_chain_connect_ends = bpy.props.BoolVectorProperty(
+ size=2,
+ name='Connect Matching Ends',
+ default=(False, False),
+ description='Create a smooth B-Bone transition if an end of the chain meets another chain going in the same direction'
+ )
+
+ super().add_parameters(params)
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ layout.prop(params, "bbones")
+
+ col = layout.column()
+ col.active = params.bbones > 1
+
+ col.prop(params, "skin_chain_use_reparent")
+
+ row = col.split(factor=0.3)
+ row.label(text="Use Scale:")
+ row = row.row(align=True)
+ row.prop(params, "skin_chain_use_scale", index=0, text="X", toggle=True)
+ row.prop(params, "skin_chain_use_scale", index=1, text="Y", toggle=True)
+ row.prop(params, "skin_chain_use_scale", index=2, text="Z", toggle=True)
+ row.prop(params, "skin_chain_use_scale", index=3, text="Ease", toggle=True)
+
+ row = col.split(factor=0.3)
+ row.label(text="Connect Mirror:")
+ row = row.row(align=True)
+ row.prop(params, "skin_chain_connect_mirror", index=0, text="Start", toggle=True)
+ row.prop(params, "skin_chain_connect_mirror", index=1, text="End", toggle=True)
+
+ row = col.split(factor=0.3)
+ row.label(text="Connect Next:")
+ row = row.row(align=True)
+ row.prop(params, "skin_chain_connect_ends", index=0, text="Start", toggle=True)
+ row.prop(params, "skin_chain_connect_ends", index=1, text="End", toggle=True)
+
+ row = col.split(factor=0.3)
+ row.label(text="Sharpen:")
+ row = row.row(align=True)
+ row.prop(params, "skin_chain_connect_sharp_angle", index=0, text="Start")
+ row.prop(params, "skin_chain_connect_sharp_angle", index=1, text="End")
+
+ super().parameters_ui(layout, params)
+
+
+def create_sample(obj):
+ from rigify.rigs.basic.copy_chain import create_sample as inner
+ obj.pose.bones[inner(obj)["bone.01"]].rigify_type = 'skin.basic_chain'
diff --git a/rigify/rigs/skin/glue.py b/rigify/rigs/skin/glue.py
new file mode 100644
index 00000000..2fffc885
--- /dev/null
+++ b/rigify/rigs/skin/glue.py
@@ -0,0 +1,321 @@
+# ====================== 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 ...utils.naming import make_derived_name
+from ...utils.widgets_basic import create_cube_widget
+from ...utils.mechanism import move_all_constraints
+
+from ...base_rig import stage
+from ...base_generate import SubstitutionRig
+
+from .skin_nodes import ControlQueryNode
+from .skin_rigs import BaseSkinRig
+
+from ..basic.raw_copy import RelinkConstraintsMixin
+
+from .basic_chain import Rig as BasicChainRig
+
+
+class Rig(SubstitutionRig):
+ """Skin rig component that injects constraints into a control generated by other rigs."""
+
+ def substitute(self):
+ # Deformation is implemented by inheriting from the chain rig, so
+ # enabling it requires switching between two different classes.
+ if self.params.skin_glue_head_mode == 'BRIDGE':
+ return [self.instantiate_rig(BridgeGlueRig, self.base_bone)]
+ else:
+ return [self.instantiate_rig(SimpleGlueRig, self.base_bone)]
+
+
+def add_parameters(params):
+ SimpleGlueRig.add_parameters(params)
+ BridgeGlueRig.add_parameters(params)
+
+
+def parameters_ui(layout, params):
+ if params.skin_glue_head_mode == 'BRIDGE':
+ BridgeGlueRig.parameters_ui(layout, params)
+ else:
+ SimpleGlueRig.parameters_ui(layout, params)
+
+
+class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
+ """Base class for the glue rigs."""
+
+ def initialize(self):
+ super().initialize()
+
+ self.glue_head_mode = self.params.skin_glue_head_mode
+
+ self.glue_use_tail = self.params.relink_constraints and self.params.skin_glue_use_tail
+ self.relink_unmarked_constraints = self.glue_use_tail
+
+ ####################################################
+ # QUERY NODES
+
+ @stage.initialize
+ def init_glue_nodes(self):
+ bone = self.get_bone(self.base_bone)
+
+ self.head_constraint_node = ControlQueryNode(
+ self, self.base_bone, point=bone.head
+ )
+
+ if self.glue_use_tail:
+ self.tail_position_node = PositionQueryNode(
+ self, self.base_bone, point=bone.tail,
+ needs_reparent=self.params.skin_glue_tail_reparent,
+ )
+
+ ####################################################
+ # GLUE CONSTRAINTS
+
+ def rig_glue_constraints(self):
+ org = self.base_bone
+ ctrl = self.head_constraint_node.control_bone
+
+ self.relink_bone_constraints(org)
+
+ # Add the built-in constraint
+ if self.glue_use_tail:
+ target = self.tail_position_node.output_bone
+ add_mode = self.params.skin_glue_add_constraint
+ inf = self.params.skin_glue_add_constraint_influence
+
+ if add_mode == 'COPY_LOCATION':
+ self.make_constraint(
+ ctrl, 'COPY_LOCATION', target, insert_index=0,
+ owner_space='LOCAL', target_space='LOCAL',
+ use_offset=True, influence=inf
+ )
+ elif add_mode == 'COPY_LOCATION_OWNER':
+ self.make_constraint(
+ ctrl, 'COPY_LOCATION', target, insert_index=0,
+ owner_space='LOCAL', target_space='LOCAL_OWNER_ORIENT',
+ use_offset=True, influence=inf
+ )
+
+ move_all_constraints(self.obj, org, ctrl)
+
+ def find_relink_target(self, spec, old_target):
+ if self.glue_use_tail and (spec == 'TARGET' or spec == '' == old_target):
+ return self.tail_position_node.output_bone
+
+ return super().find_relink_target(spec, old_target)
+
+ ####################################################
+ # SETTINGS
+
+ @classmethod
+ def add_parameters(self, params):
+ params.skin_glue_head_mode = bpy.props.EnumProperty(
+ name='Glue Mode',
+ items=[('CHILD', 'Child Of Control',
+ "The glue bone becomes a child of the control bone"),
+ ('MIRROR', 'Mirror Of Control',
+ "The glue bone becomes a sibling of the control bone with Copy Transforms"),
+ ('REPARENT', 'Mirror With Parents',
+ "The glue bone keeps its parent, but uses Copy Transforms to group both local and parent induced motion of the control into local space"),
+ ('BRIDGE', 'Deformation Bridge',
+ "Other than adding glue constraints to the control, the rig acts as a one segment basic deform chain")],
+ default='CHILD',
+ description="Specifies how the glue bone is rigged to the control at the bone head location",
+ )
+
+ params.skin_glue_use_tail = bpy.props.BoolProperty(
+ name='Use Tail Target',
+ default=False,
+ description='Find the control at the bone tail location and use it to relink TARGET or any constraints without an assigned subtarget or relink spec'
+ )
+
+ params.skin_glue_tail_reparent = bpy.props.BoolProperty(
+ name='Target Local With Parents',
+ default=False,
+ description='Include transformations induced by target parents into target local space'
+ )
+
+ params.skin_glue_add_constraint = bpy.props.EnumProperty(
+ name='Add Constraint',
+ items=[('NONE', 'No New Constraint',
+ "Don't add new constraints"),
+ ('COPY_LOCATION', 'Copy Location (Local)',
+ "Add a constraint to copy Local Location with Offset. If the owner and target control " +
+ "rest orientations are different, the global movement direction will change accordingly"),
+ ('COPY_LOCATION_OWNER', 'Copy Location (Local, Owner Orientation)',
+ "Add a constraint to copy Local Location (Owner Orientation) with Offset. Even if the owner and " +
+ "target controls have different rest orientations, the global movement direction would be the same")],
+ default='NONE',
+ description="Add one of the common constraints linking the control to the tail target",
+ )
+
+ params.skin_glue_add_constraint_influence = bpy.props.FloatProperty(
+ name="Influence",
+ default=1.0, min=0, max=1,
+ description="Influence of the added constraint",
+ )
+
+ self.add_relink_constraints_params(params)
+
+ super().add_parameters(params)
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ layout.prop(params, "skin_glue_head_mode")
+ layout.prop(params, "relink_constraints")
+
+ if params.relink_constraints:
+ col = layout.column()
+ col.prop(params, "skin_glue_use_tail")
+
+ col2 = col.column()
+ col2.active = params.skin_glue_use_tail
+ col2.prop(params, "skin_glue_tail_reparent")
+
+ col = layout.column()
+ col.active = params.skin_glue_use_tail
+ col.prop(params, "skin_glue_add_constraint", text="Add")
+
+ col3 = col.column()
+ col3.active = params.skin_glue_add_constraint != 'NONE'
+ col3.prop(params, "skin_glue_add_constraint_influence", slider=True)
+
+ layout.label(text="All constraints are moved to the control bone.", icon='INFO')
+
+ super().parameters_ui(layout, params)
+
+
+class SimpleGlueRig(BaseGlueRig):
+ """Normal glue rig that only does glue."""
+
+ def find_org_bones(self, bone):
+ return bone.name
+
+ ####################################################
+ # QUERY NODES
+
+ @stage.initialize
+ def init_glue_nodes(self):
+ super().init_glue_nodes()
+
+ bone = self.get_bone(self.base_bone)
+
+ self.head_position_node = PositionQueryNode(
+ self, self.base_bone, point=bone.head,
+ rig_org=self.glue_head_mode != 'CHILD',
+ needs_reparent=self.glue_head_mode == 'REPARENT',
+ )
+
+ ##############################
+ # ORG chain
+
+ @stage.parent_bones
+ def parent_org_bone(self):
+ if self.glue_head_mode == 'CHILD':
+ self.set_bone_parent(self.bones.org, self.head_position_node.output_bone)
+
+ @stage.rig_bones
+ def rig_org_bone(self):
+ # This executes before head_position_node owned a by generator plugin
+ self.rig_glue_constraints()
+
+
+class BridgeGlueRig(BaseGlueRig, BasicChainRig):
+ """Glue rig that also behaves like a deformation chain rig."""
+
+ def find_org_bones(self, bone):
+ # Still only bind to one bone
+ return [bone.name]
+
+ # Assign lowest priority
+ chain_priority = -20
+
+ # Orientation is irrelevant since controls should be merged into others
+ use_skin_control_orientation_bone = False
+
+ ####################################################
+ # QUERY NODES
+
+ @stage.prepare_bones
+ def prepare_glue_nodes(self):
+ # Verify that all nodes of the chain have been merged into others
+ for node in self.control_nodes:
+ if node.is_master_node:
+ self.raise_error('glue control {} was not merged', node.name)
+
+ ##############################
+ # ORG chain
+
+ @stage.rig_bones
+ def rig_org_chain(self):
+ # Move the user constraints away before the chain adds new ones
+ self.rig_glue_constraints()
+
+ super().rig_org_chain()
+
+
+class PositionQueryNode(ControlQueryNode):
+ """Finds the position of the highest layer control and rig reparent and/or org bone"""
+
+ def __init__(self, rig, org, *, point=None, needs_reparent=False, rig_org=False):
+ super().__init__(rig, org, point=point, find_highest_layer=True)
+
+ self.needs_reparent = needs_reparent
+ self.rig_org = rig_org
+
+ @property
+ def output_bone(self):
+ if self.rig_org:
+ return self.org
+ elif self.needs_reparent:
+ return self.reparent_bone
+ else:
+ return self.control_bone
+
+ def initialize(self):
+ if self.needs_reparent:
+ parent = self.build_parent()
+
+ if not self.rig_org:
+ self.merged_master.request_reparent(parent)
+
+ def parent_bones(self):
+ if self.rig_org:
+ if self.needs_reparent:
+ parent = self.node_parent.output_bone
+ else:
+ parent = self.get_bone_parent(self.control_bone)
+
+ self.set_bone_parent(self.org, parent, inherit_scale='AVERAGE')
+
+ def apply_bones(self):
+ if self.rig_org:
+ self.get_bone(self.org).matrix = self.merged_master.matrix
+
+ def rig_bones(self):
+ if self.rig_org:
+ self.make_constraint(self.org, 'COPY_TRANSFORMS', self.control_bone)
+
+
+def create_sample(obj):
+ from rigify.rigs.basic.super_copy import create_sample as inner
+ obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.glue'
diff --git a/rigify/rigs/skin/skin_nodes.py b/rigify/rigs/skin/skin_nodes.py
new file mode 100644
index 00000000..2fd04f9d
--- /dev/null
+++ b/rigify/rigs/skin/skin_nodes.py
@@ -0,0 +1,520 @@
+# ====================== 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 enum
+
+from mathutils import Vector, Quaternion
+
+from ...utils.layers import set_bone_layers
+from ...utils.naming import NameSides, make_derived_name, get_name_base_and_sides, change_name_side, Side, SideZ
+from ...utils.bones import BoneUtilityMixin, set_bone_widget_transform
+from ...utils.widgets_basic import create_cube_widget, create_sphere_widget
+from ...utils.mechanism import MechanismUtilityMixin
+from ...utils.rig import get_parent_rigs
+
+from ...utils.node_merger import MainMergeNode, QueryMergeNode
+
+from .skin_parents import ControlBoneParentLayer, ControlBoneWeakParentLayer
+from .skin_rigs import BaseSkinRig, BaseSkinChainRig
+
+
+class ControlNodeLayer(enum.IntEnum):
+ FREE = 0
+ MIDDLE_PIVOT = 10
+ TWEAK = 20
+
+
+class ControlNodeIcon(enum.IntEnum):
+ TWEAK = 0
+ MIDDLE_PIVOT = 1
+ FREE = 2
+ CUSTOM = 3
+
+
+class ControlNodeEnd(enum.IntEnum):
+ START = -1
+ MIDDLE = 0
+ END = 1
+
+
+class BaseSkinNode(MechanismUtilityMixin, BoneUtilityMixin):
+ """Base class for skin control and query nodes."""
+
+ node_parent_built = False
+
+ def do_build_parent(self):
+ """Create and intern the parent mechanism generator."""
+ assert self.rig.generator.stage == 'initialize'
+
+ result = self.rig.build_own_control_node_parent(self)
+ parents = self.rig.get_all_parent_skin_rigs()
+
+ for rig in reversed(parents):
+ result = rig.extend_control_node_parent(result, self)
+
+ for rig in parents:
+ result = rig.extend_control_node_parent_post(result, self)
+
+ result = self.merged_master.intern_parent(self, result)
+ result.is_parent_frozen = True
+ return result
+
+ def build_parent(self, use=True):
+ """Create and activate if needed the parent mechanism for this node."""
+ if not self.node_parent_built:
+ self.node_parent = self.do_build_parent()
+ self.node_parent_built = True
+
+ if use:
+ self.merged_master.register_use_parent(self.node_parent)
+
+ return self.node_parent
+
+ @property
+ def control_bone(self):
+ """The generated control bone."""
+ return self.merged_master._control_bone
+
+ @property
+ def reparent_bone(self):
+ """The generated reparent bone for this node's parent mechanism."""
+ return self.merged_master.get_reparent_bone(self.node_parent)
+
+
+class ControlBoneNode(MainMergeNode, BaseSkinNode):
+ """Node representing controls of skin chain rigs."""
+
+ merge_domain = 'ControlNetNode'
+
+ def __init__(
+ self, rig, org, name, *, point=None, size=None,
+ needs_parent=False, needs_reparent=False, allow_scale=False,
+ chain_end=ControlNodeEnd.MIDDLE,
+ layer=ControlNodeLayer.FREE, index=None, icon=ControlNodeIcon.TWEAK,
+ ):
+ assert isinstance(rig, BaseSkinChainRig)
+
+ super().__init__(rig, name, point or rig.get_bone(org).head)
+
+ self.org = org
+
+ self.name_split = get_name_base_and_sides(name)
+
+ self.name_merged = None
+ self.name_merged_split = None
+
+ self.size = size or rig.get_bone(org).length
+ self.layer = layer
+ self.icon = icon
+ self.rotation = None
+ self.chain_end = chain_end
+
+ # Create the parent mechanism even if not master
+ self.node_needs_parent = needs_parent
+ # If this node's own parent mechanism differs from master, generate a conversion bone
+ self.node_needs_reparent = needs_reparent
+
+ # Generate the control as a MCH bone to hide it from the user
+ self.hide_control = False
+ # Unlock scale channels
+ self.allow_scale = allow_scale
+
+ # For use by the owner rig: index in chain
+ self.index = index
+ # If this node is the end of a chain, points to the next one
+ self.chain_end_neighbor = None
+
+ def can_merge_into(self, other):
+ # Only merge up the layers (towards more mechanism)
+ dprio = self.rig.chain_priority - other.rig.chain_priority
+ return (
+ dprio <= 0 and
+ (self.layer <= other.layer or dprio < 0) and
+ super().can_merge_into(other)
+ )
+
+ def get_merge_priority(self, other):
+ # Prefer higher and closest layer
+ if self.layer <= other.layer:
+ return -abs(self.layer - other.layer)
+ else:
+ return -abs(self.layer - other.layer) - 100
+
+ def is_better_cluster(self, other):
+ """Check if the current bone is preferrable as master when choosing of same sized groups."""
+
+ # Prefer bones that have strictly more parents
+ my_parents = list(reversed(get_parent_rigs(self.rig.rigify_parent)))
+ other_parents = list(reversed(get_parent_rigs(other.rig.rigify_parent)))
+
+ if len(my_parents) > len(other_parents) and my_parents[0:len(other_parents)] == other_parents:
+ return True
+ if len(other_parents) > len(my_parents) and other_parents[0:len(other_parents)] == my_parents:
+ return False
+
+ # Prefer side chains
+ side_x_my, side_z_my = map(abs, self.name_split[1:])
+ side_x_other, side_z_other = map(abs, other.name_split[1:])
+
+ if ((side_x_my < side_x_other and side_z_my <= side_z_other) or
+ (side_x_my <= side_x_other and side_z_my < side_z_other)):
+ return False
+ if ((side_x_my > side_x_other and side_z_my >= side_z_other) or
+ (side_x_my >= side_x_other and side_z_my > side_z_other)):
+ return True
+
+ return False
+
+ def merge_done(self):
+ if self.is_master_node:
+ self.parent_subrig_cache = []
+ self.parent_subrig_names = {}
+ self.reparent_requests = []
+ self.used_parents = {}
+
+ super().merge_done()
+
+ self.find_mirror_siblings()
+
+ def find_mirror_siblings(self):
+ """Find merged nodes that have their names in mirror symmetry with this one."""
+
+ self.mirror_siblings = {}
+ self.mirror_sides_x = set()
+ self.mirror_sides_z = set()
+
+ for node in self.get_merged_siblings():
+ if node.name_split.base == self.name_split.base:
+ self.mirror_siblings[node.name_split] = node
+ self.mirror_sides_x.add(node.name_split.side)
+ self.mirror_sides_z.add(node.name_split.side_z)
+
+ assert self.mirror_siblings[self.name_split] is self
+
+ # Remove sides that merged with a mirror from the name
+ side_x = Side.MIDDLE if len(self.mirror_sides_x) > 1 else self.name_split.side
+ side_z = SideZ.MIDDLE if len(self.mirror_sides_z) > 1 else self.name_split.side_z
+
+ self.name_merged = change_name_side(self.name, side=side_x, side_z=side_z)
+ self.name_merged_split = NameSides(self.name_split.base, side_x, side_z)
+
+ def get_best_mirror(self):
+ """Find best mirror sibling for connecting via mirror."""
+
+ base, side, sidez = self.name_split
+
+ for flip in [(base, -side, -sidez), (base, -side, sidez), (base, side, -sidez)]:
+ mirror = self.mirror_siblings.get(flip, None)
+ if mirror and mirror is not self:
+ return mirror
+
+ return None
+
+ def intern_parent(self, node, parent):
+ """De-duplicate the parent layer chain within this merge group."""
+
+ # Quick check for the same object
+ if id(parent) in self.parent_subrig_names:
+ return parent
+
+ # Find if an identical parent is already in the cache
+ cache = self.parent_subrig_cache
+
+ for previous in cache:
+ if previous == parent:
+ previous.is_parent_frozen = True
+ return previous
+
+ # Add to cache and intern the layer parent if exists
+ cache.append(parent)
+
+ self.parent_subrig_names[id(parent)] = node.name
+
+ if isinstance(parent, ControlBoneParentLayer):
+ parent.parent = self.intern_parent(node, parent.parent)
+
+ return parent
+
+ def register_use_parent(self, parent):
+ """Activate this parent mechanism generator."""
+ self.used_parents[id(parent)] = parent
+
+ def request_reparent(self, parent):
+ """Request a reparent bone to be generated for this parent mechanism."""
+ requests = self.reparent_requests
+
+ if parent not in requests:
+ # If the actual reparent would be generated, weak parent will be needed.
+ if self.has_weak_parent and not self.use_weak_parent:
+ if self.use_mix_parent or parent != self.node_parent:
+ self.use_weak_parent = True
+
+ for weak_parent in self.node_parent_list_weak:
+ self.register_use_parent(weak_parent)
+
+ self.register_use_parent(parent)
+ requests.append(parent)
+
+ def get_reparent_bone(self, parent):
+ """Returns the generated reparent bone for this parent mechanism."""
+ return self.reparent_bones[id(parent)]
+
+ def get_rotation(self):
+ """Returns the orientation quaternion provided for this node by parents."""
+ if self.rotation is None:
+ self.rotation = self.rig.get_final_control_node_rotation(self)
+
+ return self.rotation
+
+ def initialize(self):
+ if self.is_master_node:
+ sibling_list = self.get_merged_siblings()
+ mirror_sibling_list = self.mirror_siblings.values()
+
+ # Compute size
+ best = max(sibling_list, key=lambda n: n.icon)
+ best_mirror = best.mirror_siblings.values()
+
+ self.size = sum(node.size for node in best_mirror) / len(best_mirror)
+
+ # Compute orientation
+ self.rotation = sum(
+ (node.get_rotation() for node in mirror_sibling_list),
+ Quaternion((0, 0, 0, 0))
+ ).normalized()
+
+ self.matrix = self.rotation.to_matrix().to_4x4()
+ self.matrix.translation = self.point
+
+ # Create parents and decide if mix would be needed
+ parent_list = [node.build_parent(use=False) for node in mirror_sibling_list]
+
+ if all(parent == self.node_parent for parent in parent_list):
+ self.use_mix_parent = False
+ parent_list = [self.node_parent]
+ else:
+ self.use_mix_parent = True
+
+ # Prepare parenting without weak layers
+ self.use_weak_parent = False
+ self.node_parent_list_weak = parent_list
+
+ self.node_parent_list = [ControlBoneWeakParentLayer.strip(p) for p in parent_list]
+ self.has_weak_parent = any((p is not pw)
+ for p, pw in zip(self.node_parent_list, parent_list))
+
+ for parent in self.node_parent_list:
+ self.register_use_parent(parent)
+
+ # All nodes
+ if self.node_needs_parent or self.node_needs_reparent:
+ parent = self.build_parent()
+ if self.node_needs_reparent:
+ self.merged_master.request_reparent(parent)
+
+ def prepare_bones(self):
+ # Activate parent components once all reparents are registered
+ if self.is_master_node:
+ for parent in self.used_parents.values():
+ parent.enable_component()
+
+ self.used_parents = None
+
+ def make_bone(self, name, scale, *, rig=None, orientation=None):
+ """
+ Creates a bone associated with this node, using the appropriate
+ orientation, location and size.
+ """
+ name = (rig or self).copy_bone(self.org, name)
+
+ if orientation is not None:
+ matrix = orientation.to_matrix().to_4x4()
+ matrix.translation = self.merged_master.point
+ else:
+ matrix = self.merged_master.matrix
+
+ bone = self.get_bone(name)
+ bone.matrix = matrix
+ bone.length = self.merged_master.size * scale
+
+ return name
+
+ def find_master_name_node(self):
+ """Find which node to name the control bone from."""
+
+ # Chain end nodes have sub-par names, so try to find another chain
+ if self.chain_end == ControlNodeEnd.END:
+ # Choose possible other nodes so that it doesn't lose mirror tags
+ siblings = [
+ node for node in self.get_merged_siblings()
+ if self.mirror_sides_x.issubset(node.mirror_sides_x)
+ and self.mirror_sides_z.issubset(node.mirror_sides_z)
+ ]
+
+ # Prefer chain start, then middle nodes
+ candidates = [node for node in siblings if node.chain_end == ControlNodeEnd.START]
+
+ if not candidates:
+ candidates = [node for node in siblings if node.chain_end == ControlNodeEnd.MIDDLE]
+
+ # Choose based on priority and name alphabetical order
+ if candidates:
+ return min(candidates, key=lambda c: (-c.rig.chain_priority, c.name_merged))
+
+ return self
+
+ def generate_bones(self):
+ if self.is_master_node:
+ # Make control bone
+ self._control_bone = self.make_master_bone()
+
+ # Make weak parent bone
+ if self.use_weak_parent:
+ self.weak_parent_bone = self.make_bone(
+ make_derived_name(self._control_bone, 'mch', '_weak_parent'), 1/2)
+
+ # Make mix parent if needed
+ self.reparent_bones = {}
+
+ if self.use_mix_parent:
+ self.mix_parent_bone = self.make_bone(
+ make_derived_name(self._control_bone, 'mch', '_mix_parent'), 1/2)
+ else:
+ self.reparent_bones[id(self.node_parent)] = self._control_bone
+
+ # Make requested reparents
+ self.reparent_bones_fake = set(self.reparent_bones.values())
+
+ for parent in self.reparent_requests:
+ if id(parent) not in self.reparent_bones:
+ parent_name = self.parent_subrig_names[id(parent)]
+ bone = self.make_bone(make_derived_name(parent_name, 'mch', '_reparent'), 1/3)
+ self.reparent_bones[id(parent)] = bone
+
+ def make_master_bone(self):
+ choice = self.find_master_name_node()
+ name = choice.name_merged
+
+ if self.hide_control:
+ name = make_derived_name(name, 'mch')
+
+ return choice.make_bone(name, 1)
+
+ def parent_bones(self):
+ if self.is_master_node:
+ if self.use_mix_parent:
+ self.set_bone_parent(self._control_bone, self.mix_parent_bone,
+ inherit_scale='AVERAGE')
+ self.rig.generator.disable_auto_parent(self.mix_parent_bone)
+ else:
+ self.set_bone_parent(self._control_bone, self.node_parent_list[0].output_bone,
+ inherit_scale='AVERAGE')
+
+ if self.use_weak_parent:
+ if self.use_mix_parent:
+ self.rig.generator.disable_auto_parent(self.weak_parent_bone)
+ else:
+ parent = self.node_parent_list_weak[0]
+ self.set_bone_parent(self.weak_parent_bone, parent.output_bone,
+ inherit_scale=parent.inherit_scale_mode)
+
+ for parent in self.reparent_requests:
+ bone = self.reparent_bones[id(parent)]
+ if bone not in self.reparent_bones_fake:
+ self.set_bone_parent(bone, parent.output_bone, inherit_scale='AVERAGE')
+
+ def configure_bones(self):
+ if self.is_master_node:
+ if not any(node.allow_scale for node in self.get_merged_siblings()):
+ self.get_bone(self.control_bone).lock_scale = (True, True, True)
+
+ layers = self.rig.get_control_node_layers(self)
+ if layers:
+ bone = self.get_bone(self.control_bone).bone
+ set_bone_layers(bone, layers, not self.is_master_node)
+
+ def rig_bones(self):
+ if self.is_master_node:
+ # Rig the mixed parent
+ if self.use_mix_parent:
+ targets = [parent.output_bone for parent in self.node_parent_list]
+ self.make_constraint(self.mix_parent_bone, 'ARMATURE',
+ targets=targets, use_deform_preserve_volume=True)
+
+ # Invoke parent rig callbacks
+ for rig in reversed(self.rig.get_all_parent_skin_rigs()):
+ rig.extend_control_node_rig(self)
+
+ # Rig reparent bones
+ reparent_source = self.control_bone
+
+ if self.use_weak_parent:
+ reparent_source = self.weak_parent_bone
+
+ self.make_constraint(reparent_source, 'COPY_TRANSFORMS',
+ self.control_bone, space='LOCAL')
+
+ if self.use_mix_parent:
+ targets = [parent.output_bone for parent in self.node_parent_list_weak]
+ self.make_constraint(self.weak_parent_bone, 'ARMATURE',
+ targets=targets, use_deform_preserve_volume=True)
+
+ set_bone_widget_transform(self.obj, self.control_bone, reparent_source)
+
+ for parent in self.reparent_requests:
+ bone = self.reparent_bones[id(parent)]
+ if bone not in self.reparent_bones_fake:
+ self.make_constraint(bone, 'COPY_TRANSFORMS', reparent_source)
+
+ def generate_widgets(self):
+ if self.is_master_node:
+ best = max(self.get_merged_siblings(), key=lambda n: n.icon)
+
+ if best.icon == ControlNodeIcon.TWEAK:
+ create_sphere_widget(self.obj, self.control_bone)
+ elif best.icon in (ControlNodeIcon.MIDDLE_PIVOT, ControlNodeIcon.FREE):
+ create_cube_widget(self.obj, self.control_bone)
+ else:
+ best.rig.make_control_node_widget(best)
+
+
+class ControlQueryNode(QueryMergeNode, BaseSkinNode):
+ """Node representing controls of skin chain rigs."""
+
+ merge_domain = 'ControlNetNode'
+
+ def __init__(self, rig, org, *, name=None, point=None, find_highest_layer=False):
+ assert isinstance(rig, BaseSkinRig)
+
+ super().__init__(rig, name or org, point or rig.get_bone(org).head)
+
+ self.org = org
+ self.find_highest_layer = find_highest_layer
+
+ def can_merge_into(self, other):
+ return True
+
+ def get_merge_priority(self, other):
+ return other.layer if self.find_highest_layer else -other.layer
+
+ @property
+ def merged_master(self):
+ return self.matched_nodes[0]
diff --git a/rigify/rigs/skin/skin_parents.py b/rigify/rigs/skin/skin_parents.py
new file mode 100644
index 00000000..0cfaec36
--- /dev/null
+++ b/rigify/rigs/skin/skin_parents.py
@@ -0,0 +1,395 @@
+# ====================== 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 string import Template
+
+from ...utils.naming import make_derived_name
+from ...utils.misc import force_lazy, LazyRef
+
+from ...base_rig import LazyRigComponent, stage
+
+
+class ControlBoneParentBase(LazyRigComponent):
+ """
+ Base class for components that generate parent mechanisms for skin controls.
+ The generated parent bone is accessible through the output_bone field or property.
+ """
+
+ # Run this component after the @stage methods of the owner node and its slave nodes
+ rigify_sub_object_run_late = True
+
+ # This generator's output bone cannot be modified by generators layered on top.
+ # Otherwise they may optimize bone count by adding more constraints in place.
+ # (This generally signals the bone is shared between multiple users.)
+ is_parent_frozen = False
+
+ def __init__(self, rig, node):
+ super().__init__(node)
+
+ # Rig that provides this parent mechanism.
+ self.rig = rig
+ # Control node that the mechanism is provided for
+ self.node = node
+
+ def __eq__(self, other):
+ raise NotImplementedError()
+
+
+class ControlBoneParentOrg:
+ """Control node parent generator wrapping a single ORG bone."""
+
+ is_parent_frozen = True
+
+ def __init__(self, org):
+ self._output_bone = org
+
+ @property
+ def output_bone(self):
+ return force_lazy(self._output_bone)
+
+ def enable_component(self):
+ pass
+
+ def __eq__(self, other):
+ return isinstance(other, ControlBoneParentOrg) and self._output_bone == other._output_bone
+
+
+class ControlBoneParentArmature(ControlBoneParentBase):
+ """Control node parent generator using the Armature constraint to parent the bone."""
+
+ def __init__(self, rig, node, *, bones, orientation=None, copy_scale=None, copy_rotation=None):
+ super().__init__(rig, node)
+
+ # List of Armature constraint target specs for make_constraint (lazy).
+ self.bones = bones
+ # Orientation quaternion for the bone (lazy)
+ self.orientation = orientation
+ # Bone to copy scale from (lazy)
+ self.copy_scale = copy_scale
+ # Bone to copy rotation from (lazy)
+ self.copy_rotation = copy_rotation
+
+ if copy_scale or copy_rotation:
+ self.is_parent_frozen = True
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, ControlBoneParentArmature) and
+ self.node.point == other.node.point and
+ self.orientation == other.orientation and
+ self.bones == other.bones and
+ self.copy_scale == other.copy_scale and
+ self.copy_rotation == other.copy_rotation
+ )
+
+ def generate_bones(self):
+ self.output_bone = self.node.make_bone(
+ make_derived_name(self.node.name, 'mch', '_arm'), 1/4, rig=self.rig)
+
+ self.rig.generator.disable_auto_parent(self.output_bone)
+
+ if self.orientation:
+ matrix = force_lazy(self.orientation).to_matrix().to_4x4()
+ matrix.translation = self.node.point
+ self.get_bone(self.output_bone).matrix = matrix
+
+ def parent_bones(self):
+ self.targets = force_lazy(self.bones)
+
+ assert len(self.targets) > 0
+
+ # Single target can be simplified to parenting
+ if len(self.targets) == 1:
+ target = force_lazy(self.targets[0])
+ if isinstance(target, tuple):
+ target = target[0]
+
+ self.set_bone_parent(
+ self.output_bone, target,
+ inherit_scale='NONE' if self.copy_scale else 'FIX_SHEAR'
+ )
+
+ def rig_bones(self):
+ # Multiple targets use the Armature constraint
+ if len(self.targets) > 1:
+ self.make_constraint(
+ self.output_bone, 'ARMATURE', targets=self.targets,
+ use_deform_preserve_volume=True
+ )
+
+ self.make_constraint(self.output_bone, 'LIMIT_ROTATION')
+
+ if self.copy_rotation:
+ self.make_constraint(self.output_bone, 'COPY_ROTATION', self.copy_rotation)
+ if self.copy_scale:
+ self.make_constraint(self.output_bone, 'COPY_SCALE', self.copy_scale)
+
+
+class ControlBoneParentLayer(ControlBoneParentBase):
+ """Base class for parent generators that build on top of another mechanism."""
+
+ def __init__(self, rig, node, parent):
+ super().__init__(rig, node)
+ self.parent = parent
+
+ def enable_component(self):
+ self.parent.enable_component()
+ super().enable_component()
+
+
+class ControlBoneWeakParentLayer(ControlBoneParentLayer):
+ """
+ Base class for layered parent generator that is only used for the reparent source.
+ I.e. it doesn't affect the control for its owner rig, but only for other rigs
+ that have controls merged into this one.
+ """
+
+ # Inherit mode used to parent the pseudo-control to the output of this generator.
+ inherit_scale_mode = 'AVERAGE'
+
+ @staticmethod
+ def strip(parent):
+ while isinstance(parent, ControlBoneWeakParentLayer):
+ parent = parent.parent
+
+ return parent
+
+
+class ControlBoneParentOffset(ControlBoneParentLayer):
+ """
+ Parent mechanism generator that offsets the control's location.
+
+ Supports Copy Transforms (Local) constraints and location drivers.
+ Multiple offsets can be accumulated in the same generator, which
+ will automatically create as many bones as needed.
+ """
+
+ @classmethod
+ def wrap(cls, owner, parent, node, *constructor_args):
+ return cls(owner, node, parent, *constructor_args)
+
+ def __init__(self, rig, node, parent):
+ super().__init__(rig, node, parent)
+ self.copy_local = {}
+ self.add_local = {}
+ self.add_orientations = {}
+ self.limit_distance = []
+
+ def enable_component(self):
+ # Automatically merge an unfrozen sequence of this generator instances
+ while isinstance(self.parent, ControlBoneParentOffset) and not self.parent.is_parent_frozen:
+ self.prepend_contents(self.parent)
+ self.parent = self.parent.parent
+
+ super().enable_component()
+
+ def prepend_contents(self, other):
+ """Merge all offsets stored in the other generator into the current one."""
+ for key, val in other.copy_local.items():
+ if key not in self.copy_local:
+ self.copy_local[key] = val
+ else:
+ inf, expr, cbs = val
+ inf0, expr0, cbs0 = self.copy_local[key]
+ self.copy_local[key] = [inf+inf0, expr+expr0, cbs+cbs0]
+
+ for key, val in other.add_orientations.items():
+ if key not in self.add_orientations:
+ self.add_orientations[key] = val
+
+ for key, val in other.add_local.items():
+ if key not in self.add_local:
+ self.add_local[key] = val
+ else:
+ ot0, ot1, ot2 = val
+ my0, my1, my2 = self.add_local[key]
+ self.add_local[key] = (ot0+my0, ot1+my1, ot2+my2)
+
+ self.limit_distance = other.limit_distance + self.limit_distance
+
+ def add_copy_local_location(self, target, *, influence=1, influence_expr=None, influence_vars={}):
+ """
+ Add a Copy Location (Local, Owner Orientation) offset.
+ The influence may be specified as a (lazy) constant, or a driver expression
+ with variables (using the same $var syntax as add_location_driver).
+ """
+ if target not in self.copy_local:
+ self.copy_local[target] = [0, [], []]
+
+ if influence_expr:
+ self.copy_local[target][1].append((influence_expr, influence_vars))
+ elif callable(influence):
+ self.copy_local[target][2].append(influence)
+ else:
+ self.copy_local[target][0] += influence
+
+ def add_location_driver(self, orientation, index, expression, variables):
+ """
+ Add a driver offsetting along the specified axis in the given Quaternion orientation.
+ The variables may have to be renamed due to conflicts between multiple add requests,
+ so the expression should use the $var syntax of Template to reference them.
+ """
+ assert isinstance(variables, dict)
+
+ key = tuple(round(x*10000) for x in orientation)
+
+ if key not in self.add_local:
+ self.add_orientations[key] = orientation
+ self.add_local[key] = ([], [], [])
+
+ self.add_local[key][index].append((expression, variables))
+
+ def add_limit_distance(self, target, *, ensure_order=False, **kwargs):
+ """Add a limit distance constraint with the given make_constraint arguments."""
+ self.limit_distance.append((target, kwargs))
+
+ # Prevent merging from reordering this limit
+ if ensure_order:
+ self.is_parent_frozen = True
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, ControlBoneParentOffset) and
+ self.parent == other.parent and
+ self.copy_local == other.copy_local and
+ self.add_local == other.add_local and
+ self.limit_distance == other.limit_distance
+ )
+
+ @property
+ def output_bone(self):
+ return self.mch_bones[-1] if self.mch_bones else self.parent.output_bone
+
+ def generate_bones(self):
+ self.mch_bones = []
+ self.reuse_mch = False
+
+ if self.copy_local or self.add_local or self.limit_distance:
+ mch_name = make_derived_name(self.node.name, 'mch', '_poffset')
+
+ if self.add_local:
+ # Generate a bone for every distinct orientation used for the drivers
+ for key in self.add_local:
+ self.mch_bones.append(self.node.make_bone(
+ mch_name, 1/4, rig=self.rig, orientation=self.add_orientations[key]))
+ else:
+ # Try piggybacking on the parent bone if allowed
+ if not self.parent.is_parent_frozen:
+ bone = self.get_bone(self.parent.output_bone)
+ if (bone.head - self.node.point).length < 1e-5:
+ self.reuse_mch = True
+ self.mch_bones = [bone.name]
+ return
+
+ self.mch_bones.append(self.node.make_bone(mch_name, 1/4, rig=self.rig))
+
+ def parent_bones(self):
+ if self.mch_bones:
+ if not self.reuse_mch:
+ self.rig.set_bone_parent(self.mch_bones[0], self.parent.output_bone)
+
+ self.rig.parent_bone_chain(self.mch_bones, use_connect=False)
+
+ def compile_driver(self, items):
+ variables = {}
+ expressions = []
+
+ # Loop through all expressions and combine the variable maps.
+ for expr, varset in items:
+ template = Template(expr)
+ varmap = {}
+
+ # Check that all variables are present
+ try:
+ template.substitute({k: '' for k in varset})
+ except Exception as e:
+ self.rig.raise_error('Invalid driver expression: {}\nError: {}', expr, e)
+
+ # Merge variables
+ for name, desc in varset.items():
+ # Check if the variable is used.
+ try:
+ template.substitute({k: '' for k in varset if k != name})
+ continue
+ except KeyError:
+ pass
+
+ # Descriptors may not be hashable, so linear search
+ for vn, vdesc in variables.items():
+ if vdesc == desc:
+ varmap[name] = vn
+ break
+ else:
+ # Find an unique name for the new variable and add to map
+ new_name = name
+ if new_name in variables:
+ for i in count(1):
+ new_name = '%s_%d' % (name, i)
+ if new_name not in variables:
+ break
+
+ variables[new_name] = desc
+ varmap[name] = new_name
+
+ # Substitute the new names into the expression
+ expressions.append(template.substitute(varmap))
+
+ # Add all expressions together
+ if len(expressions) > 1:
+ final_expr = '+'.join('('+expr+')' for expr in expressions)
+ else:
+ final_expr = expressions[0]
+
+ return final_expr, variables
+
+ def rig_bones(self):
+ # Emit the Copy Location constraints
+ if self.copy_local:
+ mch = self.mch_bones[0]
+ for target, (influence, drivers, lazyinf) in self.copy_local.items():
+ influence += sum(map(force_lazy, lazyinf))
+
+ con = self.make_constraint(
+ mch, 'COPY_LOCATION', target, use_offset=True,
+ target_space='LOCAL_OWNER_ORIENT', owner_space='LOCAL', influence=influence,
+ )
+
+ if drivers:
+ if influence > 0:
+ drivers.append((str(influence), {}))
+
+ expr, variables = self.compile_driver(drivers)
+ self.make_driver(con, 'influence', expression=expr, variables=variables)
+
+ # Add the direct offset drivers
+ if self.add_local:
+ for mch, (key, specs) in zip(self.mch_bones, self.add_local.items()):
+ for index, vals in enumerate(specs):
+ if vals:
+ expr, variables = self.compile_driver(vals)
+ self.make_driver(mch, 'location', index=index,
+ expression=expr, variables=variables)
+
+ # Add the limit distance constraints
+ for target, kwargs in self.limit_distance:
+ self.make_constraint(self.mch_bones[-1], 'LIMIT_DISTANCE', target, **kwargs)
diff --git a/rigify/rigs/skin/skin_rigs.py b/rigify/rigs/skin/skin_rigs.py
new file mode 100644
index 00000000..a4bc361e
--- /dev/null
+++ b/rigify/rigs/skin/skin_rigs.py
@@ -0,0 +1,241 @@
+# ====================== 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 ...utils.naming import make_derived_name
+from ...utils.misc import force_lazy, LazyRef
+
+from ...base_rig import BaseRig, stage
+
+from .skin_parents import ControlBoneParentOrg
+
+
+class BaseSkinRig(BaseRig):
+ """
+ Base type for all rigs involved in the skin system.
+ This includes chain rigs and the parent provider rigs.
+ """
+
+ def initialize(self):
+ self.rig_parent_bone = self.get_bone_parent(self.base_bone)
+
+ ##########################
+ # Utilities
+
+ def get_parent_skin_rig(self):
+ """Find the closest BaseSkinRig parent."""
+ parent = self.rigify_parent
+
+ while parent:
+ if isinstance(parent, BaseSkinRig):
+ return parent
+ parent = parent.rigify_parent
+
+ return None
+
+ def get_all_parent_skin_rigs(self):
+ """Get a list of all BaseSkinRig parents, starting with this rig."""
+ items = []
+ current = self
+ while current:
+ items.append(current)
+ current = current.get_parent_skin_rig()
+ return items
+
+ def get_child_chain_parent_next(self, rig):
+ """
+ Retrieves the parent bone for the child chain rig
+ as determined by the parent skin rig.
+ """
+ if isinstance(self.rigify_parent, BaseSkinRig):
+ return self.rigify_parent.get_child_chain_parent(rig, self.rig_parent_bone)
+ else:
+ return self.rig_parent_bone
+
+ def build_control_node_parent_next(self, node):
+ """
+ Retrieves the parent mechanism generator for the child control node
+ as determined by the parent skin rig.
+ """
+ if isinstance(self.rigify_parent, BaseSkinRig):
+ return self.rigify_parent.build_control_node_parent(node, self.rig_parent_bone)
+ else:
+ return ControlBoneParentOrg(self.rig_parent_bone)
+
+ ##########################
+ # Methods to override
+
+ def get_child_chain_parent(self, rig, parent_bone):
+ """
+ Returns the (lazy) parent bone to use for the given child chain rig.
+ The parent_bone argument specifies the actual parent bone from caller.
+ """
+ return parent_bone
+
+ def build_control_node_parent(self, node, parent_bone):
+ """
+ Returns the parent mechanism generator for the child control node.
+ The parent_bone argument specifies the actual parent bone from caller.
+ Called during the initialize stage.
+ """
+ return ControlBoneParentOrg(self.get_child_chain_parent(node.rig, parent_bone))
+
+ def build_own_control_node_parent(self, node):
+ """
+ Returns the parent mechanism generator for nodes directly owned by this rig.
+ Called during the initialize stage.
+ """
+ return self.build_control_node_parent_next(node)
+
+ def extend_control_node_parent(self, parent, node):
+ """
+ First callback pass of adjustments to the parent mechanism generator for the given node.
+ Called for all BaseSkinRig parents in parent to child order during the initialize stage.
+ """
+ return parent
+
+ def extend_control_node_parent_post(self, parent, node):
+ """
+ Second callback pass of adjustments to the parent mechanism generator for the given node.
+ Called for all BaseSkinRig parents in child to parent order during the initialize stage.
+ """
+ return parent
+
+ def extend_control_node_rig(self, node):
+ """
+ A callback pass for adding constraints directly to the generated control.
+ Called for all BaseSkinRig parents in parent to child order during the rig stage.
+ """
+ pass
+
+
+def get_bone_quaternion(obj, bone):
+ return obj.pose.bones[bone].bone.matrix_local.to_quaternion()
+
+
+class BaseSkinChainRig(BaseSkinRig):
+ """
+ Base type for all skin rigs that can own control nodes, rather than
+ only modifying nodes of their children or other rigs.
+ """
+
+ chain_priority = 0
+
+ def initialize(self):
+ super().initialize()
+
+ if type(self).chain_priority is None:
+ self.chain_priority = self.params.skin_chain_priority
+
+ def parent_bones(self):
+ self.rig_parent_bone = force_lazy(self.get_child_chain_parent_next(self))
+
+ def get_final_control_node_rotation(self, node):
+ """Returns the orientation to use for the given control node owned by this rig."""
+ return self.get_control_node_rotation(node)
+
+ ##########################
+ # Methods to override
+
+ def get_control_node_rotation(self, node):
+ """
+ Returns the rig-specific orientation to use for the given control node of this rig,
+ if not overridden by the Orientation Bone option.
+ """
+ return get_bone_quaternion(self.obj, self.base_bone)
+
+ def get_control_node_layers(self, node):
+ """Returns the armature layers to use for the given control node owned by this rig."""
+ return self.get_bone(self.base_bone).bone.layers
+
+ def make_control_node_widget(self, node):
+ """Called to generate the widget for nodes with ControlNodeIcon.CUSTOM."""
+ raise NotImplementedError()
+
+ ##########################
+ # UI
+
+ @classmethod
+ def add_parameters(self, params):
+ params.skin_chain_priority = bpy.props.IntProperty(
+ name='Chain Priority',
+ min=-10, max=10, default=0,
+ description='When merging controls, chains with higher priority always win'
+ )
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ if self.chain_priority is None:
+ layout.prop(params, "skin_chain_priority")
+
+
+class BaseSkinChainRigWithRotationOption(BaseSkinChainRig):
+ """
+ Skin chain rig with an option to override the orientation to use
+ for controls via specifying an arbitrary template bone.
+ """
+
+ use_skin_control_orientation_bone = True
+
+ def get_final_control_node_rotation(self, node):
+ bone_name = self.params.skin_control_orientation_bone
+
+ if bone_name and self.use_skin_control_orientation_bone:
+ # Retrieve the orientation from the specified ORG bone
+ try:
+ org_name = make_derived_name(bone_name, 'org')
+
+ if org_name not in self.obj.pose.bones:
+ org_name = bone_name
+
+ return get_bone_quaternion(self.obj, org_name)
+
+ except KeyError:
+ self.raise_error('Could not find orientation bone {}', bone_name)
+
+ else:
+ # Use the rig-specific orientation
+ return self.get_control_node_rotation(node)
+
+ @classmethod
+ def add_parameters(self, params):
+ params.skin_control_orientation_bone = bpy.props.StringProperty(
+ name="Orientation Bone",
+ description="If set, control orientation is taken from the specified bone",
+ )
+
+ super().add_parameters(params)
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ if self.use_skin_control_orientation_bone:
+ from rigify.operators.copy_mirror_parameters import make_copy_parameter_button
+
+ row = layout.row()
+ row.prop_search(params, "skin_control_orientation_bone",
+ bpy.context.active_object.pose, "bones", text="Orientation")
+
+ make_copy_parameter_button(
+ row, "skin_control_orientation_bone", mirror_bone=True,
+ base_class=BaseSkinChainRigWithRotationOption
+ )
+
+ super().parameters_ui(layout, params)
diff --git a/rigify/rigs/skin/stretchy_chain.py b/rigify/rigs/skin/stretchy_chain.py
new file mode 100644
index 00000000..ac3d7784
--- /dev/null
+++ b/rigify/rigs/skin/stretchy_chain.py
@@ -0,0 +1,422 @@
+# ====================== 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 enum
+
+from itertools import count, repeat
+from mathutils import Vector, Matrix
+from bl_math import clamp
+
+from ...utils.rig import connected_children_names
+from ...utils.layers import ControlLayersOption
+from ...utils.naming import make_derived_name
+from ...utils.bones import align_bone_orientation, align_bone_to_axis, align_bone_roll
+from ...utils.misc import map_list, LazyRef
+from ...utils.mechanism import driver_var_transform
+
+from ...base_rig import stage
+
+from .skin_nodes import ControlBoneNode, ControlNodeLayer, ControlNodeIcon
+from .skin_parents import ControlBoneWeakParentLayer, ControlBoneParentOffset
+
+from .basic_chain import Rig as BasicChainRig
+
+
+class Control(enum.IntEnum):
+ START = 0
+ MIDDLE = 1
+ END = 2
+
+
+class Rig(BasicChainRig):
+ """
+ Skin chain that propagates motion of its end and middle controls, resulting in
+ stretching the whole chain rather than just immediately connected chain segments.
+ """
+
+ min_chain_length = 2
+
+ def initialize(self):
+ if len(self.bones.org) < self.min_chain_length:
+ self.raise_error(
+ "Input to rig type must be a chain of {} or more bones.", self.min_chain_length)
+
+ super().initialize()
+
+ orgs = self.bones.org
+
+ # Check the middle pivot location
+ self.pivot_pos = self.params.skin_chain_pivot_pos
+
+ if not (0 <= self.pivot_pos < len(orgs)):
+ self.raise_error('Invalid middle control position: {}', self.pivot_pos)
+
+ # Compute cumulative chain lengths from the start
+ bone_lengths = [self.get_bone(org).length for org in orgs]
+
+ self.chain_lengths = [sum(bone_lengths[0:i]) for i in range(len(orgs)+1)]
+
+ # Compute the chain start to end direction vector
+ if not self.params.skin_chain_falloff_length:
+ self.pivot_base = self.get_bone(orgs[0]).head
+ self.pivot_vector = self.get_bone(orgs[-1]).tail - self.pivot_base
+ self.pivot_length = self.pivot_vector.length
+ self.pivot_vector.normalize()
+
+ # Compute the position of the middle pivot within the chain
+ if self.pivot_pos:
+ pivot_point = self.get_bone(orgs[self.pivot_pos]).head
+ self.middle_pivot_factor = self.get_pivot_projection(pivot_point, self.pivot_pos)
+
+ ####################################################
+ # UTILITIES
+
+ def get_pivot_projection(self, pos, index):
+ """Compute the interpolation factor within the chain for a control at pos and index."""
+ if self.params.skin_chain_falloff_length:
+ # Position along the length of the chain
+ return self.chain_lengths[index] / self.chain_lengths[-1]
+ else:
+ # Position projected on the line connecting chain ends
+ return clamp((pos - self.pivot_base).dot(self.pivot_vector) / self.pivot_length)
+
+ def use_falloff_curve(self, idx):
+ """Check if the given Control has any influence on other nodes."""
+ return self.params.skin_chain_falloff[idx] > -10
+
+ def apply_falloff_curve(self, factor, idx):
+ """Compute the falloff weight at position factor for the given Control."""
+ weight = self.params.skin_chain_falloff[idx]
+
+ if self.params.skin_chain_falloff_spherical[idx]:
+ # circular falloff
+ if weight >= 0:
+ p = 2 ** weight
+ return (1 - (1 - factor) ** p) ** (1/p)
+ else:
+ p = 2 ** -weight
+ return 1 - (1 - factor ** p) ** (1/p)
+ else:
+ # parabolic falloff
+ return 1 - (1 - factor) ** (2 ** weight)
+
+ ####################################################
+ # CONTROL NODES
+
+ def make_control_node(self, i, org, is_end):
+ node = super().make_control_node(i, org, is_end)
+
+ # Chain end control nodes
+ if i == 0 or i == self.num_orgs:
+ node.layer = ControlNodeLayer.FREE
+ node.icon = ControlNodeIcon.FREE
+ if i == 0:
+ node.node_needs_reparent = self.use_falloff_curve(Control.START)
+ else:
+ node.node_needs_reparent = self.use_falloff_curve(Control.END)
+ # Middle pivot control node
+ elif i == self.pivot_pos:
+ node.layer = ControlNodeLayer.MIDDLE_PIVOT
+ node.icon = ControlNodeIcon.MIDDLE_PIVOT
+ node.node_needs_reparent = self.use_falloff_curve(Control.MIDDLE)
+ # Other (tweak) control nodes
+ else:
+ node.layer = ControlNodeLayer.TWEAK
+ node.icon = ControlNodeIcon.TWEAK
+
+ return node
+
+ def extend_control_node_parent(self, parent, node):
+ if node.rig != self or node.index in (0, self.num_orgs):
+ return parent
+
+ parent = ControlBoneParentOffset(self, node, parent)
+
+ # Add offsets from the end controls to other nodes
+ factor = self.get_pivot_projection(node.point, node.index)
+
+ if self.use_falloff_curve(Control.START):
+ parent.add_copy_local_location(
+ LazyRef(self.control_nodes[0], 'reparent_bone'),
+ influence=self.apply_falloff_curve(1 - factor, Control.START),
+ )
+
+ if self.use_falloff_curve(Control.END):
+ parent.add_copy_local_location(
+ LazyRef(self.control_nodes[-1], 'reparent_bone'),
+ influence=self.apply_falloff_curve(factor, Control.END),
+ )
+
+ # Add offset from the middle pivot
+ if self.pivot_pos and node.index != self.pivot_pos:
+ if self.use_falloff_curve(Control.MIDDLE):
+ if node.index < self.pivot_pos:
+ factor = factor / self.middle_pivot_factor
+ else:
+ factor = (1 - factor) / (1 - self.middle_pivot_factor)
+
+ parent.add_copy_local_location(
+ LazyRef(self.control_nodes[self.pivot_pos], 'reparent_bone'),
+ influence=self.apply_falloff_curve(clamp(factor), Control.MIDDLE),
+ )
+
+ # If Propagate To Controls is set, add an extra wrapper for twist/scale
+ if node.index != self.pivot_pos and self.params.skin_chain_falloff_to_controls:
+ if self.params.skin_chain_falloff_twist or self.params.skin_chain_falloff_scale:
+ parent = ControlBoneChainPropagate(self, node, parent)
+
+ return parent
+
+ def get_control_node_layers(self, node):
+ layers = None
+
+ # Secondary Layers used for the middle pivot
+ if self.pivot_pos and node.index == self.pivot_pos:
+ layers = ControlLayersOption.SKIN_SECONDARY.get(self.params)
+
+ # Primary Layers used for the end controls, and middle if secondary not set
+ if not layers and node.index in (0, self.num_orgs, self.pivot_pos):
+ layers = ControlLayersOption.SKIN_PRIMARY.get(self.params)
+
+ return layers or super().get_control_node_layers(node)
+
+ ####################################################
+ # B-Bone handle MCH
+
+ def rig_mch_handle_user(self, i, mch, prev_node, node, next_node, pre):
+ super().rig_mch_handle_user(i, mch, prev_node, node, next_node, pre)
+
+ self.rig_propagate(mch, node)
+
+ def rig_propagate(self, mch, node):
+ # Interpolate chain twist and/or scale between pivots
+ if node.index not in (0, self.num_orgs, self.pivot_pos):
+ index1, index2, factor = self.get_propagate_spec(node)
+
+ if self.params.skin_chain_falloff_twist:
+ self.rig_propagate_twist(mch, index1, index2, factor)
+
+ if self.use_scale and self.params.skin_chain_falloff_scale:
+ self.rig_propagate_scale(mch, index1, index2, factor)
+
+ def get_propagate_spec(self, node):
+ """Compute source handle indices and factor for propagating scale and twist to node."""
+ index1 = 0
+ index2 = self.num_orgs
+
+ len_cur = self.chain_lengths[node.index]
+ len_end = self.chain_lengths[-1]
+
+ if self.pivot_pos:
+ len_pivot = self.chain_lengths[self.pivot_pos]
+
+ if node.index < self.pivot_pos:
+ factor = len_cur / len_pivot
+ index2 = self.pivot_pos
+ else:
+ factor = (len_cur - len_pivot) / (len_end - len_pivot)
+ index1 = self.pivot_pos
+ else:
+ factor = len_cur / len_end
+
+ return index1, index2, factor
+
+ def rig_propagate_twist(self, mch, index1, index2, factor):
+ handles = self.get_all_mch_handles()
+ handles_pre = self.get_all_mch_handles_pre()
+
+ # Get Y Twist rotation of the input handles
+ variables = {
+ 'y1': driver_var_transform(
+ self.obj, handles[index1], type='ROT_Y',
+ space='LOCAL', rotation_mode='SWING_TWIST_Y'
+ ),
+ 'y2': driver_var_transform(
+ self.obj, handles[index2], type='ROT_Y',
+ space='LOCAL', rotation_mode='SWING_TWIST_Y'
+ ),
+ }
+
+ # If pre handles are used, exclude the pre-handle twist,
+ # since it is caused by mechanisms and not user animation.
+ if handles_pre[index1] != handles[index1]:
+ variables['p1'] = driver_var_transform(
+ self.obj, handles_pre[index1], type='ROT_Y',
+ space='LOCAL', rotation_mode='SWING_TWIST_Y'
+ )
+ expr1 = 'y1-p1'
+ else:
+ expr1 = 'y1'
+
+ if handles_pre[index2] != handles[index2]:
+ variables['p2'] = driver_var_transform(
+ self.obj, handles_pre[index2], type='ROT_Y',
+ space='LOCAL', rotation_mode='SWING_TWIST_Y'
+ )
+ expr2 = 'y2-p2'
+ else:
+ expr2 = 'y2'
+
+ # Create the driver for Y Euler Rotation
+ bone = self.get_bone(mch)
+ bone.rotation_mode = 'YXZ'
+
+ self.make_driver(
+ bone, 'rotation_euler', index=1,
+ expression=f'lerp({expr1},{expr2},{clamp(factor)})',
+ variables=variables
+ )
+
+ def rig_propagate_scale(self, mch, index1, index2, factor, use_y=False):
+ handles = self.get_all_mch_handles()
+
+ self.make_constraint(
+ mch, 'COPY_SCALE', handles[index1], space='LOCAL',
+ use_x=True, use_y=use_y, use_z=True,
+ use_offset=True, power=clamp(1-factor)
+ )
+ self.make_constraint(
+ mch, 'COPY_SCALE', handles[index2], space='LOCAL',
+ use_x=True, use_y=use_y, use_z=True,
+ use_offset=True, power=clamp(factor)
+ )
+
+ ####################################################
+ # SETTINGS
+
+ @classmethod
+ def add_parameters(self, params):
+ params.skin_chain_pivot_pos = bpy.props.IntProperty(
+ name='Middle Control Position',
+ default=0,
+ min=0,
+ description='Position of the middle control, disabled if zero'
+ )
+
+ params.skin_chain_falloff_spherical = bpy.props.BoolVectorProperty(
+ size=3,
+ name='Spherical Falloff',
+ default=(False, False, False),
+ description='Falloff curve tries to form a circle at +1 instead of a parabola',
+ )
+
+ params.skin_chain_falloff = bpy.props.FloatVectorProperty(
+ size=3,
+ name='Control Falloff',
+ default=(0.0, 1.0, 0.0),
+ soft_min=-2, min=-10, soft_max=2,
+ description='Falloff curve coefficient: 0 is linear, and higher value is wider influence. Set to -10 to disable influence completely',
+ )
+
+ params.skin_chain_falloff_length = bpy.props.BoolProperty(
+ name='Falloff Along Chain Curve',
+ default=False,
+ description='Falloff is computed along the curve of the chain, instead of projecting on the axis connecting the start and end points',
+ )
+
+ params.skin_chain_falloff_twist = bpy.props.BoolProperty(
+ name='Propagate Twist',
+ default=True,
+ description='Propagate twist from main controls throughout the chain',
+ )
+
+ params.skin_chain_falloff_scale = bpy.props.BoolProperty(
+ name='Propagate Scale',
+ default=False,
+ description='Propagate scale from main controls throughout the chain',
+ )
+
+ params.skin_chain_falloff_to_controls = bpy.props.BoolProperty(
+ name='Propagate To Controls',
+ default=False,
+ description='Expose scale and/or twist propagated to tweak controls to be seen as ' +
+ 'parent motion by glue or other chains using Merge Parent Rotation And ' +
+ 'Scale. Otherwise it is only propagated internally within this chain',
+ )
+
+ ControlLayersOption.SKIN_PRIMARY.add_parameters(params)
+ ControlLayersOption.SKIN_SECONDARY.add_parameters(params)
+
+ super().add_parameters(params)
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ layout.prop(params, "skin_chain_pivot_pos")
+
+ col = layout.column(align=True)
+
+ row = col.row(align=True)
+ row.label(text="Falloff:")
+
+ for i in range(3):
+ row2 = row.row(align=True)
+ row2.active = i != 1 or params.skin_chain_pivot_pos > 0
+ row2.prop(params, "skin_chain_falloff", text="", index=i)
+ row2.prop(params, "skin_chain_falloff_spherical", text="", icon='SPHERECURVE', index=i)
+
+ col.prop(params, "skin_chain_falloff_length")
+
+ row = col.split(factor=0.25)
+ row.label(text="Propagate:")
+ row = row.row(align=True)
+ row.prop(params, "skin_chain_falloff_twist", text="Twist", toggle=True)
+ row.prop(params, "skin_chain_falloff_scale", text="Scale", toggle=True)
+ row.prop(params, "skin_chain_falloff_to_controls", text="To Controls", toggle=True)
+
+ ControlLayersOption.SKIN_PRIMARY.parameters_ui(layout, params)
+
+ if params.skin_chain_pivot_pos > 0:
+ ControlLayersOption.SKIN_SECONDARY.parameters_ui(layout, params)
+
+ super().parameters_ui(layout, params)
+
+
+class ControlBoneChainPropagate(ControlBoneWeakParentLayer):
+ """
+ Parent mechanism generator that propagates chain twist/scale
+ to the reparent system, if Propagate To Controls is used.
+ """
+ inherit_scale_mode = 'FULL'
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, ControlBoneChainPropagate) and
+ self.parent == other.parent and
+ self.rig == other.rig and
+ self.node.index == other.node.index
+ )
+
+ def generate_bones(self):
+ # The parent bone is based on the handle and aligned appropriately.
+ handle = self.rig.bones.mch.handles[self.node.index]
+ self.output_bone = self.copy_bone(handle, make_derived_name(handle, 'mch', '_parent'))
+
+ def parent_bones(self):
+ self.set_bone_parent(self.output_bone, self.parent.output_bone, inherit_scale='AVERAGE')
+
+ def rig_bones(self):
+ # Add the twist/scale propagation rigging to the bone like the handle.
+ self.rig.rig_propagate(self.output_bone, self.node)
+
+
+def create_sample(obj):
+ from rigify.rigs.basic.copy_chain import create_sample as inner
+ obj.pose.bones[inner(obj)["bone.01"]].rigify_type = 'skin.stretchy_chain'
diff --git a/rigify/rigs/skin/transform/basic.py b/rigify/rigs/skin/transform/basic.py
new file mode 100644
index 00000000..2069615a
--- /dev/null
+++ b/rigify/rigs/skin/transform/basic.py
@@ -0,0 +1,148 @@
+# ====================== 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 math
+
+from itertools import count, repeat
+from mathutils import Vector, Matrix
+
+from ....utils.naming import make_derived_name
+from ....utils.widgets_basic import create_cube_widget
+from ....utils.misc import LazyRef
+
+from ....base_rig import stage
+
+from ..skin_parents import ControlBoneParentArmature
+from ..skin_rigs import BaseSkinRig
+
+
+class Rig(BaseSkinRig):
+ """
+ This rig transforms its child nodes' locations, but keeps
+ their rotation and scale stable. This demonstrates implementing
+ a basic parent controller rig.
+ """
+
+ def find_org_bones(self, bone):
+ return bone.name
+
+ def initialize(self):
+ super().initialize()
+
+ self.make_control = self.params.make_control
+
+ # Choose the parent bone for the child nodes
+ if self.make_control:
+ self.input_ref = LazyRef(self.bones.ctrl, 'master')
+ else:
+ self.input_ref = self.base_bone
+
+ # Retrieve the orientation of the control
+ matrix = self.get_bone(self.base_bone).bone.matrix_local
+
+ self.transform_orientation = matrix.to_quaternion()
+
+ ####################################################
+ # Control Nodes
+
+ def build_control_node_parent(self, node, parent_bone):
+ # Parent nodes to the control bone, but isolate rotation and scale
+ return ControlBoneParentArmature(
+ self, node, bones=[self.input_ref],
+ orientation=self.transform_orientation,
+ copy_scale=LazyRef(self.bones.mch, 'template'),
+ copy_rotation=LazyRef(self.bones.mch, 'template'),
+ )
+
+ def get_child_chain_parent(self, rig, parent_bone):
+ # Forward child chain parenting to the next rig, so that
+ # only control nodes are affected by this one.
+ return self.get_child_chain_parent_next(rig)
+
+ ####################################################
+ # BONES
+ #
+ # ctrl:
+ # master
+ # Master control
+ # mch:
+ # template
+ # Bone used to lock rotation and scale of child nodes.
+ #
+ ####################################################
+
+ ####################################################
+ # Master control
+
+ @stage.generate_bones
+ def make_master_control(self):
+ if self.make_control:
+ self.bones.ctrl.master = self.copy_bone(
+ self.bones.org, make_derived_name(self.bones.org, 'ctrl'), parent=True)
+
+ @stage.configure_bones
+ def configure_master_control(self):
+ if self.make_control:
+ self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
+
+ @stage.generate_widgets
+ def make_master_control_widget(self):
+ if self.make_control:
+ create_cube_widget(self.obj, self.bones.ctrl.master)
+
+ ####################################################
+ # Template MCH
+
+ @stage.generate_bones
+ def make_mch_template_bone(self):
+ self.bones.mch.template = self.copy_bone(
+ self.bones.org, make_derived_name(self.bones.org, 'mch', '_orient'), parent=True)
+
+ @stage.parent_bones
+ def parent_mch_template_bone(self):
+ self.set_bone_parent(self.bones.mch.template, self.get_child_chain_parent_next(self))
+
+ ####################################################
+ # ORG bone
+
+ @stage.rig_bones
+ def rig_org_bone(self):
+ pass
+
+ ####################################################
+ # SETTINGS
+
+ @classmethod
+ def add_parameters(self, params):
+ params.make_control = bpy.props.BoolProperty(
+ name="Control",
+ default=True,
+ description="Create a control bone for the copy"
+ )
+
+ @classmethod
+ def parameters_ui(self, layout, params):
+ layout.prop(params, "make_control", text="Generate Control")
+
+
+def create_sample(obj):
+ from rigify.rigs.basic.super_copy import create_sample as inner
+ obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.transform.basic'
diff --git a/rigify/utils/layers.py b/rigify/utils/layers.py
index 1f65863d..bc5a8c56 100644
--- a/rigify/utils/layers.py
+++ b/rigify/utils/layers.py
@@ -160,3 +160,16 @@ ControlLayersOption.TWEAK = ControlLayersOption('tweak', description="Layers for
# Layer parameters used by the super_face rig.
ControlLayersOption.FACE_PRIMARY = ControlLayersOption('primary', description="Layers for the primary controls to be on")
ControlLayersOption.FACE_SECONDARY = ControlLayersOption('secondary', description="Layers for the secondary controls to be on")
+
+# Layer parameters used by the skin rigs
+ControlLayersOption.SKIN_PRIMARY = ControlLayersOption(
+ 'skin_primary', toggle_default=False,
+ toggle_name="Primary Control Layers",
+ description="Layers for the primary controls to be on",
+)
+
+ControlLayersOption.SKIN_SECONDARY = ControlLayersOption(
+ 'skin_secondary', toggle_default=False,
+ toggle_name="Secondary Control Layers",
+ description="Layers for the secondary controls to be on",
+)