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