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:
parent
4e7ed6259a
commit
ad22263327
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = [
|
||||
|
|
Loading…
Reference in New Issue