Rigify: annotate and fix warnings in skin rigs.

- Extract an even more bare version of ControlBoneParentBase that can
  act as a common superclass of no-op parent builders like Org.
- Introduce ControlBoneParentBase.replace_nested for proper polymorphism.
- Introduce BaseSkinNode.control_node to fix armature parent builder.
- Annotate types and fix warnings in all skin and new face rigs.

This should cause no behavior changes. Now rigify/rigs/ is warning free.
This commit is contained in:
Alexander Gavrilov 2022-11-16 19:29:03 +02:00
parent 4e7ed6259a
commit ad22263327
13 changed files with 838 additions and 404 deletions

View File

@ -1,7 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
import math
from itertools import count
@ -17,7 +16,7 @@ from ..widgets import create_jaw_widget
class Rig(TweakChainRig):
"""Basic tongue from the original PitchiPoy face rig."""
"""Basic tongue from the original PitchiPoy face rig.""" # noqa
min_chain_length = 3
@ -28,15 +27,19 @@ class Rig(TweakChainRig):
####################################################
# BONES
#
# ctrl:
# master:
# Master control.
# mch:
# follow[]:
# Partial follow master bones.
#
####################################################
class CtrlBones(TweakChainRig.CtrlBones):
master: str # Master control.
class MchBones(TweakChainRig.MchBones):
follow: list[str] # Partial follow master bones.
bones: TweakChainRig.ToplevelBones[
list[str],
'Rig.CtrlBones',
'Rig.MchBones',
list[str]
]
####################################################
# Control chain
@ -71,7 +74,7 @@ class Rig(TweakChainRig):
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):
def make_mch_follow_bone(self, _i: int, org: str):
name = self.copy_bone(org, make_derived_name(org, 'mch'))
copy_bone_position(self.obj, self.base_bone, name)
flip_bone(self.obj, name)
@ -104,7 +107,7 @@ class Rig(TweakChainRig):
# SETTINGS
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.bbones = bpy.props.IntProperty(
name='B-Bone Segments',
default=10,
@ -115,7 +118,7 @@ class Rig(TweakChainRig):
ControlLayersOption.SKIN_PRIMARY.add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
def parameters_ui(cls, layout, params):
layout.prop(params, 'bbones')
ControlLayersOption.SKIN_PRIMARY.parameters_ui(layout, params)

View File

@ -2,24 +2,26 @@
import bpy
import math
import functools
import mathutils
from itertools import count
from typing import Optional
from bpy.types import PoseBone
from mathutils import Vector, Matrix
from ...rig_ui_template import PanelLayout
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.bones import align_bone_z_axis, put_bone, TypedBoneDict
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 ...utils.misc import 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_nodes import ControlBoneNode, BaseSkinNode
from ..skin.skin_parents import ControlBoneParentOffset, ControlBoneParentBase
from ..skin.skin_rigs import BaseSkinRig
from ..skin.basic_chain import Rig as BasicChainRig
@ -31,11 +33,20 @@ class Rig(BaseSkinRig):
connect at their ends using T/B symmetry.
"""
def find_org_bones(self, bone):
def find_org_bones(self, bone: PoseBone) -> str:
return bone.name
cluster_control = None
center: Vector
axis: Vector
eye_corner_nodes: list[ControlBoneNode]
eye_corner_matrix: Optional[Matrix]
eye_corner_range: tuple[float, float]
child_chains: list[BasicChainRig]
def initialize(self):
super().initialize()
@ -58,10 +69,11 @@ class Rig(BaseSkinRig):
####################################################
# UTILITIES
def is_eye_control_node(self, node):
def is_eye_control_node(self, node: ControlBoneNode) -> bool:
return node.rig in self.child_chains and node.is_master_node
def is_eye_corner_node(self, node):
# noinspection PyMethodMayBeStatic
def is_eye_corner_node(self, node: ControlBoneNode) -> bool:
# 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)
@ -82,67 +94,68 @@ class Rig(BaseSkinRig):
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)))
angle_min, angle_max = sorted(map(self.get_eye_corner_angle, self.eye_corner_nodes))
if not (amin <= 0 <= amax):
self.eye_corner_range = (angle_min, angle_max)
if not (angle_min <= 0 <= angle_max):
self.raise_error('Bad relative angles of eye corners: {}..{}',
math.degrees(amin), math.degrees(amax))
math.degrees(angle_min), math.degrees(angle_max))
def get_eye_corner_angle(self, node):
def get_eye_corner_angle(self, node: ControlBoneNode) -> float:
"""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):
def get_master_control_position(self) -> Vector:
"""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]
corner_points = [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]
self.center, self.center + self.axis, corner_points[0], corner_points[1]
)
return point
def get_lid_follow_influence(self, node):
def get_lid_follow_influence(self, node: ControlBoneNode):
"""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
angle_min, angle_max = 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
if angle_min < angle < 0:
return 1 - min(1.0, angle/angle_min) ** 2
elif 0 < angle < angle_max:
return 1 - min(1.0, angle/angle_max) ** 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
#
####################################################
class CtrlBones(BaseSkinRig.CtrlBones):
master: str # Parent control for moving the whole eye.
target: str # Individual target this eye aims for.
class MchBones(BaseSkinRig.MchBones):
master: str # Bone that rotates to track ctrl.target.
track: str # Bone that translates to follow mch.master tail.
class DeformBones(TypedBoneDict):
master: str # Deform mirror of ctrl.master.
eye: str # Deform bone that rotates with mch.master.
iris: str # Iris deform bone at master tail that scales with ctrl.target
bones: BaseSkinRig.ToplevelBones[
str,
'Rig.CtrlBones',
'Rig.MchBones',
'Rig.DeformBones'
]
####################################################
# CHILD CHAINS
@ -154,13 +167,16 @@ class Rig(BaseSkinRig):
for child in self.child_chains:
self.patch_chain(child)
def patch_chain(self, child):
def patch_chain(self, child: BasicChainRig):
return EyelidChainPatch(child, self)
####################################################
# CONTROL NODES
def extend_control_node_parent(self, parent, node):
def extend_control_node_parent(self, parent, node: BaseSkinNode):
if not isinstance(node, ControlBoneNode):
return parent
if self.is_eye_control_node(node):
if self.is_eye_corner_node(node):
# Remember corners for later computations
@ -172,7 +188,7 @@ class Rig(BaseSkinRig):
return parent
def extend_mid_node_parent(self, parent, node):
def extend_mid_node_parent(self, parent: ControlBoneParentBase, node: ControlBoneNode):
parent = ControlBoneParentOffset(self, node, parent)
# Add movement of the eye to the eyelid controls
@ -183,6 +199,7 @@ class Rig(BaseSkinRig):
# If Limit Distance on the control can be disabled, add another one to the mch
if self.params.eyelid_detach_option:
# noinspection SpellCheckingInspection
parent.add_limit_distance(
self.bones.org,
distance=(node.point - self.center).length,
@ -195,9 +212,10 @@ class Rig(BaseSkinRig):
return parent
def extend_control_node_rig(self, node):
def extend_control_node_rig(self, node: ControlBoneNode):
if self.is_eye_control_node(node):
# Add Limit Distance to enforce following the surface of the eye to the control
# noinspection SpellCheckingInspection
con = self.make_constraint(
node.control_bone, 'LIMIT_DISTANCE', self.bones.org,
distance=(node.point - self.center).length,
@ -229,17 +247,17 @@ class Rig(BaseSkinRig):
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)'
description='Eyelids follow eye movement (X and Z)'
)
else:
self.make_property(target, 'lid_follow', 1.0,
description='Eylids follow eye movement')
description='Eyelids follow eye movement')
if self.params.eyelid_detach_option:
self.make_property(target, 'lid_attach', 1.0,
description='Eylids follow eye surface')
description='Eyelids follow eye surface')
def add_ui_sliders(self, panel, *, add_name=False):
def add_ui_sliders(self, panel: PanelLayout, *, add_name=False):
target = self.bones.ctrl.target
name_tail = f' ({target})' if add_name else ''
@ -377,7 +395,7 @@ class Rig(BaseSkinRig):
# SETTINGS
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.make_deform = bpy.props.BoolProperty(
name="Deform",
default=True,
@ -404,9 +422,9 @@ class Rig(BaseSkinRig):
)
@classmethod
def parameters_ui(self, layout, params):
def parameters_ui(cls, layout, params):
col = layout.column()
col.prop(params, "make_deform", text="Eyball And Iris Deforms")
col.prop(params, "make_deform", text="Eyeball And Iris Deforms")
col.prop(params, "eyelid_detach_option")
col.prop(params, "eyelid_follow_split")
@ -421,7 +439,9 @@ class EyelidChainPatch(RigComponent):
rigify_sub_object_run_late = True
def __init__(self, owner, eye):
owner: BasicChainRig
def __init__(self, owner: BasicChainRig, eye: Rig):
super().__init__(owner)
self.eye = eye
@ -454,7 +474,21 @@ class EyelidChainPatch(RigComponent):
class EyeClusterControl(RigComponent):
"""Component generating a common control for an eye cluster."""
def __init__(self, owner):
owner: Rig
rig_list: list[Rig]
rig_count: int
size: float
matrix: Matrix # Cluster plane matrix
inv_matrix: Matrix # World to cluster plane
rig_points: dict[Rig, Vector] # Eye projections in cluster plane space
master_bone: str
child_bones: list[str]
def __init__(self, owner: Rig):
super().__init__(owner)
self.find_cluster_rigs()
@ -505,7 +539,7 @@ class EyeClusterControl(RigComponent):
self.matrix = matrix
self.inv_matrix = matrix.inverted()
def project_rig_control(self, rig):
def project_rig_control(self, rig: Rig):
"""Intersect the given eye Y axis with the cluster plane, returns (x,y,0)."""
bone = self.get_bone(rig.base_bone)
@ -525,7 +559,7 @@ class EyeClusterControl(RigComponent):
return name
def get_rig_control_matrix(self, rig):
def get_rig_control_matrix(self, rig: Rig):
"""Compute a matrix for an individual eye sub-control."""
matrix = self.matrix.copy()
matrix.translation = self.matrix @ self.rig_points[rig]
@ -570,7 +604,7 @@ class EyeClusterControl(RigComponent):
bone.layers = self.get_master_control_layers()
return name
def make_child_control(self, rig):
def make_child_control(self, rig: 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)
@ -625,10 +659,10 @@ def create_eye_widget(geom, *, size=1):
@widget_generator
def create_eye_cluster_widget(geom, *, size=1, points):
hpoints = [points[i] for i in mathutils.geometry.convex_hull_2d(points)]
hull_points = [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)
generate_circle_hull_geometry(geom, hull_points, size*0.75, size*0.6)
generate_circle_hull_geometry(geom, hull_points, size, size*0.85)
def create_sample(obj):

