Rigify: move new face rig components from the experimental feature set.

Apart from imports the files are identical to the latest version.

Ref T89808
This commit is contained in:
Alexander Gavrilov 2021-08-10 21:19:11 +03:00
parent bb587e5bf8
commit 2acf22b593
Notes: blender-bot 2023-02-14 18:34:42 +01:00
Referenced by issue #89808, Adopting a new standard face rig for Rigify
12 changed files with 4615 additions and 0 deletions

View File

@ -0,0 +1,206 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
import math
from itertools import count
from ...utils.naming import make_derived_name
from ...utils.bones import flip_bone, copy_bone_position
from ...utils.layers import ControlLayersOption
from ...utils.misc import map_list
from ...base_rig import stage
from ..chain_rigs import TweakChainRig
from ..widgets import create_jaw_widget
class Rig(TweakChainRig):
"""Basic tongue from the original PitchiPoy face rig."""
min_chain_length = 3
def initialize(self):
super().initialize()
self.bbone_segments = self.params.bbones
####################################################
# BONES
#
# ctrl:
# master:
# Master control.
# mch:
# follow[]:
# Partial follow master bones.
#
####################################################
####################################################
# Control chain
@stage.generate_bones
def make_control_chain(self):
org = self.bones.org[0]
name = self.copy_bone(org, make_derived_name(org, 'ctrl'), parent=True)
flip_bone(self.obj, name)
self.bones.ctrl.master = name
@stage.parent_bones
def parent_control_chain(self):
pass
@stage.configure_bones
def configure_control_chain(self):
master = self.bones.ctrl.master
self.copy_bone_properties(self.bones.org[0], master)
ControlLayersOption.SKIN_PRIMARY.assign(self.params, self.obj, [master])
@stage.generate_widgets
def make_control_widgets(self):
create_jaw_widget(self.obj, self.bones.ctrl.master)
####################################################
# Mechanism chain
@stage.generate_bones
def make_follow_chain(self):
self.bones.mch.follow = map_list(self.make_mch_follow_bone, count(1), self.bones.org[1:])
def make_mch_follow_bone(self, i, org):
name = self.copy_bone(org, make_derived_name(org, 'mch'))
copy_bone_position(self.obj, self.base_bone, name)
flip_bone(self.obj, name)
return name
@stage.parent_bones
def parent_follow_chain(self):
for mch in self.bones.mch.follow:
self.set_bone_parent(mch, self.rig_parent_bone)
@stage.rig_bones
def rig_follow_chain(self):
master = self.bones.ctrl.master
num_orgs = len(self.bones.org)
for i, mch in enumerate(self.bones.mch.follow):
self.make_constraint(mch, 'COPY_TRANSFORMS', master, influence=1-(1+i)/num_orgs)
####################################################
# Tweak chain
@stage.parent_bones
def parent_tweak_chain(self):
ctrl = self.bones.ctrl
parents = [ctrl.master, *self.bones.mch.follow, self.rig_parent_bone]
for tweak, main in zip(ctrl.tweak, parents):
self.set_bone_parent(tweak, main)
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
params.bbones = bpy.props.IntProperty(
name='B-Bone Segments',
default=10,
min=1,
description='Number of B-Bone segments'
)
ControlLayersOption.SKIN_PRIMARY.add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
layout.prop(params, 'bbones')
ControlLayersOption.SKIN_PRIMARY.parameters_ui(layout, params)
def create_sample(obj):
# generated by rigify.utils.write_metarig
bpy.ops.object.mode_set(mode='EDIT')
arm = obj.data
bones = {}
bone = arm.edit_bones.new('tongue')
bone.head = 0.0000, 0.0000, 0.0000
bone.tail = 0.0000, 0.0161, 0.0074
bone.roll = 0.0000
bone.use_connect = False
bones['tongue'] = bone.name
bone = arm.edit_bones.new('tongue.001')
bone.head = 0.0000, 0.0161, 0.0074
bone.tail = 0.0000, 0.0375, 0.0091
bone.roll = 0.0000
bone.use_connect = True
bone.parent = arm.edit_bones[bones['tongue']]
bones['tongue.001'] = bone.name
bone = arm.edit_bones.new('tongue.002')
bone.head = 0.0000, 0.0375, 0.0091
bone.tail = 0.0000, 0.0605, -0.0029
bone.roll = 0.0000
bone.use_connect = True
bone.parent = arm.edit_bones[bones['tongue.001']]
bones['tongue.002'] = bone.name
bpy.ops.object.mode_set(mode='OBJECT')
pbone = obj.pose.bones[bones['tongue']]
pbone.rigify_type = 'face.basic_tongue'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['tongue.001']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['tongue.002']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
bpy.ops.object.mode_set(mode='EDIT')
for bone in arm.edit_bones:
bone.select = False
bone.select_head = False
bone.select_tail = False
for b in bones:
bone = arm.edit_bones[bones[b]]
bone.select = True
bone.select_head = True
bone.select_tail = True
bone.bbone_x = bone.bbone_z = bone.length * 0.05
arm.edit_bones.active = bone
return bones

View File

