Rigify: add utility and operator classes for the upcoming face rigs.
- LazyRef utility class that provides a hashable field reference. - NodeMerger plugin for grouping abstract points by distance. - pose.rigify_copy_single_parameter operator for copying a single property to all selected rigs that inherit from a specific class (intended to be used via property panel buttons).
This commit is contained in:
parent
735cdeec78
commit
046114b96a
|
@ -64,6 +64,7 @@ initial_load_order = [
|
|||
'rig_ui_template',
|
||||
'generate',
|
||||
'rot_mode',
|
||||
'operators',
|
||||
'ui',
|
||||
]
|
||||
|
||||
|
@ -449,6 +450,7 @@ def register():
|
|||
ui.register()
|
||||
feature_set_list.register()
|
||||
metarig_menu.register()
|
||||
operators.register()
|
||||
|
||||
# Classes.
|
||||
for cls in classes:
|
||||
|
@ -597,6 +599,7 @@ def unregister():
|
|||
clear_rigify_parameters()
|
||||
|
||||
# Sub-modules.
|
||||
operators.unregister()
|
||||
metarig_menu.unregister()
|
||||
ui.unregister()
|
||||
feature_set_list.unregister()
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# ====================== 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 importlib
|
||||
|
||||
|
||||
# Submodules to load during register
|
||||
submodules = (
|
||||
'copy_mirror_parameters',
|
||||
)
|
||||
|
||||
loaded_submodules = []
|
||||
|
||||
|
||||
def register():
|
||||
# Lazily load modules to make reloading easier. Loading this way
|
||||
# hides the sub-modules and their dependencies from initial_load_order.
|
||||
loaded_submodules[:] = [
|
||||
importlib.import_module(__name__ + '.' + name) for name in submodules
|
||||
]
|
||||
|
||||
for mod in loaded_submodules:
|
||||
mod.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
for mod in reversed(loaded_submodules):
|
||||
mod.unregister()
|
||||
|
||||
loaded_submodules.clear()
|
|
@ -0,0 +1,129 @@
|
|||
# ====================== 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 importlib
|
||||
|
||||
from ..utils.naming import Side, get_name_base_and_sides, mirror_name
|
||||
|
||||
from ..utils.rig import get_rigify_type
|
||||
from ..rig_lists import get_rig_class
|
||||
|
||||
|
||||
# =============================================
|
||||
# Single parameter copy button
|
||||
|
||||
class POSE_OT_rigify_copy_single_parameter(bpy.types.Operator):
|
||||
bl_idname = "pose.rigify_copy_single_parameter"
|
||||
bl_label = "Copy Option To Selected Rigs"
|
||||
bl_description = "Copy this property value to all selected rigs of the appropriate type"
|
||||
bl_options = {'UNDO', 'INTERNAL'}
|
||||
|
||||
property_name: bpy.props.StringProperty(name='Property Name')
|
||||
mirror_bone: bpy.props.BoolProperty(name='Mirror As Bone Name')
|
||||
|
||||
module_name: bpy.props.StringProperty(name='Module Name')
|
||||
class_name: bpy.props.StringProperty(name='Class Name')
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
context.active_object and context.active_object.type == 'ARMATURE'
|
||||
and context.active_pose_bone
|
||||
and context.active_object.data.get('rig_id') is None
|
||||
and get_rigify_type(context.active_pose_bone)
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
return context.window_manager.invoke_confirm(self, event)
|
||||
|
||||
def execute(self, context):
|
||||
try:
|
||||
module = importlib.import_module(self.module_name)
|
||||
filter_rig_class = getattr(module, self.class_name)
|
||||
except (KeyError, AttributeError, ImportError):
|
||||
self.report(
|
||||
{'ERROR'}, f"Cannot find class {self.class_name} in {self.module_name}")
|
||||
return {'CANCELLED'}
|
||||
|
||||
active_pbone = context.active_pose_bone
|
||||
active_split = get_name_base_and_sides(active_pbone.name)
|
||||
|
||||
value = getattr(active_pbone.rigify_parameters, self.property_name)
|
||||
num_copied = 0
|
||||
|
||||
# Copy to different bones of appropriate rig types
|
||||
for sel_pbone in context.selected_pose_bones:
|
||||
rig_type = get_rigify_type(sel_pbone)
|
||||
|
||||
if rig_type and sel_pbone != active_pbone:
|
||||
rig_class = get_rig_class(rig_type)
|
||||
|
||||
if rig_class and issubclass(rig_class, filter_rig_class):
|
||||
new_value = value
|
||||
|
||||
# If mirror requested and copying to a different side bone, mirror the value
|
||||
if self.mirror_bone and active_split.side != Side.MIDDLE and value:
|
||||
sel_split = get_name_base_and_sides(sel_pbone.name)
|
||||
|
||||
if sel_split.side == -active_split.side:
|
||||
new_value = mirror_name(value)
|
||||
|
||||
# Assign the final value
|
||||
setattr(sel_pbone.rigify_parameters,
|
||||
self.property_name, new_value)
|
||||
num_copied += 1
|
||||
|
||||
if num_copied:
|
||||
self.report({'INFO'}, f"Copied the value to {num_copied} bones.")
|
||||
return {'FINISHED'}
|
||||
else:
|
||||
self.report({'WARNING'}, "No suitable selected bones to copy to.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
def make_copy_parameter_button(layout, property_name, *, base_class, mirror_bone=False):
|
||||
"""Displays a button that copies the property to selected rig of the specified base type."""
|
||||
props = layout.operator(
|
||||
POSE_OT_rigify_copy_single_parameter.bl_idname, icon='DUPLICATE', text='')
|
||||
props.property_name = property_name
|
||||
props.mirror_bone = mirror_bone
|
||||
props.module_name = base_class.__module__
|
||||
props.class_name = base_class.__name__
|
||||
|
||||
|
||||
# =============================================
|
||||
# Registration
|
||||
|
||||
classes = (
|
||||
POSE_OT_rigify_copy_single_parameter,
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
|
||||
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
for cls in classes:
|
||||
unregister_class(cls)
|
|
@ -80,6 +80,12 @@ def get_rigs(base_dir, base_path, *, path=[], feature_set=feature_set_list.DEFAU
|
|||
rigs = {}
|
||||
implementation_rigs = {}
|
||||
|
||||
def get_rig_class(name):
|
||||
try:
|
||||
return rigs[name]["module"].Rig
|
||||
except (KeyError, AttributeError):
|
||||
return None
|
||||
|
||||
def get_internal_rigs():
|
||||
global rigs, implementation_rigs
|
||||
|
||||
|
|
|
@ -153,17 +153,60 @@ def map_apply(func, *inputs):
|
|||
|
||||
|
||||
#=============================================
|
||||
# Misc
|
||||
# Lazy references
|
||||
#=============================================
|
||||
|
||||
|
||||
def force_lazy(value):
|
||||
"""If the argument is callable, invokes it without arguments. Otherwise returns the argument as is."""
|
||||
if callable(value):
|
||||
return value()
|
||||
else:
|
||||
return value
|
||||
|
||||
|
||||
class LazyRef:
|
||||
"""Hashable lazy reference. When called, evaluates (foo, 'a', 'b'...) as foo('a','b')
|
||||
if foo is callable. Otherwise the remaining arguments are used as attribute names or
|
||||
keys, like foo.a.b or foo.a[b] etc."""
|
||||
|
||||
def __init__(self, first, *args):
|
||||
self.first = first
|
||||
self.args = tuple(args)
|
||||
self.first_hashable = first.__hash__ is not None
|
||||
|
||||
def __repr__(self):
|
||||
return 'LazyRef{}'.format(tuple(self.first, *self.args))
|
||||
|
||||
def __eq__(self, other):
|
||||
return (
|
||||
isinstance(other, LazyRef) and
|
||||
(self.first == other.first if self.first_hashable else self.first is other.first) and
|
||||
self.args == other.args
|
||||
)
|
||||
|
||||
def __hash__(self):
|
||||
return (hash(self.first) if self.first_hashable else hash(id(self.first))) ^ hash(self.args)
|
||||
|
||||
def __call__(self):
|
||||
first = self.first
|
||||
if callable(first):
|
||||
return first(*self.args)
|
||||
|
||||
for item in self.args:
|
||||
if isinstance(first, (dict, list)):
|
||||
first = first[item]
|
||||
else:
|
||||
first = getattr(first, item)
|
||||
|
||||
return first
|
||||
|
||||
|
||||
#=============================================
|
||||
# Misc
|
||||
#=============================================
|
||||
|
||||
|
||||
def copy_attributes(a, b):
|
||||
keys = dir(a)
|
||||
for key in keys:
|
||||
|
|
|
@ -162,6 +162,9 @@ class SideZ(enum.IntEnum):
|
|||
return combine_name(parts, side_z=new_side)
|
||||
|
||||
|
||||
NameSides = collections.namedtuple('NameSides', ['base', 'side', 'side_z'])
|
||||
|
||||
|
||||
def get_name_side(name):
|
||||
return Side.from_parts(split_name(name))
|
||||
|
||||
|
@ -173,7 +176,7 @@ def get_name_side_z(name):
|
|||
def get_name_base_and_sides(name):
|
||||
parts = split_name(name)
|
||||
base = combine_name(parts, side='', side_z='')
|
||||
return base, Side.from_parts(parts), SideZ.from_parts(parts)
|
||||
return NameSides(base, Side.from_parts(parts), SideZ.from_parts(parts))
|
||||
|
||||
|
||||
def change_name_side(name, side=None, *, side_z=None):
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
# ====================== 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 collections
|
||||
import heapq
|
||||
import operator
|
||||
|
||||
from mathutils import Vector
|
||||
from mathutils.kdtree import KDTree
|
||||
|
||||
from .errors import MetarigError
|
||||
from ..base_rig import stage, GenerateCallbackHost
|
||||
from ..base_generate import GeneratorPlugin
|
||||
|
||||
|
||||
class NodeMerger(GeneratorPlugin):
|
||||
"""
|
||||
Utility that allows rigs to interact based on common points in space.
|
||||
|
||||
Rigs can register node objects representing locations during the
|
||||
initialize stage, and at the end the plugin sorts them into buckets
|
||||
based on proximity. For each such bucket a group object is created
|
||||
and allowed to further process the nodes.
|
||||
|
||||
Nodes chosen by the groups as being 'final' become sub-objects of
|
||||
the plugin and receive stage callbacks.
|
||||
|
||||
The domain parameter allows potentially having multiple completely
|
||||
separate layers of nodes with different purpose.
|
||||
"""
|
||||
|
||||
epsilon = 1e-5
|
||||
|
||||
def __init__(self, generator, domain):
|
||||
super().__init__(generator)
|
||||
|
||||
assert domain is not None
|
||||
assert generator.stage == 'initialize'
|
||||
|
||||
self.domain = domain
|
||||
self.nodes = []
|
||||
self.final_nodes = []
|
||||
self.groups = []
|
||||
self.frozen = False
|
||||
|
||||
def register_node(self, node):
|
||||
assert not self.frozen
|
||||
node.generator_plugin = self
|
||||
self.nodes.append(node)
|
||||
|
||||
def initialize(self):
|
||||
self.frozen = True
|
||||
|
||||
nodes = self.nodes
|
||||
tree = KDTree(len(nodes))
|
||||
|
||||
for i, node in enumerate(nodes):
|
||||
tree.insert(node.point, i)
|
||||
|
||||
tree.balance()
|
||||
processed = set()
|
||||
final_nodes = []
|
||||
groups = []
|
||||
|
||||
for i in range(len(nodes)):
|
||||
if i in processed:
|
||||
continue
|
||||
|
||||
# Find points to merge
|
||||
pending = [i]
|
||||
merge_set = set(pending)
|
||||
|
||||
while pending:
|
||||
added = set()
|
||||
for j in pending:
|
||||
for co, idx, dist in tree.find_range(nodes[j].point, self.epsilon):
|
||||
added.add(idx)
|
||||
pending = added.difference(merge_set)
|
||||
merge_set.update(added)
|
||||
|
||||
assert merge_set.isdisjoint(processed)
|
||||
|
||||
processed.update(merge_set)
|
||||
|
||||
# Group the points
|
||||
merge_list = [nodes[i] for i in merge_set]
|
||||
merge_list.sort(key=lambda x: x.name)
|
||||
|
||||
group_class = merge_list[0].group_class
|
||||
|
||||
for item in merge_list[1:]:
|
||||
cls = item.group_class
|
||||
|
||||
if issubclass(cls, group_class):
|
||||
group_class = cls
|
||||
elif not issubclass(group_class, cls):
|
||||
raise MetarigError(
|
||||
'Group class conflict: {} and {} from {} of {}'.format(
|
||||
group_class, cls, item.name, item.rig.base_bone,
|
||||
)
|
||||
)
|
||||
|
||||
group = group_class(merge_list)
|
||||
group.build(final_nodes)
|
||||
|
||||
groups.append(group)
|
||||
|
||||
self.final_nodes = self.rigify_sub_objects = final_nodes
|
||||
self.groups = groups
|
||||
|
||||
|
||||
class MergeGroup(object):
|
||||
"""
|
||||
Standard node group, merges nodes based on certain rules and priorities.
|
||||
|
||||
1. Nodes are classified into main and query nodes; query nodes are not merged.
|
||||
2. Nodes owned by the same rig cannot merge with each other.
|
||||
3. Node can only merge into target if node.can_merge_into(target) is true.
|
||||
4. Among multiple candidates in one rig, node.get_merge_priority(target) is used.
|
||||
5. The largest clusters of nodes that can merge are picked until none are left.
|
||||
|
||||
The master nodes of the chosen clusters, plus query nodes, become 'final'.
|
||||
"""
|
||||
|
||||
def __init__(self, nodes):
|
||||
self.nodes = nodes
|
||||
|
||||
for node in nodes:
|
||||
node.group = self
|
||||
|
||||
def is_main(node):
|
||||
return isinstance(node, MainMergeNode)
|
||||
|
||||
self.main_nodes = [n for n in nodes if is_main(n)]
|
||||
self.query_nodes = [n for n in nodes if not is_main(n)]
|
||||
|
||||
def build(self, final_nodes):
|
||||
main_nodes = self.main_nodes
|
||||
|
||||
# Sort nodes into rig buckets - can't merge within the same rig
|
||||
rig_table = collections.defaultdict(list)
|
||||
|
||||
for node in main_nodes:
|
||||
rig_table[node.rig].append(node)
|
||||
|
||||
# Build a 'can merge' table
|
||||
merge_table = {n: set() for n in main_nodes}
|
||||
|
||||
for node in main_nodes:
|
||||
for rig, tgt_nodes in rig_table.items():
|
||||
if rig is not node.rig:
|
||||
nodes = [n for n in tgt_nodes if node.can_merge_into(n)]
|
||||
if nodes:
|
||||
best_node = max(nodes, key=node.get_merge_priority)
|
||||
merge_table[best_node].add(node)
|
||||
|
||||
# Output groups starting with largest
|
||||
self.final_nodes = []
|
||||
|
||||
pending = set(main_nodes)
|
||||
|
||||
while pending:
|
||||
# Find largest group
|
||||
nodes = [n for n in main_nodes if n in pending]
|
||||
max_len = max(len(merge_table[n]) for n in nodes)
|
||||
|
||||
nodes = [n for n in nodes if len(merge_table[n]) == max_len]
|
||||
|
||||
# If a tie, try to resolve using comparison
|
||||
if len(nodes) > 1:
|
||||
weighted_nodes = [
|
||||
(n, sum(
|
||||
1 if (n.is_better_cluster(n2)
|
||||
and not n2.is_better_cluster(n)) else 0
|
||||
for n2 in nodes
|
||||
))
|
||||
for n in nodes
|
||||
]
|
||||
max_weight = max(wn[1] for wn in weighted_nodes)
|
||||
nodes = [wn[0] for wn in weighted_nodes if wn[1] == max_weight]
|
||||
|
||||
# Final tie breaker is the name
|
||||
best = min(nodes, key=lambda n: n.name)
|
||||
child_set = merge_table[best]
|
||||
|
||||
# Link children
|
||||
best.point = sum((c.point for c in child_set),
|
||||
best.point) / (len(child_set) + 1)
|
||||
|
||||
for child in [n for n in main_nodes if n in child_set]:
|
||||
child.point = best.point
|
||||
best.merge_from(child)
|
||||
child.merge_into(best)
|
||||
|
||||
final_nodes.append(best)
|
||||
self.final_nodes.append(best)
|
||||
|
||||
best.merge_done()
|
||||
|
||||
# Remove merged nodes from the table
|
||||
pending.remove(best)
|
||||
pending -= child_set
|
||||
|
||||
for children in merge_table.values():
|
||||
children &= pending
|
||||
|
||||
for node in self.query_nodes:
|
||||
node.merge_done()
|
||||
|
||||
final_nodes += self.query_nodes
|
||||
|
||||
|
||||
class BaseMergeNode(GenerateCallbackHost):
|
||||
"""Base class of mergeable nodes."""
|
||||
|
||||
merge_domain = None
|
||||
merger = NodeMerger
|
||||
group_class = MergeGroup
|
||||
|
||||
def __init__(self, rig, name, point, *, domain=None):
|
||||
self.rig = rig
|
||||
self.obj = rig.obj
|
||||
self.name = name
|
||||
self.point = Vector(point)
|
||||
|
||||
merger = self.merger(rig.generator, domain or self.merge_domain)
|
||||
merger.register_node(self)
|
||||
|
||||
def register_new_bone(self, new_name, old_name=None):
|
||||
self.generator_plugin.register_new_bone(new_name, old_name)
|
||||
|
||||
def can_merge_into(self, other):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_merge_priority(self, other):
|
||||
"Rank candidates to merge into."
|
||||
return 0
|
||||
|
||||
|
||||
class MainMergeNode(BaseMergeNode):
|
||||
"""
|
||||
Base class of standard mergeable nodes. Each node can either be
|
||||
a master of its cluster or a merged child node. Children become
|
||||
sub-objects of their master to receive callbacks in defined order.
|
||||
"""
|
||||
|
||||
def __init__(self, rig, name, point, *, domain=None):
|
||||
super().__init__(rig, name, point, domain=domain)
|
||||
|
||||
self.merged_into = None
|
||||
self.merged = []
|
||||
|
||||
def get_merged_siblings(self):
|
||||
master = self.merged_master
|
||||
return [master, *master.merged]
|
||||
|
||||
def is_better_cluster(self, other):
|
||||
"Compare with the other node to choose between cluster masters."
|
||||
return False
|
||||
|
||||
def can_merge_from(self, other):
|
||||
return True
|
||||
|
||||
def can_merge_into(self, other):
|
||||
return other.can_merge_from(self)
|
||||
|
||||
def merge_into(self, other):
|
||||
self.merged_into = other
|
||||
|
||||
def merge_from(self, other):
|
||||
self.merged.append(other)
|
||||
|
||||
@property
|
||||
def is_master_node(self):
|
||||
return not self.merged_into
|
||||
|
||||
def merge_done(self):
|
||||
self.merged_master = self.merged_into or self
|
||||
self.rigify_sub_objects = list(self.merged)
|
||||
|
||||
for child in self.merged:
|
||||
child.merge_done()
|
||||
|
||||
|
||||
class QueryMergeNode(BaseMergeNode):
|
||||
"""Base class for special nodes used only to query which nodes are at a certain location."""
|
||||
|
||||
is_master_node = False
|
||||
require_match = True
|
||||
|
||||
def merge_done(self):
|
||||
self.matched_nodes = [
|
||||
n for n in self.group.final_nodes if self.can_merge_into(n)
|
||||
]
|
||||
self.matched_nodes.sort(key=self.get_merge_priority, reverse=True)
|
||||
|
||||
if self.require_match and not self.matched_nodes:
|
||||
self.rig.raise_error(
|
||||
'Could not match control node for {}', self.name)
|
Loading…
Reference in New Issue