Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Gavrilov <angavrilov@gmail.com>2019-03-30 22:00:55 +0300
committerAlexander Gavrilov <angavrilov@gmail.com>2019-09-14 09:29:26 +0300
commit3423174b37a0784dc12035ff3f2fb536835099e1 (patch)
tree3a54580902cdebdef5ebacd6099e86cc79ba75b3 /rigify/utils/switch_parent.py
parent12af8a28c14b608e9b9b08568d981273c86590c1 (diff)
Rigify: redesign generate.py and introduce a base rig class.
The main goals are to provide an official way for rigs to interact in a structured way, and to remove mode switching within rigs. This involves introducing a base class for rigs that holds rig-to-rig and rig-to-bone references, converting the main generator into a class and passing it to rigs, and splitting the single generate method into multiple passes. For backward compatibility, old rigs are automatically handled via a wrapper that translates between old and new API. In addition, a way to create objects that receive the generate callbacks that aren't rigs is introduced via the GeneratorPlugin class. The UI script generation code is converted into a plugin. Making generic rig 'template' classes that are intended to be subclassed in specific rigs involves splitting operations done in each stage into multiple methods that can be overridden separately. The main callback thus ends up simply calling a sequence of other methods. To make such code cleaner it's better to allow registering those methods as new callbacks that would be automatically called by the system. This can be done via decorators. A new metaclass used for all rig and generate plugin classes builds and validates a table of all decorated methods, and allows calling them all together with the main callback. A new way to switch parents for IK bones based on the new features is introduced, and used in the existing limb rigs. Reviewers: icappiello campbellbarton Differential Revision: https://developer.blender.org/D4624
Diffstat (limited to 'rigify/utils/switch_parent.py')
-rw-r--r--rigify/utils/switch_parent.py438
1 files changed, 438 insertions, 0 deletions
diff --git a/rigify/utils/switch_parent.py b/rigify/utils/switch_parent.py
new file mode 100644
index 00000000..5daa6a6c
--- /dev/null
+++ b/rigify/utils/switch_parent.py
@@ -0,0 +1,438 @@
+import bpy
+
+import re
+import itertools
+import bisect
+import json
+
+from .errors import MetarigError
+from .naming import strip_prefix, make_derived_name
+from .mechanism import MechanismUtilityMixin
+from .misc import map_list, map_apply
+
+from ..base_rig import *
+from ..base_generate import GeneratorPlugin
+
+from itertools import count, repeat
+
+def _auto_call(value):
+ if callable(value):
+ return value()
+ else:
+ return value
+
+def _rig_is_child(rig, parent):
+ if parent is None:
+ return True
+
+ while rig:
+ if rig is parent:
+ return True
+
+ rig = rig.rigify_parent
+
+ return False
+
+
+class SwitchParentBuilder(GeneratorPlugin, MechanismUtilityMixin):
+ """
+ Implements centralized generation of switchable parent mechanisms.
+ Allows all rigs to register their bones as possible parents for other rigs.
+ """
+
+ def __init__(self, generator):
+ super().__init__(generator)
+
+ self.child_list = []
+ self.global_parents = []
+ self.local_parents = []
+ self.child_map = {}
+ self.frozen = False
+
+ self.register_parent(None, 'root', name='Root', is_global=True)
+
+
+ ##############################
+ # API
+
+ def register_parent(self, rig, bone, *, name=None, is_global=False, exclude_self=False):
+ """
+ Registers a bone of the specified rig as a possible parent.
+
+ Parameters:
+ rig Owner of the bone.
+ bone Actual name of the parent bone.
+ name Name of the parent for mouse-over hint.
+ is_global The parent is accessible to all rigs, instead of just children of owner.
+ exclude_self The parent is invisible to the owner rig itself.
+
+ Lazy creation:
+ The bone parameter may be a function creating the bone on demand and
+ returning its name. It is guaranteed to be called at most once.
+ """
+
+ assert not self.frozen
+ assert isinstance(bone, str) or callable(bone)
+
+ entry = {
+ 'rig': rig, 'bone': bone, 'name': name,
+ 'is_global': is_global, 'exclude_self': exclude_self, 'used': False,
+ }
+
+ if is_global:
+ self.global_parents.append(entry)
+ else:
+ self.local_parents.append(entry)
+
+
+ def build_child(self, rig, bone, *, use_parent_mch=True, **options):
+ """
+ Build a switchable parent mechanism for the specified bone.
+
+ Parameters:
+ rig Owner of the child bone.
+ bone Name of the child bone.
+ extra_parents List of bone names or (name, user_name) pairs to use as additional parents.
+ use_parent_mch Create an intermediate MCH bone for the constraints and parent the child to it.
+ select_parent Select the specified bone instead of the last one.
+ ignore_global Ignore the is_global flag of potential parents.
+ context_rig Rig to use for selecting parents.
+
+ prop_bone Name of the bone to add the property to.
+ prop_id Actual name of the control property.
+ prop_name Name of the property to use in the UI script.
+ controls Collection of controls to bind property UI to.
+
+ ctrl_bone User visible control bone that depends on this parent (for switch & keep transform)
+ no_fix_* Disable "Switch and Keep Transform" correction for specific channels.
+ copy_* Override the specified components by copying from another bone.
+
+ Lazy parameters:
+ 'extra_parents', 'select_parent', 'prop_bone', 'controls', 'copy_*'
+ may be a function returning the value. They are called in the configure_bones stage.
+ """
+ assert self.generator.stage == 'generate_bones' and not self.frozen
+ assert rig is not None
+ assert isinstance(bone, str)
+ assert bone not in self.child_map
+
+ # Create MCH proxy
+ if use_parent_mch:
+ mch_bone = rig.copy_bone(bone, make_derived_name(bone, 'mch', '.parent'), scale=1/3)
+ else:
+ mch_bone = bone
+
+ child = {
+ **self.child_option_table,
+ 'rig':rig, 'bone': bone, 'mch_bone': mch_bone,
+ 'is_done': False, 'is_configured': False,
+ }
+ self.assign_child_options(child, options)
+ self.child_list.append(child)
+ self.child_map[bone] = child
+
+
+ def amend_child(self, rig, bone, **options):
+ """
+ Change parameters assigned in a previous build_child call.
+
+ Provided to make it more convenient to change rig behavior by subclassing.
+ """
+ assert self.generator.stage == 'generate_bones' and not self.frozen
+ child = self.child_map[bone]
+ assert child['rig'] == rig
+ self.assign_child_options(child, options)
+
+
+ def rig_child_now(self, bone):
+ """Create the constraints immediately."""
+ assert self.generator.stage == 'rig_bones'
+ child = self.child_map[bone]
+ assert not child['is_done']
+ self.__rig_child(child)
+
+ ##############################
+ # Implementation
+
+ child_option_table = {
+ 'extra_parents': None,
+ 'prop_bone': None, 'prop_id': None, 'prop_name': None, 'controls': None,
+ 'select_parent': None, 'ignore_global': False, 'context_rig': None,
+ 'ctrl_bone': None,
+ 'no_fix_location': False, 'no_fix_rotation': False, 'no_fix_scale': False,
+ 'copy_location': None, 'copy_rotation': None, 'copy_scale': None,
+ }
+
+ def assign_child_options(self, child, options):
+ if 'context_rig' in options:
+ assert _rig_is_child(child['rig'], options['context_rig'])
+
+ for name, value in options.items():
+ if name not in self.child_option_table:
+ raise AttributeError('invalid child option: '+name)
+
+ child[name] = value
+
+ def generate_bones(self):
+ self.frozen = True
+ self.parent_list = self.global_parents + self.local_parents
+
+ # Link children to parents
+ for child in self.child_list:
+ child_rig = child['context_rig'] or child['rig']
+ parents = []
+
+ for parent in self.parent_list:
+ if parent['rig'] is child_rig:
+ if parent['exclude_self']:
+ continue
+ elif parent['is_global'] and not child['ignore_global']:
+ # Can't use parents from own children, even if global (cycle risk)
+ if _rig_is_child(parent['rig'], child_rig):
+ continue
+ else:
+ # Required to be a child of the parent's rig
+ if not _rig_is_child(child_rig, parent['rig']):
+ continue
+
+ parent['used'] = True
+ parents.append(parent)
+
+ child['parents'] = parents
+
+ # Call lazy creation for parents
+ for parent in self.parent_list:
+ if parent['used']:
+ parent['bone'] = _auto_call(parent['bone'])
+
+ def parent_bones(self):
+ for child in self.child_list:
+ rig = child['rig']
+ mch = child['mch_bone']
+
+ # Remove real parent from the child
+ rig.set_bone_parent(mch, None)
+ self.generator.disable_auto_parent(mch)
+
+ # Parent child to the MCH proxy
+ if mch != child['bone']:
+ rig.set_bone_parent(child['bone'], mch)
+
+ def configure_bones(self):
+ for child in self.child_list:
+ self.__configure_child(child)
+
+ def __configure_child(self, child):
+ if child['is_configured']:
+ return
+
+ child['is_configured'] = True
+
+ bone = child['bone']
+
+ # Build the final list of parent bone names
+ parent_map = dict()
+
+ for parent in child['parents']:
+ if parent['bone'] not in parent_map:
+ parent_map[parent['bone']] = parent['name']
+
+ last_main_parent_bone = child['parents'][-1]['bone']
+ num_main_parents = len(parent_map.items())
+
+ for parent in _auto_call(child['extra_parents'] or []):
+ if not isinstance(parent, tuple):
+ parent = (parent, None)
+ if parent[0] not in parent_map:
+ parent_map[parent[0]] = parent[1]
+
+ parent_bones = list(parent_map.items())
+ child['parent_bones'] = parent_bones
+
+ # Find which bone to select
+ select_bone = _auto_call(child['select_parent']) or last_main_parent_bone
+ select_index = num_main_parents
+
+ try:
+ select_index = 1 + next(i for i, (bone, _) in enumerate(parent_bones) if bone == select_bone)
+ except StopIteration:
+ print("RIGIFY ERROR: Can't find bone '%s' to select as default parent of '%s'\n" % (select_bone, bone))
+
+ # Create the controlling property
+ prop_bone = child['prop_bone'] = _auto_call(child['prop_bone']) or bone
+ prop_name = child['prop_name'] or child['prop_id'] or 'Parent Switch'
+ prop_id = child['prop_id'] = child['prop_id'] or 'parent_switch'
+
+ parent_names = [ parent[1] or strip_prefix(parent[0]) for parent in [(None, 'None'), *parent_bones] ]
+ parent_str = ', '.join([ '%s (%d)' % (name, i) for i, name in enumerate(parent_names) ])
+
+ ctrl_bone = child['ctrl_bone'] or bone
+
+ self.make_property(
+ prop_bone, prop_id, select_index,
+ min=0, max=len(parent_bones),
+ description='Switch parent of %s: %s' % (ctrl_bone, parent_str)
+ )
+
+ # Find which channels don't depend on the parent
+
+ no_fix = [ child[n] for n in ['no_fix_location', 'no_fix_rotation', 'no_fix_scale'] ]
+
+ child['copy'] = [ _auto_call(child[n]) for n in ['copy_location', 'copy_rotation', 'copy_scale'] ]
+
+ locks = tuple(bool(nofix or copy) for nofix, copy in zip(no_fix, child['copy']))
+
+ # Create the script for the property
+ controls = _auto_call(child['controls']) or set([prop_bone, bone])
+
+ script = self.generator.script
+ panel = script.panel_with_selected_check(child['rig'], controls)
+
+ panel.use_bake_settings()
+ script.add_utilities(SCRIPT_UTILITIES_OP_SWITCH_PARENT)
+ script.register_classes(SCRIPT_REGISTER_OP_SWITCH_PARENT)
+
+ op_props = {
+ 'bone': ctrl_bone, 'prop_bone': prop_bone, 'prop_id': prop_id,
+ 'parent_names': json.dumps(parent_names), 'locks': locks,
+ }
+
+ row = panel.row(align=True)
+ lsplit = row.split(factor=0.75, align=True)
+ lsplit.operator('pose.rigify_switch_parent_{rig_id}', text=prop_name, icon='DOWNARROW_HLT', properties=op_props)
+ lsplit.custom_prop(prop_bone, prop_id, text='')
+ row.operator('pose.rigify_switch_parent_bake_{rig_id}', text='', icon='ACTION_TWEAK', properties=op_props)
+
+ def rig_bones(self):
+ for child in self.child_list:
+ self.__rig_child(child)
+
+ def __rig_child(self, child):
+ if child['is_done']:
+ return
+
+ child['is_done'] = True
+
+ # Implement via an Armature constraint
+ mch = child['mch_bone']
+ con = self.make_constraint(mch, 'ARMATURE', name='SWITCH_PARENT')
+
+ prop_var = [(child['prop_bone'], child['prop_id'])]
+
+ for i, (parent, parent_name) in enumerate(child['parent_bones']):
+ tgt = con.targets.new()
+
+ tgt.target = self.obj
+ tgt.subtarget = parent
+ tgt.weight = 0.0
+
+ expr = 'var == %d' % (i+1)
+ self.make_driver(tgt, 'weight', expression=expr, variables=prop_var)
+
+ # Add copy constraints
+ copy = child['copy']
+
+ if copy[0]:
+ self.make_constraint(mch, 'COPY_LOCATION', copy[0])
+ if copy[1]:
+ self.make_constraint(mch, 'COPY_ROTATION', copy[1])
+ if copy[2]:
+ self.make_constraint(mch, 'COPY_SCALE', copy[2])
+
+
+SCRIPT_REGISTER_OP_SWITCH_PARENT = ['POSE_OT_rigify_switch_parent', 'POSE_OT_rigify_switch_parent_bake']
+
+SCRIPT_UTILITIES_OP_SWITCH_PARENT = ['''
+################################
+## Switchable Parent operator ##
+################################
+
+class RigifySwitchParentBase:
+ bone: StringProperty(name="Control Bone")
+ prop_bone: StringProperty(name="Property Bone")
+ prop_id: StringProperty(name="Property")
+ parent_names: StringProperty(name="Parent Names")
+ locks: bpy.props.BoolVectorProperty(name="Locked", size=3, default=[False,False,False])
+
+ parent_items = [('0','None','None')]
+
+ selected: bpy.props.EnumProperty(
+ name='Selected Parent',
+ items=lambda s,c: RigifySwitchParentBase.parent_items
+ )
+
+ keyflags = None
+ keyflags_switch = None
+
+ def save_frame_state(self, context, obj):
+ return get_transform_matrix(obj, self.bone, with_constraints=False)
+
+ def apply_frame_state(self, context, obj, old_matrix):
+ # Change the parent
+ set_custom_property_value(
+ obj, self.prop_bone, self.prop_id, int(self.selected),
+ keyflags=self.keyflags_switch
+ )
+
+ context.view_layer.update()
+
+ # Set the transforms to restore position
+ set_transform_from_matrix(
+ obj, self.bone, old_matrix, keyflags=self.keyflags,
+ no_loc=self.locks[0], no_rot=self.locks[1], no_scale=self.locks[2]
+ )
+
+ def get_bone_props(self):
+ props = set()
+ if not self.locks[0]:
+ props |= TRANSFORM_PROPS_LOCATION
+ if not self.locks[1]:
+ props |= TRANSFORM_PROPS_ROTATION
+ if not self.locks[2]:
+ props |= TRANSFORM_PROPS_SCALE
+ return props
+
+ def init_invoke(self, context):
+ pose = context.active_object.pose
+
+ if (not pose or not self.parent_names
+ or self.bone not in pose.bones
+ or self.prop_bone not in pose.bones
+ or self.prop_id not in pose.bones[self.prop_bone]):
+ self.report({'ERROR'}, "Invalid parameters")
+ return {'CANCELLED'}
+
+ parents = json.loads(self.parent_names)
+ pitems = [(str(i), name, name) for i, name in enumerate(parents)]
+
+ RigifySwitchParentBase.parent_items = pitems
+
+ self.selected = str(pose.bones[self.prop_bone][self.prop_id])
+
+
+class POSE_OT_rigify_switch_parent(RigifySwitchParentBase, RigifySingleUpdateMixin, bpy.types.Operator):
+ bl_idname = "pose.rigify_switch_parent_" + rig_id
+ bl_label = "Switch Parent (Keep Transform)"
+ bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
+ bl_description = "Switch parent, preserving the bone position and orientation"
+
+ def draw(self, _context):
+ col = self.layout.column()
+ col.prop(self, 'selected', expand=True)
+
+
+class POSE_OT_rigify_switch_parent_bake(RigifySwitchParentBase, RigifyBakeKeyframesMixin, bpy.types.Operator):
+ bl_idname = "pose.rigify_switch_parent_bake_" + rig_id
+ bl_label = "Apply Switch Parent To Keyframes"
+ bl_options = {'UNDO', 'INTERNAL'}
+ bl_description = "Switch parent over a frame range, adjusting keys to preserve the bone position and orientation"
+
+ def execute_scan_curves(self, context, obj):
+ return self.bake_add_bone_frames(self.bone, self.get_bone_props())
+
+ def execute_before_apply(self, context, obj, range, range_raw):
+ self.bake_replace_custom_prop_keys_constant(self.prop_bone, self.prop_id, int(self.selected))
+
+ def draw(self, context):
+ self.layout.prop(self, 'selected', text='')
+''']