View File

@ -1,20 +1,23 @@
# SPDX-License-Identifier: GPL-2.0-or-later
from typing import Optional
import bpy
import math
from itertools import count, repeat
from mathutils import Vector, Matrix
from itertools import repeat
from bpy.types import PoseBone
from mathutils import Vector, Matrix, Quaternion
from bl_math import clamp
from ...utils.bones import TypedBoneDict
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.misc import map_list, matrix_from_axis_pair, LazyRef, Lazy
from ...utils.widgets_basic import create_circle_widget
from ...base_rig import stage, RigComponent
from ...base_rig import stage
from ..skin.skin_nodes import ControlBoneNode
from ..skin.skin_nodes import ControlBoneNode, BaseSkinNode
from ..skin.skin_parents import ControlBoneParentOrg, ControlBoneParentArmature
from ..skin.skin_rigs import BaseSkinRig
@ -29,9 +32,25 @@ class Rig(BaseSkinRig):
must connect together at their ends using L/R and T/B symmetry.
"""
def find_org_bones(self, bone):
def find_org_bones(self, bone: PoseBone) -> str:
return bone.name
mouth_orientation: Quaternion
chain_to_layer: Optional[dict[BasicChainRig, int]]
num_layers: int
child_chains: list[BasicChainRig]
corners: dict[Side | SideZ, list[ControlBoneNode]]
chains_by_side: dict[Side | SideZ, set[BasicChainRig]]
mouth_center: Vector
mouth_space: Matrix
to_mouth_space: Matrix
left_sign: int
layer_width: list[float]
def initialize(self):
super().initialize()
@ -43,13 +62,13 @@ class Rig(BaseSkinRig):
####################################################
# UTILITIES
def get_mouth_orientation(self):
def get_mouth_orientation(self) -> Quaternion:
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):
def is_corner_node(self, node: ControlBoneNode) -> Side | SideZ | None:
# 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]
@ -72,32 +91,35 @@ class Rig(BaseSkinRig):
####################################################
# 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.
#
####################################################
class CtrlBones(BaseSkinRig.CtrlBones):
master: str # Main jaw open control.
mouth: str # Main control for adjusting mouth position and scale.
class MchBones(BaseSkinRig.MchBones):
lock: str # Jaw master mirror for the locked mouth.
top: list[str] # Jaw master mirrors for the loop top.
bottom: list[str] # Jaw master mirrors for the loop bottom.
middle: list[str] # Middle position between top[] and bottom[].
mouth_parent: str # Parent for ctrl.mouth, mouth_layers and *_in (= middle[0])
mouth_layers: list[str] # Apply fade out of ctrl.mouth motion for outer loops.
# Combine mouth and jaw motions via Copy Custom to Local.
top_out: list[str]
bottom_out: list[str]
middle_out: list[str]
class DeformBones(TypedBoneDict):
master: str # Deform mirror of ctrl.master.
bones: BaseSkinRig.ToplevelBones[
str,
'Rig.CtrlBones',
'Rig.MchBones',
'Rig.DeformBones'
]
####################################################
# CHILD CHAINS
@ -155,18 +177,23 @@ class Rig(BaseSkinRig):
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)
ordered: list[ControlBoneNode] = sorted(v, key=lambda p: (p.point - center).length)
chain_set = set()
self.corners[k] = ordered
chain_set: set[BasicChainRig] = set()
for i, node in enumerate(ordered):
for sibling in node.get_merged_siblings():
if sibling.rig in self.child_chains:
assert isinstance(sibling.rig, BasicChainRig)
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)
"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)
@ -201,7 +228,7 @@ class Rig(BaseSkinRig):
for i in range(self.num_layers)
]
def position_mouth_bone(self, name, scale):
def position_mouth_bone(self, name: str, scale: float):
self.arrange_child_chains()
bone = self.get_bone(name)
@ -211,10 +238,13 @@ class Rig(BaseSkinRig):
####################################################
# CONTROL NODES
def get_node_parent_bones(self, node):
def get_node_parent_bones(self, node: ControlBoneNode
) -> list[tuple[Lazy[str], float] | Lazy[str]]:
"""Get parent bones and their armature weights for the given control node."""
self.arrange_child_chains()
assert isinstance(node.rig, BasicChainRig)
# Choose correct layer bones
layer = self.chain_to_layer[node.rig]
@ -246,7 +276,7 @@ class Rig(BaseSkinRig):
return [(side_mch, factor), (middle_mch, 1-factor)]
def get_parent_for_name(self, name, parent_bone):
def get_parent_for_name(self, name: str, parent_bone: str) -> Lazy[str]:
"""Get single replacement parent for the given child bone."""
if parent_bone == self.base_bone:
side = get_name_side_z(name)
@ -257,11 +287,13 @@ class Rig(BaseSkinRig):
return parent_bone
def get_child_chain_parent(self, rig, parent_bone):
def get_child_chain_parent(self, rig: BaseSkinRig, parent_bone: str) -> str:
return self.get_parent_for_name(rig.base_bone, parent_bone)
def build_control_node_parent(self, node, parent_bone):
def build_control_node_parent(self, node: BaseSkinNode, parent_bone: str):
if node.rig in self.child_chains:
assert isinstance(node, ControlBoneNode)
return ControlBoneParentArmature(
self, node,
bones=self.get_node_parent_bones(node),
@ -344,13 +376,13 @@ class Rig(BaseSkinRig):
mch.mouth_parent = mch.middle[0]
def make_mch_top_bone(self, i, org):
def make_mch_top_bone(self, _i: int, org: str):
return self.copy_bone(org, make_derived_name(org, 'mch', '_top'), scale=1/4, parent=True)
def make_mch_bottom_bone(self, i, org):
def make_mch_bottom_bone(self, _i: int, org: str):
return self.copy_bone(org, make_derived_name(org, 'mch', '_bottom'), scale=1/3, parent=True)
def make_mch_middle_bone(self, i, org):
def make_mch_middle_bone(self, _i: int, org: str):
return self.copy_bone(org, make_derived_name(org, 'mch', '_middle'), scale=2/3, parent=True)
@stage.parent_bones
@ -398,13 +430,13 @@ class Rig(BaseSkinRig):
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
fac = 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))
self.make_constraint(name, 'COPY_TRANSFORMS', mch.top[0], influence=fac ** (1+i))
for i, name in enumerate(mch.bottom[1:]):
self.make_constraint(name, 'COPY_TRANSFORMS', mch.bottom[0], influence=coeff ** (1+i))
self.make_constraint(name, 'COPY_TRANSFORMS', mch.bottom[0], influence=fac ** (1+i))
# Middle bones interpolate the middle between top and bottom
for mid, bottom in zip(mch.middle, mch.bottom):
@ -427,12 +459,12 @@ class Rig(BaseSkinRig):
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):
def make_mch_mouth_bone(self, _i: int, suffix: str, size: float):
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):
def make_mch_mouth_inout_bone(self, _i: int, suffix: str, size: float):
return self.copy_bone(self.bones.org, make_derived_name(self.bones.org, 'mch', suffix), scale=size)
@stage.parent_bones
@ -467,7 +499,7 @@ class Rig(BaseSkinRig):
space_object=self.obj, space_subtarget=mch.mouth_parent,
)
def rig_mch_mouth_layer_bone(self, i, mch, ctrl):
def rig_mch_mouth_layer_bone(self, i: int, mch: str, ctrl: str):
# Fade location and rotation based on influence decay
inf = self.params.jaw_secondary_influence ** i
@ -492,7 +524,6 @@ class Rig(BaseSkinRig):
@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
@ -504,7 +535,7 @@ class Rig(BaseSkinRig):
# SETTINGS
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.jaw_mouth_influence = bpy.props.FloatProperty(
name="Bottom Lip Influence",
default=0.5, min=0, max=1,
@ -524,7 +555,7 @@ class Rig(BaseSkinRig):
)
@classmethod
def parameters_ui(self, layout, params):
def parameters_ui(cls, 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)

View File

@ -2,6 +2,8 @@
import bpy
from bpy.types import PoseBone
from ...utils.naming import make_derived_name
from ...utils.widgets import layout_widget_dropdown, create_registered_widget
from ...utils.mechanism import move_all_constraints
@ -19,7 +21,9 @@ class Rig(BaseSkinChainRigWithRotationOption, RelinkConstraintsMixin):
chain_priority = 20
def find_org_bones(self, bone):
make_deform: bool
def find_org_bones(self, bone: PoseBone) -> str:
return bone.name
def initialize(self):
@ -27,9 +31,21 @@ class Rig(BaseSkinChainRigWithRotationOption, RelinkConstraintsMixin):
self.make_deform = self.params.make_extra_deform
####################################################
# BONES
bones: BaseSkinChainRigWithRotationOption.ToplevelBones[
str,
'Rig.CtrlBones',
'Rig.MchBones',
str
]
####################################################
# CONTROL NODES
control_node: ControlBoneNode
@stage.initialize
def init_control_nodes(self):
org = self.bones.org
@ -79,7 +95,7 @@ class Rig(BaseSkinChainRigWithRotationOption, RelinkConstraintsMixin):
# SETTINGS
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.make_extra_deform = bpy.props.BoolProperty(
name="Extra Deform",
default=False,
@ -98,12 +114,12 @@ class Rig(BaseSkinChainRigWithRotationOption, RelinkConstraintsMixin):
description="Choose the type of the widget to create"
)
self.add_relink_constraints_params(params)
cls.add_relink_constraints_params(params)
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
def parameters_ui(cls, layout, params):
col = layout.column()
col.prop(params, "make_extra_deform", text='Generate Deform Bone')
col.prop(params, "skin_anchor_hide")

View File

@ -3,18 +3,20 @@
import bpy
import math
from typing import Sequence, Optional
from itertools import count, repeat
from mathutils import Vector, Matrix, Quaternion
from bpy.types import PoseBone
from mathutils import 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.bones import align_bone_roll
from ...utils.mechanism import driver_var_distance
from ...utils.widgets_basic import create_cube_widget, create_sphere_widget
from ...utils.widgets_basic import create_sphere_widget
from ...utils.misc import map_list, matrix_from_axis_roll
from ...base_rig import stage
@ -31,7 +33,17 @@ class Rig(BaseSkinChainRigWithRotationOption):
chain_priority = None
def find_org_bones(self, bone):
bbone_segments: int
use_bbones: bool
use_connect_mirror: Sequence[bool]
use_connect_ends: Sequence[bool]
use_scale: bool
use_reparent_handles: bool
num_orgs: int
length: float
def find_org_bones(self, bone: PoseBone) -> list[str]:
return [bone.name] + connected_children_names(self.obj, bone.name)
def initialize(self):
@ -52,48 +64,63 @@ class Rig(BaseSkinChainRigWithRotationOption):
####################################################
# OVERRIDES
def get_control_node_rotation(self, node):
def get_control_node_rotation(self, node: ControlBoneNode) -> Quaternion:
"""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()
quaternions = [get_bone_quaternion(self.obj, name) for name in bones]
result = sum(quaternions, 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]
node_prev = chain[node.index]
node_next = chain[node.index+2]
if nprev and nnext:
if node_prev and node_next:
# Apply only swing to preserve roll; tgt roll thus doesn't matter
tgt = matrix_from_axis_roll(nnext.point - nprev.point, 0).to_quaternion()
tgt = matrix_from_axis_roll(node_next.point - node_prev.point, 0).to_quaternion()
swing, _ = (result.inverted() @ tgt).to_swing_twist('Y')
result = result @ swing
return result
def get_all_controls(self):
def get_all_controls(self) -> list[str]:
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.
#
####################################################
class MchBones(BaseSkinChainRigWithRotationOption.MchBones):
handles: list[str] # Final B-Bone handles.
handles_pre: list[str] # Mechanism bones that emulate Auto handle behavior.
bones: BaseSkinChainRigWithRotationOption.ToplevelBones[
list[str],
'Rig.CtrlBones',
'Rig.MchBones',
list[str]
]
####################################################
# CONTROL NODES
control_nodes: list[ControlBoneNode]
# List of control nodes extended with the two adjacent chained nodes below
control_node_chain: Optional[list[ControlBoneNode | None]]
# Connected chain continuation nodes, and corner setting values
prev_node: Optional[ControlBoneNode]
prev_corner: float
next_node: Optional[ControlBoneNode]
next_corner: float
# Next chained rig if the end connects to the start of another chain
next_chain_rig: Optional['Rig']
@stage.initialize
def init_control_nodes(self):
orgs = self.bones.org
@ -110,7 +137,7 @@ class Rig(BaseSkinChainRigWithRotationOption):
nodes[0].chain_end_neighbor = nodes[1]
nodes[-1].chain_end_neighbor = nodes[-2]
def make_control_node(self, i, org, is_end):
def make_control_node(self, i: int, org: str, is_end: bool) -> ControlBoneNode:
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
@ -128,7 +155,7 @@ class Rig(BaseSkinChainRigWithRotationOption):
chain_end=chain_end,
)
def make_control_node_widget(self, node):
def make_control_node_widget(self, node: ControlBoneNode):
create_sphere_widget(self.obj, node.control_bone)
####################################################
@ -140,12 +167,18 @@ class Rig(BaseSkinChainRigWithRotationOption):
# 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]
def get_connected_node(self, node: ControlBoneNode
) -> tuple[Optional[ControlBoneNode], Optional[ControlBoneNode], float]:
"""
Find which other chain to connect this chain to at this node.
# First try merge through mirror
Returns:
(Connected counterpart node, Its chain neighbour, Average sharp angle setting value)
"""
is_end = 1 if node.index != 0 else 0
corner: float = self.params.skin_chain_connect_sharp_angle[is_end]
# First try merging through mirror
if self.use_connect_mirror[is_end]:
mirror = node.get_best_mirror()
@ -194,19 +227,22 @@ class Rig(BaseSkinChainRigWithRotationOption):
# Optimize connect next by sharing last handle mch
if next_link and next_link.index == 0:
assert isinstance(next_link.rig, Rig)
self.next_chain_rig = next_link.rig
else:
self.next_chain_rig = None
return self.control_node_chain
def get_all_mch_handles(self):
def get_all_mch_handles(self) -> list[str]:
"""Returns the list of all handle bones, referencing the next chained rig if needed."""
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):
"""Returns the list of all pre-handle bones, referencing the next chained rig if needed."""
if self.next_chain_rig:
return self.bones.mch.handles_pre + [self.next_chain_rig.bones.mch.handles_pre[0]]
else:
@ -230,20 +266,24 @@ class Rig(BaseSkinChainRigWithRotationOption):
else:
mch.handles_pre = mch.handles
def make_mch_handle_bone(self, i, prev_node, node, next_node):
def make_mch_handle_bone(self, _i: int,
prev_node: Optional[ControlBoneNode],
node: ControlBoneNode,
next_node: Optional[ControlBoneNode]
) -> str:
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()
handle_start = prev_node or node
handle_end = next_node or node
handle_axis = (handle_end.point - handle_start.point).normalized()
bone = self.get_bone(name)
bone.tail = bone.head + haxis * self.length * 3/4
bone.tail = bone.head + handle_axis * self.length * 3/4
align_bone_roll(self.obj, name, node.org)
return name
def make_mch_pre_handle_bone(self, i, handle):
def make_mch_pre_handle_bone(self, _i: int, handle: str) -> str:
return self.copy_bone(handle, make_derived_name(handle, 'mch', '_pre'))
@stage.parent_bones
@ -272,15 +312,22 @@ class Rig(BaseSkinChainRigWithRotationOption):
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
def rig_mch_handle_auto(self, _i: int, mch: str,
prev_node: Optional[ControlBoneNode],
node: ControlBoneNode,
next_node: Optional[ControlBoneNode]):
handle_start = prev_node or node
handle_end = 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')
self.make_constraint(mch, 'COPY_LOCATION', handle_start.control_bone, name='locate_prev')
self.make_constraint(mch, 'DAMPED_TRACK', handle_end.control_bone, name='track_next')
def rig_mch_handle_user(self, i, mch, prev_node, node, next_node, pre):
def rig_mch_handle_user(self, _i: int, mch: str,
prev_node: Optional[ControlBoneNode],
node: ControlBoneNode,
next_node: Optional[ControlBoneNode],
pre: str):
# Copy from the pre handle if used. Before Full is used to allow
# drivers on local transform channels to still work.
if pre != mch:
@ -318,7 +365,7 @@ class Rig(BaseSkinChainRigWithRotationOption):
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):
def rig_org_bone(self, i: int, org: str, node: ControlBoneNode, next_node: ControlBoneNode):
if i == 0:
self.make_constraint(org, 'COPY_LOCATION', node.control_bone)
@ -331,7 +378,7 @@ class Rig(BaseSkinChainRigWithRotationOption):
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):
def make_deform_bone(self, _i: int, org: str):
name = self.copy_bone(org, make_derived_name(org, 'def'), bbone=True)
self.get_bone(name).bbone_segments = self.bbone_segments
return name
@ -365,19 +412,29 @@ class Rig(BaseSkinChainRigWithRotationOption):
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):
# noinspection SpellCheckingInspection
def rig_deform_bone(self, i: int, deform: str, org: str):
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)
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)
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):
def make_corner_driver(self, bbone: str, field: str,
corner_node: ControlBoneNode,
next_node1: ControlBoneNode, next_node2: ControlBoneNode,
angle_threshold: float):
"""
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.
@ -388,29 +445,33 @@ class Rig(BaseSkinChainRigWithRotationOption):
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),
var_map = {
'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)
init_val = -1+2*smoothstep(-1, 1, acos((a*a+b*b-c*c)/max(2*a*b, 1e-10)) / angle_threshold)
setattr(pbone.bone, field, initval)
setattr(pbone.bone, field, init_val)
# 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
)
bias = -1 - init_val
# noinspection SpellCheckingInspection
expr = f'{bias}+2*smoothstep(-1,1,acos((a*a+b*b-c*c)/max(2*a*b,1e-10))/{angle_threshold})'
self.make_driver(pbone, field, expression=expr, variables=var_map)
####################################################
# SETTINGS
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.bbones = bpy.props.IntProperty(
name='B-Bone Segments',
default=10,
@ -421,8 +482,8 @@ class Rig(BaseSkinChainRigWithRotationOption):
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 ' +
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',
)
@ -446,7 +507,8 @@ class Rig(BaseSkinChainRigWithRotationOption):
default=(0, 0),
min=0,
max=math.pi,
description='Create a mechanism to sharpen a connected corner when the angle is below this value',
description='Create a mechanism to sharpen a connected corner when the angle is '
'below this value',
unit='ROTATION',
)
@ -454,13 +516,14 @@ class Rig(BaseSkinChainRigWithRotationOption):
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'
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):
def parameters_ui(cls, layout, params):
layout.prop(params, "bbones")
col = layout.column()

View File

@ -1,9 +1,8 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.types import PoseBone
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
@ -44,6 +43,9 @@ def parameters_ui(layout, params):
class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
"""Base class for the glue rigs."""
glue_head_mode: str
glue_use_tail: bool
def initialize(self):
super().initialize()
@ -55,6 +57,9 @@ class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
####################################################
# QUERY NODES
head_constraint_node: ControlQueryNode
tail_position_node: 'PositionQueryNode'
@stage.initialize
def init_glue_nodes(self):
bone = self.get_bone(self.base_bone)
@ -109,7 +114,7 @@ class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
# SETTINGS
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.skin_glue_head_mode = bpy.props.EnumProperty(
name='Glue Mode',
items=[('CHILD', 'Child Of Control',
@ -117,17 +122,21 @@ class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
('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"),
"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")],
"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",
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'
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(
@ -141,11 +150,13 @@ class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
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"),
"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")],
"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",
)
@ -156,12 +167,12 @@ class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
description="Influence of the added constraint",
)
self.add_relink_constraints_params(params)
cls.add_relink_constraints_params(params)
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
def parameters_ui(cls, layout, params):
layout.prop(params, "skin_glue_head_mode")
layout.prop(params, "relink_constraints")
@ -189,12 +200,24 @@ class BaseGlueRig(BaseSkinRig, RelinkConstraintsMixin):
class SimpleGlueRig(BaseGlueRig):
"""Normal glue rig that only does glue."""
def find_org_bones(self, bone):
def find_org_bones(self, bone: PoseBone) -> str:
return bone.name
####################################################
# BONES
bones: BaseSkinRig.ToplevelBones[
str,
'SimpleGlueRig.CtrlBones',
'SimpleGlueRig.MchBones',
str
]
####################################################
# QUERY NODES
head_position_node: 'PositionQueryNode'
@stage.initialize
def init_glue_nodes(self):
super().init_glue_nodes()
@ -224,11 +247,11 @@ class SimpleGlueRig(BaseGlueRig):
class BridgeGlueRig(BaseGlueRig, BasicChainRig):
"""Glue rig that also behaves like a deformation chain rig."""
def find_org_bones(self, bone):
def find_org_bones(self, bone: PoseBone) -> list[str]:
# Still only bind to one bone
return [bone.name]
# Assign lowest priority
# Assign the lowest priority
chain_priority = -20
# Orientation is irrelevant since controls should be merged into others

View File

@ -1,20 +1,23 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
import enum
from mathutils import Vector, Quaternion
from functools import partial
from typing import Optional
from mathutils import Vector, Quaternion, Matrix
from ...utils.layers import set_bone_layers
from ...utils.misc import ArmatureObject
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 ...utils.node_merger import MainMergeNode, QueryMergeNode, BaseMergeNode
from .skin_parents import ControlBoneParentLayer, ControlBoneWeakParentLayer, ControlBoneParentMix
from .skin_parents import ControlBoneWeakParentLayer, ControlBoneParentMix, ControlBoneParentBase
from .skin_rigs import BaseSkinRig, BaseSkinChainRig
@ -37,12 +40,22 @@ class ControlNodeEnd(enum.IntEnum):
END = 1
class BaseSkinNode(MechanismUtilityMixin, BoneUtilityMixin):
# noinspection PyAbstractClass
class BaseSkinNode(BaseMergeNode, MechanismUtilityMixin, BoneUtilityMixin):
"""Base class for skin control and query nodes."""
rig: BaseSkinRig
obj: ArmatureObject
name: str
point: Vector
merged_master: 'ControlBoneNode'
control_node: 'ControlBoneNode'
node_parent: ControlBoneParentBase
node_parent_built = False
def do_build_parent(self):
def do_build_parent(self) -> ControlBoneParentBase:
"""Create and intern the parent mechanism generator."""
assert self.rig.generator.stage == 'initialize'
@ -59,8 +72,16 @@ class BaseSkinNode(MechanismUtilityMixin, BoneUtilityMixin):
result.is_parent_frozen = True
return result
def build_parent(self, use=True, reparent=False):
"""Create and activate if needed the parent mechanism for this node."""
def build_parent(self, use=True, reparent=False) -> ControlBoneParentBase:
"""
Create and activate if needed the parent mechanism for this node.
Args:
use: Immediately mark the parent as in use, ensuring generation.
reparent: Immediately request reparent bone generation.
Returns:
Newly created parent.
"""
if not self.node_parent_built:
self.node_parent = self.do_build_parent()
self.node_parent_built = True
@ -76,6 +97,7 @@ class BaseSkinNode(MechanismUtilityMixin, BoneUtilityMixin):
@property
def control_bone(self):
"""The generated control bone."""
# noinspection PyProtectedMember
return self.merged_master._control_bone
@property
@ -89,12 +111,68 @@ class ControlBoneNode(MainMergeNode, BaseSkinNode):
merge_domain = 'ControlNetNode'
rig: BaseSkinChainRig
merged_master: 'ControlBoneNode'
size: float
name_split: NameSides
name_merged: Optional[str]
name_merged_split: Optional[NameSides]
rotation: Optional[Quaternion]
# For use by the owner rig: index in chain
index: Optional[int]
# If this node is the end of a chain, points to the next one
chain_end_neighbor: Optional['ControlBoneNode']
mirror_siblings: dict[NameSides, 'ControlBoneNode']
mirror_sides_x: set[Side]
mirror_sides_z: set[SideZ]
parent_subrig_cache: list[ControlBoneParentBase]
parent_subrig_names: dict[int, str]
reparent_requests: list[ControlBoneParentBase]
used_parents: dict[int, ControlBoneParentBase] | None
reparent_bones: dict[int, str]
reparent_bones_fake: set[str]
matrix: Matrix
_control_bone: str
has_weak_parent: bool
node_parent_weak: ControlBoneParentBase
use_weak_parent: bool
weak_parent_bone: str
def __init__(
self, rig, org, name, *, point=None, size=None,
self, rig: BaseSkinChainRig, org: str, name: str, *,
point: Optional[Vector] = None, size: Optional[float] = None,
needs_parent=False, needs_reparent=False, allow_scale=False,
chain_end=ControlNodeEnd.MIDDLE,
layer=ControlNodeLayer.FREE, index=None, icon=ControlNodeIcon.TWEAK,
layer=ControlNodeLayer.FREE,
index: Optional[int] = None,
icon=ControlNodeIcon.TWEAK,
):
"""
Construct a node generating a visible control.
Args:
rig: Owning skin chain rig.
org: ORG bone that is associated with the node.
name: Name of the node.
point: Location of the node; defaults to org head.
size: Size of the control; defaults to org length.
needs_parent: Create the parent mechanism even if not master.
needs_reparent: If this node's own parent mechanism differs from master,
generate a conversion bone.
allow_scale: Unlock scale channels.
chain_end: Logical location within the chain.
layer: Control dependency layer within the chain.
index: Index within the chain.
icon: Widget to use for the control.
"""
assert isinstance(rig, BaseSkinChainRig)
super().__init__(rig, name, point or rig.get_bone(org).head)
@ -112,38 +190,39 @@ class ControlBoneNode(MainMergeNode, BaseSkinNode):
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):
@property
def control_node(self) -> 'ControlBoneNode':
return self
def get_merged_siblings(self) -> list['ControlBoneNode']:
return super().get_merged_siblings()
def can_merge_into(self, other: 'ControlBoneNode'):
# Only merge up the layers (towards more mechanism)
dprio = self.rig.chain_priority - other.rig.chain_priority
delta_prio = self.rig.chain_priority - other.rig.chain_priority
return (
dprio <= 0 and
(self.layer <= other.layer or dprio < 0) and
delta_prio <= 0 and
(self.layer <= other.layer or delta_prio < 0) and
super().can_merge_into(other)
)
def get_merge_priority(self, other):
def get_merge_priority(self, other: 'ControlBoneNode'):
# 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):
def is_better_cluster(self, other: 'ControlBoneNode'):
"""Check if the current bone is preferable as master when choosing of same sized groups."""
# Prefer bones that have strictly more parents
@ -201,20 +280,29 @@ class ControlBoneNode(MainMergeNode, BaseSkinNode):
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):
def get_best_mirror(self) -> Optional['ControlBoneNode']:
"""Find best mirror sibling for connecting via mirror."""
base, side, sidez = self.name_split
base, side, side_z = self.name_split
for flip in [(base, -side, -sidez), (base, -side, sidez), (base, side, -sidez)]:
for flip in [(base, -side, -side_z), (base, -side, side_z), (base, side, -side_z)]:
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."""
def intern_parent(self, node: BaseSkinNode, parent: ControlBoneParentBase
) -> ControlBoneParentBase:
"""
De-duplicate the parent layer chain within this merge group.
Args:
node: Node that introduced this parent mechanism.
parent: Parent mechanism to register.
Returns:
The input parent mechanism, or its already interned equivalent.
"""
# Quick check for the same object
if id(parent) in self.parent_subrig_names:
@ -233,18 +321,16 @@ class ControlBoneNode(MainMergeNode, BaseSkinNode):
self.parent_subrig_names[id(parent)] = node.name
if isinstance(parent, ControlBoneParentLayer):
parent.parent = self.intern_parent(node, parent.parent)
elif isinstance(parent, ControlBoneParentMix):
parent.parents = [self.intern_parent(node, p) for p in parent.parents]
# Recursively apply to any inner references
parent.replace_nested(partial(self.intern_parent, node))
return parent
def register_use_parent(self, parent):
def register_use_parent(self, parent: ControlBoneParentBase):
"""Activate this parent mechanism generator."""
self.used_parents[id(parent)] = parent
def request_reparent(self, parent):
def request_reparent(self, parent: ControlBoneParentBase):
"""Request a reparent bone to be generated for this parent mechanism."""
requests = self.reparent_requests
@ -258,11 +344,11 @@ class ControlBoneNode(MainMergeNode, BaseSkinNode):
self.register_use_parent(parent)
requests.append(parent)
def get_reparent_bone(self, parent):
def get_reparent_bone(self, parent: ControlBoneParentBase) -> str:
"""Returns the generated reparent bone for this parent mechanism."""
return self.reparent_bones[id(parent)]
def get_rotation(self):
def get_rotation(self) -> Quaternion:
"""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)
@ -331,10 +417,18 @@ class ControlBoneNode(MainMergeNode, BaseSkinNode):
self.used_parents = None
def make_bone(self, name, scale, *, rig=None, orientation=None):
def make_bone(self, name: str, scale: float, *,
rig: Optional[BaseSkinRig] = None,
orientation: Optional[Quaternion] = None) -> str:
"""
Creates a bone associated with this node, using the appropriate
orientation, location and size.
Args:
name: Name for the new bone.
scale: Scale factor for the bone relative to default size.
rig: Optionally, the rig that should be registered as the owner the bone.
orientation: Optional override for the orientation but not location.
"""
name = (rig or self).copy_bone(self.org, name)
@ -350,7 +444,7 @@ class ControlBoneNode(MainMergeNode, BaseSkinNode):
return name
def find_master_name_node(self):
def find_master_name_node(self) -> 'ControlBoneNode':
"""Find which node to name the control bone from."""
# Chain end nodes have sub-par names, so try to find another chain
@ -394,7 +488,8 @@ class ControlBoneNode(MainMergeNode, BaseSkinNode):
bone = self.make_bone(make_derived_name(parent_name, 'mch', '_reparent'), 1/3)
self.reparent_bones[id(parent)] = bone
def make_master_bone(self):
def make_master_bone(self) -> str:
"""Generate the master control bone for the node."""
choice = self.find_master_name_node()
name = choice.name_merged
@ -466,7 +561,22 @@ class ControlQueryNode(QueryMergeNode, BaseSkinNode):
merge_domain = 'ControlNetNode'
def __init__(self, rig, org, *, name=None, point=None, find_highest_layer=False):
matched_nodes: list['ControlBoneNode']
def __init__(self, rig: BaseSkinRig, org: str, *,
name: Optional[str] = None,
point: Optional[Vector] = None,
find_highest_layer=False):
"""
Create a skin query node.
Args:
rig: Rig that owns this node.
org: ORG bone associated with this node.
name: Name for this node, defaults to org.
point: Location of the node, defaults to org head.
find_highest_layer: Choose the highest layer master instead of lowest.
"""
assert isinstance(rig, BaseSkinRig)
super().__init__(rig, name or org, point or rig.get_bone(org).head)
@ -474,12 +584,16 @@ class ControlQueryNode(QueryMergeNode, BaseSkinNode):
self.org = org
self.find_highest_layer = find_highest_layer
def can_merge_into(self, other):
def can_merge_into(self, other: ControlBoneNode):
return True
def get_merge_priority(self, other):
return other.layer if self.find_highest_layer else -other.layer
def get_merge_priority(self, other: ControlBoneNode) -> float:
return int(other.layer if self.find_highest_layer else -other.layer)
@property
def merged_master(self):
def merged_master(self) -> ControlBoneNode:
return self.matched_nodes[0]
@property
def control_node(self) -> ControlBoneNode:
return self.matched_nodes[0]

View File

@ -1,31 +1,58 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from itertools import count
from string import Template
from typing import TYPE_CHECKING, Optional, NamedTuple, Any, Sequence, Callable
from dataclasses import dataclass, field
from mathutils import Quaternion
from ...utils.naming import make_derived_name
from ...utils.misc import force_lazy, LazyRef
from ...utils.misc import force_lazy, Lazy, OptionalLazy
from ...base_rig import LazyRigComponent, stage
from ...base_rig import LazyRigComponent
if TYPE_CHECKING:
from .skin_nodes import ControlBoneNode, BaseSkinNode
from .skin_rigs import BaseSkinRig
class ControlBoneParentBase(LazyRigComponent):
class ControlBoneParentBase:
"""
Base class for components that generate parent mechanisms for skin controls.
The generated parent bone is accessible through the output_bone field or property.
"""
# 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 = True
# The output bone field or property
output_bone: str
def __eq__(self, other):
raise NotImplementedError()
def replace_nested(self,
callback: Callable[['ControlBoneParentBase'], 'ControlBoneParentBase']):
"""Replace all nested parent objects with the result of applying the callback."""
pass
def enable_component(self):
pass
# noinspection PyAbstractClass
class ControlBoneParentImplBase(LazyRigComponent, ControlBoneParentBase):
"""Base class for a control node parent generator that actually generates bones."""
# 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):
def __init__(self, rig: 'BaseSkinRig', node: 'BaseSkinNode'):
super().__init__(node)
# Rig that provides this parent mechanism.
@ -33,33 +60,35 @@ class ControlBoneParentBase(LazyRigComponent):
# Control node that the mechanism is provided for
self.node = node
def __eq__(self, other):
raise NotImplementedError()
@property
def control_node(self) -> 'ControlBoneNode':
return self.node.control_node
class ControlBoneParentOrg:
class ControlBoneParentOrg(ControlBoneParentBase):
"""Control node parent generator wrapping a single ORG bone."""
is_parent_frozen = True
def __init__(self, org):
def __init__(self, org: Lazy[str]):
self._output_bone = org
@property
def output_bone(self):
def output_bone(self) -> str:
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):
class ControlBoneParentArmature(ControlBoneParentImplBase):
"""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):
targets: list[str | tuple | dict]
def __init__(self, rig: 'BaseSkinRig', node: 'BaseSkinNode', *,
bones: Lazy[list[str | tuple | dict]],
orientation: OptionalLazy[Quaternion] = None,
copy_scale: OptionalLazy[str] = None,
copy_rotation: OptionalLazy[str] = None):
super().__init__(rig, node)
# List of Armature constraint target specs for make_constraint (lazy).
@ -85,7 +114,7 @@ class ControlBoneParentArmature(ControlBoneParentBase):
)
def generate_bones(self):
self.output_bone = self.node.make_bone(
self.output_bone = self.control_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)
@ -127,10 +156,15 @@ class ControlBoneParentArmature(ControlBoneParentBase):
self.make_constraint(self.output_bone, 'COPY_SCALE', self.copy_scale)
class ControlBoneParentMix(ControlBoneParentBase):
class ControlBoneParentMix(ControlBoneParentImplBase):
"""Combine multiple parent mechanisms using the Armature constraint."""
def __init__(self, rig, node, parents, *, suffix=None):
parents: list[ControlBoneParentBase]
parent_weights: list[float]
def __init__(self, rig: 'BaseSkinRig', node: 'ControlBoneNode',
parents: list[tuple[ControlBoneParentBase, float] | ControlBoneParentBase], *,
suffix: Optional[str] = None):
super().__init__(rig, node)
self.parents = []
@ -139,6 +173,9 @@ class ControlBoneParentMix(ControlBoneParentBase):
self.add_parents(parents)
def replace_nested(self, callback):
self.parents = [callback(item) for item in self.parents]
def add_parents(self, parents):
for item in parents:
if isinstance(item, tuple):
@ -168,14 +205,14 @@ class ControlBoneParentMix(ControlBoneParentBase):
)
def generate_bones(self):
self.output_bone = self.node.make_bone(
self.output_bone = self.control_node.make_bone(
make_derived_name(self.node.name, 'mch', self.suffix or '_mix'), 1/2, rig=self.rig)
self.rig.generator.disable_auto_parent(self.output_bone)
def parent_bones(self):
if len(self.parents) == 1:
self.set_bone_parent(self.output_bone, target)
self.set_bone_parent(self.output_bone, self.parents[0].output_bone)
def rig_bones(self):
if len(self.parents) > 1:
@ -187,21 +224,26 @@ class ControlBoneParentMix(ControlBoneParentBase):
)
class ControlBoneParentLayer(ControlBoneParentBase):
# noinspection PyAbstractClass
class ControlBoneParentLayer(ControlBoneParentImplBase):
"""Base class for parent generators that build on top of another mechanism."""
def __init__(self, rig, node, parent):
def __init__(self, rig: 'BaseSkinRig', node: 'ControlBoneNode', parent: ControlBoneParentBase):
super().__init__(rig, node)
self.parent = parent
def replace_nested(self, callback):
self.parent = callback(self.parent)
def enable_component(self):
self.parent.enable_component()
super().enable_component()
# noinspection PyAbstractClass
class ControlBoneWeakParentLayer(ControlBoneParentLayer):
"""
Base class for layered parent generator that is only used for the reparent source.
Base class for layered parent generator that is only used for the re-parent 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.
"""
@ -223,11 +265,31 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
will automatically create as many bones as needed.
"""
class DriverEntry(NamedTuple):
expression: str
variables: dict[str, Any]
@dataclass
class CopyLocalEntry:
influence: float = 0
drivers: list['ControlBoneParentOffset.DriverEntry'] = field(default_factory=list)
lazy_entries: list[Lazy[float]] = field(default_factory=list)
copy_local: dict[str, 'ControlBoneParentOffset.CopyLocalEntry']
add_orientations: dict[Sequence[float], Quaternion]
add_local: dict[Sequence[float], Sequence[list['ControlBoneParentOffset.DriverEntry']]]
limit_distance: list[tuple[str, dict]]
reuse_mch: bool
mch_bones: list[str]
@classmethod
def wrap(cls, owner, parent, node, *constructor_args):
def wrap(cls, owner: 'BaseSkinRig', parent: ControlBoneParentBase, node: 'ControlBoneNode',
*constructor_args):
# noinspection PyArgumentList
return cls(owner, node, parent, *constructor_args)
def __init__(self, rig, node, parent):
def __init__(self, rig: 'BaseSkinRig', node: 'ControlBoneNode', parent: ControlBoneParentBase):
super().__init__(rig, node, parent)
self.copy_local = {}
self.add_local = {}
@ -250,7 +312,7 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
else:
inf, expr, cbs = val
inf0, expr0, cbs0 = self.copy_local[key]
self.copy_local[key] = [inf+inf0, expr+expr0, cbs+cbs0]
self.copy_local[key] = self.CopyLocalEntry(inf+inf0, expr+expr0, cbs+cbs0)
for key, val in other.add_orientations.items():
if key not in self.add_orientations:
@ -266,23 +328,28 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
self.limit_distance = other.limit_distance + self.limit_distance
def add_copy_local_location(self, target, *, influence=1, influence_expr=None, influence_vars={}):
# noinspection PyDefaultArgument
def add_copy_local_location(self, target: Lazy[str], *, influence: Lazy[float] = 1,
influence_expr: Optional[str] = None,
influence_vars: dict[str, Any] = {}):
"""
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, [], []]
self.copy_local[target] = self.CopyLocalEntry()
if influence_expr:
self.copy_local[target][1].append((influence_expr, influence_vars))
self.copy_local[target].drivers.append(
self.DriverEntry(influence_expr, influence_vars))
elif callable(influence):
self.copy_local[target][2].append(influence)
self.copy_local[target].lazy_entries.append(influence)
else:
self.copy_local[target][0] += influence
self.copy_local[target].influence += influence
def add_location_driver(self, orientation, index, expression, variables):
def add_location_driver(self, orientation: Quaternion, index: int,
expression: str, variables: dict[str, Any]):
"""
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,
@ -290,15 +357,15 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
"""
assert isinstance(variables, dict)
key = tuple(round(x*10000) for x in orientation)
key: tuple[float, ...] = 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))
self.add_local[key][index].append(self.DriverEntry(expression, variables))
def add_limit_distance(self, target, *, ensure_order=False, **kwargs):
def add_limit_distance(self, target: str, *, ensure_order=False, **kwargs):
"""Add a limit distance constraint with the given make_constraint arguments."""
self.limit_distance.append((target, kwargs))
@ -324,12 +391,13 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
self.reuse_mch = False
if self.copy_local or self.add_local or self.limit_distance:
# noinspection SpellCheckingInspection
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(
self.mch_bones.append(self.control_node.make_bone(
mch_name, 1/4, rig=self.rig, orientation=self.add_orientations[key]))
else:
# Try piggybacking on the parent bone if allowed
@ -340,7 +408,7 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
self.mch_bones = [bone.name]
return
self.mch_bones.append(self.node.make_bone(mch_name, 1/4, rig=self.rig))
self.mch_bones.append(self.control_node.make_bone(mch_name, 1/4, rig=self.rig))
def parent_bones(self):
if self.mch_bones:
@ -349,37 +417,37 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
self.rig.parent_bone_chain(self.mch_bones, use_connect=False)
def compile_driver(self, items):
def compile_driver(self, items: list['ControlBoneParentOffset.DriverEntry']):
variables = {}
expressions = []
# Loop through all expressions and combine the variable maps.
for expr, varset in items:
for expr, var_set in items:
template = Template(expr)
varmap = {}
var_map = {}
# Check that all variables are present
try:
template.substitute({k: '' for k in varset})
template.substitute({k: '' for k in var_set})
except Exception as e:
self.rig.raise_error('Invalid driver expression: {}\nError: {}', expr, e)
# Merge variables
for name, desc in varset.items():
for name, desc in var_set.items():
# Check if the variable is used.
try:
template.substitute({k: '' for k in varset if k != name})
template.substitute({k: '' for k in var_set 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
for var_name, var_desc in variables.items():
if var_desc == desc:
var_map[name] = var_name
break
else:
# Find an unique name for the new variable and add to map
# Find a unique name for the new variable and add to map
new_name = name
if new_name in variables:
for i in count(1):
@ -388,10 +456,10 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
break
variables[new_name] = desc
varmap[name] = new_name
var_map[name] = new_name
# Substitute the new names into the expression
expressions.append(template.substitute(varmap))
expressions.append(template.substitute(var_map))
# Add all expressions together
if len(expressions) > 1:
@ -405,17 +473,20 @@ class ControlBoneParentOffset(ControlBoneParentLayer):
# 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))
for target, entry in self.copy_local.items():
influence = entry.influence
influence += sum(map(force_lazy, entry.lazy_entries))
con = self.make_constraint(
mch, 'COPY_LOCATION', target, use_offset=True,
target_space='LOCAL_OWNER_ORIENT', owner_space='LOCAL', influence=influence,
)
drivers = entry.drivers
if drivers:
if influence > 0:
drivers.append((str(influence), {}))
drivers.append(self.DriverEntry(str(influence), {}))
expr, variables = self.compile_driver(drivers)
self.make_driver(con, 'influence', expression=expr, variables=variables)

View File

@ -2,27 +2,48 @@
import bpy
from typing import TYPE_CHECKING, Optional
from mathutils import Quaternion
from ...utils.bones import BoneDict
from ...utils.naming import make_derived_name
from ...utils.misc import force_lazy, LazyRef
from ...utils.misc import force_lazy, ArmatureObject
from ...base_rig import BaseRig, stage
from ...base_rig import BaseRig
from .skin_parents import ControlBoneParentOrg
from .skin_parents import ControlBoneParentOrg, ControlBoneParentBase
if TYPE_CHECKING:
from .skin_nodes import BaseSkinNode, ControlBoneNode
# noinspection PyMethodMayBeStatic
class BaseSkinRig(BaseRig):
"""
Base type for all rigs involved in the skin system.
This includes chain rigs and the parent provider rigs.
"""
rig_parent_bone: str
def initialize(self):
self.rig_parent_bone = self.get_bone_parent(self.base_bone)
##########################
# BONES
bones: BaseRig.ToplevelBones[
str | list[str] | BoneDict,
BaseRig.CtrlBones,
BaseRig.MchBones,
str | list[str] | BoneDict,
]
##########################
# Utilities
def get_parent_skin_rig(self):
def get_parent_skin_rig(self) -> Optional['BaseSkinRig']:
"""Find the closest BaseSkinRig parent."""
parent = self.rigify_parent
@ -33,7 +54,7 @@ class BaseSkinRig(BaseRig):
return None
def get_all_parent_skin_rigs(self):
def get_all_parent_skin_rigs(self) -> list['BaseSkinRig']:
"""Get a list of all BaseSkinRig parents, starting with this rig."""
items = []
current = self
@ -42,7 +63,7 @@ class BaseSkinRig(BaseRig):
current = current.get_parent_skin_rig()
return items
def get_child_chain_parent_next(self, rig):
def get_child_chain_parent_next(self, rig: 'BaseSkinRig') -> str:
"""
Retrieves the parent bone for the child chain rig
as determined by the parent skin rig.
@ -52,7 +73,7 @@ class BaseSkinRig(BaseRig):
else:
return self.rig_parent_bone
def build_control_node_parent_next(self, node):
def build_control_node_parent_next(self, node: 'BaseSkinNode') -> ControlBoneParentBase:
"""
Retrieves the parent mechanism generator for the child control node
as determined by the parent skin rig.
@ -65,14 +86,15 @@ class BaseSkinRig(BaseRig):
##########################
# Methods to override
def get_child_chain_parent(self, rig, parent_bone):
def get_child_chain_parent(self, rig: 'BaseSkinRig', parent_bone: str) -> str:
"""
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):
def build_control_node_parent(self, node: 'BaseSkinNode',
parent_bone: str) -> ControlBoneParentBase:
"""
Returns the parent mechanism generator for the child control node.
The parent_bone argument specifies the actual parent bone from caller.
@ -80,28 +102,30 @@ class BaseSkinRig(BaseRig):
"""
return ControlBoneParentOrg(self.get_child_chain_parent(node.rig, parent_bone))
def build_own_control_node_parent(self, node):
def build_own_control_node_parent(self, node: 'BaseSkinNode') -> ControlBoneParentBase:
"""
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):
def extend_control_node_parent(self, parent: ControlBoneParentBase,
_node: 'BaseSkinNode') -> ControlBoneParentBase:
"""
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):
def extend_control_node_parent_post(self, parent: ControlBoneParentBase,
_node: 'BaseSkinNode') -> ControlBoneParentBase:
"""
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):
def extend_control_node_rig(self, _node: 'ControlBoneNode'):
"""
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.
@ -109,7 +133,7 @@ class BaseSkinRig(BaseRig):
pass
def get_bone_quaternion(obj, bone):
def get_bone_quaternion(obj: ArmatureObject, bone: str):
return obj.pose.bones[bone].bone.matrix_local.to_quaternion()
@ -119,7 +143,7 @@ class BaseSkinChainRig(BaseSkinRig):
only modifying nodes of their children or other rigs.
"""
chain_priority = 0
chain_priority: float = 0
def initialize(self):
super().initialize()
@ -130,25 +154,25 @@ class BaseSkinChainRig(BaseSkinRig):
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):
def get_final_control_node_rotation(self, node: 'ControlBoneNode') -> Quaternion:
"""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):
def get_control_node_rotation(self, node: 'ControlBoneNode') -> Quaternion:
"""
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):
def get_control_node_layers(self, node: 'ControlBoneNode') -> list[bool]:
"""Returns the armature layers to use for the given control node owned by this rig."""
return self.get_bone(self.base_bone).bone.layers
return list(self.get_bone(self.base_bone).bone.layers)
def make_control_node_widget(self, node):
def make_control_node_widget(self, node: 'ControlBoneNode'):
"""Called to generate the widget for nodes with ControlNodeIcon.CUSTOM."""
raise NotImplementedError()
@ -156,7 +180,7 @@ class BaseSkinChainRig(BaseSkinRig):
# UI
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.skin_chain_priority = bpy.props.IntProperty(
name='Chain Priority',
min=-10, max=10, default=0,
@ -164,11 +188,12 @@ class BaseSkinChainRig(BaseSkinRig):
)
@classmethod
def parameters_ui(self, layout, params):
if self.chain_priority is None:
def parameters_ui(cls, layout, params):
if cls.chain_priority is None:
layout.prop(params, "skin_chain_priority")
# noinspection PyAbstractClass
class BaseSkinChainRigWithRotationOption(BaseSkinChainRig):
"""
Skin chain rig with an option to override the orientation to use
@ -177,7 +202,7 @@ class BaseSkinChainRigWithRotationOption(BaseSkinChainRig):
use_skin_control_orientation_bone = True
def get_final_control_node_rotation(self, node):
def get_final_control_node_rotation(self, node: 'ControlBoneNode') -> Quaternion:
bone_name = self.params.skin_control_orientation_bone
if bone_name and self.use_skin_control_orientation_bone:
@ -198,7 +223,7 @@ class BaseSkinChainRigWithRotationOption(BaseSkinChainRig):
return self.get_control_node_rotation(node)
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.skin_control_orientation_bone = bpy.props.StringProperty(
name="Orientation Bone",
description="If set, control orientation is taken from the specified bone",
@ -207,8 +232,8 @@ class BaseSkinChainRigWithRotationOption(BaseSkinChainRig):
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
if self.use_skin_control_orientation_bone:
def parameters_ui(cls, layout, params):
if cls.use_skin_control_orientation_bone:
from rigify.operators.copy_mirror_parameters import make_copy_parameter_button
row = layout.row()

View File

@ -3,21 +3,16 @@
import bpy
import enum
from itertools import count, repeat
from mathutils import Vector, Matrix
from bl_math import clamp
from mathutils import Vector
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.misc import 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 .skin_nodes import ControlBoneNode, ControlNodeLayer, ControlNodeIcon, BaseSkinNode
from .skin_parents import ControlBoneWeakParentLayer, ControlBoneParentOffset, ControlBoneParentBase
from .basic_chain import Rig as BasicChainRig
@ -36,6 +31,14 @@ class Rig(BasicChainRig):
min_chain_length = 2
pivot_pos: int
chain_lengths: list[float]
pivot_base: Vector
pivot_vector: Vector
pivot_length: float
middle_pivot_factor: float
def initialize(self):
if len(self.bones.org) < self.min_chain_length:
self.raise_error(
@ -71,7 +74,7 @@ class Rig(BasicChainRig):
####################################################
# UTILITIES
def get_pivot_projection(self, pos, index):
def get_pivot_projection(self, pos: Vector, index: int) -> float:
"""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
@ -80,11 +83,11 @@ class Rig(BasicChainRig):
# 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):
def use_falloff_curve(self, idx: int) -> bool:
"""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):
def apply_falloff_curve(self, factor: float, idx: int) -> float:
"""Compute the falloff weight at position factor for the given Control."""
weight = self.params.skin_chain_falloff[idx]
@ -103,7 +106,7 @@ class Rig(BasicChainRig):
####################################################
# CONTROL NODES
def make_control_node(self, i, org, is_end):
def make_control_node(self, i: int, org: str, is_end: bool) -> ControlBoneNode:
node = super().make_control_node(i, org, is_end)
# Chain end control nodes
@ -126,8 +129,14 @@ class Rig(BasicChainRig):
return node
def extend_control_node_parent(self, parent, node):
if node.rig != self or node.index in (0, self.num_orgs):
def extend_control_node_parent(self, parent: ControlBoneParentBase,
node: BaseSkinNode) -> ControlBoneParentBase:
if node.rig != self:
return parent
assert isinstance(node, ControlBoneNode)
if node.index in (0, self.num_orgs):
return parent
parent = ControlBoneParentOffset(self, node, parent)
@ -167,7 +176,7 @@ class Rig(BasicChainRig):
return parent
def get_control_node_layers(self, node):
def get_control_node_layers(self, node: ControlBoneNode) -> list[bool]:
layers = None
# Secondary Layers used for the middle pivot
@ -188,7 +197,7 @@ class Rig(BasicChainRig):
self.rig_propagate(mch, node)
def rig_propagate(self, mch, node):
def rig_propagate(self, mch: str, node: ControlBoneNode):
# 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)
@ -199,7 +208,7 @@ class Rig(BasicChainRig):
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):
def get_propagate_spec(self, node: ControlBoneNode) -> tuple[int, int, float]:
"""Compute source handle indices and factor for propagating scale and twist to node."""
index1 = 0
index2 = self.num_orgs
@ -221,7 +230,7 @@ class Rig(BasicChainRig):
return index1, index2, factor
def rig_propagate_twist(self, mch, index1, index2, factor):
def rig_propagate_twist(self, mch: str, index1: int, index2: int, factor: float):
handles = self.get_all_mch_handles()
handles_pre = self.get_all_mch_handles_pre()
@ -261,13 +270,14 @@ class Rig(BasicChainRig):
bone = self.get_bone(mch)
bone.rotation_mode = 'YXZ'
# noinspection SpellCheckingInspection
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):
def rig_propagate_scale(self, mch: str, index1: int, index2: int, factor: float, use_y=False):
handles = self.get_all_mch_handles()
self.make_constraint(
@ -285,7 +295,7 @@ class Rig(BasicChainRig):
# SETTINGS
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.skin_chain_pivot_pos = bpy.props.IntProperty(
name='Middle Control Position',
default=0,
@ -305,13 +315,15 @@ class Rig(BasicChainRig):
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',
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',
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(
@ -329,8 +341,8 @@ class Rig(BasicChainRig):
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 ' +
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',
)
@ -340,7 +352,7 @@ class Rig(BasicChainRig):
super().add_parameters(params)
@classmethod
def parameters_ui(self, layout, params):
def parameters_ui(cls, layout, params):
layout.prop(params, "skin_chain_pivot_pos")
col = layout.column(align=True)
@ -352,6 +364,7 @@ class Rig(BasicChainRig):
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)
# noinspection SpellCheckingInspection
row2.prop(params, "skin_chain_falloff_spherical", text="", icon='SPHERECURVE', index=i)
col.prop(params, "skin_chain_falloff_length")
@ -377,6 +390,12 @@ class ControlBoneChainPropagate(ControlBoneWeakParentLayer):
to the reparent system, if Propagate To Controls is used.
"""
rig: Rig
node: ControlBoneNode
def __init__(self, rig: Rig, node: ControlBoneNode, parent: ControlBoneParentBase):
super().__init__(rig, node, parent)
def __eq__(self, other):
return (
isinstance(other, ControlBoneChainPropagate) and

View File

@ -1,18 +1,18 @@
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
import math
from itertools import count, repeat
from mathutils import Vector, Matrix
from bpy.types import PoseBone
from mathutils import Quaternion
from ..skin_nodes import BaseSkinNode
from ....utils.naming import make_derived_name
from ....utils.widgets_basic import create_cube_widget
from ....utils.misc import LazyRef
from ....utils.misc import LazyRef, Lazy
from ....base_rig import stage
from ..skin_parents import ControlBoneParentArmature
from ..skin_parents import ControlBoneParentArmature, ControlBoneParentBase
from ..skin_rigs import BaseSkinRig
@ -23,9 +23,13 @@ class Rig(BaseSkinRig):
a basic parent controller rig.
"""
def find_org_bones(self, bone):
def find_org_bones(self, bone: PoseBone) -> str:
return bone.name
make_control: bool
input_ref: Lazy[str]
transform_orientation: Quaternion
def initialize(self):
super().initialize()
@ -45,7 +49,8 @@ class Rig(BaseSkinRig):
####################################################
# Control Nodes
def build_control_node_parent(self, node, parent_bone):
def build_control_node_parent(self, node: BaseSkinNode,
parent_bone: str) -> ControlBoneParentBase:
# Parent nodes to the control bone, but isolate rotation and scale
return ControlBoneParentArmature(
self, node, bones=[self.input_ref],
@ -54,22 +59,26 @@ class Rig(BaseSkinRig):
copy_rotation=LazyRef(self.bones.mch, 'template'),
)
def get_child_chain_parent(self, rig, parent_bone):
def get_child_chain_parent(self, rig: BaseSkinRig, parent_bone: str) -> str:
# 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.
#
####################################################
class CtrlBones(BaseSkinRig.CtrlBones):
master: str # Master control
class MchBones(BaseSkinRig.MchBones):
template: str # Bone used to lock rotation and scale of child nodes.
bones: BaseSkinRig.ToplevelBones[
str,
'Rig.CtrlBones',
'Rig.MchBones',
str
]
####################################################
# Master control
@ -113,7 +122,7 @@ class Rig(BaseSkinRig):
# SETTINGS
@classmethod
def add_parameters(self, params):
def add_parameters(cls, params):
params.make_control = bpy.props.BoolProperty(
name="Control",
default=True,
@ -121,7 +130,7 @@ class Rig(BaseSkinRig):
)
@classmethod
def parameters_ui(self, layout, params):
def parameters_ui(cls, layout, params):
layout.prop(params, "make_control", text="Generate Control")

View File

@ -11,6 +11,7 @@ from rna_prop_ui import rna_idprop_value_to_python
T = typing.TypeVar('T')
AnyVector = Vector | typing.Sequence[float]
##############################################
# Math
@ -71,7 +72,7 @@ matrix_from_axis_roll = bpy.types.Bone.MatrixFromAxisRoll
axis_roll_from_matrix = bpy.types.Bone.AxisRollFromMatrix
def matrix_from_axis_pair(y_axis: Vector, other_axis: Vector, axis_name: str):
def matrix_from_axis_pair(y_axis: AnyVector, other_axis: AnyVector, axis_name: str):
assert axis_name in 'xz'
y_axis = Vector(y_axis).normalized()
@ -176,7 +177,7 @@ def force_lazy(value: OptionalLazy[T]) -> T:
return value
class LazyRef:
class LazyRef(typing.Generic[T]):
"""Hashable lazy reference. When called, evaluates (foo, 'a', 'b'...) as foo('a','b')
if foo is callable. Otherwise, the remaining arguments are used as attribute names or
keys, like foo.a.b or foo.a[b] etc."""
@ -200,7 +201,7 @@ class LazyRef:
return (hash(self.first) if self.first_hashable
else hash(id(self.first))) ^ hash(self.args)
def __call__(self):
def __call__(self) -> T:
first = self.first
if callable(first):
return first(*self.args)
@ -282,7 +283,6 @@ class TypedObject(bpy.types.Object, typing.Generic[T]):
ArmatureObject = TypedObject[bpy.types.Armature]
MeshObject = TypedObject[bpy.types.Mesh]
AnyVector = Vector | typing.Sequence[float]
def verify_armature_obj(obj: bpy.types.Object) -> ArmatureObject:

View File

@ -7,6 +7,7 @@ from mathutils import Vector
from mathutils.kdtree import KDTree
from .errors import MetarigError
from .misc import ArmatureObject
from ..base_rig import BaseRig, GenerateCallbackHost
from ..base_generate import GeneratorPlugin
@ -34,6 +35,11 @@ class NodeMerger(GeneratorPlugin):
groups: list['MergeGroup']
def __init__(self, generator, domain: Any):
"""
Construct a new merger instance.
@param domain: An arbitrary identifier to allow multiple independent merging domains.
"""
super().__init__(generator)
assert domain is not None
@ -46,6 +52,9 @@ class NodeMerger(GeneratorPlugin):
self.frozen = False
def register_node(self, node: 'BaseMergeNode'):
"""
Add a new node to generation, before merging is frozen.
"""
assert not self.frozen
node.generator_plugin = self
self.nodes.append(node)
@ -126,9 +135,9 @@ class MergeGroup(object):
The master nodes of the chosen clusters, plus query nodes, become 'final'.
"""
main_nodes: list['MainMergeNode']
query_nodes: list['QueryMergeNode']
final_nodes: list['MainMergeNode']
main_nodes: list['MainMergeNode'] # All main nodes in the group.
query_nodes: list['QueryMergeNode'] # All query nodes in the group.
final_nodes: list['MainMergeNode'] # All main nodes not merged into any other node.
def __init__(self, nodes: list['BaseMergeNode']):
self.nodes = nodes
@ -219,6 +228,11 @@ class MergeGroup(object):
class BaseMergeNode(GenerateCallbackHost):
"""Base class of merge-able nodes."""
rig: BaseRig
obj: ArmatureObject
name: str
point: Vector
merge_domain: Any = None
merger = NodeMerger
group_class = MergeGroup
@ -239,13 +253,18 @@ class BaseMergeNode(GenerateCallbackHost):
def register_new_bone(self, new_name: str, old_name: Optional[str] = None):
self.generator_plugin.register_new_bone(new_name, old_name)
def can_merge_into(self, other: 'MainMergeNode'):
def can_merge_into(self, other: 'MainMergeNode') -> bool:
"""Checks if this main or query node can merge into the specified master node."""
raise NotImplementedError
def get_merge_priority(self, other: 'BaseMergeNode'):
"""Rank candidates to merge into."""
def get_merge_priority(self, other: 'MainMergeNode') -> float:
"""Rank potential candidates to merge into."""
return 0
def merge_done(self):
"""Called after all merging operations are complete."""
pass
class MainMergeNode(BaseMergeNode):
"""
@ -254,9 +273,9 @@ class MainMergeNode(BaseMergeNode):
sub-objects of their master to receive callbacks in defined order.
"""
merged_master: 'MainMergeNode'
merged_into: Optional['MainMergeNode']
merged: list['MainMergeNode']
merged_master: 'MainMergeNode' # Master of this merge cluster; may be self.
merged_into: Optional['MainMergeNode'] # Master of this cluster if not self.
merged: list['MainMergeNode'] # List of nodes merged into this one.
def __init__(self, rig, name, point, *, domain=None):
super().__init__(rig, name, point, domain=domain)
@ -265,6 +284,8 @@ class MainMergeNode(BaseMergeNode):
self.merged = []
def get_merged_siblings(self):
"""Retrieve the list of all nodes merged together with this one,
starting with the master node."""
master = self.merged_master
return [master, *master.merged]
@ -274,19 +295,24 @@ class MainMergeNode(BaseMergeNode):
# noinspection PyMethodMayBeStatic
def can_merge_from(self, _other: 'MainMergeNode'):
"""Checks if the other node can be merged into this one."""
return True
def can_merge_into(self, other: 'MainMergeNode'):
"""Checks if this node can merge into the specified master."""
return other.can_merge_from(self)
def merge_into(self, other: 'MainMergeNode'):
"""Called when it's decided to merge this node into a different master node."""
self.merged_into = other
def merge_from(self, other: 'MainMergeNode'):
"""Called when it's decided to merge a different node into this master node."""
self.merged.append(other)
@property
def is_master_node(self):
"""Returns if this node is a master of a merge cluster."""
return not self.merged_into
def merge_done(self):
@ -304,7 +330,7 @@ class QueryMergeNode(BaseMergeNode):
is_master_node = False
require_match = True
matched_nodes: list['MainMergeNode']
matched_nodes: list['MainMergeNode'] # Master nodes this query matched with.
def merge_done(self):
self.matched_nodes = [