diff --git a/rigify/rigs/face/basic_tongue.py b/rigify/rigs/face/basic_tongue.py
new file mode 100644
index 0000000000000000000000000000000000000000..380e14dfd6cf44efc9a7df7bd1586aef2f2e52c1
--- /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 0000000000000000000000000000000000000000..498a90c4df8cde954aa657bf4b1c90e7cb9648d4
--- /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 0000000000000000000000000000000000000000..6829818c8d7b104e9af3b14aa583ed2733cb66c5
--- /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 0000000000000000000000000000000000000000..0392761f74a70c7bbb466d7394863b083696972c
--- /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 0000000000000000000000000000000000000000..b2cac8a6e0e529d241ad2daf253d6e6729802256
--- /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 0000000000000000000000000000000000000000..2fffc8857491a8b698e4258b29eeaeeb21ff7e30
--- /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 0000000000000000000000000000000000000000..2fd04f9d850bc5a59f48bdbc248c8f92cc016a9a
--- /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 0000000000000000000000000000000000000000..0cfaec36069ff534e2ad2541ab1d7dd45906d6b2
--- /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 0000000000000000000000000000000000000000..a4bc361ead69a122712edb6a7a530bbef5918afc
--- /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 0000000000000000000000000000000000000000..ac3d7784301d56c0ff0088eb4d01b85f19666f27
--- /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 0000000000000000000000000000000000000000..2069615a585a6ddae42c8dbb5f8c7c54be1f5bea
--- /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 1f65863de7c61d3a5fe38658410b29167cd69fe5..bc5a8c56a4beb469eaa792adf81b00972e6ff971 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",
+)