@ -0,0 +1,825 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
import math
import functools
import mathutils
from itertools import count
from mathutils import Vector, Matrix
from ...utils.naming import make_derived_name, mirror_name, change_name_side, Side, SideZ
from ...utils.bones import align_bone_z_axis, put_bone
from ...utils.widgets import (widget_generator, generate_circle_geometry,
generate_circle_hull_geometry)
from ...utils.widgets_basic import create_circle_widget
from ...utils.switch_parent import SwitchParentBuilder
from ...utils.misc import map_list, matrix_from_axis_pair, LazyRef
from ...base_rig import stage, RigComponent
from ..skin.skin_nodes import ControlBoneNode
from ..skin.skin_parents import ControlBoneParentOffset
from ..skin.skin_rigs import BaseSkinRig
from ..skin.basic_chain import Rig as BasicChainRig
class Rig(BaseSkinRig):
"""
Eye rig that manages two child eyelid chains. The chains must
connect at their ends using T/B symmetry.
"""
def find_org_bones(self, bone):
return bone.name
cluster_control = None
def initialize(self):
super().initialize()
bone = self.get_bone(self.base_bone)
self.center = bone.head
self.axis = bone.vector
self.eye_corner_nodes = []
self.eye_corner_matrix = None
# Create the cluster control (it will assign self.cluster_control)
if not self.cluster_control:
self.create_cluster_control()
self.init_child_chains()
def create_cluster_control(self):
return EyeClusterControl(self)
####################################################
# UTILITIES
def is_eye_control_node(self, node):
return node.rig in self.child_chains and node.is_master_node
def is_eye_corner_node(self, node):
# Corners are nodes where the two T and B chains merge
sides = set(n.name_split.side_z for n in node.get_merged_siblings())
return {SideZ.BOTTOM, SideZ.TOP}.issubset(sides)
def init_eye_corner_space(self):
"""Initialize the coordinate space of the eye based on two corners."""
if self.eye_corner_matrix:
return
if len(self.eye_corner_nodes) != 2:
self.raise_error('Expected 2 eye corners, but found {}', len(self.eye_corner_nodes))
# Build a coordinate space with XY plane based on center and two corners,
# and Y axis oriented as close to the eye axis as possible.
vecs = [(node.point - self.center).normalized() for node in self.eye_corner_nodes]
normal = vecs[0].cross(vecs[1])
space_axis = self.axis - self.axis.project(normal)
matrix = matrix_from_axis_pair(space_axis, normal, 'z').to_4x4()
matrix.translation = self.center
self.eye_corner_matrix = matrix.inverted()
# Compute signed angles from space_axis to the eye corners
amin, amax = self.eye_corner_range = list(
sorted(map(self.get_eye_corner_angle, self.eye_corner_nodes)))
if not (amin <= 0 <= amax):
self.raise_error('Bad relative angles of eye corners: {}..{}',
math.degrees(amin), math.degrees(amax))
def get_eye_corner_angle(self, node):
"""Compute a signed Z rotation angle from the eye axis to the node."""
pt = self.eye_corner_matrix @ node.point
return math.atan2(pt.x, pt.y)
def get_master_control_position(self):
"""Compute suitable position for the master control."""
self.init_eye_corner_space()
# Place the control between the two corners on the eye axis
pcorners = [node.point for node in self.eye_corner_nodes]
point, _ = mathutils.geometry.intersect_line_line(
self.center, self.center + self.axis, pcorners[0], pcorners[1]
)
return point
def get_lid_follow_influence(self, node):
"""Compute the influence factor of the eye movement on this eyelid control node."""
self.init_eye_corner_space()
# Interpolate from axis to corners based on Z angle
angle = self.get_eye_corner_angle(node)
amin, amax = self.eye_corner_range
if amin < angle < 0:
return 1 - min(1, angle/amin) ** 2
elif 0 < angle < amax:
return 1 - min(1, angle/amax) ** 2
else:
return 0
####################################################
# BONES
#
# ctrl:
# master:
# Parent control for moving the whole eye.
# target:
# Individual target this eye aims for.
# mch:
# master:
# Bone that rotates to track ctrl.target.
# track:
# Bone that translates to follow mch.master tail.
# deform:
# master:
# Deform mirror of ctrl.master.
# eye:
# Deform bone that rotates with mch.master.
# iris:
# Iris deform bone at master tail that scales with ctrl.target
#
####################################################
####################################################
# CHILD CHAINS
def init_child_chains(self):
self.child_chains = [rig for rig in self.rigify_children if isinstance(rig, BasicChainRig)]
# Inject a component twisting handles to the eye radius
for child in self.child_chains:
self.patch_chain(child)
def patch_chain(self, child):
return EyelidChainPatch(child, self)
####################################################
# CONTROL NODES
def extend_control_node_parent(self, parent, node):
if self.is_eye_control_node(node):
if self.is_eye_corner_node(node):
# Remember corners for later computations
assert not self.eye_corner_matrix
self.eye_corner_nodes.append(node)
else:
# Non-corners get extra motion applied to them
return self.extend_mid_node_parent(parent, node)
return parent
def extend_mid_node_parent(self, parent, node):
parent = ControlBoneParentOffset(self, node, parent)
# Add movement of the eye to the eyelid controls
parent.add_copy_local_location(
LazyRef(self.bones.mch, 'track'),
influence=LazyRef(self.get_lid_follow_influence, node)
)
# If Limit Distance on the control can be disabled, add another one to the mch
if self.params.eyelid_detach_option:
parent.add_limit_distance(
self.bones.org,
distance=(node.point - self.center).length,
limit_mode='LIMITDIST_ONSURFACE', use_transform_limit=True,
# Use custom space to accomodate scaling
space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
# Don't allow reordering this limit and subsequent offsets
ensure_order=True,
)
return parent
def extend_control_node_rig(self, node):
if self.is_eye_control_node(node):
# Add Limit Distance to enforce following the surface of the eye to the control
con = self.make_constraint(
node.control_bone, 'LIMIT_DISTANCE', self.bones.org,
distance=(node.point - self.center).length,
limit_mode='LIMITDIST_ONSURFACE', use_transform_limit=True,
# Use custom space to accomodate scaling
space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
)
if self.params.eyelid_detach_option:
self.make_driver(con, 'influence',
variables=[(self.bones.ctrl.target, 'lid_attach')])
####################################################
# SCRIPT
@stage.configure_bones
def configure_script_panels(self):
ctrl = self.bones.ctrl
controls = sum((chain.get_all_controls() for chain in self.child_chains), ctrl.flatten())
panel = self.script.panel_with_selected_check(self, controls)
self.add_custom_properties()
self.add_ui_sliders(panel)
def add_custom_properties(self):
target = self.bones.ctrl.target
if self.params.eyelid_follow_split:
self.make_property(
target, 'lid_follow', list(self.params.eyelid_follow_default),
description='Eylids follow eye movement (X and Z)'
)
else:
self.make_property(target, 'lid_follow', 1.0,
description='Eylids follow eye movement')
if self.params.eyelid_detach_option:
self.make_property(target, 'lid_attach', 1.0,
description='Eylids follow eye surface')
def add_ui_sliders(self, panel, *, add_name=False):
target = self.bones.ctrl.target
name_tail = f' ({target})' if add_name else ''
follow_text = f'Eyelids Follow{name_tail}'
if self.params.eyelid_follow_split:
row = panel.split(factor=0.66, align=True)
row.custom_prop(target, 'lid_follow', index=0, text=follow_text, slider=True)
row.custom_prop(target, 'lid_follow', index=1, text='', slider=True)
else:
panel.custom_prop(target, 'lid_follow', text=follow_text, slider=True)
if self.params.eyelid_detach_option:
panel.custom_prop(
target, 'lid_attach', text=f'Eyelids Attached{name_tail}', slider=True)
####################################################
# Master control
@stage.generate_bones
def make_master_control(self):
org = self.bones.org
name = self.copy_bone(org, make_derived_name(org, 'ctrl', '_master'), parent=True)
put_bone(self.obj, name, self.get_master_control_position())
self.bones.ctrl.master = name
@stage.configure_bones
def configure_master_control(self):
self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
@stage.generate_widgets
def make_master_control_widget(self):
ctrl = self.bones.ctrl.master
create_circle_widget(self.obj, ctrl, radius=1, head_tail=0.25)
####################################################
# Tracking MCH
@stage.generate_bones
def make_mch_track_bones(self):
org = self.bones.org
mch = self.bones.mch
mch.master = self.copy_bone(org, make_derived_name(org, 'mch'))
mch.track = self.copy_bone(org, make_derived_name(org, 'mch', '_track'), scale=1/4)
put_bone(self.obj, mch.track, self.get_bone(org).tail)
@stage.parent_bones
def parent_mch_track_bones(self):
mch = self.bones.mch
ctrl = self.bones.ctrl
self.set_bone_parent(mch.master, ctrl.master)
self.set_bone_parent(mch.track, ctrl.master)
@stage.rig_bones
def rig_mch_track_bones(self):
mch = self.bones.mch
ctrl = self.bones.ctrl
# Rotationally track the target bone in mch.master
self.make_constraint(mch.master, 'DAMPED_TRACK', ctrl.target)
# Translate to track the tail of mch.master in mch.track. Its local
# location is then copied to the control nodes.
# Two constraints are used to provide different X and Z influence values.
con_x = self.make_constraint(
mch.track, 'COPY_LOCATION', mch.master, head_tail=1, name='lid_follow_x',
use_xyz=(True, False, False),
space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
)
con_z = self.make_constraint(
mch.track, 'COPY_LOCATION', mch.master, head_tail=1, name='lid_follow_z',
use_xyz=(False, False, True),
space='CUSTOM', space_object=self.obj, space_subtarget=self.bones.org,
)
# Apply follow slider influence(s)
if self.params.eyelid_follow_split:
self.make_driver(con_x, 'influence', variables=[(ctrl.target, 'lid_follow', 0)])
self.make_driver(con_z, 'influence', variables=[(ctrl.target, 'lid_follow', 1)])
else:
factor = self.params.eyelid_follow_default
self.make_driver(
con_x, 'influence', expression=f'var*{factor[0]}',
variables=[(ctrl.target, 'lid_follow')]
)
self.make_driver(
con_z, 'influence', expression=f'var*{factor[1]}',
variables=[(ctrl.target, 'lid_follow')]
)
####################################################
# ORG bone
@stage.parent_bones
def parent_org_chain(self):
self.set_bone_parent(self.bones.org, self.bones.ctrl.master, inherit_scale='FULL')
####################################################
# Deform bones
@stage.generate_bones
def make_deform_bone(self):
org = self.bones.org
deform = self.bones.deform
deform.master = self.copy_bone(org, make_derived_name(org, 'def', '_master'), scale=3/2)
if self.params.make_deform:
deform.eye = self.copy_bone(org, make_derived_name(org, 'def'))
deform.iris = self.copy_bone(org, make_derived_name(org, 'def', '_iris'), scale=1/2)
put_bone(self.obj, deform.iris, self.get_bone(org).tail)
@stage.parent_bones
def parent_deform_chain(self):
deform = self.bones.deform
self.set_bone_parent(deform.master, self.bones.org)
if self.params.make_deform:
self.set_bone_parent(deform.eye, self.bones.mch.master)
self.set_bone_parent(deform.iris, deform.eye)
@stage.rig_bones
def rig_deform_chain(self):
if self.params.make_deform:
# Copy XZ local scale from the eye target control
self.make_constraint(
self.bones.deform.iris, 'COPY_SCALE', self.bones.ctrl.target,
owner_space='LOCAL', target_space='LOCAL_OWNER_ORIENT', use_y=False,
)
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
params.make_deform = bpy.props.BoolProperty(
name="Deform",
default=True,
description="Create a deform bone for the copy"
)
params.eyelid_detach_option = bpy.props.BoolProperty(
name="Eyelid Detach Option",
default=False,
description="Create an option to detach eyelids from the eye surface"
)
params.eyelid_follow_split = bpy.props.BoolProperty(
name="Split Eyelid Follow Slider",
default=False,
description="Create separate eyelid follow influence sliders for X and Z"
)
params.eyelid_follow_default = bpy.props.FloatVectorProperty(
size=2,
name="Eyelids Follow Default",
default=(0.2, 0.7), min=0, max=1,
description="Default setting for the Eyelids Follow sliders (X and Z)",
)
@classmethod
def parameters_ui(self, layout, params):
col = layout.column()
col.prop(params, "make_deform", text="Eyball And Iris Deforms")
col.prop(params, "eyelid_detach_option")
col.prop(params, "eyelid_follow_split")
row = col.row(align=True)
row.prop(params, "eyelid_follow_default", index=0, text="Follow X", slider=True)
row.prop(params, "eyelid_follow_default", index=1, text="Follow Z", slider=True)
class EyelidChainPatch(RigComponent):
"""Component injected into child chains to twist handles aiming Z axis at the eye center."""
rigify_sub_object_run_late = True
def __init__(self, owner, eye):
super().__init__(owner)
self.eye = eye
self.owner.use_pre_handles = True
def align_bone(self, name):
"""Align bone rest orientation to aim Z axis at the eye center."""
align_bone_z_axis(self.obj, name, self.eye.center - self.get_bone(name).head)
def prepare_bones(self):
for org in self.owner.bones.org:
self.align_bone(org)
def generate_bones(self):
if self.owner.use_bbones:
mch = self.owner.bones.mch
for pre in [*mch.handles_pre, *mch.handles]:
self.align_bone(pre)
def rig_bones(self):
if self.owner.use_bbones:
for pre, node in zip(self.owner.bones.mch.handles_pre, self.owner.control_nodes):
self.make_constraint(pre, 'COPY_LOCATION', node.control_bone, name='locate_cur')
self.make_constraint(
pre, 'LOCKED_TRACK', self.eye.bones.org, name='track_center',
track_axis='TRACK_Z', lock_axis='LOCK_Y',
)
class EyeClusterControl(RigComponent):
"""Component generating a common control for an eye cluster."""
def __init__(self, owner):
super().__init__(owner)
self.find_cluster_rigs()
def find_cluster_rigs(self):
"""Find and register all other eyes that belong to this cluster."""
owner = self.owner
owner.cluster_control = self
self.rig_list = [owner]
# Collect all sibling eye rigs
parent_rig = owner.rigify_parent
if parent_rig:
for rig in parent_rig.rigify_children:
if isinstance(rig, Rig) and rig != owner:
rig.cluster_control = self
self.rig_list.append(rig)
self.rig_count = len(self.rig_list)
####################################################
# UTILITIES
def find_cluster_position(self):
"""Compute the eye cluster control position and orientation."""
# Average location and Y axis of all the eyes
axis = Vector((0, 0, 0))
center = Vector((0, 0, 0))
length = 0
for rig in self.rig_list:
bone = self.get_bone(rig.base_bone)
axis += bone.y_axis
center += bone.head
length += bone.length
axis /= self.rig_count
center /= self.rig_count
length /= self.rig_count
# Create the matrix from the average Y and world Z
matrix = matrix_from_axis_pair((0, 0, 1), axis, 'z').to_4x4()
matrix.translation = center + axis * length * 5
self.size = length * 3 / 4
self.matrix = matrix
self.inv_matrix = matrix.inverted()
def project_rig_control(self, rig):
"""Intersect the given eye Y axis with the cluster plane, returns (x,y,0)."""
bone = self.get_bone(rig.base_bone)
head = self.inv_matrix @ bone.head
tail = self.inv_matrix @ bone.tail
axis = tail - head
return head + axis * (-head.z / axis.z)
def get_common_rig_name(self):
"""Choose a name for the cluster control based on the members."""
names = set(rig.base_bone for rig in self.rig_list)
name = min(names)
if mirror_name(name) in names:
return change_name_side(name, side=Side.MIDDLE)
return name
def get_rig_control_matrix(self, rig):
"""Compute a matrix for an individual eye sub-control."""
matrix = self.matrix.copy()
matrix.translation = self.matrix @ self.rig_points[rig]
return matrix
def get_master_control_layers(self):
"""Combine layers of all eyes for the cluster control."""
all_layers = [list(self.get_bone(rig.base_bone).layers) for rig in self.rig_list]
return [any(items) for items in zip(*all_layers)]
def get_all_rig_control_bones(self):
"""Make a list of all control bones of all clustered eyes."""
return list(set(sum((rig.bones.ctrl.flatten() for rig in self.rig_list), [self.master_bone])))
####################################################
# STAGES
def initialize(self):
self.find_cluster_position()
self.rig_points = {rig: self.project_rig_control(rig) for rig in self.rig_list}
def generate_bones(self):
if self.rig_count > 1:
self.master_bone = self.make_master_control()
self.child_bones = []
for rig in self.rig_list:
rig.bones.ctrl.target = child = self.make_child_control(rig)
self.child_bones.append(child)
else:
self.master_bone = self.make_child_control(self.rig_list[0])
self.child_bones = [self.master_bone]
self.owner.bones.ctrl.target = self.master_bone
self.build_parent_switch()
def make_master_control(self):
name = self.new_bone(make_derived_name(self.get_common_rig_name(), 'ctrl', '_common'))
bone = self.get_bone(name)
bone.matrix = self.matrix
bone.length = self.size
bone.layers = self.get_master_control_layers()
return name
def make_child_control(self, rig):
name = rig.copy_bone(
rig.base_bone, make_derived_name(rig.base_bone, 'ctrl'), length=self.size)
self.get_bone(name).matrix = self.get_rig_control_matrix(rig)
return name
def build_parent_switch(self):
pbuilder = SwitchParentBuilder(self.owner.generator)
org_parent = self.owner.rig_parent_bone
parents = [org_parent] if org_parent else []
pbuilder.build_child(
self.owner, self.master_bone,
prop_name=f'Parent ({self.master_bone})',
extra_parents=parents, select_parent=org_parent,
controls=self.get_all_rig_control_bones
)
def parent_bones(self):
if self.rig_count > 1:
for child in self.child_bones:
self.set_bone_parent(child, self.master_bone)
def configure_bones(self):
for child in self.child_bones:
bone = self.get_bone(child)
bone.lock_rotation = (True, True, True)
bone.lock_rotation_w = True
# When the cluster master control is selected, show sliders for all eyes
if self.rig_count > 1:
panel = self.owner.script.panel_with_selected_check(self.owner, [self.master_bone])
for rig in self.rig_list:
rig.add_ui_sliders(panel, add_name=True)
def generate_widgets(self):
for child in self.child_bones:
create_eye_widget(self.obj, child)
if self.rig_count > 1:
pt2d = [p.to_2d() / self.size for p in self.rig_points.values()]
create_eye_cluster_widget(self.obj, self.master_bone, points=pt2d)
@widget_generator
def create_eye_widget(geom, *, size=1):
generate_circle_geometry(geom, Vector((0, 0, 0)), size/2)
@widget_generator
def create_eye_cluster_widget(geom, *, size=1, points):
hpoints = [points[i] for i in mathutils.geometry.convex_hull_2d(points)]
generate_circle_hull_geometry(geom, hpoints, size*0.75, size*0.6)
generate_circle_hull_geometry(geom, hpoints, size, size*0.85)
def create_sample(obj):
# generated by rigify.utils.write_metarig
bpy.ops.object.mode_set(mode='EDIT')
arm = obj.data
bones = {}
bone = arm.edit_bones.new('eye.L')
bone.head = 0.0000, 0.0000, 0.0000
bone.tail = 0.0000, -0.0125, 0.0000
bone.roll = 0.0000
bone.use_connect = False
bones['eye.L'] = bone.name
bone = arm.edit_bones.new('lid1.T.L')
bone.head = 0.0155, -0.0006, -0.0003
bone.tail = 0.0114, -0.0099, 0.0029
bone.roll = 2.9453
bone.use_connect = False
bone.parent = arm.edit_bones[bones['eye.L']]
bones['lid1.T.L'] = bone.name
bone = arm.edit_bones.new('lid1.B.L')
bone.head = 0.0155, -0.0006, -0.0003
bone.tail = 0.0112, -0.0095, -0.0039
bone.roll = -0.0621
bone.use_connect = False
bone.parent = arm.edit_bones[bones['eye.L']]
bones['lid1.B.L'] = bone.name
bone = arm.edit_bones.new('lid2.T.L')
bone.head = 0.0114, -0.0099, 0.0029
bone.tail = 0.0034, -0.0149, 0.0040
bone.roll = 2.1070
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lid1.T.L']]
bones['lid2.T.L'] = bone.name
bone = arm.edit_bones.new('lid2.B.L')
bone.head = 0.0112, -0.0095, -0.0039
bone.tail = 0.0029, -0.0140, -0.0057
bone.roll = 0.8337
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lid1.B.L']]
bones['lid2.B.L'] = bone.name
bone = arm.edit_bones.new('lid3.T.L')
bone.head = 0.0034, -0.0149, 0.0040
bone.tail = -0.0046, -0.0157, 0.0026
bone.roll = 1.7002
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lid2.T.L']]
bones['lid3.T.L'] = bone.name
bone = arm.edit_bones.new('lid3.B.L')
bone.head = 0.0029, -0.0140, -0.0057
bone.tail = -0.0041, -0.0145, -0.0057
bone.roll = 1.0671
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lid2.B.L']]
bones['lid3.B.L'] = bone.name
bone = arm.edit_bones.new('lid4.T.L')
bone.head = -0.0046, -0.0157, 0.0026
bone.tail = -0.0123, -0.0140, -0.0049
bone.roll = 1.0850
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lid3.T.L']]
bones['lid4.T.L'] = bone.name
bone = arm.edit_bones.new('lid4.B.L')
bone.head = -0.0041, -0.0145, -0.0057
bone.tail = -0.0123, -0.0140, -0.0049
bone.roll = 1.1667
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lid3.B.L']]
bones['lid4.B.L'] = bone.name
bpy.ops.object.mode_set(mode='OBJECT')
pbone = obj.pose.bones[bones['eye.L']]
pbone.rigify_type = 'face.skin_eye'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lid1.T.L']]
pbone.rigify_type = 'skin.stretchy_chain'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
try:
pbone.rigify_parameters.skin_chain_pivot_pos = 2
except AttributeError:
pass
try:
pbone.rigify_parameters.bbones = 5
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_connect_mirror = [False, False]
except AttributeError:
pass
pbone = obj.pose.bones[bones['lid1.B.L']]
pbone.rigify_type = 'skin.stretchy_chain'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
try:
pbone.rigify_parameters.skin_chain_pivot_pos = 2
except AttributeError:
pass
try:
pbone.rigify_parameters.bbones = 5
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_connect_mirror = [False, False]
except AttributeError:
pass
pbone = obj.pose.bones[bones['lid2.T.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lid2.B.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lid3.T.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lid3.B.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lid4.T.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lid4.B.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
bpy.ops.object.mode_set(mode='EDIT')
for bone in arm.edit_bones:
bone.select = False
bone.select_head = False
bone.select_tail = False
for b in bones:
bone = arm.edit_bones[bones[b]]
bone.select = True
bone.select_head = True
bone.select_tail = True
bone.bbone_x = bone.bbone_z = bone.length * 0.05
arm.edit_bones.active = bone
return bones

View File

@ -0,0 +1,862 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
import math
from itertools import count, repeat
from mathutils import Vector, Matrix
from bl_math import clamp
from ...utils.naming import make_derived_name, Side, SideZ, get_name_side_z
from ...utils.bones import align_bone_z_axis, put_bone
from ...utils.misc import map_list, matrix_from_axis_pair, LazyRef
from ...utils.widgets_basic import create_circle_widget
from ...base_rig import stage, RigComponent
from ..skin.skin_nodes import ControlBoneNode
from ..skin.skin_parents import ControlBoneParentOrg, ControlBoneParentArmature
from ..skin.skin_rigs import BaseSkinRig
from ..skin.basic_chain import Rig as BasicChainRig
from ..widgets import create_jaw_widget
class Rig(BaseSkinRig):
"""
Jaw rig that manages loops of four mouth chains each. The chains
must connect together at their ends using L/R and T/B symmetry.
"""
def find_org_bones(self, bone):
return bone.name
def initialize(self):
super().initialize()
self.mouth_orientation = self.get_mouth_orientation()
self.chain_to_layer = None
self.init_child_chains()
####################################################
# UTILITIES
def get_mouth_orientation(self):
jaw_axis = self.get_bone(self.base_bone).y_axis.copy()
jaw_axis[2] = 0
return matrix_from_axis_pair(jaw_axis, (0, 0, 1), 'z').to_quaternion()
def is_corner_node(self, node):
# Corners are nodes where two T/B or L/R chains meet.
siblings = [n for n in node.get_merged_siblings() if n.rig in self.child_chains]
sides_x = set(n.name_split.side for n in siblings)
sides_z = set(n.name_split.side_z for n in siblings)
if {SideZ.BOTTOM, SideZ.TOP}.issubset(sides_z):
if Side.LEFT in sides_x:
return Side.LEFT
else:
return Side.RIGHT
if {Side.LEFT, Side.RIGHT}.issubset(sides_x):
if SideZ.TOP in sides_z:
return SideZ.TOP
else:
return SideZ.BOTTOM
return None
####################################################
# BONES
#
# ctrl:
# master:
# Main jaw open control.
# mouth:
# Main control for adjusting mouth position and scale.
# mch:
# lock:
# Jaw master mirror for the locked mouth.
# top[]:
# Jaw master mirrors for the loop top.
# bottom[]:
# Jaw master mirrors for the loop bottom.
# middle[]:
# Middle position between top[] and bottom[].
# mouth_parent = middle[0]:
# Parent for ctrl.mouth, mouth_layers and *_in
# mouth_layers[]:
# Apply fade out of ctrl.mouth motion for outer loops.
# top_out[], bottom_out[], middle_out[]:
# Combine mouth and jaw motions via Copy Custom to Local.
# deform:
# master:
# Deform mirror of ctrl.master.
#
####################################################
####################################################
# CHILD CHAINS
def init_child_chains(self):
self.child_chains = [
rig
for rig in self.rigify_children
if isinstance(rig, BasicChainRig) and get_name_side_z(rig.base_bone) != SideZ.MIDDLE
]
self.corners = {Side.LEFT: [], Side.RIGHT: [], SideZ.TOP: [], SideZ.BOTTOM: []}
def arrange_child_chains(self):
"""Sort child chains into their corresponding mouth loops."""
if self.chain_to_layer is not None:
return
# Index child node corners
for child in self.child_chains:
for node in child.control_nodes:
corner = self.is_corner_node(node)
if corner:
if node.merged_master not in self.corners[corner]:
self.corners[corner].append(node.merged_master)
self.num_layers = len(self.corners[SideZ.TOP])
for k, v in self.corners.items():
if len(v) == 0:
self.raise_error("Could not find all mouth corners")
if len(v) != self.num_layers:
self.raise_error(
"Mouth corner counts differ: {} vs {}",
[n.name for n in v], [n.name for n in self.corners[SideZ.TOP]]
)
# Find inner top/bottom corners
anchor = self.corners[SideZ.BOTTOM][0].point
inner_top = min(self.corners[SideZ.TOP], key=lambda p: (p.point - anchor).length)
anchor = inner_top.point
inner_bottom = min(self.corners[SideZ.BOTTOM], key=lambda p: (p.point - anchor).length)
# Compute the mouth space
self.mouth_center = center = (inner_top.point + inner_bottom.point) / 2
matrix = self.mouth_orientation.to_matrix().to_4x4()
matrix.translation = center
self.mouth_space = matrix
self.to_mouth_space = matrix.inverted()
# Build a mapping of child chain to layer (i.e. sort multiple mouth loops)
self.chain_to_layer = {}
self.chains_by_side = {}
for k, v in list(self.corners.items()):
self.corners[k] = ordered = sorted(v, key=lambda p: (p.point - center).length)
chain_set = set()
for i, node in enumerate(ordered):
for sibling in node.get_merged_siblings():
if sibling.rig in self.child_chains:
cur_layer = self.chain_to_layer.get(sibling.rig)
if cur_layer is not None and cur_layer != i:
self.raise_error(
"Conflicting mouth chain layer on {}: {} and {}", sibling.rig.base_bone, i, cur_layer)
self.chain_to_layer[sibling.rig] = i
chain_set.add(sibling.rig)
self.chains_by_side[k] = chain_set
for child in self.child_chains:
if child not in self.chain_to_layer:
self.raise_error("Could not determine chain layer on {}", child.base_bone)
if not self.chains_by_side[Side.LEFT].isdisjoint(self.chains_by_side[Side.RIGHT]):
self.raise_error("Left/right conflict in mouth")
if not self.chains_by_side[SideZ.TOP].isdisjoint(self.chains_by_side[SideZ.BOTTOM]):
self.raise_error("Top/bottom conflict in mouth")
# Find left/right direction
pt = self.to_mouth_space @ self.corners[Side.LEFT][0].point
self.left_sign = 1 if pt.x > 0 else -1
for node in self.corners[Side.LEFT]:
if (self.to_mouth_space @ node.point).x * self.left_sign <= 0:
self.raise_error("Bad left corner location: {}", node.name)
for node in self.corners[Side.RIGHT]:
if (self.to_mouth_space @ node.point).x * self.left_sign >= 0:
self.raise_error("Bad right corner location: {}", node.name)
# Find layer loop widths
self.layer_width = [
(self.corners[Side.LEFT][i].point - self.corners[Side.RIGHT][i].point).length
for i in range(self.num_layers)
]
def position_mouth_bone(self, name, scale):
self.arrange_child_chains()
bone = self.get_bone(name)
bone.matrix = self.mouth_space
bone.length = self.layer_width[0] * scale
####################################################
# CONTROL NODES
def get_node_parent_bones(self, node):
"""Get parent bones and their armature weights for the given control node."""
self.arrange_child_chains()
# Choose correct layer bones
layer = self.chain_to_layer[node.rig]
top_mch = LazyRef(self.bones.mch, 'top_out', layer)
bottom_mch = LazyRef(self.bones.mch, 'bottom_out', layer)
middle_mch = LazyRef(self.bones.mch, 'middle_out', layer)
# Corners have one input
corner = self.is_corner_node(node)
if corner:
if corner == SideZ.TOP:
return [top_mch]
elif corner == SideZ.BOTTOM:
return [bottom_mch]
else:
return [middle_mch]
# Otherwise blend two
if node.rig in self.chains_by_side[SideZ.TOP]:
side_mch = top_mch
else:
side_mch = bottom_mch
pt_x = (self.to_mouth_space @ node.point).x
side = Side.LEFT if pt_x * self.left_sign >= 0 else Side.RIGHT
corner_x = (self.to_mouth_space @ self.corners[side][layer].point).x
factor = math.sqrt(1 - clamp(pt_x / corner_x) ** 2)
return [(side_mch, factor), (middle_mch, 1-factor)]
def get_parent_for_name(self, name, parent_bone):
"""Get single replacement parent for the given child bone."""
if parent_bone == self.base_bone:
side = get_name_side_z(name)
if side == SideZ.TOP:
return LazyRef(self.bones.mch, 'top', -1)
if side == SideZ.BOTTOM:
return LazyRef(self.bones.mch, 'bottom', -1)
return parent_bone
def get_child_chain_parent(self, rig, parent_bone):
return self.get_parent_for_name(rig.base_bone, parent_bone)
def build_control_node_parent(self, node, parent_bone):
if node.rig in self.child_chains:
return ControlBoneParentArmature(
self, node,
bones=self.get_node_parent_bones(node),
orientation=self.mouth_orientation,
copy_scale=LazyRef(self.bones.mch, 'mouth_parent'),
)
return ControlBoneParentOrg(self.get_parent_for_name(node.name, parent_bone))
####################################################
# Master control
@stage.generate_bones
def make_master_control(self):
org = self.bones.org
name = self.copy_bone(org, make_derived_name(org, 'ctrl'), parent=True)
self.bones.ctrl.master = name
@stage.configure_bones
def configure_master_control(self):
self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
self.get_bone(self.bones.ctrl.master).lock_scale = (True, True, True)
@stage.generate_widgets
def make_master_control_widget(self):
ctrl = self.bones.ctrl.master
create_jaw_widget(self.obj, ctrl)
####################################################
# Mouth control
@stage.generate_bones
def make_mouth_control(self):
org = self.bones.org
name = self.copy_bone(org, make_derived_name(org, 'ctrl', '_mouth'))
self.position_mouth_bone(name, 1)
self.bones.ctrl.mouth = name
@stage.parent_bones
def parent_mouth_control(self):
self.set_bone_parent(self.bones.ctrl.mouth, self.bones.mch.mouth_parent)
@stage.configure_bones
def configure_mouth_control(self):
pass
@stage.generate_widgets
def make_mouth_control_widget(self):
ctrl = self.bones.ctrl.mouth
width = (self.corners[Side.LEFT][0].point - self.corners[Side.RIGHT][0].point).length
height = (self.corners[SideZ.TOP][0].point - self.corners[SideZ.BOTTOM][0].point).length
back = (self.corners[Side.LEFT][0].point + self.corners[Side.RIGHT][0].point) / 2
front = (self.corners[SideZ.TOP][0].point + self.corners[SideZ.BOTTOM][0].point) / 2
depth = (front - back).length
create_circle_widget(
self.obj, ctrl,
radius=0.2 + 0.5 * (height / width), radius_x=0.7,
head_tail=0.2, head_tail_x=0.2 - (depth / width)
)
####################################################
# Jaw Motion MCH
@stage.generate_bones
def make_mch_lock_bones(self):
org = self.bones.org
mch = self.bones.mch
self.arrange_child_chains()
mch.lock = self.copy_bone(
org, make_derived_name(org, 'mch', '_lock'), scale=1/2, parent=True)
mch.top = map_list(self.make_mch_top_bone, range(self.num_layers), repeat(org))
mch.bottom = map_list(self.make_mch_bottom_bone, range(self.num_layers), repeat(org))
mch.middle = map_list(self.make_mch_middle_bone, range(self.num_layers), repeat(org))
mch.mouth_parent = mch.middle[0]
def make_mch_top_bone(self, i, org):
return self.copy_bone(org, make_derived_name(org, 'mch', '_top'), scale=1/4, parent=True)
def make_mch_bottom_bone(self, i, org):
return self.copy_bone(org, make_derived_name(org, 'mch', '_bottom'), scale=1/3, parent=True)
def make_mch_middle_bone(self, i, org):
return self.copy_bone(org, make_derived_name(org, 'mch', '_middle'), scale=2/3, parent=True)
@stage.parent_bones
def parent_mch_lock_bones(self):
mch = self.bones.mch
ctrl = self.bones.ctrl
for mid, top in zip(mch.middle, mch.top):
self.set_bone_parent(mid, top)
for bottom in mch.bottom[1:]:
self.set_bone_parent(bottom, ctrl.master)
@stage.configure_bones
def configure_mch_lock_bones(self):
ctrl = self.bones.ctrl
panel = self.script.panel_with_selected_check(self, [ctrl.master, ctrl.mouth])
self.make_property(ctrl.master, 'mouth_lock', 0.0, description='Mouth is locked closed')
panel.custom_prop(ctrl.master, 'mouth_lock', text='Mouth Lock', slider=True)
@stage.rig_bones
def rig_mch_track_bones(self):
mch = self.bones.mch
ctrl = self.bones.ctrl
# Lock position follows jaw master with configured influence
self.make_constraint(
mch.lock, 'COPY_TRANSFORMS', ctrl.master,
influence=self.params.jaw_locked_influence,
)
# Innermost top bone follows lock position according to slider
con = self.make_constraint(mch.top[0], 'COPY_TRANSFORMS', mch.lock)
self.make_driver(con, 'influence', variables=[(ctrl.master, 'mouth_lock')])
# Innermost bottom bone follows jaw master with configured influence, and then lock
self.make_constraint(
mch.bottom[0], 'COPY_TRANSFORMS', ctrl.master,
influence=self.params.jaw_mouth_influence,
)
con = self.make_constraint(mch.bottom[0], 'COPY_TRANSFORMS', mch.lock)
self.make_driver(con, 'influence', variables=[(ctrl.master, 'mouth_lock')])
# Outer layer bones interpolate toward innermost based on influence decay
coeff = self.params.jaw_secondary_influence
for i, name in enumerate(mch.top[1:]):
self.make_constraint(name, 'COPY_TRANSFORMS', mch.top[0], influence=coeff ** (1+i))
for i, name in enumerate(mch.bottom[1:]):
self.make_constraint(name, 'COPY_TRANSFORMS', mch.bottom[0], influence=coeff ** (1+i))
# Middle bones interpolate the middle between top and bottom
for mid, bottom in zip(mch.middle, mch.bottom):
self.make_constraint(mid, 'COPY_TRANSFORMS', bottom, influence=0.5)
####################################################
# Mouth MCH
@stage.generate_bones
def make_mch_mouth_bones(self):
mch = self.bones.mch
mch.mouth_layers = map_list(self.make_mch_mouth_bone,
range(1, self.num_layers), repeat('_mouth_layer'), repeat(0.6))
mch.top_out = map_list(self.make_mch_mouth_inout_bone,
range(self.num_layers), repeat('_top_out'), repeat(0.4))
mch.bottom_out = map_list(self.make_mch_mouth_inout_bone,
range(self.num_layers), repeat('_bottom_out'), repeat(0.35))
mch.middle_out = map_list(self.make_mch_mouth_inout_bone,
range(self.num_layers), repeat('_middle_out'), repeat(0.3))
def make_mch_mouth_bone(self, i, suffix, size):
name = self.copy_bone(self.bones.org, make_derived_name(self.bones.org, 'mch', suffix))
self.position_mouth_bone(name, size)
return name
def make_mch_mouth_inout_bone(self, i, suffix, size):
return self.copy_bone(self.bones.org, make_derived_name(self.bones.org, 'mch', suffix), scale=size)
@stage.parent_bones
def parent_mch_mouth_bones(self):
mch = self.bones.mch
layers = [self.bones.ctrl.mouth, *mch.mouth_layers]
for name in mch.mouth_layers:
self.set_bone_parent(name, mch.mouth_parent)
for name_list in [mch.top_out, mch.bottom_out, mch.middle_out]:
for name, parent in zip(name_list, layers):
self.set_bone_parent(name, parent)
@stage.rig_bones
def rig_mch_mouth_bones(self):
mch = self.bones.mch
ctrl = self.bones.ctrl.mouth
# Mouth influence fade out
for i, name in enumerate(mch.mouth_layers):
self.rig_mch_mouth_layer_bone(i+1, name, ctrl)
# Transfer and combine jaw motion with mouth
all_jaw = mch.top + mch.bottom + mch.middle
all_out = mch.top_out + mch.bottom_out + mch.middle_out
for dest, src in zip(all_out, all_jaw):
self.make_constraint(
dest, 'COPY_TRANSFORMS', src,
owner_space='LOCAL', target_space='CUSTOM',
space_object=self.obj, space_subtarget=mch.mouth_parent,
)
def rig_mch_mouth_layer_bone(self, i, mch, ctrl):
# Fade location and rotation based on influence decay
inf = self.params.jaw_secondary_influence ** i
self.make_constraint(mch, 'COPY_LOCATION', ctrl, influence=inf)
self.make_constraint(mch, 'COPY_ROTATION', ctrl, influence=inf)
# For scale, additionally take radius into account
inf_scale = inf * self.layer_width[0] / self.layer_width[i]
self.make_constraint(mch, 'COPY_SCALE', ctrl, influence=inf_scale)
####################################################
# ORG bone
@stage.parent_bones
def parent_org_chain(self):
self.set_bone_parent(self.bones.org, self.bones.ctrl.master, inherit_scale='FULL')
####################################################
# Deform bones
@stage.generate_bones
def make_deform_bone(self):
org = self.bones.org
deform = self.bones.deform
self.bones.deform.master = self.copy_bone(org, make_derived_name(org, 'def'))
@stage.parent_bones
def parent_deform_chain(self):
deform = self.bones.deform
self.set_bone_parent(deform.master, self.bones.org)
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
params.jaw_mouth_influence = bpy.props.FloatProperty(
name="Bottom Lip Influence",
default=0.5, min=0, max=1,
description="Influence of the jaw on the bottom lip chains"
)
params.jaw_locked_influence = bpy.props.FloatProperty(
name="Locked Influence",
default=0.2, min=0, max=1,
description="Influence of the jaw on the locked mouth"
)
params.jaw_secondary_influence = bpy.props.FloatProperty(
name="Secondary Influence Falloff",
default=0.5, min=0, max=1,
description="Reduction factor for each level of secondary mouth loops"
)
@classmethod
def parameters_ui(self, layout, params):
layout.prop(params, "jaw_mouth_influence", slider=True)
layout.prop(params, "jaw_locked_influence", slider=True)
layout.prop(params, "jaw_secondary_influence", slider=True)
def create_sample(obj):
# generated by rigify.utils.write_metarig
bpy.ops.object.mode_set(mode='EDIT')
arm = obj.data
bones = {}
bone = arm.edit_bones.new('jaw')
bone.head = 0.0000, 0.0000, 0.0000
bone.tail = 0.0000, -0.0585, -0.0489
bone.roll = 0.0000
bone.use_connect = False
bones['jaw'] = bone.name
bone = arm.edit_bones.new('teeth.T')
bone.head = 0.0000, -0.0589, 0.0080
bone.tail = 0.0000, -0.0283, 0.0080
bone.roll = 0.0000
bone.use_connect = False
bones['teeth.T'] = bone.name
bone = arm.edit_bones.new('lip.T.L')
bone.head = -0.0000, -0.0684, 0.0030
bone.tail = 0.0105, -0.0655, 0.0033
bone.roll = -0.0000
bone.use_connect = False
bone.parent = arm.edit_bones[bones['jaw']]
bones['lip.T.L'] = bone.name
bone = arm.edit_bones.new('lip.B.L')
bone.head = -0.0000, -0.0655, -0.0078
bone.tail = 0.0107, -0.0625, -0.0053
bone.roll = -0.0551
bone.use_connect = False
bone.parent = arm.edit_bones[bones['jaw']]
bones['lip.B.L'] = bone.name
bone = arm.edit_bones.new('lip.T.R')
bone.head = 0.0000, -0.0684, 0.0030
bone.tail = -0.0105, -0.0655, 0.0033
bone.roll = 0.0000
bone.use_connect = False
bone.parent = arm.edit_bones[bones['jaw']]
bones['lip.T.R'] = bone.name
bone = arm.edit_bones.new('lip.B.R')
bone.head = 0.0000, -0.0655, -0.0078
bone.tail = -0.0107, -0.0625, -0.0053
bone.roll = 0.0551
bone.use_connect = False
bone.parent = arm.edit_bones[bones['jaw']]
bones['lip.B.R'] = bone.name
bone = arm.edit_bones.new('teeth.B')
bone.head = 0.0000, -0.0543, -0.0136
bone.tail = 0.0000, -0.0237, -0.0136
bone.roll = 0.0000
bone.use_connect = False
bone.parent = arm.edit_bones[bones['jaw']]
bones['teeth.B'] = bone.name
bone = arm.edit_bones.new('lip1.T.L')
bone.head = 0.0105, -0.0655, 0.0033
bone.tail = 0.0193, -0.0586, 0.0007
bone.roll = -0.0257
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lip.T.L']]
bones['lip1.T.L'] = bone.name
bone = arm.edit_bones.new('lip1.B.L')
bone.head = 0.0107, -0.0625, -0.0053
bone.tail = 0.0194, -0.0573, -0.0029
bone.roll = 0.0716
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lip.B.L']]
bones['lip1.B.L'] = bone.name
bone = arm.edit_bones.new('lip1.T.R')
bone.head = -0.0105, -0.0655, 0.0033
bone.tail = -0.0193, -0.0586, 0.0007
bone.roll = 0.0257
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lip.T.R']]
bones['lip1.T.R'] = bone.name
bone = arm.edit_bones.new('lip1.B.R')
bone.head = -0.0107, -0.0625, -0.0053
bone.tail = -0.0194, -0.0573, -0.0029
bone.roll = -0.0716
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lip.B.R']]
bones['lip1.B.R'] = bone.name
bone = arm.edit_bones.new('lip2.T.L')
bone.head = 0.0193, -0.0586, 0.0007
bone.tail = 0.0236, -0.0539, -0.0014
bone.roll = 0.0324
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lip1.T.L']]
bones['lip2.T.L'] = bone.name
bone = arm.edit_bones.new('lip2.B.L')
bone.head = 0.0194, -0.0573, -0.0029
bone.tail = 0.0236, -0.0539, -0.0014
bone.roll = 0.0467
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lip1.B.L']]
bones['lip2.B.L'] = bone.name
bone = arm.edit_bones.new('lip2.T.R')
bone.head = -0.0193, -0.0586, 0.0007
bone.tail = -0.0236, -0.0539, -0.0014
bone.roll = -0.0324
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lip1.T.R']]
bones['lip2.T.R'] = bone.name
bone = arm.edit_bones.new('lip2.B.R')
bone.head = -0.0194, -0.0573, -0.0029
bone.tail = -0.0236, -0.0539, -0.0014
bone.roll = -0.0467
bone.use_connect = True
bone.parent = arm.edit_bones[bones['lip1.B.R']]
bones['lip2.B.R'] = bone.name
bpy.ops.object.mode_set(mode='OBJECT')
pbone = obj.pose.bones[bones['jaw']]
pbone.rigify_type = 'face.skin_jaw'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['teeth.T']]
pbone.rigify_type = 'basic.super_copy'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
try:
pbone.rigify_parameters.make_deform = False
except AttributeError:
pass
try:
pbone.rigify_parameters.super_copy_widget_type = "teeth"
except AttributeError:
pass
pbone = obj.pose.bones[bones['lip.T.L']]
pbone.rigify_type = 'skin.stretchy_chain'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
try:
pbone.rigify_parameters.bbones = 3
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
except AttributeError:
pass
pbone = obj.pose.bones[bones['lip.B.L']]
pbone.rigify_type = 'skin.stretchy_chain'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
try:
pbone.rigify_parameters.bbones = 3
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
except AttributeError:
pass
pbone = obj.pose.bones[bones['lip.T.R']]
pbone.rigify_type = 'skin.stretchy_chain'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
try:
pbone.rigify_parameters.bbones = 3
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
except AttributeError:
pass
pbone = obj.pose.bones[bones['lip.B.R']]
pbone.rigify_type = 'skin.stretchy_chain'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
try:
pbone.rigify_parameters.bbones = 3
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_falloff_spherical = [True, False, True]
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_falloff = [0.5, 1.0, -0.5]
except AttributeError:
pass
try:
pbone.rigify_parameters.skin_chain_connect_mirror = [True, False]
except AttributeError:
pass
pbone = obj.pose.bones[bones['teeth.B']]
pbone.rigify_type = 'basic.super_copy'
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
try:
pbone.rigify_parameters.super_copy_widget_type = "teeth"
except AttributeError:
pass
try:
pbone.rigify_parameters.make_deform = False
except AttributeError:
pass
pbone = obj.pose.bones[bones['lip1.T.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lip1.B.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lip1.T.R']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lip1.B.R']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lip2.T.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lip2.B.L']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lip2.T.R']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
pbone = obj.pose.bones[bones['lip2.B.R']]
pbone.rigify_type = ''
pbone.lock_location = (False, False, False)
pbone.lock_rotation = (False, False, False)
pbone.lock_rotation_w = False
pbone.lock_scale = (False, False, False)
pbone.rotation_mode = 'QUATERNION'
bpy.ops.object.mode_set(mode='EDIT')
for bone in arm.edit_bones:
bone.select = False
bone.select_head = False
bone.select_tail = False
for b in bones:
bone = arm.edit_bones[bones[b]]
bone.select = True
bone.select_head = True
bone.select_tail = True
bone.bbone_x = bone.bbone_z = bone.length * 0.05
arm.edit_bones.active = bone
return bones

142
rigify/rigs/skin/anchor.py Normal file
View File

@ -0,0 +1,142 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
from ...utils.naming import make_derived_name
from ...utils.widgets import layout_widget_dropdown, create_registered_widget
from ...utils.mechanism import move_all_constraints
from ...base_rig import stage
from .skin_nodes import ControlBoneNode, ControlNodeIcon, ControlNodeEnd
from .skin_rigs import BaseSkinChainRigWithRotationOption
from ..basic.raw_copy import RelinkConstraintsMixin
class Rig(BaseSkinChainRigWithRotationOption, RelinkConstraintsMixin):
"""Custom skin control node."""
chain_priority = 20
def find_org_bones(self, bone):
return bone.name
def initialize(self):
super().initialize()
self.make_deform = self.params.make_extra_deform
####################################################
# CONTROL NODES
@stage.initialize
def init_control_nodes(self):
org = self.bones.org
name = make_derived_name(org, 'ctrl')
self.control_node = node = ControlBoneNode(
self, org, name, icon=ControlNodeIcon.CUSTOM, chain_end=ControlNodeEnd.START)
node.hide_control = self.params.skin_anchor_hide
def make_control_node_widget(self, node):
create_registered_widget(self.obj, node.control_bone,
self.params.pivot_master_widget_type or 'cube')
def extend_control_node_rig(self, node):
if node.rig == self:
org = self.bones.org
self.copy_bone_properties(org, node.control_bone)
self.relink_bone_constraints(org)
move_all_constraints(self.obj, org, node.control_bone)
##############################
# ORG chain
@stage.parent_bones
def parent_org_chain(self):
self.set_bone_parent(self.bones.org, self.control_node.control_bone)
##############################
# Deform bone
@stage.generate_bones
def make_deform_bone(self):
if self.make_deform:
self.bones.deform = self.copy_bone(
self.bones.org, make_derived_name(self.bones.org, 'def'))
@stage.parent_bones
def parent_deform_chain(self):
if self.make_deform:
self.set_bone_parent(self.bones.deform, self.bones.org)
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
params.make_extra_deform = bpy.props.BoolProperty(
name="Extra Deform",
default=False,
description="Create an optional deform bone"
)
params.skin_anchor_hide = bpy.props.BoolProperty(
name='Suppress Control',
default=False,
description='Make the control bone a mechanism bone invisible to the user and only affected by constraints'
)
params.pivot_master_widget_type = bpy.props.StringProperty(
name="Widget Type",
default='cube',
description="Choose the type of the widget to create"
)
self.add_relink_constraints_params(params)
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
col = layout.column()
col.prop(params, "make_extra_deform", text='Generate Deform Bone')
col.prop(params, "skin_anchor_hide")
row = layout.row()
row.active = not params.skin_anchor_hide
layout_widget_dropdown(row, params, "pivot_master_widget_type")
layout.prop(params, "relink_constraints")
layout.label(text="All constraints are moved to the control bone.", icon='INFO')
super().parameters_ui(layout, params)
def create_sample(obj):
from rigify.rigs.basic.super_copy import create_sample as inner
obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.anchor'

View File

@ -0,0 +1,520 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
import math
from itertools import count, repeat
from mathutils import Vector, Matrix, Quaternion
from math import acos
from bl_math import smoothstep
from ...utils.rig import connected_children_names, rig_is_child
from ...utils.layers import ControlLayersOption
from ...utils.naming import make_derived_name
from ...utils.bones import align_bone_orientation, align_bone_to_axis, align_bone_roll
from ...utils.mechanism import driver_var_distance
from ...utils.widgets_basic import create_cube_widget, create_sphere_widget
from ...utils.misc import map_list, matrix_from_axis_roll
from ...base_rig import stage
from .skin_nodes import ControlBoneNode, ControlNodeEnd
from .skin_rigs import BaseSkinChainRigWithRotationOption, get_bone_quaternion
class Rig(BaseSkinChainRigWithRotationOption):
"""
Base deform rig of the skin system, implementing a B-Bone chain without
any automation on the control nodes.
"""
chain_priority = None
def find_org_bones(self, bone):
return [bone.name] + connected_children_names(self.obj, bone.name)
def initialize(self):
super().initialize()
self.bbone_segments = self.params.bbones
self.use_bbones = self.bbone_segments > 1
self.use_connect_mirror = self.params.skin_chain_connect_mirror
self.use_connect_ends = self.params.skin_chain_connect_ends
self.use_scale = any(self.params.skin_chain_use_scale)
self.use_reparent_handles = self.params.skin_chain_use_reparent
orgs = self.bones.org
self.num_orgs = len(orgs)
self.length = sum([self.get_bone(b).length for b in orgs]) / len(orgs)
####################################################
# OVERRIDES
def get_control_node_rotation(self, node):
"""Compute the chain-aligned control orientation."""
orgs = self.bones.org
# Average the adjoining org bone orientations
bones = orgs[max(0, node.index-1):node.index+1]
quats = [get_bone_quaternion(self.obj, name) for name in bones]
result = sum(quats, Quaternion((0, 0, 0, 0))).normalized()
# For end bones, align to the connected chain tangent
if node.index in (0, self.num_orgs):
chain = self.get_node_chain_with_mirror()
nprev = chain[node.index]
nnext = chain[node.index+2]
if nprev and nnext:
# Apply only swing to preserve roll; tgt roll thus doesn't matter
tgt = matrix_from_axis_roll(nnext.point - nprev.point, 0).to_quaternion()
swing, _ = (result.inverted() @ tgt).to_swing_twist('Y')
result = result @ swing
return result
def get_all_controls(self):
return [node.control_bone for node in self.control_nodes]
####################################################
# BONES
#
# mch:
# handles[]
# Final B-Bone handles.
# handles_pre[] (optional, may be copy of handles[])
# Mechanism bones that emulate Auto handle behavior.
# deform[]:
# Deformation B-Bones.
#
####################################################
####################################################
# CONTROL NODES
@stage.initialize
def init_control_nodes(self):
orgs = self.bones.org
self.control_nodes = nodes = [
# Bone head nodes
*map_list(self.make_control_node, count(0), orgs, repeat(False)),
# Tail of the final bone
self.make_control_node(len(orgs), orgs[-1], True),
]
self.control_node_chain = None
nodes[0].chain_end_neighbor = nodes[1]
nodes[-1].chain_end_neighbor = nodes[-2]
def make_control_node(self, i, org, is_end):
bone = self.get_bone(org)
name = make_derived_name(org, 'ctrl', '_end' if is_end else '')
pos = bone.tail if is_end else bone.head
if i == 0:
chain_end = ControlNodeEnd.START
elif is_end:
chain_end = ControlNodeEnd.END
else:
chain_end = ControlNodeEnd.MIDDLE
return ControlBoneNode(
self, org, name, point=pos, size=self.length/3, index=i,
allow_scale=self.use_scale, needs_reparent=self.use_reparent_handles,
chain_end=chain_end,
)
def make_control_node_widget(self, node):
create_sphere_widget(self.obj, node.control_bone)
####################################################
# B-Bone handle MCH
# Generate two layers of handle bones, 'pre' for the auto handle mechanism,
# and final handles combining that with user transformation. This flag may
# be enabled by parent controller rigs when needed in order to be able to
# inject more automatic handle positioning mechanisms.
use_pre_handles = False
def get_connected_node(self, node):
"""Find which other chain to connect this chain to at this node."""
is_end = 1 if node.index != 0 else 0
corner = self.params.skin_chain_connect_sharp_angle[is_end]
# First try merge through mirror
if self.use_connect_mirror[is_end]:
mirror = node.get_best_mirror()
if mirror and mirror.chain_end_neighbor and isinstance(mirror.rig, Rig):
# Connect the same chain end
s_is_end = 1 if mirror.index != 0 else 0
if is_end == s_is_end and mirror.rig.use_connect_mirror[is_end]:
mirror_corner = mirror.rig.params.skin_chain_connect_sharp_angle[is_end]
return mirror, mirror.chain_end_neighbor, (corner + mirror_corner)/2
# Then try connecting ends
if self.use_connect_ends[is_end]:
# Find chains that want to connect ends at this node group
groups = ([], [])
for sibling in node.get_merged_siblings():
if isinstance(sibling.rig, Rig) and sibling.chain_end_neighbor:
s_is_end = 1 if sibling.index != 0 else 0
if sibling.rig.use_connect_ends[s_is_end]:
groups[s_is_end].append(sibling)
# Only connect if the pairing is unambiguous
if len(groups[0]) == 1 and len(groups[1]) == 1:
assert node == groups[is_end][0]
link = groups[1 - is_end][0]
link_corner = link.rig.params.skin_chain_connect_sharp_angle[1 - is_end]
return link, link.chain_end_neighbor, (corner + link_corner)/2
return None, None, 0
def get_node_chain_with_mirror(self):
"""Get node chain with connected node extensions at the ends."""
if self.control_node_chain is not None:
return self.control_node_chain
nodes = self.control_nodes
prev_link, self.prev_node, self.prev_corner = self.get_connected_node(nodes[0])
next_link, self.next_node, self.next_corner = self.get_connected_node(nodes[-1])
self.control_node_chain = [self.prev_node, *nodes, self.next_node]
# Optimize connect next by sharing last handle mch
if next_link and next_link.index == 0:
self.next_chain_rig = next_link.rig
else:
self.next_chain_rig = None
return self.control_node_chain
def get_all_mch_handles(self):
if self.next_chain_rig:
return self.bones.mch.handles + [self.next_chain_rig.bones.mch.handles[0]]
else:
return self.bones.mch.handles
def get_all_mch_handles_pre(self):
if self.next_chain_rig:
return self.bones.mch.handles_pre + [self.next_chain_rig.bones.mch.handles_pre[0]]
else:
return self.bones.mch.handles_pre
@stage.generate_bones
def make_mch_handle_bones(self):
if self.use_bbones:
mch = self.bones.mch
chain = self.get_node_chain_with_mirror()
# If the last handle mch will be shared, drop it from chain
if self.next_chain_rig:
chain = chain[0:-1]
mch.handles = map_list(self.make_mch_handle_bone, count(0),
chain, chain[1:], chain[2:])
if self.use_pre_handles:
mch.handles_pre = map_list(self.make_mch_pre_handle_bone, count(0), mch.handles)
else:
mch.handles_pre = mch.handles
def make_mch_handle_bone(self, i, prev_node, node, next_node):
name = self.copy_bone(node.org, make_derived_name(node.name, 'mch', '_handle'))
hstart = prev_node or node
hend = next_node or node
haxis = (hend.point - hstart.point).normalized()
bone = self.get_bone(name)
bone.tail = bone.head + haxis * self.length * 3/4
align_bone_roll(self.obj, name, node.org)
return name
def make_mch_pre_handle_bone(self, i, handle):
return self.copy_bone(handle, make_derived_name(handle, 'mch', '_pre'))
@stage.parent_bones
def parent_mch_handle_bones(self):
if self.use_bbones:
mch = self.bones.mch
if self.use_pre_handles:
for pre in mch.handles_pre:
self.set_bone_parent(pre, self.rig_parent_bone, inherit_scale='AVERAGE')
for handle in mch.handles:
self.set_bone_parent(handle, self.rig_parent_bone, inherit_scale='AVERAGE')
@stage.rig_bones
def rig_mch_handle_bones(self):
if self.use_bbones:
mch = self.bones.mch
chain = self.get_node_chain_with_mirror()
# Rig Auto-handle emulation (on pre handles)
for args in zip(count(0), mch.handles_pre, chain, chain[1:], chain[2:]):
self.rig_mch_handle_auto(*args)
# Apply user transformation to the final handles
for args in zip(count(0), mch.handles, chain, chain[1:], chain[2:], mch.handles_pre):
self.rig_mch_handle_user(*args)
def rig_mch_handle_auto(self, i, mch, prev_node, node, next_node):
hstart = prev_node or node
hend = next_node or node
# Emulate auto handle
self.make_constraint(mch, 'COPY_LOCATION', hstart.control_bone, name='locate_prev')
self.make_constraint(mch, 'DAMPED_TRACK', hend.control_bone, name='track_next')
def rig_mch_handle_user(self, i, mch, prev_node, node, next_node, pre):
# Copy from the pre handle if used. Before Full is used to allow
# drivers on local transform channels to still work.
if pre != mch:
self.make_constraint(
mch, 'COPY_TRANSFORMS', pre, name='copy_pre',
space='LOCAL', mix_mode='BEFORE_FULL',
)
# Apply user rotation and scale.
# If the node belongs to a parent of this rig, there is a good chance this
# may cause weird double transformation, so skip it in that case.
if not rig_is_child(self, node.merged_master.rig, strict=True):
input_bone = node.reparent_bone if self.use_reparent_handles else node.control_bone
self.make_constraint(
mch, 'COPY_TRANSFORMS', input_bone, name='copy_user',
target_space='LOCAL_OWNER_ORIENT', owner_space='LOCAL',
mix_mode='BEFORE_FULL',
)
# Remove any shear created by the previous steps
self.make_constraint(mch, 'LIMIT_ROTATION', name='remove_shear')
##############################
# ORG chain
@stage.parent_bones
def parent_org_chain(self):
orgs = self.bones.org
self.set_bone_parent(orgs[0], self.rig_parent_bone, inherit_scale='AVERAGE')
self.parent_bone_chain(orgs, use_connect=True, inherit_scale='AVERAGE')
@stage.rig_bones
def rig_org_chain(self):
for args in zip(count(0), self.bones.org, self.control_nodes, self.control_nodes[1:]):
self.rig_org_bone(*args)
def rig_org_bone(self, i, org, node, next_node):
if i == 0:
self.make_constraint(org, 'COPY_LOCATION', node.control_bone)
self.make_constraint(org, 'STRETCH_TO', next_node.control_bone, keep_axis='SWING_Y')
##############################
# Deform chain
@stage.generate_bones
def make_deform_chain(self):
self.bones.deform = map_list(self.make_deform_bone, count(0), self.bones.org)
def make_deform_bone(self, i, org):
name = self.copy_bone(org, make_derived_name(org, 'def'), bbone=True)
self.get_bone(name).bbone_segments = self.bbone_segments
return name
@stage.parent_bones
def parent_deform_chain(self):
deform = self.bones.deform
self.set_bone_parent(deform[0], self.rig_parent_bone, inherit_scale='AVERAGE')
self.parent_bone_chain(deform, use_connect=True, inherit_scale='AVERAGE')
if self.use_bbones:
handles = self.get_all_mch_handles()
for name, start_handle, end_handle in zip(deform, handles, handles[1:]):
bone = self.get_bone(name)
bone.bbone_handle_type_start = 'TANGENT'
bone.bbone_custom_handle_start = self.get_bone(start_handle)
bone.bbone_handle_type_end = 'TANGENT'
bone.bbone_custom_handle_end = self.get_bone(end_handle)
if self.use_scale:
bone.bbone_handle_use_scale_start = self.params.skin_chain_use_scale[0:3]
bone.bbone_handle_use_scale_end = self.params.skin_chain_use_scale[0:3]
bone.bbone_handle_use_ease_start = self.params.skin_chain_use_scale[3]
bone.bbone_handle_use_ease_end = self.params.skin_chain_use_scale[3]
@stage.rig_bones
def rig_deform_chain(self):
for args in zip(count(0), self.bones.deform, self.bones.org):
self.rig_deform_bone(*args)
def rig_deform_bone(self, i, deform, org):
self.make_constraint(deform, 'COPY_TRANSFORMS', org)
if self.use_bbones:
if i == 0 and self.prev_corner > 1e-3:
self.make_corner_driver(
deform, 'bbone_easein', self.control_nodes[0], self.control_nodes[1], self.prev_node, self.prev_corner)
elif i == self.num_orgs-1 and self.next_corner > 1e-3:
self.make_corner_driver(
deform, 'bbone_easeout', self.control_nodes[-1], self.control_nodes[-2], self.next_node, self.next_corner)
def make_corner_driver(self, bbone, field, corner_node, next_node1, next_node2, angle):
"""
Create a driver adjusting B-Bone Ease based on the angle between controls,
gradually making the corner sharper when the angle drops below the threshold.
"""
pbone = self.get_bone(bbone)
a = (corner_node.point - next_node1.point).length
b = (corner_node.point - next_node2.point).length
c = (next_node1.point - next_node2.point).length
varmap = {
'a': driver_var_distance(self.obj, bone1=corner_node.control_bone, bone2=next_node1.control_bone),
'b': driver_var_distance(self.obj, bone1=corner_node.control_bone, bone2=next_node2.control_bone),
'c': driver_var_distance(self.obj, bone1=next_node1.control_bone, bone2=next_node2.control_bone),
}
# Compute and set the ease in rest pose
initval = -1+2*smoothstep(-1, 1, acos((a*a+b*b-c*c)/max(2*a*b, 1e-10))/angle)
setattr(pbone.bone, field, initval)
# Create the actual driver
self.make_driver(
pbone, field,
expression='%f+2*smoothstep(-1,1,acos((a*a+b*b-c*c)/max(2*a*b,1e-10))/%f)' % (-1-initval, angle),
variables=varmap
)
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
params.bbones = bpy.props.IntProperty(
name='B-Bone Segments',
default=10,
min=1,
description='Number of B-Bone segments'
)
params.skin_chain_use_reparent = bpy.props.BoolProperty(
name='Merge Parent Rotation And Scale',
default=False,
description='When controls are merged into ones owned by other chains, include ' +
'parent-induced rotation/scale difference into handle motion. Otherwise ' +
'only local motion of the control bone is used',
)
params.skin_chain_use_scale = bpy.props.BoolVectorProperty(
size=4,
name='Use Handle Scale',
default=(False, False, False, False),
description='Use control scaling to scale the B-Bone'
)
params.skin_chain_connect_mirror = bpy.props.BoolVectorProperty(
size=2,
name='Connect With Mirror',
default=(True, True),
description='Create a smooth B-Bone transition if an end of the chain meets its mirror'
)
params.skin_chain_connect_sharp_angle = bpy.props.FloatVectorProperty(
size=2,
name='Sharpen Corner',
default=(0, 0),
min=0,
max=math.pi,
description='Create a mechanism to sharpen a connected corner when the angle is below this value',
unit='ROTATION',
)
params.skin_chain_connect_ends = bpy.props.BoolVectorProperty(
size=2,
name='Connect Matching Ends',
default=(False, False),
description='Create a smooth B-Bone transition if an end of the chain meets another chain going in the same direction'
)
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
layout.prop(params, "bbones")
col = layout.column()
col.active = params.bbones > 1
col.prop(params, "skin_chain_use_reparent")
row = col.split(factor=0.3)
row.label(text="Use Scale:")
row = row.row(align=True)
row.prop(params, "skin_chain_use_scale", index=0, text="X", toggle=True)
row.prop(params, "skin_chain_use_scale", index=1, text="Y", toggle=True)
row.prop(params, "skin_chain_use_scale", index=2, text="Z", toggle=True)
row.prop(params, "skin_chain_use_scale", index=3, text="Ease", toggle=True)
row = col.split(factor=0.3)
row.label(text="Connect Mirror:")
row = row.row(align=True)
row.prop(params, "skin_chain_connect_mirror", index=0, text="Start", toggle=True)
row.prop(params, "skin_chain_connect_mirror", index=1, text="End", toggle=True)
row = col.split(factor=0.3)
row.label(text="Connect Next:")
row = row.row(align=True)
row.prop(params, "skin_chain_connect_ends", index=0, text="Start", toggle=True)
row.prop(params, "skin_chain_connect_ends", index=1, text="End", toggle=True)
row = col.split(factor=0.3)
row.label(text="Sharpen:")
row = row.row(align=True)
row.prop(params, "skin_chain_connect_sharp_angle", index=0, text="Start")
row.prop(params, "skin_chain_connect_sharp_angle", index=1, text="End")
super().parameters_ui(layout, params)
def create_sample(obj):
from rigify.rigs.basic.copy_chain import create_sample as inner
obj.pose.bones[inner(obj)["bone.01"]].rigify_type = 'skin.basic_chain'

321
rigify/rigs/skin/glue.py Normal file
View File

@ -0,0 +1,321 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
from ...utils.naming import make_derived_name
from ...utils.widgets_basic import create_cube_widget
from ...utils.mechanism import move_all_constraints
from ...base_rig import stage
from ...base_generate import SubstitutionRig
from .skin_nodes import ControlQueryNode
from .skin_rigs import BaseSkinRig
from ..basic.raw_copy import RelinkConstraintsMixin
from .basic_chain import Rig as BasicChainRig
class Rig(SubstitutionRig):
"""Skin rig component that injects constraints into a control generated by other rigs."""
def substitute(self):
# Deformation is implemented by inheriting from the chain rig, so
# enabling it requires switching between two different classes.
if self.params.skin_glue_head_mode == 'BRIDGE':
return [self.instantiate_rig(BridgeGlueRig, self.base_bone)]
else:
return [self.instantiate_rig(SimpleGlueRig, self.base_bone)]
def add_parameters(params):
SimpleGlueRig.add_parameters(params)
BridgeGlueRig.add_parameters(params)
def parameters_ui(layout, params):
if params.skin_glue_head_mode == 'BRIDGE':
BridgeGlueRig.parameters_ui(layout, params)
else:
SimpleGlueRig.parameters_ui(layout, params)
class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
"""Base class for the glue rigs."""
def initialize(self):
super().initialize()
self.glue_head_mode = self.params.skin_glue_head_mode
self.glue_use_tail = self.params.relink_constraints and self.params.skin_glue_use_tail
self.relink_unmarked_constraints = self.glue_use_tail
####################################################
# QUERY NODES
@stage.initialize
def init_glue_nodes(self):
bone = self.get_bone(self.base_bone)
self.head_constraint_node = ControlQueryNode(
self, self.base_bone, point=bone.head
)
if self.glue_use_tail:
self.tail_position_node = PositionQueryNode(
self, self.base_bone, point=bone.tail,
needs_reparent=self.params.skin_glue_tail_reparent,
)
####################################################
# GLUE CONSTRAINTS
def rig_glue_constraints(self):
org = self.base_bone
ctrl = self.head_constraint_node.control_bone
self.relink_bone_constraints(org)
# Add the built-in constraint
if self.glue_use_tail:
target = self.tail_position_node.output_bone
add_mode = self.params.skin_glue_add_constraint
inf = self.params.skin_glue_add_constraint_influence
if add_mode == 'COPY_LOCATION':
self.make_constraint(
ctrl, 'COPY_LOCATION', target, insert_index=0,
owner_space='LOCAL', target_space='LOCAL',
use_offset=True, influence=inf
)
elif add_mode == 'COPY_LOCATION_OWNER':
self.make_constraint(
ctrl, 'COPY_LOCATION', target, insert_index=0,
owner_space='LOCAL', target_space='LOCAL_OWNER_ORIENT',
use_offset=True, influence=inf
)
move_all_constraints(self.obj, org, ctrl)
def find_relink_target(self, spec, old_target):
if self.glue_use_tail and (spec == 'TARGET' or spec == '' == old_target):
return self.tail_position_node.output_bone
return super().find_relink_target(spec, old_target)
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
params.skin_glue_head_mode = bpy.props.EnumProperty(
name='Glue Mode',
items=[('CHILD', 'Child Of Control',
"The glue bone becomes a child of the control bone"),
('MIRROR', 'Mirror Of Control',
"The glue bone becomes a sibling of the control bone with Copy Transforms"),
('REPARENT', 'Mirror With Parents',
"The glue bone keeps its parent, but uses Copy Transforms to group both local and parent induced motion of the control into local space"),
('BRIDGE', 'Deformation Bridge',
"Other than adding glue constraints to the control, the rig acts as a one segment basic deform chain")],
default='CHILD',
description="Specifies how the glue bone is rigged to the control at the bone head location",
)
params.skin_glue_use_tail = bpy.props.BoolProperty(
name='Use Tail Target',
default=False,
description='Find the control at the bone tail location and use it to relink TARGET or any constraints without an assigned subtarget or relink spec'
)
params.skin_glue_tail_reparent = bpy.props.BoolProperty(
name='Target Local With Parents',
default=False,
description='Include transformations induced by target parents into target local space'
)
params.skin_glue_add_constraint = bpy.props.EnumProperty(
name='Add Constraint',
items=[('NONE', 'No New Constraint',
"Don't add new constraints"),
('COPY_LOCATION', 'Copy Location (Local)',
"Add a constraint to copy Local Location with Offset. If the owner and target control " +
"rest orientations are different, the global movement direction will change accordingly"),
('COPY_LOCATION_OWNER', 'Copy Location (Local, Owner Orientation)',
"Add a constraint to copy Local Location (Owner Orientation) with Offset. Even if the owner and " +
"target controls have different rest orientations, the global movement direction would be the same")],
default='NONE',
description="Add one of the common constraints linking the control to the tail target",
)
params.skin_glue_add_constraint_influence = bpy.props.FloatProperty(
name="Influence",
default=1.0, min=0, max=1,
description="Influence of the added constraint",
)
self.add_relink_constraints_params(params)
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
layout.prop(params, "skin_glue_head_mode")
layout.prop(params, "relink_constraints")
if params.relink_constraints:
col = layout.column()
col.prop(params, "skin_glue_use_tail")
col2 = col.column()
col2.active = params.skin_glue_use_tail
col2.prop(params, "skin_glue_tail_reparent")
col = layout.column()
col.active = params.skin_glue_use_tail
col.prop(params, "skin_glue_add_constraint", text="Add")
col3 = col.column()
col3.active = params.skin_glue_add_constraint != 'NONE'
col3.prop(params, "skin_glue_add_constraint_influence", slider=True)
layout.label(text="All constraints are moved to the control bone.", icon='INFO')
super().parameters_ui(layout, params)
class SimpleGlueRig(BaseGlueRig):
"""Normal glue rig that only does glue."""
def find_org_bones(self, bone):
return bone.name
####################################################
# QUERY NODES
@stage.initialize
def init_glue_nodes(self):
super().init_glue_nodes()
bone = self.get_bone(self.base_bone)
self.head_position_node = PositionQueryNode(
self, self.base_bone, point=bone.head,
rig_org=self.glue_head_mode != 'CHILD',
needs_reparent=self.glue_head_mode == 'REPARENT',
)
##############################
# ORG chain
@stage.parent_bones
def parent_org_bone(self):
if self.glue_head_mode == 'CHILD':
self.set_bone_parent(self.bones.org, self.head_position_node.output_bone)
@stage.rig_bones
def rig_org_bone(self):
# This executes before head_position_node owned a by generator plugin
self.rig_glue_constraints()
class BridgeGlueRig(BaseGlueRig, BasicChainRig):
"""Glue rig that also behaves like a deformation chain rig."""
def find_org_bones(self, bone):
# Still only bind to one bone
return [bone.name]
# Assign lowest priority
chain_priority = -20
# Orientation is irrelevant since controls should be merged into others
use_skin_control_orientation_bone = False
####################################################
# QUERY NODES
@stage.prepare_bones
def prepare_glue_nodes(self):
# Verify that all nodes of the chain have been merged into others
for node in self.control_nodes:
if node.is_master_node:
self.raise_error('glue control {} was not merged', node.name)
##############################
# ORG chain
@stage.rig_bones
def rig_org_chain(self):
# Move the user constraints away before the chain adds new ones
self.rig_glue_constraints()
super().rig_org_chain()
class PositionQueryNode(ControlQueryNode):
"""Finds the position of the highest layer control and rig reparent and/or org bone"""
def __init__(self, rig, org, *, point=None, needs_reparent=False, rig_org=False):
super().__init__(rig, org, point=point, find_highest_layer=True)
self.needs_reparent = needs_reparent
self.rig_org = rig_org
@property
def output_bone(self):
if self.rig_org:
return self.org
elif self.needs_reparent:
return self.reparent_bone
else:
return self.control_bone
def initialize(self):
if self.needs_reparent:
parent = self.build_parent()
if not self.rig_org:
self.merged_master.request_reparent(parent)
def parent_bones(self):
if self.rig_org:
if self.needs_reparent:
parent = self.node_parent.output_bone
else:
parent = self.get_bone_parent(self.control_bone)
self.set_bone_parent(self.org, parent, inherit_scale='AVERAGE')
def apply_bones(self):
if self.rig_org:
self.get_bone(self.org).matrix = self.merged_master.matrix
def rig_bones(self):
if self.rig_org:
self.make_constraint(self.org, 'COPY_TRANSFORMS', self.control_bone)
def create_sample(obj):
from rigify.rigs.basic.super_copy import create_sample as inner
obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.glue'

View File

@ -0,0 +1,520 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
import enum
from mathutils import Vector, Quaternion
from ...utils.layers import set_bone_layers
from ...utils.naming import NameSides, make_derived_name, get_name_base_and_sides, change_name_side, Side, SideZ
from ...utils.bones import BoneUtilityMixin, set_bone_widget_transform
from ...utils.widgets_basic import create_cube_widget, create_sphere_widget
from ...utils.mechanism import MechanismUtilityMixin
from ...utils.rig import get_parent_rigs
from ...utils.node_merger import MainMergeNode, QueryMergeNode
from .skin_parents import ControlBoneParentLayer, ControlBoneWeakParentLayer
from .skin_rigs import BaseSkinRig, BaseSkinChainRig
class ControlNodeLayer(enum.IntEnum):
FREE = 0
MIDDLE_PIVOT = 10
TWEAK = 20
class ControlNodeIcon(enum.IntEnum):
TWEAK = 0
MIDDLE_PIVOT = 1
FREE = 2
CUSTOM = 3
class ControlNodeEnd(enum.IntEnum):
START = -1
MIDDLE = 0
END = 1
class BaseSkinNode(MechanismUtilityMixin, BoneUtilityMixin):
"""Base class for skin control and query nodes."""
node_parent_built = False
def do_build_parent(self):
"""Create and intern the parent mechanism generator."""
assert self.rig.generator.stage == 'initialize'
result = self.rig.build_own_control_node_parent(self)
parents = self.rig.get_all_parent_skin_rigs()
for rig in reversed(parents):
result = rig.extend_control_node_parent(result, self)
for rig in parents:
result = rig.extend_control_node_parent_post(result, self)
result = self.merged_master.intern_parent(self, result)
result.is_parent_frozen = True
return result
def build_parent(self, use=True):
"""Create and activate if needed the parent mechanism for this node."""
if not self.node_parent_built:
self.node_parent = self.do_build_parent()
self.node_parent_built = True
if use:
self.merged_master.register_use_parent(self.node_parent)
return self.node_parent
@property
def control_bone(self):
"""The generated control bone."""
return self.merged_master._control_bone
@property
def reparent_bone(self):
"""The generated reparent bone for this node's parent mechanism."""
return self.merged_master.get_reparent_bone(self.node_parent)
class ControlBoneNode(MainMergeNode, BaseSkinNode):
"""Node representing controls of skin chain rigs."""
merge_domain = 'ControlNetNode'
def __init__(
self, rig, org, name, *, point=None, size=None,
needs_parent=False, needs_reparent=False, allow_scale=False,
chain_end=ControlNodeEnd.MIDDLE,
layer=ControlNodeLayer.FREE, index=None, icon=ControlNodeIcon.TWEAK,
):
assert isinstance(rig, BaseSkinChainRig)
super().__init__(rig, name, point or rig.get_bone(org).head)
self.org = org
self.name_split = get_name_base_and_sides(name)
self.name_merged = None
self.name_merged_split = None
self.size = size or rig.get_bone(org).length
self.layer = layer
self.icon = icon
self.rotation = None
self.chain_end = chain_end
# Create the parent mechanism even if not master
self.node_needs_parent = needs_parent
# If this node's own parent mechanism differs from master, generate a conversion bone
self.node_needs_reparent = needs_reparent
# Generate the control as a MCH bone to hide it from the user
self.hide_control = False
# Unlock scale channels
self.allow_scale = allow_scale
# For use by the owner rig: index in chain
self.index = index
# If this node is the end of a chain, points to the next one
self.chain_end_neighbor = None
def can_merge_into(self, other):
# Only merge up the layers (towards more mechanism)
dprio = self.rig.chain_priority - other.rig.chain_priority
return (
dprio <= 0 and
(self.layer <= other.layer or dprio < 0) and
super().can_merge_into(other)
)
def get_merge_priority(self, other):
# Prefer higher and closest layer
if self.layer <= other.layer:
return -abs(self.layer - other.layer)
else:
return -abs(self.layer - other.layer) - 100
def is_better_cluster(self, other):
"""Check if the current bone is preferrable as master when choosing of same sized groups."""
# Prefer bones that have strictly more parents
my_parents = list(reversed(get_parent_rigs(self.rig.rigify_parent)))
other_parents = list(reversed(get_parent_rigs(other.rig.rigify_parent)))
if len(my_parents) > len(other_parents) and my_parents[0:len(other_parents)] == other_parents:
return True
if len(other_parents) > len(my_parents) and other_parents[0:len(other_parents)] == my_parents:
return False
# Prefer side chains
side_x_my, side_z_my = map(abs, self.name_split[1:])
side_x_other, side_z_other = map(abs, other.name_split[1:])
if ((side_x_my < side_x_other and side_z_my <= side_z_other) or
(side_x_my <= side_x_other and side_z_my < side_z_other)):
return False
if ((side_x_my > side_x_other and side_z_my >= side_z_other) or
(side_x_my >= side_x_other and side_z_my > side_z_other)):
return True
return False
def merge_done(self):
if self.is_master_node:
self.parent_subrig_cache = []
self.parent_subrig_names = {}
self.reparent_requests = []
self.used_parents = {}
super().merge_done()
self.find_mirror_siblings()
def find_mirror_siblings(self):
"""Find merged nodes that have their names in mirror symmetry with this one."""
self.mirror_siblings = {}
self.mirror_sides_x = set()
self.mirror_sides_z = set()
for node in self.get_merged_siblings():
if node.name_split.base == self.name_split.base:
self.mirror_siblings[node.name_split] = node
self.mirror_sides_x.add(node.name_split.side)
self.mirror_sides_z.add(node.name_split.side_z)
assert self.mirror_siblings[self.name_split] is self
# Remove sides that merged with a mirror from the name
side_x = Side.MIDDLE if len(self.mirror_sides_x) > 1 else self.name_split.side
side_z = SideZ.MIDDLE if len(self.mirror_sides_z) > 1 else self.name_split.side_z
self.name_merged = change_name_side(self.name, side=side_x, side_z=side_z)
self.name_merged_split = NameSides(self.name_split.base, side_x, side_z)
def get_best_mirror(self):
"""Find best mirror sibling for connecting via mirror."""
base, side, sidez = self.name_split
for flip in [(base, -side, -sidez), (base, -side, sidez), (base, side, -sidez)]:
mirror = self.mirror_siblings.get(flip, None)
if mirror and mirror is not self:
return mirror
return None
def intern_parent(self, node, parent):
"""De-duplicate the parent layer chain within this merge group."""
# Quick check for the same object
if id(parent) in self.parent_subrig_names:
return parent
# Find if an identical parent is already in the cache
cache = self.parent_subrig_cache
for previous in cache:
if previous == parent:
previous.is_parent_frozen = True
return previous
# Add to cache and intern the layer parent if exists
cache.append(parent)
self.parent_subrig_names[id(parent)] = node.name
if isinstance(parent, ControlBoneParentLayer):
parent.parent = self.intern_parent(node, parent.parent)
return parent
def register_use_parent(self, parent):
"""Activate this parent mechanism generator."""
self.used_parents[id(parent)] = parent
def request_reparent(self, parent):
"""Request a reparent bone to be generated for this parent mechanism."""
requests = self.reparent_requests
if parent not in requests:
# If the actual reparent would be generated, weak parent will be needed.
if self.has_weak_parent and not self.use_weak_parent:
if self.use_mix_parent or parent != self.node_parent:
self.use_weak_parent = True
for weak_parent in self.node_parent_list_weak:
self.register_use_parent(weak_parent)
self.register_use_parent(parent)
requests.append(parent)
def get_reparent_bone(self, parent):
"""Returns the generated reparent bone for this parent mechanism."""
return self.reparent_bones[id(parent)]
def get_rotation(self):
"""Returns the orientation quaternion provided for this node by parents."""
if self.rotation is None:
self.rotation = self.rig.get_final_control_node_rotation(self)
return self.rotation
def initialize(self):
if self.is_master_node:
sibling_list = self.get_merged_siblings()
mirror_sibling_list = self.mirror_siblings.values()
# Compute size
best = max(sibling_list, key=lambda n: n.icon)
best_mirror = best.mirror_siblings.values()
self.size = sum(node.size for node in best_mirror) / len(best_mirror)
# Compute orientation
self.rotation = sum(
(node.get_rotation() for node in mirror_sibling_list),
Quaternion((0, 0, 0, 0))
).normalized()
self.matrix = self.rotation.to_matrix().to_4x4()
self.matrix.translation = self.point
# Create parents and decide if mix would be needed
parent_list = [node.build_parent(use=False) for node in mirror_sibling_list]
if all(parent == self.node_parent for parent in parent_list):
self.use_mix_parent = False
parent_list = [self.node_parent]
else:
self.use_mix_parent = True
# Prepare parenting without weak layers
self.use_weak_parent = False
self.node_parent_list_weak = parent_list
self.node_parent_list = [ControlBoneWeakParentLayer.strip(p) for p in parent_list]
self.has_weak_parent = any((p is not pw)
for p, pw in zip(self.node_parent_list, parent_list))
for parent in self.node_parent_list:
self.register_use_parent(parent)
# All nodes
if self.node_needs_parent or self.node_needs_reparent:
parent = self.build_parent()
if self.node_needs_reparent:
self.merged_master.request_reparent(parent)
def prepare_bones(self):
# Activate parent components once all reparents are registered
if self.is_master_node:
for parent in self.used_parents.values():
parent.enable_component()
self.used_parents = None
def make_bone(self, name, scale, *, rig=None, orientation=None):
"""
Creates a bone associated with this node, using the appropriate
orientation, location and size.
"""
name = (rig or self).copy_bone(self.org, name)
if orientation is not None:
matrix = orientation.to_matrix().to_4x4()
matrix.translation = self.merged_master.point
else:
matrix = self.merged_master.matrix
bone = self.get_bone(name)
bone.matrix = matrix
bone.length = self.merged_master.size * scale
return name
def find_master_name_node(self):
"""Find which node to name the control bone from."""
# Chain end nodes have sub-par names, so try to find another chain
if self.chain_end == ControlNodeEnd.END:
# Choose possible other nodes so that it doesn't lose mirror tags
siblings = [
node for node in self.get_merged_siblings()
if self.mirror_sides_x.issubset(node.mirror_sides_x)
and self.mirror_sides_z.issubset(node.mirror_sides_z)
]
# Prefer chain start, then middle nodes
candidates = [node for node in siblings if node.chain_end == ControlNodeEnd.START]
if not candidates:
candidates = [node for node in siblings if node.chain_end == ControlNodeEnd.MIDDLE]
# Choose based on priority and name alphabetical order
if candidates:
return min(candidates, key=lambda c: (-c.rig.chain_priority, c.name_merged))
return self
def generate_bones(self):
if self.is_master_node:
# Make control bone
self._control_bone = self.make_master_bone()
# Make weak parent bone
if self.use_weak_parent:
self.weak_parent_bone = self.make_bone(
make_derived_name(self._control_bone, 'mch', '_weak_parent'), 1/2)
# Make mix parent if needed
self.reparent_bones = {}
if self.use_mix_parent:
self.mix_parent_bone = self.make_bone(
make_derived_name(self._control_bone, 'mch', '_mix_parent'), 1/2)
else:
self.reparent_bones[id(self.node_parent)] = self._control_bone
# Make requested reparents
self.reparent_bones_fake = set(self.reparent_bones.values())
for parent in self.reparent_requests:
if id(parent) not in self.reparent_bones:
parent_name = self.parent_subrig_names[id(parent)]
bone = self.make_bone(make_derived_name(parent_name, 'mch', '_reparent'), 1/3)
self.reparent_bones[id(parent)] = bone
def make_master_bone(self):
choice = self.find_master_name_node()
name = choice.name_merged
if self.hide_control:
name = make_derived_name(name, 'mch')
return choice.make_bone(name, 1)
def parent_bones(self):
if self.is_master_node:
if self.use_mix_parent:
self.set_bone_parent(self._control_bone, self.mix_parent_bone,
inherit_scale='AVERAGE')
self.rig.generator.disable_auto_parent(self.mix_parent_bone)
else:
self.set_bone_parent(self._control_bone, self.node_parent_list[0].output_bone,
inherit_scale='AVERAGE')
if self.use_weak_parent:
if self.use_mix_parent:
self.rig.generator.disable_auto_parent(self.weak_parent_bone)
else:
parent = self.node_parent_list_weak[0]
self.set_bone_parent(self.weak_parent_bone, parent.output_bone,
inherit_scale=parent.inherit_scale_mode)
for parent in self.reparent_requests:
bone = self.reparent_bones[id(parent)]
if bone not in self.reparent_bones_fake:
self.set_bone_parent(bone, parent.output_bone, inherit_scale='AVERAGE')
def configure_bones(self):
if self.is_master_node:
if not any(node.allow_scale for node in self.get_merged_siblings()):
self.get_bone(self.control_bone).lock_scale = (True, True, True)
layers = self.rig.get_control_node_layers(self)
if layers:
bone = self.get_bone(self.control_bone).bone
set_bone_layers(bone, layers, not self.is_master_node)
def rig_bones(self):
if self.is_master_node:
# Rig the mixed parent
if self.use_mix_parent:
targets = [parent.output_bone for parent in self.node_parent_list]
self.make_constraint(self.mix_parent_bone, 'ARMATURE',
targets=targets, use_deform_preserve_volume=True)
# Invoke parent rig callbacks
for rig in reversed(self.rig.get_all_parent_skin_rigs()):
rig.extend_control_node_rig(self)
# Rig reparent bones
reparent_source = self.control_bone
if self.use_weak_parent:
reparent_source = self.weak_parent_bone
self.make_constraint(reparent_source, 'COPY_TRANSFORMS',
self.control_bone, space='LOCAL')
if self.use_mix_parent:
targets = [parent.output_bone for parent in self.node_parent_list_weak]
self.make_constraint(self.weak_parent_bone, 'ARMATURE',
targets=targets, use_deform_preserve_volume=True)
set_bone_widget_transform(self.obj, self.control_bone, reparent_source)
for parent in self.reparent_requests:
bone = self.reparent_bones[id(parent)]
if bone not in self.reparent_bones_fake:
self.make_constraint(bone, 'COPY_TRANSFORMS', reparent_source)
def generate_widgets(self):
if self.is_master_node:
best = max(self.get_merged_siblings(), key=lambda n: n.icon)
if best.icon == ControlNodeIcon.TWEAK:
create_sphere_widget(self.obj, self.control_bone)
elif best.icon in (ControlNodeIcon.MIDDLE_PIVOT, ControlNodeIcon.FREE):
create_cube_widget(self.obj, self.control_bone)
else:
best.rig.make_control_node_widget(best)
class ControlQueryNode(QueryMergeNode, BaseSkinNode):
"""Node representing controls of skin chain rigs."""
merge_domain = 'ControlNetNode'
def __init__(self, rig, org, *, name=None, point=None, find_highest_layer=False):
assert isinstance(rig, BaseSkinRig)
super().__init__(rig, name or org, point or rig.get_bone(org).head)
self.org = org
self.find_highest_layer = find_highest_layer
def can_merge_into(self, other):
return True
def get_merge_priority(self, other):
return other.layer if self.find_highest_layer else -other.layer
@property
def merged_master(self):
return self.matched_nodes[0]

View File

@ -0,0 +1,395 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
from itertools import count
from string import Template
from ...utils.naming import make_derived_name
from ...utils.misc import force_lazy, LazyRef
from ...base_rig import LazyRigComponent, stage
class ControlBoneParentBase(LazyRigComponent):
"""
Base class for components that generate parent mechanisms for skin controls.
The generated parent bone is accessible through the output_bone field or property.
"""
# Run this component after the @stage methods of the owner node and its slave nodes
rigify_sub_object_run_late = True
# This generator's output bone cannot be modified by generators layered on top.
# Otherwise they may optimize bone count by adding more constraints in place.
# (This generally signals the bone is shared between multiple users.)
is_parent_frozen = False
def __init__(self, rig, node):
super().__init__(node)
# Rig that provides this parent mechanism.
self.rig = rig
# Control node that the mechanism is provided for
self.node = node
def __eq__(self, other):
raise NotImplementedError()
class ControlBoneParentOrg:
"""Control node parent generator wrapping a single ORG bone."""
is_parent_frozen = True
def __init__(self, org):
self._output_bone = org
@property
def output_bone(self):
return force_lazy(self._output_bone)
def enable_component(self):
pass
def __eq__(self, other):
return isinstance(other, ControlBoneParentOrg) and self._output_bone == other._output_bone
class ControlBoneParentArmature(ControlBoneParentBase):
"""Control node parent generator using the Armature constraint to parent the bone."""
def __init__(self, rig, node, *, bones, orientation=None, copy_scale=None, copy_rotation=None):
super().__init__(rig, node)
# List of Armature constraint target specs for make_constraint (lazy).
self.bones = bones
# Orientation quaternion for the bone (lazy)
self.orientation = orientation
# Bone to copy scale from (lazy)
self.copy_scale = copy_scale
# Bone to copy rotation from (lazy)
self.copy_rotation = copy_rotation
if copy_scale or copy_rotation:
self.is_parent_frozen = True
def __eq__(self, other):
return (
isinstance(other, ControlBoneParentArmature) and
self.node.point == other.node.point and
self.orientation == other.orientation and
self.bones == other.bones and
self.copy_scale == other.copy_scale and
self.copy_rotation == other.copy_rotation
)
def generate_bones(self):
self.output_bone = self.node.make_bone(
make_derived_name(self.node.name, 'mch', '_arm'), 1/4, rig=self.rig)
self.rig.generator.disable_auto_parent(self.output_bone)
if self.orientation:
matrix = force_lazy(self.orientation).to_matrix().to_4x4()
matrix.translation = self.node.point
self.get_bone(self.output_bone).matrix = matrix
def parent_bones(self):
self.targets = force_lazy(self.bones)
assert len(self.targets) > 0
# Single target can be simplified to parenting
if len(self.targets) == 1:
target = force_lazy(self.targets[0])
if isinstance(target, tuple):
target = target[0]
self.set_bone_parent(
self.output_bone, target,
inherit_scale='NONE' if self.copy_scale else 'FIX_SHEAR'
)
def rig_bones(self):
# Multiple targets use the Armature constraint
if len(self.targets) > 1:
self.make_constraint(
self.output_bone, 'ARMATURE', targets=self.targets,
use_deform_preserve_volume=True
)
self.make_constraint(self.output_bone, 'LIMIT_ROTATION')
if self.copy_rotation:
self.make_constraint(self.output_bone, 'COPY_ROTATION', self.copy_rotation)
if self.copy_scale:
self.make_constraint(self.output_bone, 'COPY_SCALE', self.copy_scale)
class ControlBoneParentLayer(ControlBoneParentBase):
"""Base class for parent generators that build on top of another mechanism."""
def __init__(self, rig, node, parent):
super().__init__(rig, node)
self.parent = parent
def enable_component(self):
self.parent.enable_component()
super().enable_component()
class ControlBoneWeakParentLayer(ControlBoneParentLayer):
"""
Base class for layered parent generator that is only used for the reparent source.
I.e. it doesn't affect the control for its owner rig, but only for other rigs
that have controls merged into this one.
"""
# Inherit mode used to parent the pseudo-control to the output of this generator.
inherit_scale_mode = 'AVERAGE'
@staticmethod
def strip(parent):
while isinstance(parent, ControlBoneWeakParentLayer):
parent = parent.parent
return parent
class ControlBoneParentOffset(ControlBoneParentLayer):
"""
Parent mechanism generator that offsets the control's location.
Supports Copy Transforms (Local) constraints and location drivers.
Multiple offsets can be accumulated in the same generator, which
will automatically create as many bones as needed.
"""
@classmethod
def wrap(cls, owner, parent, node, *constructor_args):
return cls(owner, node, parent, *constructor_args)
def __init__(self, rig, node, parent):
super().__init__(rig, node, parent)
self.copy_local = {}
self.add_local = {}
self.add_orientations = {}
self.limit_distance = []
def enable_component(self):
# Automatically merge an unfrozen sequence of this generator instances
while isinstance(self.parent, ControlBoneParentOffset) and not self.parent.is_parent_frozen:
self.prepend_contents(self.parent)
self.parent = self.parent.parent
super().enable_component()
def prepend_contents(self, other):
"""Merge all offsets stored in the other generator into the current one."""
for key, val in other.copy_local.items():
if key not in self.copy_local:
self.copy_local[key] = val
else:
inf, expr, cbs = val
inf0, expr0, cbs0 = self.copy_local[key]
self.copy_local[key] = [inf+inf0, expr+expr0, cbs+cbs0]
for key, val in other.add_orientations.items():
if key not in self.add_orientations:
self.add_orientations[key] = val
for key, val in other.add_local.items():
if key not in self.add_local:
self.add_local[key] = val
else:
ot0, ot1, ot2 = val
my0, my1, my2 = self.add_local[key]
self.add_local[key] = (ot0+my0, ot1+my1, ot2+my2)
self.limit_distance = other.limit_distance + self.limit_distance
def add_copy_local_location(self, target, *, influence=1, influence_expr=None, influence_vars={}):
"""
Add a Copy Location (Local, Owner Orientation) offset.
The influence may be specified as a (lazy) constant, or a driver expression
with variables (using the same $var syntax as add_location_driver).
"""
if target not in self.copy_local:
self.copy_local[target] = [0, [], []]
if influence_expr:
self.copy_local[target][1].append((influence_expr, influence_vars))
elif callable(influence):
self.copy_local[target][2].append(influence)
else:
self.copy_local[target][0] += influence
def add_location_driver(self, orientation, index, expression, variables):
"""
Add a driver offsetting along the specified axis in the given Quaternion orientation.
The variables may have to be renamed due to conflicts between multiple add requests,
so the expression should use the $var syntax of Template to reference them.
"""
assert isinstance(variables, dict)
key = tuple(round(x*10000) for x in orientation)
if key not in self.add_local:
self.add_orientations[key] = orientation
self.add_local[key] = ([], [], [])
self.add_local[key][index].append((expression, variables))
def add_limit_distance(self, target, *, ensure_order=False, **kwargs):
"""Add a limit distance constraint with the given make_constraint arguments."""
self.limit_distance.append((target, kwargs))
# Prevent merging from reordering this limit
if ensure_order:
self.is_parent_frozen = True
def __eq__(self, other):
return (
isinstance(other, ControlBoneParentOffset) and
self.parent == other.parent and
self.copy_local == other.copy_local and
self.add_local == other.add_local and
self.limit_distance == other.limit_distance
)
@property
def output_bone(self):
return self.mch_bones[-1] if self.mch_bones else self.parent.output_bone
def generate_bones(self):
self.mch_bones = []
self.reuse_mch = False
if self.copy_local or self.add_local or self.limit_distance:
mch_name = make_derived_name(self.node.name, 'mch', '_poffset')
if self.add_local:
# Generate a bone for every distinct orientation used for the drivers
for key in self.add_local:
self.mch_bones.append(self.node.make_bone(
mch_name, 1/4, rig=self.rig, orientation=self.add_orientations[key]))
else:
# Try piggybacking on the parent bone if allowed
if not self.parent.is_parent_frozen:
bone = self.get_bone(self.parent.output_bone)
if (bone.head - self.node.point).length < 1e-5:
self.reuse_mch = True
self.mch_bones = [bone.name]
return
self.mch_bones.append(self.node.make_bone(mch_name, 1/4, rig=self.rig))
def parent_bones(self):
if self.mch_bones:
if not self.reuse_mch:
self.rig.set_bone_parent(self.mch_bones[0], self.parent.output_bone)
self.rig.parent_bone_chain(self.mch_bones, use_connect=False)
def compile_driver(self, items):
variables = {}
expressions = []
# Loop through all expressions and combine the variable maps.
for expr, varset in items:
template = Template(expr)
varmap = {}
# Check that all variables are present
try:
template.substitute({k: '' for k in varset})
except Exception as e:
self.rig.raise_error('Invalid driver expression: {}\nError: {}', expr, e)
# Merge variables
for name, desc in varset.items():
# Check if the variable is used.
try:
template.substitute({k: '' for k in varset if k != name})
continue
except KeyError:
pass
# Descriptors may not be hashable, so linear search
for vn, vdesc in variables.items():
if vdesc == desc:
varmap[name] = vn
break
else:
# Find an unique name for the new variable and add to map
new_name = name
if new_name in variables:
for i in count(1):
new_name = '%s_%d' % (name, i)
if new_name not in variables:
break
variables[new_name] = desc
varmap[name] = new_name
# Substitute the new names into the expression
expressions.append(template.substitute(varmap))
# Add all expressions together
if len(expressions) > 1:
final_expr = '+'.join('('+expr+')' for expr in expressions)
else:
final_expr = expressions[0]
return final_expr, variables
def rig_bones(self):
# Emit the Copy Location constraints
if self.copy_local:
mch = self.mch_bones[0]
for target, (influence, drivers, lazyinf) in self.copy_local.items():
influence += sum(map(force_lazy, lazyinf))
con = self.make_constraint(
mch, 'COPY_LOCATION', target, use_offset=True,
target_space='LOCAL_OWNER_ORIENT', owner_space='LOCAL', influence=influence,
)
if drivers:
if influence > 0:
drivers.append((str(influence), {}))
expr, variables = self.compile_driver(drivers)
self.make_driver(con, 'influence', expression=expr, variables=variables)
# Add the direct offset drivers
if self.add_local:
for mch, (key, specs) in zip(self.mch_bones, self.add_local.items()):
for index, vals in enumerate(specs):
if vals:
expr, variables = self.compile_driver(vals)
self.make_driver(mch, 'location', index=index,
expression=expr, variables=variables)
# Add the limit distance constraints
for target, kwargs in self.limit_distance:
self.make_constraint(self.mch_bones[-1], 'LIMIT_DISTANCE', target, **kwargs)

View File

@ -0,0 +1,241 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
from ...utils.naming import make_derived_name
from ...utils.misc import force_lazy, LazyRef
from ...base_rig import BaseRig, stage
from .skin_parents import ControlBoneParentOrg
class BaseSkinRig(BaseRig):
"""
Base type for all rigs involved in the skin system.
This includes chain rigs and the parent provider rigs.
"""
def initialize(self):
self.rig_parent_bone = self.get_bone_parent(self.base_bone)
##########################
# Utilities
def get_parent_skin_rig(self):
"""Find the closest BaseSkinRig parent."""
parent = self.rigify_parent
while parent:
if isinstance(parent, BaseSkinRig):
return parent
parent = parent.rigify_parent
return None
def get_all_parent_skin_rigs(self):
"""Get a list of all BaseSkinRig parents, starting with this rig."""
items = []
current = self
while current:
items.append(current)
current = current.get_parent_skin_rig()
return items
def get_child_chain_parent_next(self, rig):
"""
Retrieves the parent bone for the child chain rig
as determined by the parent skin rig.
"""
if isinstance(self.rigify_parent, BaseSkinRig):
return self.rigify_parent.get_child_chain_parent(rig, self.rig_parent_bone)
else:
return self.rig_parent_bone
def build_control_node_parent_next(self, node):
"""
Retrieves the parent mechanism generator for the child control node
as determined by the parent skin rig.
"""
if isinstance(self.rigify_parent, BaseSkinRig):
return self.rigify_parent.build_control_node_parent(node, self.rig_parent_bone)
else:
return ControlBoneParentOrg(self.rig_parent_bone)
##########################
# Methods to override
def get_child_chain_parent(self, rig, parent_bone):
"""
Returns the (lazy) parent bone to use for the given child chain rig.
The parent_bone argument specifies the actual parent bone from caller.
"""
return parent_bone
def build_control_node_parent(self, node, parent_bone):
"""
Returns the parent mechanism generator for the child control node.
The parent_bone argument specifies the actual parent bone from caller.
Called during the initialize stage.
"""
return ControlBoneParentOrg(self.get_child_chain_parent(node.rig, parent_bone))
def build_own_control_node_parent(self, node):
"""
Returns the parent mechanism generator for nodes directly owned by this rig.
Called during the initialize stage.
"""
return self.build_control_node_parent_next(node)
def extend_control_node_parent(self, parent, node):
"""
First callback pass of adjustments to the parent mechanism generator for the given node.
Called for all BaseSkinRig parents in parent to child order during the initialize stage.
"""
return parent
def extend_control_node_parent_post(self, parent, node):
"""
Second callback pass of adjustments to the parent mechanism generator for the given node.
Called for all BaseSkinRig parents in child to parent order during the initialize stage.
"""
return parent
def extend_control_node_rig(self, node):
"""
A callback pass for adding constraints directly to the generated control.
Called for all BaseSkinRig parents in parent to child order during the rig stage.
"""
pass
def get_bone_quaternion(obj, bone):
return obj.pose.bones[bone].bone.matrix_local.to_quaternion()
class BaseSkinChainRig(BaseSkinRig):
"""
Base type for all skin rigs that can own control nodes, rather than
only modifying nodes of their children or other rigs.
"""
chain_priority = 0
def initialize(self):
super().initialize()
if type(self).chain_priority is None:
self.chain_priority = self.params.skin_chain_priority
def parent_bones(self):
self.rig_parent_bone = force_lazy(self.get_child_chain_parent_next(self))
def get_final_control_node_rotation(self, node):
"""Returns the orientation to use for the given control node owned by this rig."""
return self.get_control_node_rotation(node)
##########################
# Methods to override
def get_control_node_rotation(self, node):
"""
Returns the rig-specific orientation to use for the given control node of this rig,
if not overridden by the Orientation Bone option.
"""
return get_bone_quaternion(self.obj, self.base_bone)
def get_control_node_layers(self, node):
"""Returns the armature layers to use for the given control node owned by this rig."""
return self.get_bone(self.base_bone).bone.layers
def make_control_node_widget(self, node):
"""Called to generate the widget for nodes with ControlNodeIcon.CUSTOM."""
raise NotImplementedError()
##########################
# UI
@classmethod
def add_parameters(self, params):
params.skin_chain_priority = bpy.props.IntProperty(
name='Chain Priority',
min=-10, max=10, default=0,
description='When merging controls, chains with higher priority always win'
)
@classmethod
def parameters_ui(self, layout, params):
if self.chain_priority is None:
layout.prop(params, "skin_chain_priority")
class BaseSkinChainRigWithRotationOption(BaseSkinChainRig):
"""
Skin chain rig with an option to override the orientation to use
for controls via specifying an arbitrary template bone.
"""
use_skin_control_orientation_bone = True
def get_final_control_node_rotation(self, node):
bone_name = self.params.skin_control_orientation_bone
if bone_name and self.use_skin_control_orientation_bone:
# Retrieve the orientation from the specified ORG bone
try:
org_name = make_derived_name(bone_name, 'org')
if org_name not in self.obj.pose.bones:
org_name = bone_name
return get_bone_quaternion(self.obj, org_name)
except KeyError:
self.raise_error('Could not find orientation bone {}', bone_name)
else:
# Use the rig-specific orientation
return self.get_control_node_rotation(node)
@classmethod
def add_parameters(self, params):
params.skin_control_orientation_bone = bpy.props.StringProperty(
name="Orientation Bone",
description="If set, control orientation is taken from the specified bone",
)
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
if self.use_skin_control_orientation_bone:
from rigify.operators.copy_mirror_parameters import make_copy_parameter_button
row = layout.row()
row.prop_search(params, "skin_control_orientation_bone",
bpy.context.active_object.pose, "bones", text="Orientation")
make_copy_parameter_button(
row, "skin_control_orientation_bone", mirror_bone=True,
base_class=BaseSkinChainRigWithRotationOption
)
super().parameters_ui(layout, params)

View File

@ -0,0 +1,422 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
import enum
from itertools import count, repeat
from mathutils import Vector, Matrix
from bl_math import clamp
from ...utils.rig import connected_children_names
from ...utils.layers import ControlLayersOption
from ...utils.naming import make_derived_name
from ...utils.bones import align_bone_orientation, align_bone_to_axis, align_bone_roll
from ...utils.misc import map_list, LazyRef
from ...utils.mechanism import driver_var_transform
from ...base_rig import stage
from .skin_nodes import ControlBoneNode, ControlNodeLayer, ControlNodeIcon
from .skin_parents import ControlBoneWeakParentLayer, ControlBoneParentOffset
from .basic_chain import Rig as BasicChainRig
class Control(enum.IntEnum):
START = 0
MIDDLE = 1
END = 2
class Rig(BasicChainRig):
"""
Skin chain that propagates motion of its end and middle controls, resulting in
stretching the whole chain rather than just immediately connected chain segments.
"""
min_chain_length = 2
def initialize(self):
if len(self.bones.org) < self.min_chain_length:
self.raise_error(
"Input to rig type must be a chain of {} or more bones.", self.min_chain_length)
super().initialize()
orgs = self.bones.org
# Check the middle pivot location
self.pivot_pos = self.params.skin_chain_pivot_pos
if not (0 <= self.pivot_pos < len(orgs)):
self.raise_error('Invalid middle control position: {}', self.pivot_pos)
# Compute cumulative chain lengths from the start
bone_lengths = [self.get_bone(org).length for org in orgs]
self.chain_lengths = [sum(bone_lengths[0:i]) for i in range(len(orgs)+1)]
# Compute the chain start to end direction vector
if not self.params.skin_chain_falloff_length:
self.pivot_base = self.get_bone(orgs[0]).head
self.pivot_vector = self.get_bone(orgs[-1]).tail - self.pivot_base
self.pivot_length = self.pivot_vector.length
self.pivot_vector.normalize()
# Compute the position of the middle pivot within the chain
if self.pivot_pos:
pivot_point = self.get_bone(orgs[self.pivot_pos]).head
self.middle_pivot_factor = self.get_pivot_projection(pivot_point, self.pivot_pos)
####################################################
# UTILITIES
def get_pivot_projection(self, pos, index):
"""Compute the interpolation factor within the chain for a control at pos and index."""
if self.params.skin_chain_falloff_length:
# Position along the length of the chain
return self.chain_lengths[index] / self.chain_lengths[-1]
else:
# Position projected on the line connecting chain ends
return clamp((pos - self.pivot_base).dot(self.pivot_vector) / self.pivot_length)
def use_falloff_curve(self, idx):
"""Check if the given Control has any influence on other nodes."""
return self.params.skin_chain_falloff[idx] > -10
def apply_falloff_curve(self, factor, idx):
"""Compute the falloff weight at position factor for the given Control."""
weight = self.params.skin_chain_falloff[idx]
if self.params.skin_chain_falloff_spherical[idx]:
# circular falloff
if weight >= 0:
p = 2 ** weight
return (1 - (1 - factor) ** p) ** (1/p)
else:
p = 2 ** -weight
return 1 - (1 - factor ** p) ** (1/p)
else:
# parabolic falloff
return 1 - (1 - factor) ** (2 ** weight)
####################################################
# CONTROL NODES
def make_control_node(self, i, org, is_end):
node = super().make_control_node(i, org, is_end)
# Chain end control nodes
if i == 0 or i == self.num_orgs:
node.layer = ControlNodeLayer.FREE
node.icon = ControlNodeIcon.FREE
if i == 0:
node.node_needs_reparent = self.use_falloff_curve(Control.START)
else:
node.node_needs_reparent = self.use_falloff_curve(Control.END)
# Middle pivot control node
elif i == self.pivot_pos:
node.layer = ControlNodeLayer.MIDDLE_PIVOT
node.icon = ControlNodeIcon.MIDDLE_PIVOT
node.node_needs_reparent = self.use_falloff_curve(Control.MIDDLE)
# Other (tweak) control nodes
else:
node.layer = ControlNodeLayer.TWEAK
node.icon = ControlNodeIcon.TWEAK
return node
def extend_control_node_parent(self, parent, node):
if node.rig != self or node.index in (0, self.num_orgs):
return parent
parent = ControlBoneParentOffset(self, node, parent)
# Add offsets from the end controls to other nodes
factor = self.get_pivot_projection(node.point, node.index)
if self.use_falloff_curve(Control.START):
parent.add_copy_local_location(
LazyRef(self.control_nodes[0], 'reparent_bone'),
influence=self.apply_falloff_curve(1 - factor, Control.START),
)
if self.use_falloff_curve(Control.END):
parent.add_copy_local_location(
LazyRef(self.control_nodes[-1], 'reparent_bone'),
influence=self.apply_falloff_curve(factor, Control.END),
)
# Add offset from the middle pivot
if self.pivot_pos and node.index != self.pivot_pos:
if self.use_falloff_curve(Control.MIDDLE):
if node.index < self.pivot_pos:
factor = factor / self.middle_pivot_factor
else:
factor = (1 - factor) / (1 - self.middle_pivot_factor)
parent.add_copy_local_location(
LazyRef(self.control_nodes[self.pivot_pos], 'reparent_bone'),
influence=self.apply_falloff_curve(clamp(factor), Control.MIDDLE),
)
# If Propagate To Controls is set, add an extra wrapper for twist/scale
if node.index != self.pivot_pos and self.params.skin_chain_falloff_to_controls:
if self.params.skin_chain_falloff_twist or self.params.skin_chain_falloff_scale:
parent = ControlBoneChainPropagate(self, node, parent)
return parent
def get_control_node_layers(self, node):
layers = None
# Secondary Layers used for the middle pivot
if self.pivot_pos and node.index == self.pivot_pos:
layers = ControlLayersOption.SKIN_SECONDARY.get(self.params)
# Primary Layers used for the end controls, and middle if secondary not set
if not layers and node.index in (0, self.num_orgs, self.pivot_pos):
layers = ControlLayersOption.SKIN_PRIMARY.get(self.params)
return layers or super().get_control_node_layers(node)
####################################################
# B-Bone handle MCH
def rig_mch_handle_user(self, i, mch, prev_node, node, next_node, pre):
super().rig_mch_handle_user(i, mch, prev_node, node, next_node, pre)
self.rig_propagate(mch, node)
def rig_propagate(self, mch, node):
# Interpolate chain twist and/or scale between pivots
if node.index not in (0, self.num_orgs, self.pivot_pos):
index1, index2, factor = self.get_propagate_spec(node)
if self.params.skin_chain_falloff_twist:
self.rig_propagate_twist(mch, index1, index2, factor)
if self.use_scale and self.params.skin_chain_falloff_scale:
self.rig_propagate_scale(mch, index1, index2, factor)
def get_propagate_spec(self, node):
"""Compute source handle indices and factor for propagating scale and twist to node."""
index1 = 0
index2 = self.num_orgs
len_cur = self.chain_lengths[node.index]
len_end = self.chain_lengths[-1]
if self.pivot_pos:
len_pivot = self.chain_lengths[self.pivot_pos]
if node.index < self.pivot_pos:
factor = len_cur / len_pivot
index2 = self.pivot_pos
else:
factor = (len_cur - len_pivot) / (len_end - len_pivot)
index1 = self.pivot_pos
else:
factor = len_cur / len_end
return index1, index2, factor
def rig_propagate_twist(self, mch, index1, index2, factor):
handles = self.get_all_mch_handles()
handles_pre = self.get_all_mch_handles_pre()
# Get Y Twist rotation of the input handles
variables = {
'y1': driver_var_transform(
self.obj, handles[index1], type='ROT_Y',
space='LOCAL', rotation_mode='SWING_TWIST_Y'
),
'y2': driver_var_transform(
self.obj, handles[index2], type='ROT_Y',
space='LOCAL', rotation_mode='SWING_TWIST_Y'
),
}
# If pre handles are used, exclude the pre-handle twist,
# since it is caused by mechanisms and not user animation.
if handles_pre[index1] != handles[index1]:
variables['p1'] = driver_var_transform(
self.obj, handles_pre[index1], type='ROT_Y',
space='LOCAL', rotation_mode='SWING_TWIST_Y'
)
expr1 = 'y1-p1'
else:
expr1 = 'y1'
if handles_pre[index2] != handles[index2]:
variables['p2'] = driver_var_transform(
self.obj, handles_pre[index2], type='ROT_Y',
space='LOCAL', rotation_mode='SWING_TWIST_Y'
)
expr2 = 'y2-p2'
else:
expr2 = 'y2'
# Create the driver for Y Euler Rotation
bone = self.get_bone(mch)
bone.rotation_mode = 'YXZ'
self.make_driver(
bone, 'rotation_euler', index=1,
expression=f'lerp({expr1},{expr2},{clamp(factor)})',
variables=variables
)
def rig_propagate_scale(self, mch, index1, index2, factor, use_y=False):
handles = self.get_all_mch_handles()
self.make_constraint(
mch, 'COPY_SCALE', handles[index1], space='LOCAL',
use_x=True, use_y=use_y, use_z=True,
use_offset=True, power=clamp(1-factor)
)
self.make_constraint(
mch, 'COPY_SCALE', handles[index2], space='LOCAL',
use_x=True, use_y=use_y, use_z=True,
use_offset=True, power=clamp(factor)
)
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
params.skin_chain_pivot_pos = bpy.props.IntProperty(
name='Middle Control Position',
default=0,
min=0,
description='Position of the middle control, disabled if zero'
)
params.skin_chain_falloff_spherical = bpy.props.BoolVectorProperty(
size=3,
name='Spherical Falloff',
default=(False, False, False),
description='Falloff curve tries to form a circle at +1 instead of a parabola',
)
params.skin_chain_falloff = bpy.props.FloatVectorProperty(
size=3,
name='Control Falloff',
default=(0.0, 1.0, 0.0),
soft_min=-2, min=-10, soft_max=2,
description='Falloff curve coefficient: 0 is linear, and higher value is wider influence. Set to -10 to disable influence completely',
)
params.skin_chain_falloff_length = bpy.props.BoolProperty(
name='Falloff Along Chain Curve',
default=False,
description='Falloff is computed along the curve of the chain, instead of projecting on the axis connecting the start and end points',
)
params.skin_chain_falloff_twist = bpy.props.BoolProperty(
name='Propagate Twist',
default=True,
description='Propagate twist from main controls throughout the chain',
)
params.skin_chain_falloff_scale = bpy.props.BoolProperty(
name='Propagate Scale',
default=False,
description='Propagate scale from main controls throughout the chain',
)
params.skin_chain_falloff_to_controls = bpy.props.BoolProperty(
name='Propagate To Controls',
default=False,
description='Expose scale and/or twist propagated to tweak controls to be seen as ' +
'parent motion by glue or other chains using Merge Parent Rotation And ' +
'Scale. Otherwise it is only propagated internally within this chain',
)
ControlLayersOption.SKIN_PRIMARY.add_parameters(params)
ControlLayersOption.SKIN_SECONDARY.add_parameters(params)
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
layout.prop(params, "skin_chain_pivot_pos")
col = layout.column(align=True)
row = col.row(align=True)
row.label(text="Falloff:")
for i in range(3):
row2 = row.row(align=True)
row2.active = i != 1 or params.skin_chain_pivot_pos > 0
row2.prop(params, "skin_chain_falloff", text="", index=i)
row2.prop(params, "skin_chain_falloff_spherical", text="", icon='SPHERECURVE', index=i)
col.prop(params, "skin_chain_falloff_length")
row = col.split(factor=0.25)
row.label(text="Propagate:")
row = row.row(align=True)
row.prop(params, "skin_chain_falloff_twist", text="Twist", toggle=True)
row.prop(params, "skin_chain_falloff_scale", text="Scale", toggle=True)
row.prop(params, "skin_chain_falloff_to_controls", text="To Controls", toggle=True)
ControlLayersOption.SKIN_PRIMARY.parameters_ui(layout, params)
if params.skin_chain_pivot_pos > 0:
ControlLayersOption.SKIN_SECONDARY.parameters_ui(layout, params)
super().parameters_ui(layout, params)
class ControlBoneChainPropagate(ControlBoneWeakParentLayer):
"""
Parent mechanism generator that propagates chain twist/scale
to the reparent system, if Propagate To Controls is used.
"""
inherit_scale_mode = 'FULL'
def __eq__(self, other):
return (
isinstance(other, ControlBoneChainPropagate) and
self.parent == other.parent and
self.rig == other.rig and
self.node.index == other.node.index
)
def generate_bones(self):
# The parent bone is based on the handle and aligned appropriately.
handle = self.rig.bones.mch.handles[self.node.index]
self.output_bone = self.copy_bone(handle, make_derived_name(handle, 'mch', '_parent'))
def parent_bones(self):
self.set_bone_parent(self.output_bone, self.parent.output_bone, inherit_scale='AVERAGE')
def rig_bones(self):
# Add the twist/scale propagation rigging to the bone like the handle.
self.rig.rig_propagate(self.output_bone, self.node)
def create_sample(obj):
from rigify.rigs.basic.copy_chain import create_sample as inner
obj.pose.bones[inner(obj)["bone.01"]].rigify_type = 'skin.stretchy_chain'

View File

@ -0,0 +1,148 @@
# ====================== BEGIN GPL LICENSE BLOCK ======================
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ======================= END GPL LICENSE BLOCK ========================
# <pep8 compliant>
import bpy
import math
from itertools import count, repeat
from mathutils import Vector, Matrix
from ....utils.naming import make_derived_name
from ....utils.widgets_basic import create_cube_widget
from ....utils.misc import LazyRef
from ....base_rig import stage
from ..skin_parents import ControlBoneParentArmature
from ..skin_rigs import BaseSkinRig
class Rig(BaseSkinRig):
"""
This rig transforms its child nodes' locations, but keeps
their rotation and scale stable. This demonstrates implementing
a basic parent controller rig.
"""
def find_org_bones(self, bone):
return bone.name
def initialize(self):
super().initialize()
self.make_control = self.params.make_control
# Choose the parent bone for the child nodes
if self.make_control:
self.input_ref = LazyRef(self.bones.ctrl, 'master')
else:
self.input_ref = self.base_bone
# Retrieve the orientation of the control
matrix = self.get_bone(self.base_bone).bone.matrix_local
self.transform_orientation = matrix.to_quaternion()
####################################################
# Control Nodes
def build_control_node_parent(self, node, parent_bone):
# Parent nodes to the control bone, but isolate rotation and scale
return ControlBoneParentArmature(
self, node, bones=[self.input_ref],
orientation=self.transform_orientation,
copy_scale=LazyRef(self.bones.mch, 'template'),
copy_rotation=LazyRef(self.bones.mch, 'template'),
)
def get_child_chain_parent(self, rig, parent_bone):
# Forward child chain parenting to the next rig, so that
# only control nodes are affected by this one.
return self.get_child_chain_parent_next(rig)
####################################################
# BONES
#
# ctrl:
# master
# Master control
# mch:
# template
# Bone used to lock rotation and scale of child nodes.
#
####################################################
####################################################
# Master control
@stage.generate_bones
def make_master_control(self):
if self.make_control:
self.bones.ctrl.master = self.copy_bone(
self.bones.org, make_derived_name(self.bones.org, 'ctrl'), parent=True)
@stage.configure_bones
def configure_master_control(self):
if self.make_control:
self.copy_bone_properties(self.bones.org, self.bones.ctrl.master)
@stage.generate_widgets
def make_master_control_widget(self):
if self.make_control:
create_cube_widget(self.obj, self.bones.ctrl.master)
####################################################
# Template MCH
@stage.generate_bones
def make_mch_template_bone(self):
self.bones.mch.template = self.copy_bone(
self.bones.org, make_derived_name(self.bones.org, 'mch', '_orient'), parent=True)
@stage.parent_bones
def parent_mch_template_bone(self):
self.set_bone_parent(self.bones.mch.template, self.get_child_chain_parent_next(self))
####################################################
# ORG bone
@stage.rig_bones
def rig_org_bone(self):
pass
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
params.make_control = bpy.props.BoolProperty(
name="Control",
default=True,
description="Create a control bone for the copy"
)
@classmethod
def parameters_ui(self, layout, params):
layout.prop(params, "make_control", text="Generate Control")
def create_sample(obj):
from rigify.rigs.basic.super_copy import create_sample as inner
obj.pose.bones[inner(obj)["Bone"]].rigify_type = 'skin.transform.basic'

View File

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