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