From ff4bd4968482afbaea576850f6674556b514abaf Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 27 Nov 2018 14:36:17 +0300 Subject: [PATCH 1/4] Rewrite generate.py into a class 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. --- __init__.py | 13 +- base_generate.py | 365 +++++++++++++ base_rig.py | 205 ++++++++ generate.py | 927 +++++++++++++++------------------ rig_ui_template.py | 176 ++++++- rigs/experimental/glue_bone.py | 7 +- rigs/faces/super_face.py | 2 +- ui.py | 6 +- utils/__init__.py | 6 +- utils/bones.py | 245 +++++++-- utils/layers.py | 9 + utils/mechanism.py | 20 +- utils/metaclass.py | 41 ++ utils/misc.py | 37 ++ utils/naming.py | 3 + utils/rig.py | 13 + 16 files changed, 1487 insertions(+), 588 deletions(-) create mode 100644 base_generate.py create mode 100644 base_rig.py create mode 100644 utils/metaclass.py diff --git a/__init__.py b/__init__.py index 0e9b40e..b79dd45 100644 --- a/__init__.py +++ b/__init__.py @@ -32,14 +32,17 @@ if "bpy" in locals(): import importlib + importlib.reload(utils) + importlib.reload(base_rig) + importlib.reload(base_generate) + importlib.reload(rig_ui_template) + importlib.reload(rig_lists) importlib.reload(generate) importlib.reload(ui) - importlib.reload(utils) importlib.reload(metarig_menu) - importlib.reload(rig_lists) importlib.reload(feature_sets) else: - from . import (utils, rig_lists, generate, ui, metarig_menu, feature_sets) + from . import (utils, base_rig, base_generate, rig_ui_template, rig_lists, generate, ui, metarig_menu, feature_sets) import bpy import sys @@ -449,7 +452,9 @@ def update_mode(self, context): pass else: for rig in rig_lists.rigs: - r = rig_lists.rigs[rig]['module'] + rig_module = rig_lists.rigs[rig]['module'] + rig_class = rig_module.Rig + r = rig_class if issubclass(rig_class, base_rig.BaseRig) else rig_module try: r.add_parameters(RigifyParameterValidator(RigifyParameters, rig, RIGIFY_PARAMETER_TABLE)) except AttributeError: diff --git a/base_generate.py b/base_generate.py new file mode 100644 index 0000000..4ca90b0 --- /dev/null +++ b/base_generate.py @@ -0,0 +1,365 @@ +#====================== 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 ======================== + +# + +import bpy +import sys +import traceback + +from .utils.errors import MetarigError +from .utils.naming import random_id +from .utils.metaclass import SingletonPluginMetaclass + +from . import base_rig + + +#============================================= +# Generator Plugin +#============================================= + + +class GeneratorPlugin(base_rig.GenerateCallbackHost, metaclass=SingletonPluginMetaclass): + """ + Base class for generator plugins. + + Generator plugins are per-Generator singleton utility + classes that receive the same stage callbacks as rigs. + + Useful for building entities shared by multiple rigs + (e.g. the python script), or for making fire-and-forget + utilities that actually require multiple stages to + complete. + + This will create only one instance per set of args: + + instance = PluginClass(generator, ...init args) + """ + + priority = 0 + + def __init__(self, generator): + self.generator = generator + self.obj = generator.obj + + def register_new_bone(self, new_name, old_name=None): + self.generator.bone_owners[new_name] = None + + +#============================================= +# Legacy Rig Wrapper +#============================================= + + +class LegacyRig(base_rig.BaseRig): + """Wrapper around legacy style rigs without a common base class""" + + def __init__(self, generator, bone, wrapped_class): + self.wrapped_rig = None + self.wrapped_class = wrapped_class + + super(LegacyRig,self).__init__(generator, bone) + + def find_org_bones(self, bone): + if not self.wrapped_rig: + self.wrapped_rig = self.wrapped_class(self.obj, self.base_bone, self.params) + + # Try to extract the main list of bones - old rigs often have it. + # This is not actually strictly necessary, so failing is OK. + if hasattr(self.wrapped_rig, 'org_bones'): + bones = self.wrapped_rig.org_bones + if isinstance(bones, list): + return bones + + return [bone.name] + + def generate_bones(self): + # Old rigs only have one generate method, so call it from + # generate_bones, which is the only stage allowed to add bones. + scripts = self.wrapped_rig.generate() + + # Switch back to EDIT mode if the rig changed it + if self.obj.mode != 'EDIT': + bpy.ops.object.mode_set(mode='EDIT') + + if isinstance(scripts, dict): + if 'script' in scripts: + self.script.add_panel_code(scripts['script']) + if 'imports' in scripts: + self.script.add_imports(scripts['imports']) + if 'utilities' in scripts: + self.script.add_utilities(scripts['utilities']) + if 'register' in scripts: + self.script.register_classes(scripts['register']) + if 'register_drivers' in scripts: + self.script.register_driver_functions(scripts['register_drivers']) + if 'register_props' in scripts: + for prop, val in scripts['register_props']: + self.script.register_property(prop, val) + if 'noparent_bones' in scripts: + for bone in scripts['noparent_bones']: + self.generator.disable_auto_parent(bone) + elif scripts is not None: + self.script.add_panel_code([scripts[0]]) + + def finalize(self): + if hasattr(self.wrapped_rig, 'glue'): + self.wrapped_rig.glue() + + +#============================================= +# Base Generate Engine +#============================================= + + +class BaseGenerator(object): + """Base class for the main generator object. Contains rig and plugin management code.""" + + def __init__(self, context, metarig): + self.context = context + self.scene = context.scene + self.metarig = metarig + self.obj = None + + # List of all rig instances + self.rig_list = [] + # List of rigs that don't have a parent + self.root_rigs = [] + # Map from bone names to their rigs + self.bone_owners = {} + + # Set of plugins + self.plugin_list = [] + self.plugin_map = {} + + # Current execution stage so plugins could check they are used correctly + self.stage = None + + # Set of bones that should be left without parent + self.noparent_bones = set() + + # Random string with time appended so that + # different rigs don't collide id's + self.rig_id = random_id(16) + + + def disable_auto_parent(self, bone): + """Prevent automatically parenting the bone to root if parentless.""" + self.noparent_bones.add(bone) + + + def __run_object_stage(self, method_name): + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'OBJECT') + num_bones = len(self.obj.data.bones) + + self.stage = method_name + + for rig in [*self.rig_list, *self.plugin_list]: + getattr(rig, method_name)() + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'OBJECT') + assert(num_bones == len(self.obj.data.bones)) + + + def __run_edit_stage(self, method_name): + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + num_bones = len(self.obj.data.edit_bones) + + self.stage = method_name + + for rig in [*self.rig_list, *self.plugin_list]: + getattr(rig, method_name)() + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + assert(num_bones == len(self.obj.data.edit_bones)) + + + def invoke_initialize(self): + self.__run_object_stage('initialize') + + + def invoke_prepare_bones(self): + self.__run_edit_stage('prepare_bones') + + + def __auto_register_bones(self, bones, rig): + """Find bones just added and not registered by this rig.""" + for bone in bones: + name = bone.name + if name not in self.bone_owners: + self.bone_owners[name] = rig + if rig: + rig.rigify_new_bones[name] = None + + if not isinstance(rig, LegacyRig): + print("WARNING: rig %s didn't register bone %s\n" % (rig, name)) + + + def invoke_generate_bones(self): + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + + self.stage = 'generate_bones' + + for rig in self.rig_list: + rig.generate_bones() + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + + self.__auto_register_bones(self.obj.data.edit_bones, rig) + + for plugin in self.plugin_list: + plugin.generate_bones() + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'EDIT') + + self.__auto_register_bones(self.obj.data.edit_bones, None) + + + def invoke_parent_bones(self): + self.__run_edit_stage('parent_bones') + + + def invoke_configure_bones(self): + self.__run_object_stage('configure_bones') + + + def invoke_rig_bones(self): + self.__run_object_stage('rig_bones') + + + def invoke_generate_widgets(self): + self.__run_object_stage('generate_widgets') + + + def invoke_finalize(self): + self.__run_object_stage('finalize') + + + def instantiate_rig(self, rig_class, bone): + if issubclass(rig_class, base_rig.BaseRig): + return rig_class(self, bone) + else: + return LegacyRig(self, bone, rig_class) + + + def __create_rigs_rec(self, bone, halt_on_missing): + """Recursively walk bones and create rig instances.""" + + bone_name = bone.name + child_list = [bone.name for bone in bone.children] + + pose_bone = self.obj.pose.bones[bone_name] + + rig_type = pose_bone.rigify_type + rig_type = rig_type.replace(" ", "") + + if rig_type != "": + try: + rig_class = self.find_rig_class(rig_type) + rig = self.instantiate_rig(rig_class, pose_bone) + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'OBJECT') + + self.rig_list.append(rig) + + for org_name in rig.rigify_org_bones: + if org_name in self.bone_owners: + print("CONFLICT: bone %s already claimed by rig %s\n" % (org_name, self.bone_owners[org_name])) + + self.bone_owners[org_name] = rig + + except ImportError: + message = "Rig Type Missing: python module for type '%s' not found (bone: %s)" % (rig_type, bone_name) + if halt_on_missing: + raise MetarigError(message) + else: + print(message) + print('print_exc():') + traceback.print_exc(file=sys.stdout) + + child_list.sort() + + for child in child_list: + cbone = self.obj.data.bones[child] + self.__create_rigs_rec(cbone, halt_on_missing) + + + def __build_rig_tree_rec(self, bone, current_rig, handled): + """Recursively walk bones and connect rig instances into a tree.""" + + rig = self.bone_owners.get(bone.name) + + if rig: + if rig is current_rig: + pass + + elif rig not in handled: + rig.rigify_parent = current_rig + + if current_rig: + current_rig.rigify_children.append(rig) + else: + self.root_rigs.append(rig) + + handled[rig] = bone.name + + elif rig.rigify_parent is not current_rig: + raise MetarigError("CONFLICT: bone %s owned by rig %s has different parent rig from %s\n" % + (bone.name, rig.base_bone, handled[rig])) + + current_rig = rig + else: + if current_rig: + current_rig.rigify_child_bones.add(bone.name) + + self.bone_owners[bone.name] = current_rig + + for child in bone.children: + self.__build_rig_tree_rec(child, current_rig, handled) + + + def instantiate_rig_tree(self, halt_on_missing=False): + """Create rig instances and connect them into a tree.""" + + assert(self.context.active_object == self.obj) + assert(self.obj.mode == 'OBJECT') + + bone_names = [bone.name for bone in self.obj.data.bones] + bone_names.sort() + + # Construct the rig instances + for name in bone_names: + bone = self.obj.data.bones[name] + if bone.parent is None: + self.__create_rigs_rec(bone, halt_on_missing) + + # Connect rigs and bones into a tree + handled = {} + + for bone in self.obj.data.bones: + if bone.parent is None: + self.__build_rig_tree_rec(bone, None, handled) + diff --git a/base_rig.py b/base_rig.py new file mode 100644 index 0000000..1f664a1 --- /dev/null +++ b/base_rig.py @@ -0,0 +1,205 @@ +#====================== 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 ======================== + +# + +import bpy +import sys +import traceback + +from .utils.bones import BoneDict, BoneUtilityMixin +from .utils.mechanism import MechanismUtilityMixin + +# Only export certain symbols via 'from base_rig import *' +__all__ = ['BaseRig', 'RigUtility'] + +#============================================= +# Base Rig +#============================================= + +class GenerateCallbackHost(object): + """ + Standard set of callback methods to redefine. + Shared between BaseRig and GeneratorPlugin. + + These callbacks are called in this order; every one is + called for all rigs before proceeding to the next stage. + + Switching modes is not allowed in rigs for performance + reasons. Place code in the appropriate callbacks to use + the mode set by the main engine. + """ + def initialize(self): + """ + Initialize processing after all rig classes are constructed. + Called in Object mode. May not change the armature. + """ + pass + + def prepare_bones(self): + """ + Prepare ORG bones for generation, e.g. align them. + Called in Edit mode. May not add bones. + """ + pass + + def generate_bones(self): + """ + Create all bones. + Called in Edit mode. + """ + pass + + def parent_bones(self): + """ + Parent all bones and set other edit mode properties. + Called in Edit mode. May not add bones. + """ + pass + + def configure_bones(self): + """ + Configure bone properties, e.g. transform locks, layers etc. + Called in Object mode. May not do Edit mode operations. + """ + pass + + def rig_bones(self): + """ + Create and configure all constraints, drivers etc. + Called in Object mode. May not do Edit mode operations. + """ + pass + + def generate_widgets(self): + """ + Create all widget objects. + Called in Object mode. May not do Edit mode operations. + """ + pass + + def finalize(self): + """ + Finishing touches to the construction of the rig. + Called in Object mode. May not do Edit mode operations. + """ + pass + + +class BaseRig(GenerateCallbackHost, BoneUtilityMixin, MechanismUtilityMixin): + """ + Base class for all rigs. + + The main weak areas in the legacy Rigify rig class structure + was that there were no provisions for intelligent interactions + between rigs, and all processing was done via one generate + method, necessitating frequent expensive mode switches. + + This structure fixes those problems by providing a mandatory + base class that hold documented connections between rigs, bones, + and the common generator object. The generation process is also + split into multiple stages. + """ + def __init__(self, generator, pose_bone): + self.generator = generator + + self.obj = generator.obj + self.script = generator.script + self.base_bone = pose_bone.name + self.params = pose_bone.rigify_parameters + + # Collection of bone names for use in implementing the rig + self.bones = BoneDict( + # ORG bone names + org = self.find_org_bones(pose_bone), + # Control bone names + ctrl = BoneDict(), + # MCH bone names + mch = BoneDict(), + # DEF bone names + deform = BoneDict(), + ) + + # Data useful for complex rig interaction: + # Parent-child links between rigs. + self.rigify_parent = None + self.rigify_children = [] + # ORG bones directly owned by the rig. + self.rigify_org_bones = set(self.bones.flatten('org')) + # Children of bones owned by the rig. + self.rigify_child_bones = set() + # Bones created by the rig (mapped to original names) + self.rigify_new_bones = dict() + + def register_new_bone(self, new_name, old_name=None): + """Registers this rig as the owner of this new bone.""" + self.rigify_new_bones[new_name] = old_name + self.generator.bone_owners[new_name] = self + + ########################################################### + # Bone ownership + + def find_org_bones(self, pose_bone): + """ + Select bones directly owned by the rig. Returning the + same bone from multiple rigs is an error. + + May return a single name, a list, or a BoneDict. + + Called in Object mode, may not change the armature. + """ + return [pose_bone.name] + + ########################################################### + # Parameters and UI + + @classmethod + def add_parameters(cls, params): + """ + This method add more parameters to params + :param params: rigify_parameters of a pose_bone + :return: + """ + pass + + @classmethod + def parameters_ui(cls, layout, params): + """ + This method draws the UI of the rigify_parameters defined on the pose_bone + :param layout: + :param params: + :return: + """ + layout.label(text="No options") + + +#============================================= +# Rig Utility +#============================================= + + +class RigUtility(BoneUtilityMixin, MechanismUtilityMixin): + """Base class for utility classes that generate part of a rig.""" + def __init__(self, owner): + self.owner = owner + self.obj = owner.obj + + def register_new_bone(self, new_name, old_name=None): + self.owner.register_new_bone(new_name, old_name) + + diff --git a/generate.py b/generate.py index b53190d..2c7937a 100644 --- a/generate.py +++ b/generate.py @@ -24,24 +24,20 @@ import traceback import sys from rna_prop_ui import rna_idprop_ui_prop_get -from collections import OrderedDict - -from .utils import MetarigError, new_bone -from .utils import MCH_PREFIX, DEF_PREFIX, WGT_PREFIX, ROOT_NAME, make_original_name -from .utils import create_root_widget -from .utils import random_id -from .utils import copy_attributes -from .utils import gamma_correct -from . import rig_lists + +from .utils.errors import MetarigError +from .utils.bones import new_bone +from .utils.layers import ORG_LAYER, MCH_LAYER, DEF_LAYER, ROOT_LAYER +from .utils.naming import ORG_PREFIX, MCH_PREFIX, DEF_PREFIX, ROOT_NAME, make_original_name +from .utils.widgets import WGT_PREFIX, WGT_LAYERS +from .utils.widgets_special import create_root_widget +from .utils.misc import copy_attributes, gamma_correct, select_object + +from . import base_generate from . import rig_ui_template +from . import rig_lists RIG_MODULE = "rigs" -ORG_LAYER = [n == 31 for n in range(0, 32)] # Armature layer that original bones should be moved to. -MCH_LAYER = [n == 30 for n in range(0, 32)] # Armature layer that mechanism bones should be moved to. -DEF_LAYER = [n == 29 for n in range(0, 32)] # Armature layer that deformation bones should be moved to. -ROOT_LAYER = [n == 28 for n in range(0, 32)] # Armature layer that root bone should be moved to. -WGT_LAYERS = [x == 19 for x in range(0, 20)] # Widgets go on the last scene layer. - class Timer: def __init__(self): @@ -53,513 +49,442 @@ def tick(self, string): self.timez = t -# TODO: generalize to take a group as input instead of an armature. -def generate_rig(context, metarig): - """ Generates a rig from a metarig. +class Generator(base_generate.BaseGenerator): + def __init__(self, context, metarig): + super(Generator, self).__init__(context, metarig) - """ - t = Timer() + self.id_store = context.window_manager - # Random string with time appended so that - # different rigs don't collide id's - rig_id = random_id(16) + self.rig_new_name = "" + self.rig_old_name = "" - # Initial configuration - # mode_orig = context.mode # UNUSED - rest_backup = metarig.data.pose_position - metarig.data.pose_position = 'REST' - bpy.ops.object.mode_set(mode='OBJECT') + def find_rig_class(self, rig_type): + rig_module = rig_lists.rigs[rig_type]["module"] - scene = context.scene - id_store = context.window_manager - #------------------------------------------ - # Create/find the rig object and set it up + return rig_module.Rig - # Check if the generated rig already exists, so we can - # regenerate in the same object. If not, create a new - # object to generate the rig in. - print("Fetch rig.") - rig_new_name = "" - rig_old_name = "" - if id_store.rigify_rig_basename: - rig_new_name = id_store.rigify_rig_basename + "_rig" + def __create_rig_object(self): + scene = self.scene + id_store = self.id_store - if id_store.rigify_generate_mode == 'overwrite': - name = id_store.rigify_target_rig or "rig" - try: - obj = scene.objects[name] - rig_old_name = name - obj.name = rig_new_name or name - except KeyError: - rig_old_name = name - name = rig_new_name or name - obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) + # Check if the generated rig already exists, so we can + # regenerate in the same object. If not, create a new + # object to generate the rig in. + print("Fetch rig.") + + if id_store.rigify_generate_mode == 'overwrite': + name = id_store.rigify_target_rig or "rig" + try: + obj = scene.objects[name] + self.rig_old_name = name + obj.name = self.rig_new_name or name + except KeyError: + self.rig_old_name = name + name = self.rig_new_name or name + obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) + obj.draw_type = 'WIRE' + scene.objects.link(obj) + else: + name = self.rig_new_name or "rig" + obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) # in case name 'rig' exists it will be rig.001 obj.draw_type = 'WIRE' scene.objects.link(obj) - else: - name = rig_new_name or "rig" - obj = bpy.data.objects.new(name, bpy.data.armatures.new(name)) # in case name 'rig' exists it will be rig.001 - obj.draw_type = 'WIRE' - scene.objects.link(obj) - id_store.rigify_target_rig = obj.name - obj.data.pose_position = 'POSE' + id_store.rigify_target_rig = obj.name + obj.data.pose_position = 'POSE' - # Get rid of anim data in case the rig already existed - print("Clear rig animation data.") - obj.animation_data_clear() + self.obj = obj + return obj - # Select generated rig object - metarig.select = False - obj.select = True - scene.objects.active = obj - # Remove wgts if force update is set - wgts_group_name = "WGTS_" + (rig_old_name or obj.name) - if wgts_group_name in scene.objects and id_store.rigify_force_widget_update: + def __create_widget_group(self, new_group_name): + context = self.context + scene = self.scene + id_store = self.id_store + + # Remove wgts if force update is set + wgts_group_name = "WGTS_" + (self.rig_old_name or obj.name) + if wgts_group_name in scene.objects and id_store.rigify_force_widget_update: + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.select_all(action='DESELECT') + for i, lyr in enumerate(WGT_LAYERS): + if lyr: + context.scene.layers[i] = True + for wgt in bpy.data.objects[wgts_group_name].children: + wgt.select = True + bpy.ops.object.delete(use_global=False) + for i, lyr in enumerate(WGT_LAYERS): + if lyr: + context.scene.layers[i] = False + if self.rig_old_name: + bpy.data.objects[wgts_group_name].name = new_group_name + + # Create Group widget + wgts_group_name = new_group_name + if wgts_group_name not in scene.objects: + if wgts_group_name in bpy.data.objects: + bpy.data.objects[wgts_group_name].user_clear() + bpy.data.objects.remove(bpy.data.objects[wgts_group_name]) + mesh = bpy.data.meshes.new(wgts_group_name) + wgts_obj = bpy.data.objects.new(wgts_group_name, mesh) + scene.objects.link(wgts_obj) + wgts_obj.layers = WGT_LAYERS + + self.wgts_group_name = new_group_name + + + def __duplicate_rig(self): + obj = self.obj + metarig = self.metarig + context = self.context + + # Remove all bones from the generated rig armature. + bpy.ops.object.mode_set(mode='EDIT') + for bone in obj.data.edit_bones: + obj.data.edit_bones.remove(bone) bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.select_all(action='DESELECT') - for i, lyr in enumerate(WGT_LAYERS): - if lyr: - context.scene.layers[i] = True - for wgt in bpy.data.objects[wgts_group_name].children: - wgt.select = True - bpy.ops.object.delete(use_global=False) - for i, lyr in enumerate(WGT_LAYERS): - if lyr: - context.scene.layers[i] = False - if rig_old_name: - bpy.data.objects[wgts_group_name].name = "WGTS_" + obj.name - - wgts_group_name = "WGTS_" + obj.name - - # Get parented objects to restore later - childs = {} # {object: bone} - for child in obj.children: - childs[child] = child.parent_bone - - # Remove all bones from the generated rig armature. - bpy.ops.object.mode_set(mode='EDIT') - for bone in obj.data.edit_bones: - obj.data.edit_bones.remove(bone) - bpy.ops.object.mode_set(mode='OBJECT') - # Create temporary duplicates for merging - temp_rig_1 = metarig.copy() - temp_rig_1.data = metarig.data.copy() - scene.objects.link(temp_rig_1) + # Select and duplicate metarig + select_object(context, metarig, deselect_all=True) - temp_rig_2 = metarig.copy() - temp_rig_2.data = obj.data - scene.objects.link(temp_rig_2) + bpy.ops.object.duplicate() - # Select the temp rigs for merging - for objt in scene.objects: - objt.select = False # deselect all objects - temp_rig_1.select = True - temp_rig_2.select = True - scene.objects.active = temp_rig_2 + # Select the target rig and join + select_object(context, obj) - # Merge the temporary rigs - bpy.ops.object.join() + bpy.ops.object.join() - # Delete the second temp rig - bpy.ops.object.delete() + # Select the generated rig + select_object(context, obj, deselect_all=True) + + # Clean up animation data + if obj.animation_data: + obj.animation_data.action = None + + for track in obj.animation_data.nla_tracks: + obj.animation_data.nla_tracks.remove(track) + + # Freeze drivers referring to custom properties + for d in obj.animation_data.drivers: + for var in d.driver.variables: + for tar in var.targets: + # If a custom property + if var.type == 'SINGLE_PROP' \ + and re.match('^pose.bones\["[^"\]]*"\]\["[^"\]]*"\]$', tar.data_path): + tar.data_path = "RIGIFY-" + tar.data_path + + + def __rename_org_bones(self): + obj = self.obj + + #---------------------------------- + # Make a list of the original bones so we can keep track of them. + original_bones = [bone.name for bone in obj.data.bones] + + # Add the ORG_PREFIX to the original bones. + for i in range(0, len(original_bones)): + new_name = make_original_name(original_bones[i]) + obj.data.bones[original_bones[i]].name = new_name + original_bones[i] = new_name + + self.original_bones = original_bones + + + def __create_root_bone(self): + obj = self.obj + metarig = self.metarig + + #---------------------------------- + # Create the root bone. + root_bone = new_bone(obj, ROOT_NAME) + spread = get_xy_spread(metarig.data.bones) or metarig.data.bones[0].length + spread = float('%.3g' % spread) + scale = spread/0.589 + obj.data.edit_bones[root_bone].head = (0, 0, 0) + obj.data.edit_bones[root_bone].tail = (0, scale, 0) + obj.data.edit_bones[root_bone].roll = 0 + self.root_bone = root_bone + self.bone_owners[root_bone] = None + + + def __parent_bones_to_root(self): + eb = self.obj.data.edit_bones + + # Parent loose bones to root + for bone in eb: + if bone.name in self.noparent_bones: + continue + elif bone.parent is None: + bone.use_connect = False + bone.parent = eb[self.root_bone] + + + def __lock_transforms(self): + # Lock transforms on all non-control bones + r = re.compile("[A-Z][A-Z][A-Z]-") + for pb in self.obj.pose.bones: + if r.match(pb.name): + pb.lock_location = (True, True, True) + pb.lock_rotation = (True, True, True) + pb.lock_rotation_w = True + pb.lock_scale = (True, True, True) + + + def __assign_layers(self): + bones = self.obj.data.bones + + bones[self.root_bone].layers = ROOT_LAYER + + # Every bone that has a name starting with "DEF-" make deforming. All the + # others make non-deforming. + for bone in bones: + name = bone.name + + bone.use_deform = name.startswith(DEF_PREFIX) + + # Move all the original bones to their layer. + if name.startswith(ORG_PREFIX): + bone.layers = ORG_LAYER + # Move all the bones with names starting with "MCH-" to their layer. + elif name.startswith(MCH_PREFIX): + bone.layers = MCH_LAYER + # Move all the bones with names starting with "DEF-" to their layer. + elif name.startswith(DEF_PREFIX): + bone.layers = DEF_LAYER + + + def __restore_driver_vars(self): + obj = self.obj + + # Alter marked driver targets + if obj.animation_data: + for d in obj.animation_data.drivers: + for v in d.driver.variables: + for tar in v.targets: + if tar.data_path.startswith("RIGIFY-"): + temp, bone, prop = tuple([x.strip('"]') for x in tar.data_path.split('["')]) + if bone in obj.data.bones \ + and prop in obj.pose.bones[bone].keys(): + tar.data_path = tar.data_path[7:] + else: + tar.data_path = 'pose.bones["%s"]["%s"]' % (make_original_name(bone), prop) + + + def __assign_widgets(self): + obj_table = {obj.name: obj for obj in self.scene.objects} + + # Assign shapes to bones + # Object's with name WGT- get used as that bone's shape. + for bone in self.obj.pose.bones: + # Object names are limited to 63 characters... arg + wgt_name = (WGT_PREFIX + self.obj.name + '_' + bone.name)[:63] + + if wgt_name in obj_table: + bone.custom_shape = obj_table[wgt_name] + + + def __compute_visible_layers(self): + # Reveal all the layers with control bones on them + vis_layers = [False for n in range(0, 32)] + + for bone in self.obj.data.bones: + for i in range(0, 32): + vis_layers[i] = vis_layers[i] or bone.layers[i] + + for i in range(0, 32): + vis_layers[i] = vis_layers[i] and not (ORG_LAYER[i] or MCH_LAYER[i] or DEF_LAYER[i]) + + self.obj.data.layers = vis_layers + + + def generate(self): + context = self.context + metarig = self.metarig + scene = self.scene + id_store = self.id_store + t = Timer() + + bpy.ops.object.mode_set(mode='OBJECT') + + #------------------------------------------ + # Create/find the rig object and set it up + if id_store.rigify_rig_basename: + self.rig_new_name = id_store.rigify_rig_basename + "_rig" + + obj = self.__create_rig_object() + + # Get rid of anim data in case the rig already existed + print("Clear rig animation data.") + + obj.animation_data_clear() + + select_object(context, obj, deselect_all=True) + + #------------------------------------------ + # Create Group widget + self.__create_widget_group("WGTS_" + obj.name) - # Select the generated rig - for objt in scene.objects: - objt.select = False # deselect all objects - obj.select = True - scene.objects.active = obj - - # Copy over bone properties - for bone in metarig.data.bones: - bone_gen = obj.data.bones[bone.name] - - # B-bone stuff - bone_gen.bbone_segments = bone.bbone_segments - bone_gen.bbone_in = bone.bbone_in - bone_gen.bbone_out = bone.bbone_out - - # Copy over the pose_bone properties - for bone in metarig.pose.bones: - bone_gen = obj.pose.bones[bone.name] - - # Rotation mode and transform locks - bone_gen.rotation_mode = bone.rotation_mode - bone_gen.lock_rotation = tuple(bone.lock_rotation) - bone_gen.lock_rotation_w = bone.lock_rotation_w - bone_gen.lock_rotations_4d = bone.lock_rotations_4d - bone_gen.lock_location = tuple(bone.lock_location) - bone_gen.lock_scale = tuple(bone.lock_scale) - - # rigify_type and rigify_parameters - bone_gen.rigify_type = bone.rigify_type - for prop in dir(bone_gen.rigify_parameters): - if (not prop.startswith("_")) \ - and (not prop.startswith("bl_")) \ - and (prop != "rna_type"): - try: - setattr(bone_gen.rigify_parameters, prop, \ - getattr(bone.rigify_parameters, prop)) - except AttributeError: - print("FAILED TO COPY PARAMETER: " + str(prop)) - - # Custom properties - for prop in bone.keys(): - try: - bone_gen[prop] = bone[prop] - except KeyError: - pass - - # Constraints - for con1 in bone.constraints: - con2 = bone_gen.constraints.new(type=con1.type) - copy_attributes(con1, con2) - - # Set metarig target to rig target - if "target" in dir(con2): - if con2.target == metarig: - con2.target = obj - - # Copy drivers - if metarig.animation_data: - for d1 in metarig.animation_data.drivers: - d2 = obj.driver_add(d1.data_path) - copy_attributes(d1, d2) - copy_attributes(d1.driver, d2.driver) - - # Remove default modifiers, variables, etc. - for m in d2.modifiers: - d2.modifiers.remove(m) - for v in d2.driver.variables: - d2.driver.variables.remove(v) - - # Copy modifiers - for m1 in d1.modifiers: - m2 = d2.modifiers.new(type=m1.type) - copy_attributes(m1, m2) - - # Copy variables - for v1 in d1.driver.variables: - v2 = d2.driver.variables.new() - copy_attributes(v1, v2) - for i in range(len(v1.targets)): - copy_attributes(v1.targets[i], v2.targets[i]) - # Switch metarig targets to rig targets - if v2.targets[i].id == metarig: - v2.targets[i].id = obj - - # Mark targets that may need to be altered after rig generation - tar = v2.targets[i] - # If a custom property - if v2.type == 'SINGLE_PROP' \ - and re.match('^pose.bones\["[^"\]]*"\]\["[^"\]]*"\]$', tar.data_path): - tar.data_path = "RIGIFY-" + tar.data_path - - # Copy key frames - for i in range(len(d1.keyframe_points)): - d2.keyframe_points.add() - k1 = d1.keyframe_points[i] - k2 = d2.keyframe_points[i] - copy_attributes(k1, k2) - - t.tick("Duplicate rig: ") - #---------------------------------- - # Make a list of the original bones so we can keep track of them. - original_bones = [bone.name for bone in obj.data.bones] - - # Add the ORG_PREFIX to the original bones. - bpy.ops.object.mode_set(mode='OBJECT') - for i in range(0, len(original_bones)): - obj.data.bones[original_bones[i]].name = make_original_name(original_bones[i]) - original_bones[i] = make_original_name(original_bones[i]) - - # Create a sorted list of the original bones, sorted in the order we're - # going to traverse them for rigging. - # (root-most -> leaf-most, alphabetical) - bones_sorted = [] - for name in original_bones: - bones_sorted += [name] - bones_sorted.sort() # first sort by names - bones_sorted.sort(key=lambda bone: len(obj.pose.bones[bone].parent_recursive)) # then parents before children - - t.tick("Make list of org bones: ") - #---------------------------------- - # Create the root bone. - bpy.ops.object.mode_set(mode='EDIT') - root_bone = new_bone(obj, ROOT_NAME) - spread = get_xy_spread(metarig.data.bones) or metarig.data.bones[0].length - spread = float('%.3g' % spread) - scale = spread/0.589 - obj.data.edit_bones[root_bone].head = (0, 0, 0) - obj.data.edit_bones[root_bone].tail = (0, scale, 0) - obj.data.edit_bones[root_bone].roll = 0 - bpy.ops.object.mode_set(mode='OBJECT') - obj.data.bones[root_bone].layers = ROOT_LAYER - - # Put the rig_name in the armature custom properties - rna_idprop_ui_prop_get(obj.data, "rig_id", create=True) - obj.data["rig_id"] = rig_id - - t.tick("Create root bone: ") - - # Create Group widget - # wgts_group_name = "WGTS" - if wgts_group_name not in scene.objects: - if wgts_group_name in bpy.data.objects: - bpy.data.objects[wgts_group_name].user_clear() - bpy.data.objects.remove(bpy.data.objects[wgts_group_name]) - mesh = bpy.data.meshes.new(wgts_group_name) - wgts_obj = bpy.data.objects.new(wgts_group_name, mesh) - scene.objects.link(wgts_obj) - wgts_obj.layers = WGT_LAYERS t.tick("Create main WGTS: ") - # - # if id_store.rigify_generate_mode == 'new': - # bpy.ops.object.select_all(action='DESELECT') - # for wgt in bpy.data.objects[wgts_group_name].children: - # wgt.select = True - # for i, lyr in enumerate(WGT_LAYERS): - # if lyr: - # context.scene.layers[i] = True - # bpy.ops.object.make_single_user(obdata=True) - # for i, lyr in enumerate(WGT_LAYERS): - # if lyr: - # context.scene.layers[i] = False - - #---------------------------------- - try: - # Collect/initialize all the rigs. - rigs = [] - for bone in bones_sorted: - bpy.ops.object.mode_set(mode='EDIT') - rigs += get_bone_rigs(obj, bone) + + #------------------------------------------ + # Get parented objects to restore later + childs = {} # {object: bone} + for child in obj.children: + childs[child] = child.parent_bone + + #------------------------------------------ + # Copy bones from metarig to obj + self.__duplicate_rig() + + t.tick("Duplicate rig: ") + + #------------------------------------------ + # Add the ORG_PREFIX to the original bones. + bpy.ops.object.mode_set(mode='OBJECT') + + self.__rename_org_bones() + + t.tick("Make list of org bones: ") + + #------------------------------------------ + # Put the rig_name in the armature custom properties + rna_idprop_ui_prop_get(obj.data, "rig_id", create=True) + obj.data["rig_id"] = self.rig_id + + self.script = rig_ui_template.ScriptGenerator(self) + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.instantiate_rig_tree() + + t.tick("Instantiate rigs: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.invoke_initialize() + t.tick("Initialize rigs: ") - # Generate all the rigs. - armature_store = context.armature + #------------------------------------------ + bpy.ops.object.mode_set(mode='EDIT') - ui_scripts = [] - ui_imports = rig_ui_template.UI_IMPORTS.copy() - ui_utilities = rig_ui_template.UI_UTILITIES.copy() - ui_register = rig_ui_template.UI_REGISTER.copy() - noparent_bones = [] - for rig in rigs: - # Go into editmode in the rig armature - bpy.ops.object.mode_set(mode='OBJECT') - context.scene.objects.active = obj - obj.select = True - bpy.ops.object.mode_set(mode='EDIT') - scripts = rig.generate() - if isinstance(scripts, dict): - if 'script' in scripts: - ui_scripts += scripts['script'] - if 'imports' in scripts: - ui_imports += scripts['imports'] - if 'utilities' in scripts: - ui_utilities += scripts['utilities'] - if 'register' in scripts: - ui_register += scripts['register'] - if 'noparent_bones' in scripts: - noparent_bones += scripts['noparent_bones'] - elif scripts is not None: - ui_scripts += [scripts[0]] - t.tick("Generate rigs: ") + self.invoke_prepare_bones() - except Exception as e: - # Cleanup if something goes wrong - print("Rigify: failed to generate rig.") - metarig.data.pose_position = rest_backup - obj.data.pose_position = 'POSE' + t.tick("Prepare bones: ") + + #------------------------------------------ bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') - # Continue the exception - raise e + self.__create_root_bone() - #---------------------------------- - bpy.ops.object.mode_set(mode='OBJECT') + self.invoke_generate_bones() - # Get a list of all the bones in the armature - bones = [bone.name for bone in obj.data.bones] + t.tick("Generate bones: ") - # Parent any free-floating bones to the root excluding noparent_bones - noparent_bones = dict.fromkeys(noparent_bones) + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.mode_set(mode='EDIT') - for bone in bones: - if bone in noparent_bones: - continue - elif obj.data.edit_bones[bone].parent is None: - obj.data.edit_bones[bone].use_connect = False - obj.data.edit_bones[bone].parent = obj.data.edit_bones[root_bone] + self.invoke_parent_bones() - bpy.ops.object.mode_set(mode='OBJECT') + self.__parent_bones_to_root() - # Lock transforms on all non-control bones - r = re.compile("[A-Z][A-Z][A-Z]-") - for bone in bones: - if r.match(bone): - pb = obj.pose.bones[bone] - pb.lock_location = (True, True, True) - pb.lock_rotation = (True, True, True) - pb.lock_rotation_w = True - pb.lock_scale = (True, True, True) - - # Every bone that has a name starting with "DEF-" make deforming. All the - # others make non-deforming. - for bone in bones: - if obj.data.bones[bone].name.startswith(DEF_PREFIX): - obj.data.bones[bone].use_deform = True - else: - obj.data.bones[bone].use_deform = False - - # Alter marked driver targets - if obj.animation_data: - for d in obj.animation_data.drivers: - for v in d.driver.variables: - for tar in v.targets: - if tar.data_path.startswith("RIGIFY-"): - temp, bone, prop = tuple([x.strip('"]') for x in tar.data_path.split('["')]) - if bone in obj.data.bones \ - and prop in obj.pose.bones[bone].keys(): - tar.data_path = tar.data_path[7:] - else: - tar.data_path = 'pose.bones["%s"]["%s"]' % (make_original_name(bone), prop) - - # Move all the original bones to their layer. - for bone in original_bones: - obj.data.bones[bone].layers = ORG_LAYER - - # Move all the bones with names starting with "MCH-" to their layer. - for bone in bones: - if obj.data.bones[bone].name.startswith(MCH_PREFIX): - obj.data.bones[bone].layers = MCH_LAYER - - # Move all the bones with names starting with "DEF-" to their layer. - for bone in bones: - if obj.data.bones[bone].name.startswith(DEF_PREFIX): - obj.data.bones[bone].layers = DEF_LAYER - - # Create root bone widget - create_root_widget(obj, "root") - - # Assign shapes to bones - # Object's with name WGT- get used as that bone's shape. - for bone in bones: - wgt_name = (WGT_PREFIX + obj.name + '_' + obj.data.bones[bone].name)[:63] # Object names are limited to 63 characters... arg - if wgt_name in context.scene.objects: - # Weird temp thing because it won't let me index by object name - for ob in context.scene.objects: - if ob.name == wgt_name: - obj.pose.bones[bone].custom_shape = ob - break - # This is what it should do: - # obj.pose.bones[bone].custom_shape = context.scene.objects[wgt_name] - # Reveal all the layers with control bones on them - vis_layers = [False for n in range(0, 32)] - for bone in bones: - for i in range(0, 32): - vis_layers[i] = vis_layers[i] or obj.data.bones[bone].layers[i] - for i in range(0, 32): - vis_layers[i] = vis_layers[i] and not (ORG_LAYER[i] or MCH_LAYER[i] or DEF_LAYER[i]) - obj.data.layers = vis_layers - - # Ensure the collection of layer names exists - for i in range(1 + len(metarig.data.rigify_layers), 29): - metarig.data.rigify_layers.add() - - # Create list of layer name/row pairs - layer_layout = [] - for l in metarig.data.rigify_layers: - print(l.name) - layer_layout += [(l.name, l.row)] - - # Generate the UI script - if id_store.rigify_generate_mode == 'overwrite': - rig_ui_name = id_store.rigify_rig_ui or 'rig_ui.py' - else: - rig_ui_name = 'rig_ui.py' + t.tick("Parent bones: ") - if id_store.rigify_generate_mode == 'overwrite' and rig_ui_name in bpy.data.texts.keys(): - script = bpy.data.texts[rig_ui_name] - script.clear() - else: - script = bpy.data.texts.new("rig_ui.py") - - rig_ui_old_name = "" - if id_store.rigify_rig_basename: - rig_ui_old_name = script.name - script.name = id_store.rigify_rig_basename + "_rig_ui.py" - - id_store.rigify_rig_ui = script.name - - for s in OrderedDict.fromkeys(ui_imports): - script.write(s + "\n") - script.write(rig_ui_template.UI_BASE_UTILITIES % rig_id) - for s in OrderedDict.fromkeys(ui_utilities): - script.write(s + "\n") - script.write(rig_ui_template.UI_SLIDERS) - for s in ui_scripts: - script.write("\n " + s.replace("\n", "\n ") + "\n") - script.write(rig_ui_template.layers_ui(vis_layers, layer_layout)) - script.write("\ndef register():\n") - ui_register = OrderedDict.fromkeys(ui_register) - for s in ui_register: - script.write(" bpy.utils.register_class("+s+");\n") - script.write("\ndef unregister():\n") - for s in ui_register: - script.write(" bpy.utils.unregister_class("+s+");\n") - script.write("\nregister()\n") - script.use_module = True - - # Run UI script - exec(script.as_string(), {}) - - # Create Selection Sets - create_selection_sets(obj, metarig) - - # Create Bone Groups - create_bone_groups(obj, metarig) - - # Add rig_ui to logic - skip = False - ctrls = obj.game.controllers - - for c in ctrls: - if 'Python' in c.name and c.text.name == script.name: - skip = True - break - if not skip: - bpy.ops.logic.controller_add(type='PYTHON', object=obj.name) - ctrl = obj.game.controllers[-1] - ctrl.text = bpy.data.texts[script.name] - - # Do final gluing - for rig in rigs: - if hasattr(rig, "glue"): - # update glue_bone rigs - bpy.ops.object.mode_set(mode='EDIT') - rig = rig.__class__(rig.obj, rig.base_bone, rig.params) - - rig.glue() - t.tick("Glue pass") - - t.tick("The rest: ") - #---------------------------------- - # Deconfigure - bpy.ops.object.mode_set(mode='OBJECT') - metarig.data.pose_position = rest_backup - obj.data.pose_position = 'POSE' + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.invoke_configure_bones() + + t.tick("Configure bones: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.invoke_rig_bones() + + t.tick("Rig bones: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + create_root_widget(obj, "root") + + self.invoke_generate_widgets() + + t.tick("Generate widgets: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.__lock_transforms() + self.__assign_layers() + self.__compute_visible_layers() + self.__restore_driver_vars() + + t.tick("Assign layers: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.invoke_finalize() + + t.tick("Finalize: ") + + #------------------------------------------ + bpy.ops.object.mode_set(mode='OBJECT') + + self.__assign_widgets() + + # Create Selection Sets + create_selection_sets(obj, metarig) + + # Create Bone Groups + create_bone_groups(obj, metarig) + + t.tick("The rest: ") + + #---------------------------------- + # Deconfigure + bpy.ops.object.mode_set(mode='OBJECT') + obj.data.pose_position = 'POSE' + + # Restore parent to bones + for child, sub_parent in childs.items(): + if sub_parent in obj.pose.bones: + mat = child.matrix_world.copy() + child.parent_bone = sub_parent + child.matrix_world = mat + + +def generate_rig(context, metarig): + """ Generates a rig from a metarig. + + """ + # Initial configuration + rest_backup = metarig.data.pose_position + metarig.data.pose_position = 'REST' + + try: + Generator(context, metarig).generate() + + metarig.data.pose_position = rest_backup + + except Exception as e: + # Cleanup if something goes wrong + print("Rigify: failed to generate rig.") + + bpy.ops.object.mode_set(mode='OBJECT') + metarig.data.pose_position = rest_backup + + # Continue the exception + raise e - # Restore parent to bones - for child, sub_parent in childs.items(): - if sub_parent in obj.pose.bones: - mat = child.matrix_world.copy() - child.parent_bone = sub_parent - child.matrix_world = mat def create_selection_sets(obj, metarig): @@ -630,36 +555,6 @@ def create_bone_groups(obj, metarig): b.bone_group = obj.pose.bone_groups[name] -def get_bone_rigs(obj, bone_name, halt_on_missing=False): - """ Fetch all the rigs specified on a bone. - """ - rigs = [] - rig_type = obj.pose.bones[bone_name].rigify_type - rig_type = rig_type.replace(" ", "") - - if rig_type == "": - pass - else: - # Gather parameters - params = obj.pose.bones[bone_name].rigify_parameters - - # Get the rig - try: - rig = rig_lists.rigs[rig_type]["module"] - rig = rig.Rig(obj, bone_name, params) - except (KeyError, ImportError): - message = "Rig Type Missing: python module for type '%s' not found (bone: %s)" % (rig_type, bone_name) - if halt_on_missing: - raise MetarigError(message) - else: - print(message) - print('print_exc():') - traceback.print_exc(file=sys.stdout) - else: - rigs += [rig] - return rigs - - def get_xy_spread(bones): x_max = 0 y_max = 0 diff --git a/rig_ui_template.py b/rig_ui_template.py index 5346804..a661819 100644 --- a/rig_ui_template.py +++ b/rig_ui_template.py @@ -18,6 +18,16 @@ # +import bpy + +from collections import OrderedDict + +from .utils.layers import get_layers +from .utils.rig import attach_persistent_script + +from . import base_generate + + UI_IMPORTS = [ 'import bpy', 'import math', @@ -842,17 +852,15 @@ def draw(self, context): layout = self.layout pose_bones = context.active_object.pose.bones try: - selected_bones = [bone.name for bone in context.selected_pose_bones] - selected_bones += [context.active_pose_bone.name] + selected_bones = set(bone.name for bone in context.selected_pose_bones) + selected_bones.add(context.active_pose_bone.name) except (AttributeError, TypeError): return def is_selected(names): # Returns whether any of the named bones are selected. - if type(names) == list: - for name in names: - if name in selected_bones: - return True + if isinstance(names, list) or isinstance(names, set): + return not selected_bones.isdisjoint(names) elif names in selected_bones: return True return False @@ -912,3 +920,159 @@ def draw(self, context): code += " row.prop(context.active_object.data, 'layers', index=28, toggle=True, text='Root')\n" return code + + +class ScriptGenerator(base_generate.GeneratorPlugin): + """Generator plugin that builds the python script attached to the rig.""" + + priority = -100 + + def __init__(self, generator): + super(ScriptGenerator, self).__init__(generator) + + self.ui_scripts = [] + self.ui_imports = UI_IMPORTS.copy() + self.ui_utilities = UI_UTILITIES.copy() + self.ui_register = UI_REGISTER.copy() + self.ui_register_drivers = [] + self.ui_register_props = [] + + self.cur_selected_set = None + + # Structured panel code generation + def add_panel_selected_check(self, controls): + """Add a check that one of the listed bones are selected.""" + selected_set = set(controls) + if selected_set != self.cur_selected_set: + self.cur_selected_set = selected_set + self.ui_scripts.append( + "if is_selected(%r):" % (selected_set) + ) + + def add_panel_custom_prop(self, bone_name, prop_name, **params): + """Add a custom property input field to the panel.""" + assert(self.cur_selected_set is not None) + param_list = ["%s=%r" % (k, v) for k, v in params.items()] + param_str = ', '.join(["""'["%s"]'""" % prop_name, *param_list]) + self.ui_scripts.append( + " layout.prop(pose_bones[%r], %s)" % (bone_name, param_str) + ) + + def add_panel_operator(self, operator_name, properties=None, **params): + """Add an operator call button to the panel.""" + assert(self.cur_selected_set is not None) + param_list = ["%s=%r" % (k, v) for k, v in params.items()] + param_str = ', '.join([repr(operator_name), *param_list]) + call_str = "layout.operator(%s)" % (param_str) + if properties: + assign_lines = [" props.%s = %r" % (k,v) for k,v in properties.items()] + self.ui_scripts.append( + '\n'.join([" props = " + call_str] + assign_lines) + ) + else: + self.ui_scripts.append(" " + call_str) + + # Raw output + def add_panel_code(self, str_list): + """Add raw code to the panel.""" + self.cur_selected_set = None + self.ui_scripts += str_list + + def add_imports(self, str_list): + self.ui_imports += str_list + + def add_utilities(self, str_list): + self.ui_utilities += str_list + + def register_classes(self, str_list): + self.ui_register += str_list + + def register_driver_functions(self, str_list): + self.ui_register_drivers += str_list + + def register_property(self, name, definition): + self.ui_register_props.append((name, definition)) + + def finalize(self): + metarig = self.generator.metarig + id_store = self.generator.id_store + rig_id = self.generator.rig_id + + vis_layers = self.obj.data.layers + + # Ensure the collection of layer names exists + for i in range(1 + len(metarig.data.rigify_layers), 29): + metarig.data.rigify_layers.add() + + # Create list of layer name/row pairs + layer_layout = [] + for l in metarig.data.rigify_layers: + layer_layout += [(l.name, l.row)] + + # Generate the UI script + if id_store.rigify_generate_mode == 'overwrite': + rig_ui_name = id_store.rigify_rig_ui or 'rig_ui.py' + else: + rig_ui_name = 'rig_ui.py' + + if id_store.rigify_generate_mode == 'overwrite' and rig_ui_name in bpy.data.texts.keys(): + script = bpy.data.texts[rig_ui_name] + script.clear() + else: + script = bpy.data.texts.new("rig_ui.py") + + rig_ui_old_name = "" + if id_store.rigify_rig_basename: + rig_ui_old_name = script.name + script.name = id_store.rigify_rig_basename + "_rig_ui.py" + + id_store.rigify_rig_ui = script.name + + for s in OrderedDict.fromkeys(self.ui_imports): + script.write(s + "\n") + + script.write(UI_BASE_UTILITIES % rig_id) + + for s in OrderedDict.fromkeys(self.ui_utilities): + script.write(s + "\n") + + script.write(UI_SLIDERS) + + for s in self.ui_scripts: + script.write("\n " + s.replace("\n", "\n ") + "\n") + + script.write(layers_ui(vis_layers, layer_layout)) + + script.write("\ndef register():\n") + + ui_register = OrderedDict.fromkeys(self.ui_register) + for s in ui_register: + script.write(" bpy.utils.register_class("+s+");\n") + + ui_register_drivers = OrderedDict.fromkeys(self.ui_register_drivers) + for s in ui_register_drivers: + script.write(" bpy.app.driver_namespace['"+s+"'] = "+s+"\n") + + ui_register_props = OrderedDict.fromkeys(self.ui_register_props) + for s in ui_register_props: + script.write(" bpy.types.%s = %s\n " % (*s,)) + + script.write("\ndef unregister():\n") + + for s in ui_register_props: + script.write(" del bpy.types.%s\n" % s[0]) + + for s in ui_register: + script.write(" bpy.utils.unregister_class("+s+");\n") + + for s in ui_register_drivers: + script.write(" del bpy.app.driver_namespace['"+s+"']\n") + + script.write("\nregister()\n") + script.use_module = True + + # Run UI script + exec(script.as_string(), {}) + + # Attach the script to the rig + attach_persistent_script(self.obj, script) diff --git a/rigs/experimental/glue_bone.py b/rigs/experimental/glue_bone.py index 0696919..c096f15 100644 --- a/rigs/experimental/glue_bone.py +++ b/rigs/experimental/glue_bone.py @@ -154,7 +154,7 @@ def make_def_mediation(self): make_constraints_from_string(owner_pb, target=self.obj, subtarget=subtarget, fstring="CT1.0WW") - def glue(self): + def do_glue(self): """ Glue pass :return: @@ -167,6 +167,11 @@ def glue(self): self.create_mch() self.make_def_mediation() + def glue(self): + bpy.ops.object.mode_set(mode='EDIT') + rig_new = self.__class__(self.obj, self.base_bone, self.params) + rig_new.do_glue() + def generate(self): """ Glue bones generate must do nothing. Glue bones pass is meant to happen after all other rigs are generated diff --git a/rigs/faces/super_face.py b/rigs/faces/super_face.py index 2c819bc..21231e5 100644 --- a/rigs/faces/super_face.py +++ b/rigs/faces/super_face.py @@ -44,7 +44,7 @@ def __init__(self, obj, bone_name, params): grand_children += connected_children_names( self.obj, child ) self.org_bones = [bone_name] + children + grand_children - self.face_length = obj.data.edit_bones[ self.org_bones[0] ].length + self.face_length = obj.data.bones[ self.org_bones[0] ].length self.params = params if params.primary_layers_extra: diff --git a/ui.py b/ui.py index c05eaab..f52c19d 100644 --- a/ui.py +++ b/ui.py @@ -29,6 +29,8 @@ from .utils import get_keyed_frames, bones_in_frame from .utils import overwrite_prop_animation from .rigs.utils import get_limb_generated_names + +from . import base_rig from . import rig_lists from . import generate from . import rot_mode @@ -603,6 +605,8 @@ def draw(self, context): box = row.box() box.label(text="ALERT: type \"%s\" does not exist!" % rig_name) else: + if issubclass(rig.Rig, base_rig.BaseRig): + rig = rig.Rig try: rig.parameters_ui except AttributeError: @@ -790,7 +794,7 @@ class Sample(bpy.types.Operator): bl_idname = "armature.metarig_sample_add" bl_label = "Add a sample metarig for a rig type" - bl_options = {'UNDO'} + bl_options = {'UNDO', 'REGISTER'} metarig_type = StringProperty( name="Type", diff --git a/utils/__init__.py b/utils/__init__.py index 211aa16..8339c24 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -11,7 +11,7 @@ from .naming import org, make_original_name, mch, make_mechanism_name, deformer, make_deformer_name from .naming import insert_before_lr, random_id -from .bones import new_bone, copy_bone_simple, copy_bone, flip_bone, put_bone, make_nonscaling_child +from .bones import new_bone, copy_bone_simple, _legacy_copy_bone, flip_bone, put_bone, _legacy_make_nonscaling_child from .bones import align_bone_roll, align_bone_x_axis, align_bone_z_axis, align_bone_y_axis from .widgets import WGT_PREFIX, WGT_LAYERS, obj_to_bone, create_widget, write_widget, create_circle_polygon @@ -31,3 +31,7 @@ from .rig import connected_children_names, has_connected_children from .layers import get_layers, ControlLayersOption + +# Definitions so bad as to make them strictly compatibility only +copy_bone = _legacy_copy_bone +make_nonscaling_child = _legacy_make_nonscaling_child diff --git a/utils/bones.py b/utils/bones.py index cad696f..47dbc01 100644 --- a/utils/bones.py +++ b/utils/bones.py @@ -24,7 +24,8 @@ from rna_prop_ui import rna_idprop_ui_prop_get from .errors import MetarigError -from .naming import strip_org, make_mechanism_name, insert_before_lr +from .naming import get_name, strip_org, make_mechanism_name, insert_before_lr +from .misc import pairwise #======================= # Bone collection @@ -72,12 +73,14 @@ def update(self, *args, **kwargs): for key, value in dict(*args, **kwargs).items(): dict.__setitem__(self, key, BoneDict.__sanitize_attr(key, value)) - def flatten(self): - """Return all contained bones as a list.""" + def flatten(self, key=None): + """Return all contained bones or a single key as a list.""" + + items = [self[key]] if key is not None else self.values() all_bones = [] - for item in self.values(): + for item in items: if isinstance(item, BoneDict): all_bones.extend(item.flatten()) elif isinstance(item, list): @@ -90,6 +93,18 @@ def flatten(self): #======================= # Bone manipulation #======================= +# +# NOTE: PREFER USING BoneUtilityMixin IN NEW STYLE RIGS! + +def get_bone(obj, bone_name): + """Get EditBone or PoseBone by name, depending on the current mode.""" + if not bone_name: + return None + bones = obj.data.edit_bones if obj.mode == 'EDIT' else obj.pose.bones + if bone_name not in bones: + raise MetarigError("bone '%s' not found" % bone_name) + return bones[bone_name] + def new_bone(obj, bone_name): """ Adds a new bone to the given armature object. @@ -101,8 +116,6 @@ def new_bone(obj, bone_name): edit_bone.head = (0, 0, 0) edit_bone.tail = (0, 1, 0) edit_bone.roll = 0 - bpy.ops.object.mode_set(mode='OBJECT') - bpy.ops.object.mode_set(mode='EDIT') return name else: raise MetarigError("Can't add new bone '%s' outside of edit mode" % bone_name) @@ -113,32 +126,10 @@ def copy_bone_simple(obj, bone_name, assign_name=''): but only copies head, tail positions and roll. Does not address parenting either. """ - #if bone_name not in obj.data.bones: - if bone_name not in obj.data.edit_bones: - raise MetarigError("copy_bone(): bone '%s' not found, cannot copy it" % bone_name) - - if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE': - if assign_name == '': - assign_name = bone_name - # Copy the edit bone - edit_bone_1 = obj.data.edit_bones[bone_name] - edit_bone_2 = obj.data.edit_bones.new(assign_name) - bone_name_1 = bone_name - bone_name_2 = edit_bone_2.name + return copy_bone(obj, bone_name, assign_name, parent=False, bbone=False) - # Copy edit bone attributes - edit_bone_2.layers = list(edit_bone_1.layers) - edit_bone_2.head = Vector(edit_bone_1.head) - edit_bone_2.tail = Vector(edit_bone_1.tail) - edit_bone_2.roll = edit_bone_1.roll - - return bone_name_2 - else: - raise MetarigError("Cannot copy bones outside of edit mode") - - -def copy_bone(obj, bone_name, assign_name=''): +def copy_bone(obj, bone_name, assign_name='', parent=False, bbone=False): """ Makes a copy of the given bone in the given armature object. Returns the resulting bone's name. """ @@ -155,9 +146,6 @@ def copy_bone(obj, bone_name, assign_name=''): bone_name_1 = bone_name bone_name_2 = edit_bone_2.name - edit_bone_2.parent = edit_bone_1.parent - edit_bone_2.use_connect = edit_bone_1.use_connect - # Copy edit bone attributes edit_bone_2.layers = list(edit_bone_1.layers) @@ -165,17 +153,27 @@ def copy_bone(obj, bone_name, assign_name=''): edit_bone_2.tail = Vector(edit_bone_1.tail) edit_bone_2.roll = edit_bone_1.roll - edit_bone_2.use_inherit_rotation = edit_bone_1.use_inherit_rotation - edit_bone_2.use_inherit_scale = edit_bone_1.use_inherit_scale - edit_bone_2.use_local_location = edit_bone_1.use_local_location + if parent: + edit_bone_2.parent = edit_bone_1.parent + edit_bone_2.use_connect = edit_bone_1.use_connect - edit_bone_2.use_deform = edit_bone_1.use_deform - edit_bone_2.bbone_segments = edit_bone_1.bbone_segments - edit_bone_2.bbone_in = edit_bone_1.bbone_in - edit_bone_2.bbone_out = edit_bone_1.bbone_out + edit_bone_2.use_inherit_rotation = edit_bone_1.use_inherit_rotation + edit_bone_2.use_inherit_scale = edit_bone_1.use_inherit_scale + edit_bone_2.use_local_location = edit_bone_1.use_local_location - bpy.ops.object.mode_set(mode='OBJECT') + if bbone: + edit_bone_2.bbone_segments = edit_bone_1.bbone_segments + edit_bone_2.bbone_in = edit_bone_1.bbone_in + edit_bone_2.bbone_out = edit_bone_1.bbone_out + return bone_name_2 + else: + raise MetarigError("Cannot copy bones outside of edit mode") + + +def copy_bone_properties(obj, bone_name_1, bone_name_2): + """ Copy transform and custom properties from bone 1 to bone 2. """ + if obj.mode in {'OBJECT','POSE'}: # Get the pose bones pose_bone_1 = obj.pose.bones[bone_name_1] pose_bone_2 = obj.pose.bones[bone_name_2] @@ -202,18 +200,24 @@ def copy_bone(obj, bone_name, assign_name=''): pose_bone_2[key] = pose_bone_1[key] for key in prop1.keys(): prop2[key] = prop1[key] + else: + raise MetarigError("Cannot copy bone properties in edit mode") - bpy.ops.object.mode_set(mode='EDIT') - return bone_name_2 - else: - raise MetarigError("Cannot copy bones outside of edit mode") +def _legacy_copy_bone(obj, bone_name, assign_name=''): + """LEGACY ONLY, DON'T USE""" + new_name = copy_bone(obj, bone_name, assign_name, parent=True, bbone=True) + # Mode switch PER BONE CREATION?! + bpy.ops.object.mode_set(mode='OBJECT') + copy_bone_properties(obj, bone_name, new_name) + bpy.ops.object.mode_set(mode='EDIT') + return new_name def flip_bone(obj, bone_name): """ Flips an edit bone. """ - if bone_name not in obj.data.bones: + if bone_name not in obj.data.edit_bones: raise MetarigError("flip_bone(): bone '%s' not found, cannot copy it" % bone_name) if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE': @@ -230,7 +234,7 @@ def flip_bone(obj, bone_name): def put_bone(obj, bone_name, pos): """ Places a bone at the given position. """ - if bone_name not in obj.data.bones: + if bone_name not in obj.data.edit_bones: raise MetarigError("put_bone(): bone '%s' not found, cannot move it" % bone_name) if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE': @@ -242,7 +246,14 @@ def put_bone(obj, bone_name, pos): raise MetarigError("Cannot 'put' bones outside of edit mode") -def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""): +def disable_bbones(obj, bone_names): + """Disables B-Bone segments on the specified bones.""" + assert(obj.mode != 'EDIT') + for bone in bone_names: + obj.data.bones[bone].bbone_segments = 1 + + +def _legacy_make_nonscaling_child(obj, bone_name, location, child_name_postfix=""): """ Takes the named bone and creates a non-scaling child of it at the given location. The returned bone (returned by name) is not a true child, but behaves like one sans inheriting scaling. @@ -250,8 +261,10 @@ def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""): It is intended as an intermediate construction to prevent rig types from scaling with their parents. The named bone is assumed to be an ORG bone. + + LEGACY ONLY, DON'T USE """ - if bone_name not in obj.data.bones: + if bone_name not in obj.data.edit_bones: raise MetarigError("make_nonscaling_child(): bone '%s' not found, cannot copy it" % bone_name) if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE': @@ -304,11 +317,96 @@ def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""): raise MetarigError("Cannot make nonscaling child outside of edit mode") +#=================================== +# Bone manipulation as rig methods +#=================================== + + +class BoneUtilityMixin(object): + """ + Provides methods for more convenient creation of bones. + + Requires self.obj to be the armature object being worked on. + """ + def register_new_bone(self, new_name, old_name=None): + """Registers creation or renaming of a bone based on old_name""" + pass + + def new_bone(self, new_name): + """Create a new bone with the specified name.""" + name = new_bone(self.obj, bone_name) + self.register_new_bone(self, name) + return name + + def copy_bone(self, bone_name, new_name='', parent=False, bbone=False): + """Copy the bone with the given name, returning the new name.""" + name = copy_bone(self.obj, bone_name, new_name, parent=parent, bbone=bbone) + self.register_new_bone(name, bone_name) + return name + + def copy_bone_properties(self, src_name, tgt_name): + """Copy pose-mode properties of the bone.""" + copy_bone_properties(self.obj, src_name, tgt_name) + + def rename_bone(self, old_name, new_name): + """Rename the bone, returning the actual new name.""" + bone = self.get_bone(old_name) + bone.name = new_name + if bone.name != old_name: + self.register_new_bone(bone.name, old_name) + return bone.name + + def get_bone(self, bone_name): + """Get EditBone or PoseBone by name, depending on the current mode.""" + return get_bone(self.obj, bone_name) + + def get_bone_parent(self, bone_name): + """Get the name of the parent bone, or None.""" + return get_name(self.get_bone(bone_name).parent) + + def set_bone_parent(self, bone_name, parent_name, use_connect=False): + """Set the parent of the bone.""" + eb = self.obj.data.edit_bones + bone = eb[bone_name] + if use_connect is not None: + bone.use_connect = use_connect + bone.parent = (eb[parent_name] if parent_name else None) + + def parent_bone_chain(self, bones, use_connect=None): + """Link bones into a chain with parenting. First bone may be None.""" + for parent, child in pairwise(bones): + self.set_bone_parent(child, parent, use_connect=use_connect) + #============================================= # Math #============================================= +def is_same_position(obj, bone1, bone2): + head1 = get_bone(obj, bone1).head + head2 = get_bone(obj, bone2).head + + return (head1 - head2).length < 1e-5 + + +def is_connected_position(obj, bone1, bone2): + tail1 = get_bone(obj, bone1).tail + head2 = get_bone(obj, bone2).head + + return (tail1 - head2).length < 1e-5 + + +def align_bone_orientation(obj, bone, target_bone): + """ Aligns the orientation of bone to target bone. """ + bone1_e = obj.data.edit_bones[bone] + bone2_e = obj.data.edit_bones[target_bone] + + axis = bone2_e.y_axis.normalized() * bone1_e.length + + bone1_e.tail = bone1_e.head + axis + bone1_e.roll = bone2_e.roll + + def align_bone_roll(obj, bone1, bone2): """ Aligns the roll of two bones. """ @@ -415,3 +513,52 @@ def align_bone_y_axis(obj, bone, vec): vec = vec * bone_e.length bone_e.tail = bone_e.head + vec + + +def align_chain_x_axis(obj, bones): + """ + Aligns the x axis of all bones to be perpendicular + to the primary plane in which the bones lie. + """ + eb = obj.data.edit_bones + + assert(len(bones) > 1) + first_bone = eb[bones[0]] + last_bone = eb[bones[-1]] + + # Orient uarm farm bones + chain_y_axis = last_bone.tail - first_bone.head + chain_rot_axis = first_bone.y_axis.cross(chain_y_axis) # ik-plane normal axis (rotation) + if chain_rot_axis.length < first_bone.length/100: + chain_rot_axis = first_bone.x_axis.normalized() + else: + chain_rot_axis = chain_rot_axis.normalized() + + for bone in bones: + align_bone_x_axis(obj, bone, chain_rot_axis) + + +def align_bone_to_axis(obj, bone, axis, length=None, roll=0, flip=False): + """ + Aligns the Y axis of the bone to the global axis (x,y,z,-x,-y,-z), + optionally adjusting length and initially flipping the bone. + """ + bone_e = obj.data.edit_bones[bone] + + if length is None: + length = bone_e.length + if roll is None: + roll = bone_e.roll + + if axis[0] == '-': + length = -length + axis = axis[1:] + + vec = Vector((0,0,0)) + setattr(vec, axis, length) + + if flip: + bone_e.head = bone_e.tail + + bone_e.tail = bone_e.head + vec + bone_e.roll = roll diff --git a/utils/layers.py b/utils/layers.py index d21d876..6e78aa0 100644 --- a/utils/layers.py +++ b/utils/layers.py @@ -21,6 +21,12 @@ import bpy +ORG_LAYER = [n == 31 for n in range(0, 32)] # Armature layer that original bones should be moved to. +MCH_LAYER = [n == 30 for n in range(0, 32)] # Armature layer that mechanism bones should be moved to. +DEF_LAYER = [n == 29 for n in range(0, 32)] # Armature layer that deformation bones should be moved to. +ROOT_LAYER = [n == 28 for n in range(0, 32)] # Armature layer that root bone should be moved to. + + def get_layers(layers): """ Does it's best to exctract a set of layers from any data thrown at it. """ @@ -69,6 +75,9 @@ def get(self, params): def assign(self, params, bone_set, bone_list): layers = self.get(params) + if isinstance(bone_set, bpy.types.Object): + bone_set = bone_set.data.bones + if layers: for name in bone_list: bone = bone_set[name] diff --git a/utils/mechanism.py b/utils/mechanism.py index 4fdaf13..ca0f975 100644 --- a/utils/mechanism.py +++ b/utils/mechanism.py @@ -78,7 +78,7 @@ def make_constraint( # Custom property creation utilities #============================================= -def make_property(owner, name, default=0.0, min=0.0, max=1.0, soft_min=None, soft_max=None): +def make_property(owner, name, default=0.0, min=0.0, max=1.0, soft_min=None, soft_max=None, description=None): """ Creates and initializes a custom property of owner. @@ -91,6 +91,8 @@ def make_property(owner, name, default=0.0, min=0.0, max=1.0, soft_min=None, sof prop["soft_min"] = soft_min if soft_min is not None else min prop["max"] = max prop["soft_max"] = soft_max if soft_max is not None else max + if description: + prop["description"] = description return prop @@ -102,7 +104,7 @@ def _init_driver_target(drv_target, var_info, target_id): """Initialize a driver variable target from a specification.""" # Parse the simple list format for the common case. - if isinstance(var_info, list): + if isinstance(var_info, tuple) or isinstance(var_info, list): # [ (target_id,) subtarget, ...path ] # If target_id is supplied as parameter, allow omitting it @@ -144,7 +146,7 @@ def _add_driver_variable(drv, var_name, var_info, target_id): var.name = var_name # Parse the simple list format for the common case. - if isinstance(var_info, list): + if isinstance(var_info, tuple) or isinstance(var_info, list): # [ (target_id,) subtarget, ...path ] var.type = "SINGLE_PROP" @@ -182,13 +184,13 @@ def make_driver(owner, prop, index=-1, type='SUM', expression=None, variables={} Variable specifications are constructed as nested dictionaries and lists that follow the property structure of the original Blender objects, but the most - common case can be abbreviated as a simple list. + common case can be abbreviated as a simple tuple. The following specifications are equivalent: - [ target, subtarget, '.foo', 'bar' ] + ( target, subtarget, '.foo', 'bar' ) - { 'type': 'SINGLE_PROP', 'targets':[[ target, subtarget, '.foo', 'bar' ]] } + { 'type': 'SINGLE_PROP', 'targets':[( target, subtarget, '.foo', 'bar' )] } { 'type': 'SINGLE_PROP', 'targets':[{ 'id': target, 'data_path': subtarget.path_from_id() + '.foo["bar"]' }] } @@ -197,13 +199,13 @@ def make_driver(owner, prop, index=-1, type='SUM', expression=None, variables={} It is possible to specify path directly as a simple string without following items: - [ target, 'path' ] + ( target, 'path' ) { 'type': 'SINGLE_PROP', 'targets':[{ 'id': target, 'data_path': 'path' }] } If the target_id parameter is not None, it is possible to omit target: - [ subtarget, '.foo', 'bar' ] + ( subtarget, '.foo', 'bar' ) { 'type': 'SINGLE_PROP', 'targets':[{ 'id': target_id, 'data_path': subtarget.path_from_id() + '.foo["bar"]' }] } @@ -241,7 +243,7 @@ def make_driver(owner, prop, index=-1, type='SUM', expression=None, variables={} # Utility mixin #============================================= -class MechanismUtilityMixin: +class MechanismUtilityMixin(object): """ Provides methods for more convenient creation of constraints, properties and drivers within an armature (by implicitly providing context). diff --git a/utils/metaclass.py b/utils/metaclass.py new file mode 100644 index 0000000..55ef303 --- /dev/null +++ b/utils/metaclass.py @@ -0,0 +1,41 @@ +#====================== 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 ======================== + +# + +import collections + + +#============================================= +# Per-owner singleton class +#============================================= + + +class SingletonPluginMetaclass(type): + """Metaclass for maintaining one instance per owner object per constructor arg set.""" + def __call__(cls, owner, *constructor_args): + key = (cls, *constructor_args) + try: + return owner.plugin_map[key] + except KeyError: + new_obj = super(SingletonPluginMetaclass, cls).__call__(owner, *constructor_args) + owner.plugin_map[key] = new_obj + owner.plugin_list.append(new_obj) + owner.plugin_list.sort(key=lambda obj: obj.priority, reverse=True) + return new_obj + diff --git a/utils/misc.py b/utils/misc.py index 2ca7b01..0e8e709 100644 --- a/utils/misc.py +++ b/utils/misc.py @@ -19,8 +19,12 @@ # import math +import collections + +from itertools import tee from mathutils import Vector, Matrix, Color + #============================================= # Math #============================================= @@ -81,6 +85,28 @@ def gamma_correct(color): return corrected_color +#============================================= +# Iterators +#============================================= + + +def pairwise(iterable): + "s -> (s0,s1), (s1,s2), (s2,s3), ..." + a, b = tee(iterable) + next(b, None) + return zip(a, b) + + +def map_list(func, *inputs): + "[func(a0,b0...), func(a1,b1...), ...]" + return list(map(func, *inputs)) + + +def map_apply(func, *inputs): + "Apply the function to inputs like map for side effects, discarding results." + collections.deque(map(func, *inputs), maxlen=0) + + #============================================= # Misc #============================================= @@ -98,3 +124,14 @@ def copy_attributes(a, b): setattr(b, key, getattr(a, key)) except AttributeError: pass + + +def select_object(context, object, deselect_all=False): + scene = context.scene + + if deselect_all: + for objt in scene.objects: + objt.select = False # deselect all objects + + object.select = True + scene.objects.active = object diff --git a/utils/naming.py b/utils/naming.py index 3af3d85..ed95170 100644 --- a/utils/naming.py +++ b/utils/naming.py @@ -31,6 +31,9 @@ # Name manipulation #======================================================================= +def get_name(bone): + return bone.name if bone else None + def strip_trailing_number(s): m = re.search(r'\.(\d{3})$', s) diff --git a/utils/rig.py b/utils/rig.py index 444c125..caacdb5 100644 --- a/utils/rig.py +++ b/utils/rig.py @@ -113,6 +113,19 @@ def get_metarig_module(metarig_name, path=METARIG_DIR): return submod +def attach_persistent_script(obj, script): + # Add rig_ui to logic + ctrls = obj.game.controllers + + for c in ctrls: + if 'Python' in c.name and c.text.name == script.name: + return + + bpy.ops.logic.controller_add(type='PYTHON', object=obj.name) + ctrl = obj.game.controllers[-1] + ctrl.text = bpy.data.texts[script.name] + + def connected_children_names(obj, bone_name): """ Returns a list of bone names (in order) of the bones that form a single connected chain starting with the given bone as a parent. From 3a29881f87bb027711a6d985d95e194ad748878d Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Thu, 6 Dec 2018 17:58:23 +0300 Subject: [PATCH 2/4] Support declaring new callback methods via stage decorators. 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. --- base_generate.py | 8 ++-- base_rig.py | 31 +++++++++++- utils/metaclass.py | 116 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 149 insertions(+), 6 deletions(-) diff --git a/base_generate.py b/base_generate.py index 4ca90b0..dd23c7a 100644 --- a/base_generate.py +++ b/base_generate.py @@ -171,7 +171,7 @@ def __run_object_stage(self, method_name): self.stage = method_name for rig in [*self.rig_list, *self.plugin_list]: - getattr(rig, method_name)() + rig.rigify_invoke_stage(method_name) assert(self.context.active_object == self.obj) assert(self.obj.mode == 'OBJECT') @@ -186,7 +186,7 @@ def __run_edit_stage(self, method_name): self.stage = method_name for rig in [*self.rig_list, *self.plugin_list]: - getattr(rig, method_name)() + rig.rigify_invoke_stage(method_name) assert(self.context.active_object == self.obj) assert(self.obj.mode == 'EDIT') @@ -221,7 +221,7 @@ def invoke_generate_bones(self): self.stage = 'generate_bones' for rig in self.rig_list: - rig.generate_bones() + rig.rigify_invoke_stage('generate_bones') assert(self.context.active_object == self.obj) assert(self.obj.mode == 'EDIT') @@ -229,7 +229,7 @@ def invoke_generate_bones(self): self.__auto_register_bones(self.obj.data.edit_bones, rig) for plugin in self.plugin_list: - plugin.generate_bones() + plugin.rigify_invoke_stage('generate_bones') assert(self.context.active_object == self.obj) assert(self.obj.mode == 'EDIT') diff --git a/base_rig.py b/base_rig.py index 1f664a1..e94371f 100644 --- a/base_rig.py +++ b/base_rig.py @@ -24,6 +24,7 @@ from .utils.bones import BoneDict, BoneUtilityMixin from .utils.mechanism import MechanismUtilityMixin +from .utils.metaclass import BaseStagedClass # Only export certain symbols via 'from base_rig import *' __all__ = ['BaseRig', 'RigUtility'] @@ -32,7 +33,7 @@ # Base Rig #============================================= -class GenerateCallbackHost(object): +class GenerateCallbackHost(BaseStagedClass): """ Standard set of callback methods to redefine. Shared between BaseRig and GeneratorPlugin. @@ -43,7 +44,27 @@ class GenerateCallbackHost(object): Switching modes is not allowed in rigs for performance reasons. Place code in the appropriate callbacks to use the mode set by the main engine. + + Before each callback, all other methods decorated with + @stage_ are called, for instance: + + @stage_generate_bones + def foo(self): + print('first') + + def generate_bones(self): + print('second') + + Will print 'first', then 'second'. However, the order + in which different @stage_generate_bones methods in the + same rig will be called is not specified. + + When overriding such methods in a subclass the appropriate + decorator should be repeated for code clarity reasons; + a warning is printed if this is not done. """ + DEFINE_STAGES = True + def initialize(self): """ Initialize processing after all rig classes are constructed. @@ -203,3 +224,11 @@ def register_new_bone(self, new_name, old_name=None): self.owner.register_new_bone(new_name, old_name) +#============================================= +# Rig Stage Decorators +#============================================= + +# Generate and export @stage_... decorators for all valid stages +for name, decorator in GenerateCallbackHost.make_stage_decorators(): + globals()[name] = decorator + __all__.append(name) diff --git a/utils/metaclass.py b/utils/metaclass.py index 55ef303..0059a27 100644 --- a/utils/metaclass.py +++ b/utils/metaclass.py @@ -20,13 +20,127 @@ import collections +from types import FunctionType + + +#============================================= +# Class With Stages +#============================================= + + +def rigify_stage(stage): + """Decorates the method with the specified stage.""" + def process(method): + if not isinstance(method, FunctionType): + raise ValueError("Stage decorator must be applied to a method definition") + method._rigify_stage = stage + return method + return process + + +class StagedMetaclass(type): + """ + Metaclass for rigs that manages assignment of methods to stages via @stage_* decorators. + + Using 'DEFINE_STAGES = True' inside the class definition will register all non-system + method names from that definition as valid stages. After that, subclasses can + register methods to those stages, to be called via rigify_invoke_stage. + """ + def __new__(metacls, class_name, bases, namespace, **kwds): + staged_bases = [base for base in bases if isinstance(base, StagedMetaclass)] + + # Compute the set of inherited stages + stages = set().union(*[base._rigify_stages for base in staged_bases]) + + # Add methods from current class if requested + if 'DEFINE_STAGES' in namespace: + del namespace['DEFINE_STAGES'] + + for name, item in namespace.items(): + if name[0] != '_' and isinstance(item, FunctionType): + stages.add(name) + + # Create the class + result = type.__new__(metacls, class_name, bases, dict(namespace)) + + # Compute the inherited stage to method mapping + stage_map = collections.defaultdict(collections.OrderedDict) + method_map = {} + + for base in staged_bases: + for stage_name, methods in base._rigify_stage_map.items(): + for method_name, method_class in methods.items(): + if method_name in stages: + raise ValueError("Stage method '%s' inherited @stage_%s in class %s (%s)" % + (method_name, stage_name, class_name, result.__module__)) + + # Check consistency of inherited stage assignment to methods + if method_name in method_map: + if method_map[method_name] != stage_name: + print("RIGIFY CLASS %s (%s): method '%s' has inherited both @stage_%s and @stage_%s\n" % + (class_name, result.__module__, method_name, method_map[method_name], stage_name)) + else: + method_map[method_name] = stage_name + + stage_map[stage_name][method_name] = method_class + + # Scan newly defined methods for stage decorations + for method_name, item in namespace.items(): + if isinstance(item, FunctionType): + stage = getattr(item, '_rigify_stage', None) + + if stage and method_name in stages: + print("RIGIFY CLASS %s (%s): cannot use stage decorator on the stage method '%s' (@stage_%s ignored)" % + (class_name, result.__module__, method_name, stage)) + continue + + # Ensure that decorators aren't lost when redefining methods + if method_name in method_map: + if not stage: + stage = method_map[method_name] + print("RIGIFY CLASS %s (%s): missing stage decorator on method '%s' (should be @stage_%s)" % + (class_name, result.__module__, method_name, stage)) + # Check that the method is assigned to only one stage + elif stage != method_map[method_name]: + print("RIGIFY CLASS %s (%s): method '%s' has decorator @stage_%s, but inherited base has @stage_%s" % + (class_name, result.__module__, method_name, stage, method_map[method_name])) + + # Assign the method to the stage, verifying that it's valid + if stage: + if stage not in stages: + raise ValueError("Invalid stage name '%s' for method '%s' in class %s (%s)" % + (stage, method_name, class_name, result.__module__)) + else: + stage_map[stage][method_name] = result + + result._rigify_stages = frozenset(stages) + result._rigify_stage_map = stage_map + + return result + + def make_stage_decorators(self): + return [('stage_'+name, rigify_stage(name)) for name in self._rigify_stages] + + +class BaseStagedClass(object, metaclass=StagedMetaclass): + def rigify_invoke_stage(self, stage): + """Call all methods decorated with the given stage, followed by the callback.""" + cls = self.__class__ + assert(isinstance(cls, StagedMetaclass)) + assert(stage in cls._rigify_stages) + + for method_name in cls._rigify_stage_map[stage]: + getattr(self, method_name)() + + getattr(self, stage)() + #============================================= # Per-owner singleton class #============================================= -class SingletonPluginMetaclass(type): +class SingletonPluginMetaclass(StagedMetaclass): """Metaclass for maintaining one instance per owner object per constructor arg set.""" def __call__(cls, owner, *constructor_args): key = (cls, *constructor_args) From 781bad922ef3e497c231fefb64fe4491ab42dc45 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Tue, 27 Nov 2018 14:31:57 +0300 Subject: [PATCH 3/4] Convert four simplest rig types to the new base class. These types demonstrate the use of all callbacks, bone creation and parenting utilities, constraint & driver creation tools, and python script generation. --- rigs/basic/copy_chain.py | 177 +++++------- rigs/basic/super_copy.py | 149 +++++----- rigs/chain_rigs.py | 169 +++++++++++ rigs/limbs/simple_tentacle.py | 309 +++++--------------- rigs/limbs/super_finger.py | 524 ++++++++++++++-------------------- 5 files changed, 603 insertions(+), 725 deletions(-) create mode 100644 rigs/chain_rigs.py diff --git a/rigs/basic/copy_chain.py b/rigs/basic/copy_chain.py index 4e42628..1851c24 100644 --- a/rigs/basic/copy_chain.py +++ b/rigs/basic/copy_chain.py @@ -20,124 +20,97 @@ import bpy -from ...utils import MetarigError -from ...utils import copy_bone -from ...utils import connected_children_names -from ...utils import strip_org, make_deformer_name -from ...utils import create_bone_widget +from ..chain_rigs import SimpleChainRig +from ...utils.errors import MetarigError +from ...utils.rig import connected_children_names +from ...utils.naming import strip_org, make_deformer_name +from ...utils.widgets_basic import create_bone_widget +from ...utils.misc import map_list, map_apply -class Rig: +from ...base_rig import * + + +class Rig(SimpleChainRig): """ A "copy_chain" rig. All it does is duplicate the original bone chain and constrain it. This is a control and deformation rig. - """ - def __init__(self, obj, bone_name, params): + def initialize(self): + super(Rig,self).initialize() + """ Gather and validate data about the rig. """ - self.obj = obj - self.org_bones = [bone_name] + connected_children_names(obj, bone_name) - self.params = params - self.make_controls = params.make_controls - self.make_deforms = params.make_deforms + self.make_controls = self.params.make_controls + self.make_deforms = self.params.make_deforms - if len(self.org_bones) <= 1: - raise MetarigError("RIGIFY ERROR: Bone '%s': input to rig type must be a chain of 2 or more bones" % (strip_org(bone_name))) + ############################## + # Control chain - def generate(self): - """ Generate the rig. - Do NOT modify any of the original bones, except for adding constraints. - The main armature should be selected and active before this is called. + @stage_generate_bones + def make_control_chain(self): + if self.make_controls: + super(Rig,self).make_control_chain() - """ - bpy.ops.object.mode_set(mode='EDIT') - - # Create the deformation and control bone chains. - # Just copies of the original chain. - def_chain = [] - ctrl_chain = [] - for i in range(len(self.org_bones)): - name = self.org_bones[i] - - # Control bone - if self.make_controls: - # Copy - ctrl_bone = copy_bone(self.obj, name) - eb = self.obj.data.edit_bones - ctrl_bone_e = eb[ctrl_bone] - # Name - ctrl_bone_e.name = strip_org(name) - # Parenting - if i == 0: - # First bone - ctrl_bone_e.parent = eb[self.org_bones[0]].parent - else: - # The rest - ctrl_bone_e.parent = eb[ctrl_chain[-1]] - # Add to list - ctrl_chain += [ctrl_bone_e.name] - else: - ctrl_chain += [None] - - # Deformation bone - if self.make_deforms: - # Copy - def_bone = copy_bone(self.obj, name) - eb = self.obj.data.edit_bones - def_bone_e = eb[def_bone] - # Name - def_bone_e.name = make_deformer_name(strip_org(name)) - # Parenting - if i == 0: - # First bone - def_bone_e.parent = eb[self.org_bones[0]].parent - else: - # The rest - def_bone_e.parent = eb[def_chain[-1]] - # Add to list - def_chain += [def_bone_e.name] - else: - def_chain += [None] - - bpy.ops.object.mode_set(mode='OBJECT') - pb = self.obj.pose.bones - - # Constraints for org and def - for org, ctrl, defrm in zip(self.org_bones, ctrl_chain, def_chain): - if self.make_controls: - con = pb[org].constraints.new('COPY_TRANSFORMS') - con.name = "copy_transforms" - con.target = self.obj - con.subtarget = ctrl - - if self.make_deforms: - con = pb[defrm].constraints.new('COPY_TRANSFORMS') - con.name = "copy_transforms" - con.target = self.obj - con.subtarget = org - - # Create control widgets + @stage_parent_bones + def parent_control_chain(self): if self.make_controls: - for bone in ctrl_chain: - create_bone_widget(self.obj, bone) + super(Rig,self).parent_control_chain() + @stage_configure_bones + def configure_control_chain(self): + if self.make_controls: + super(Rig,self).configure_control_chain() -def add_parameters(params): - """ Add the parameters of this rig type to the - RigifyParameters PropertyGroup - """ - params.make_controls = bpy.props.BoolProperty(name="Controls", default=True, description="Create control bones for the copy") - params.make_deforms = bpy.props.BoolProperty(name="Deform", default=True, description="Create deform bones for the copy") + @stage_generate_widgets + def make_control_widgets(self): + if self.make_controls: + super(Rig,self).make_control_widgets() + ############################## + # ORG chain -def parameters_ui(layout, params): - """ Create the ui for the rig parameters. - """ - r = layout.row() - r.prop(params, "make_controls") - r = layout.row() - r.prop(params, "make_deforms") + @stage_rig_bones + def rig_org_chain(self): + if self.make_controls: + super(Rig,self).rig_org_chain() + + ############################## + # Deform chain + + @stage_generate_bones + def make_deform_chain(self): + if self.make_deforms: + super(Rig,self).make_deform_chain() + + @stage_parent_bones + def parent_deform_chain(self): + if self.make_deforms: + super(Rig,self).parent_deform_chain() + + @stage_rig_bones + def rig_deform_chain(self): + if self.make_deforms: + super(Rig,self).rig_deform_chain() + + + @classmethod + def add_parameters(self, params): + """ Add the parameters of this rig type to the + RigifyParameters PropertyGroup + """ + params.make_controls = bpy.props.BoolProperty(name="Controls", default=True, description="Create control bones for the copy") + params.make_deforms = bpy.props.BoolProperty(name="Deform", default=True, description="Create deform bones for the copy") + + + @classmethod + def parameters_ui(self, layout, params): + """ Create the ui for the rig parameters. + """ + r = layout.row() + r.prop(params, "make_controls") + r = layout.row() + r.prop(params, "make_deforms") def create_sample(obj): diff --git a/rigs/basic/super_copy.py b/rigs/basic/super_copy.py index b204534..d049aa0 100644 --- a/rigs/basic/super_copy.py +++ b/rigs/basic/super_copy.py @@ -20,107 +20,112 @@ import bpy -from ...utils import copy_bone -from ...utils import strip_org, make_deformer_name -from ...utils import create_bone_widget, create_circle_widget +from ...base_rig import BaseRig +from ...utils.naming import strip_org, make_deformer_name +from ...utils.widgets_basic import create_bone_widget, create_circle_widget -class Rig: + +class Rig(BaseRig): """ A "copy" rig. All it does is duplicate the original bone and constrain it. This is a control and deformation rig. """ - def __init__(self, obj, bone, params): + def find_org_bones(self, pose_bone): + return pose_bone.name + + + def initialize(self): """ Gather and validate data about the rig. """ - self.obj = obj - self.org_bone = bone - self.org_name = strip_org(bone) - self.params = params - self.make_control = params.make_control - self.make_widget = params.make_widget - self.make_deform = params.make_deform - - def generate(self): - """ Generate the rig. - Do NOT modify any of the original bones, except for adding constraints. - The main armature should be selected and active before this is called. + self.org_name = strip_org(self.bones.org) + + self.make_control = self.params.make_control + self.make_widget = self.params.make_widget + self.make_deform = self.params.make_deform - """ - bpy.ops.object.mode_set(mode='EDIT') + + def generate_bones(self): + bones = self.bones # Make a control bone (copy of original). if self.make_control: - bone = copy_bone(self.obj, self.org_bone, self.org_name) + bones.ctrl = self.copy_bone(bones.org, self.org_name, parent=True) # Make a deformation bone (copy of original, child of original). if self.make_deform: - def_bone = copy_bone(self.obj, self.org_bone, make_deformer_name(self.org_name)) + bones.deform = self.copy_bone(bones.org, make_deformer_name(self.org_name), bbone=True) - # Get edit bones - eb = self.obj.data.edit_bones - # UNUSED - # if self.make_control: - # bone_e = eb[bone] - if self.make_deform: - def_bone_e = eb[def_bone] - # Parent + def parent_bones(self): + bones = self.bones + if self.make_deform: - def_bone_e.use_connect = False - def_bone_e.parent = eb[self.org_bone] + self.set_bone_parent(bones.deform, bones.org, use_connect=False) + + + def configure_bones(self): + bones = self.bones - bpy.ops.object.mode_set(mode='OBJECT') - pb = self.obj.pose.bones + if self.make_control: + self.copy_bone_properties(bones.org, bones.ctrl) + + + def rig_bones(self): + bones = self.bones if self.make_control: # Constrain the original bone. - con = pb[self.org_bone].constraints.new('COPY_TRANSFORMS') - con.name = "copy_transforms" - con.target = self.obj - con.subtarget = bone + self.make_constraint(bones.org, 'COPY_TRANSFORMS', bones.ctrl) + + + def generate_widgets(self): + bones = self.bones + if self.make_control: # Create control widget if self.make_widget: - create_circle_widget(self.obj, bone, radius=0.5) + create_circle_widget(self.obj, bones.ctrl, radius=0.5) else: - create_bone_widget(self.obj, bone) + create_bone_widget(self.obj, bones.ctrl) -def add_parameters(params): - """ Add the parameters of this rig type to the - RigifyParameters PropertyGroup - """ - params.make_control = bpy.props.BoolProperty( - name = "Control", - default = True, - description = "Create a control bone for the copy" - ) - - params.make_widget = bpy.props.BoolProperty( - name = "Widget", - default = True, - description = "Choose a widget for the bone control" - ) - - params.make_deform = bpy.props.BoolProperty( - name = "Deform", - default = True, - description = "Create a deform bone for the copy" - ) - - -def parameters_ui(layout, params): - """ Create the ui for the rig parameters. - """ - r = layout.row() - r.prop(params, "make_control") - r = layout.row() - r.prop(params, "make_widget") - r.enabled = params.make_control - r = layout.row() - r.prop(params, "make_deform") + @classmethod + def add_parameters(self, params): + """ Add the parameters of this rig type to the + RigifyParameters PropertyGroup + """ + params.make_control = bpy.props.BoolProperty( + name = "Control", + default = True, + description = "Create a control bone for the copy" + ) + + params.make_widget = bpy.props.BoolProperty( + name = "Widget", + default = True, + description = "Choose a widget for the bone control" + ) + + params.make_deform = bpy.props.BoolProperty( + name = "Deform", + default = True, + description = "Create a deform bone for the copy" + ) + + + @classmethod + def parameters_ui(self, layout, params): + """ Create the ui for the rig parameters. + """ + r = layout.row() + r.prop(params, "make_control") + r = layout.row() + r.prop(params, "make_widget") + r.enabled = params.make_control + r = layout.row() + r.prop(params, "make_deform") def create_sample(obj): diff --git a/rigs/chain_rigs.py b/rigs/chain_rigs.py new file mode 100644 index 0000000..90a3fac --- /dev/null +++ b/rigs/chain_rigs.py @@ -0,0 +1,169 @@ +#====================== 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 ======================== + +# + +import bpy +from itertools import count, repeat + +from ..base_rig import * + +from ..utils.errors import MetarigError +from ..utils.rig import connected_children_names +from ..utils.naming import strip_org, make_deformer_name +from ..utils.bones import put_bone +from ..utils.widgets_basic import create_bone_widget, create_sphere_widget +from ..utils.misc import map_list, map_apply + + +class SimpleChainRig(BaseRig): + """A rig that consists of 3 connected chains of control, org and deform bones.""" + def find_org_bones(self, bone): + return [bone.name] + connected_children_names(self.obj, bone.name) + + def initialize(self): + if len(self.bones.org) <= 1: + raise MetarigError("RIGIFY ERROR: Bone '%s': input to rig type must be a chain of 2 or more bones" % (strip_org(self.base_bone))) + + bbone_segments = None + + ############################## + # Control chain + + @stage_generate_bones + def make_control_chain(self): + self.bones.ctrl.main = map_list(self.make_control_bone, self.bones.org) + + def make_control_bone(self, org): + return self.copy_bone(org, strip_org(org), parent=True) + + @stage_parent_bones + def parent_control_chain(self): + self.parent_bone_chain(self.bones.ctrl.main, use_connect=True) + + @stage_configure_bones + def configure_control_chain(self): + map_apply(self.configure_control_bone, self.bones.org, self.bones.ctrl.main) + + def configure_control_bone(self, org, ctrl): + self.copy_bone_properties(org, ctrl) + + @stage_generate_widgets + def make_control_widgets(self): + map_apply(self.make_control_widget, self.bones.ctrl.main) + + def make_control_widget(self, ctrl): + create_bone_widget(self.obj, ctrl) + + ############################## + # ORG chain + + @stage_rig_bones + def rig_org_chain(self): + map_apply(self.rig_org_bone, self.bones.org, self.bones.ctrl.main) + + def rig_org_bone(self, org, ctrl): + self.make_constraint(org, 'COPY_TRANSFORMS', ctrl) + + ############################## + # Deform chain + + @stage_generate_bones + def make_deform_chain(self): + self.bones.deform = map_list(self.make_deform_bone, self.bones.org) + + def make_deform_bone(self, org): + name = self.copy_bone(org, make_deformer_name(strip_org(org)), parent=True, bbone=True) + if self.bbone_segments: + self.get_bone(name).bbone_segments = self.bbone_segments + return name + + @stage_parent_bones + def parent_deform_chain(self): + self.parent_bone_chain(self.bones.deform, use_connect=True) + + @stage_rig_bones + def rig_deform_chain(self): + map_apply(self.rig_deform_bone, self.bones.org, self.bones.deform) + + def rig_deform_bone(self, org, deform): + self.make_constraint(deform, 'COPY_TRANSFORMS', org) + + +class TweakChainRig(SimpleChainRig): + """A rig that adds tweak controls to the triple chain.""" + + ############################## + # Tweak chain + + @stage_generate_bones + def make_tweak_chain(self): + orgs = self.bones.org + self.bones.ctrl.tweak = map_list(self.make_tweak_bone, count(0), orgs + orgs[-1:]) + + def make_tweak_bone(self, i, org): + name = self.copy_bone(org, 'tweak_' + strip_org(org), parent=False) + + self.get_bone(name).length /= 2 + + if i == len(self.bones.org): + put_bone(self.obj, name, self.get_bone(org).tail) + + return name + + @stage_parent_bones + def parent_tweak_chain(self): + ctrl = self.bones.ctrl + map_apply(self.set_bone_parent, ctrl.tweak, ctrl.main + ctrl.main[-1:]) + + @stage_configure_bones + def configure_tweak_chain(self): + map_apply(self.configure_tweak_bone, count(0), self.bones.ctrl.tweak) + + def configure_tweak_bone(self, i, tweak): + tweak_pb = self.get_bone(tweak) + + if i == len(self.bones.org): + tweak_pb.lock_rotation_w = True + tweak_pb.lock_rotation = (True, True, True) + tweak_pb.lock_scale = (True, True, True) + else: + tweak_pb.lock_rotation_w = False + tweak_pb.lock_rotation = (True, False, True) + tweak_pb.lock_scale = (False, True, False) + + @stage_generate_widgets + def make_tweak_widgets(self): + map_apply(self.make_tweak_widget, self.bones.ctrl.tweak) + + def make_tweak_widget(self, tweak): + create_sphere_widget(self.obj, tweak) + + ############################## + # ORG chain + + @stage_rig_bones + def rig_org_chain(self): + tweaks = self.bones.ctrl.tweak + map_apply(self.rig_org_bone, self.bones.org, tweaks, tweaks[1:]) + + def rig_org_bone(self, org, tweak, next_tweak): + self.make_constraint(org, 'COPY_TRANSFORMS', tweak) + if next_tweak: + self.make_constraint(org, 'DAMPED_TRACK', next_tweak) + self.make_constraint(org, 'STRETCH_TO', next_tweak) diff --git a/rigs/limbs/simple_tentacle.py b/rigs/limbs/simple_tentacle.py index 82fcafe..305e9eb 100644 --- a/rigs/limbs/simple_tentacle.py +++ b/rigs/limbs/simple_tentacle.py @@ -1,267 +1,88 @@ import bpy -from ...utils import copy_bone -from ...utils import strip_org, make_deformer_name, connected_children_names -from ...utils import put_bone, create_sphere_widget -from ...utils import create_circle_widget, align_bone_x_axis -from ...utils import MetarigError -from ...utils import ControlLayersOption +from ...utils.bones import align_chain_x_axis +from ...utils.widgets_basic import create_circle_widget +from ...utils.layers import ControlLayersOption +from ...utils.misc import map_list, map_apply -class Rig: +from ...base_rig import * +from ..chain_rigs import TweakChainRig - def __init__(self, obj, bone_name, params): - self.obj = obj - self.org_bones = [bone_name] + connected_children_names(obj, bone_name) - self.params = params - self.copy_rotation_axes = params.copy_rotation_axes +class Rig(TweakChainRig): + def initialize(self): + super(Rig,self).initialize() - if len(self.org_bones) <= 1: - raise MetarigError( - "RIGIFY ERROR: invalid rig structure on bone: %s" % (strip_org(bone_name)) - ) - - def orient_org_bones(self): - - bpy.ops.object.mode_set(mode='EDIT') - eb = self.obj.data.edit_bones + self.copy_rotation_axes = self.params.copy_rotation_axes + # Prepare + def prepare_bones(self): if self.params.roll_alignment == "automatic": - - first_bone = eb[self.org_bones[0]] - last_bone = eb[self.org_bones[-1]] - - # Orient uarm farm bones - chain_y_axis = last_bone.tail - first_bone.head - chain_rot_axis = first_bone.y_axis.cross(chain_y_axis) # ik-plane normal axis (rotation) - if chain_rot_axis.length < first_bone.length/100: - chain_rot_axis = first_bone.x_axis.normalized() - else: - chain_rot_axis = chain_rot_axis.normalized() - - for bone in self.org_bones: - align_bone_x_axis(self.obj, bone, chain_rot_axis) - - def make_controls(self): - - bpy.ops.object.mode_set(mode='EDIT') - org_bones = self.org_bones - - ctrl_chain = [] - for i in range(len(org_bones)): - name = org_bones[i] - - ctrl_bone = copy_bone( - self.obj, - name, - strip_org(name) + align_chain_x_axis(self.obj, self.bones.org) + + # Parent + @stage_parent_bones + def parent_control_chain(self): + # use_connect=False for backward compatibility + self.parent_bone_chain(self.bones.ctrl.main, use_connect=False) + + # Configure + @stage_configure_bones + def configure_tweak_chain(self): + super(Rig,self).configure_tweak_chain() + + ControlLayersOption.TWEAK.assign(self.params, self.obj, self.bones.ctrl.tweak) + + # Rig + @stage_rig_bones + def rig_control_chain(self): + ctrls = self.bones.ctrl.main + map_apply(self.rig_control_bone, ctrls, [None] + ctrls) + + def rig_control_bone(self, ctrl, prev_ctrl): + if prev_ctrl: + self.make_constraint( + ctrl, 'COPY_ROTATION', prev_ctrl, + use_xyz=self.copy_rotation_axes, + space='LOCAL', use_offset=True ) - ctrl_chain.append(ctrl_bone) - - # Make widgets - bpy.ops.object.mode_set(mode='OBJECT') - - for ctrl in ctrl_chain: - create_circle_widget(self.obj, ctrl, radius=0.3, head_tail=0.5) - - return ctrl_chain - - def make_tweaks(self): + # Widgets + def make_control_widget(self, ctrl): + create_circle_widget(self.obj, ctrl, radius=0.3, head_tail=0.5) - bpy.ops.object.mode_set(mode ='EDIT') - eb = self.obj.data.edit_bones - org_bones = self.org_bones - tweak_chain = [] - for i in range(len(org_bones) + 1): - if i == len(org_bones): - # Make final tweak at the tip of the tentacle - name = org_bones[i-1] - else: - name = org_bones[i] - - tweak_bone = copy_bone( - self.obj, - name, - "tweak_" + strip_org(name) + @classmethod + def add_parameters(self, params): + """ Add the parameters of this rig type to the + RigifyParameters PropertyGroup + """ + params.copy_rotation_axes = bpy.props.BoolVectorProperty( + size=3, + description="Automation axes", + default=tuple([i == 0 for i in range(0, 3)]) ) - tweak_e = eb[tweak_bone] - - tweak_e.length /= 2 # Set size to half - - if i == len( org_bones ): - # Position final tweak at the tip - put_bone(self.obj, tweak_bone, eb[org_bones[-1]].tail) - - tweak_chain.append(tweak_bone) - - # Make widgets - bpy.ops.object.mode_set(mode='OBJECT') - - for tweak in tweak_chain: - create_sphere_widget(self.obj, tweak) - - tweak_pb = self.obj.pose.bones[tweak] - - # Set locks - if tweak_chain.index(tweak) != len(tweak_chain) - 1: - tweak_pb.lock_rotation = (True, False, True) - tweak_pb.lock_scale = (False, True, False) - else: - tweak_pb.lock_rotation_w = True - tweak_pb.lock_rotation = (True, True, True) - tweak_pb.lock_scale = (True, True, True) - - ControlLayersOption.TWEAK.assign(self.params, self.obj.pose.bones, tweak_chain) - - return tweak_chain - - def make_deform(self): - - bpy.ops.object.mode_set(mode='EDIT') - org_bones = self.org_bones - - def_chain = [] - for i in range(len(org_bones)): - name = org_bones[i] - - def_bone = copy_bone( - self.obj, - name, - make_deformer_name(strip_org(name)) - ) - - def_chain.append(def_bone) - - return def_chain - - def parent_bones(self, all_bones): - - bpy.ops.object.mode_set(mode='EDIT') - org_bones = self.org_bones - eb = self.obj.data.edit_bones - - # Parent control bones - for bone in all_bones['control'][1:]: - previous_index = all_bones['control'].index(bone) - 1 - eb[bone].parent = eb[all_bones['control'][previous_index]] - - # Parent tweak bones - tweaks = all_bones['tweak'] - for tweak in all_bones['tweak']: - parent = '' - if tweaks.index(tweak) == len(tweaks) - 1: - parent = all_bones['control'][-1] - else: - parent = all_bones['control'][tweaks.index(tweak)] - - eb[tweak].parent = eb[parent] - - # Parent deform bones - for bone in all_bones['deform'][1:]: - previous_index = all_bones['deform'].index(bone) - 1 - - eb[bone].parent = eb[all_bones['deform'][previous_index]] - eb[bone].use_connect = True - - # Parent org bones ( to tweaks by default, or to the controls ) - for org, tweak in zip(org_bones, all_bones['tweak']): - eb[org].parent = eb[tweak] - - def make_constraints(self, all_bones): - - bpy.ops.object.mode_set(mode='OBJECT') - pb = self.obj.pose.bones - - # Deform bones' constraints - ctrls = all_bones['control'] - tweaks = all_bones['tweak'] - deforms = all_bones['deform'] - - for deform, tweak, ctrl in zip( deforms, tweaks, ctrls ): - con = pb[deform].constraints.new('COPY_TRANSFORMS') - con.target = self.obj - con.subtarget = tweak - - con = pb[deform].constraints.new('DAMPED_TRACK') - con.target = self.obj - con.subtarget = tweaks[tweaks.index(tweak) + 1] - - con = pb[deform].constraints.new('STRETCH_TO') - con.target = self.obj - con.subtarget = tweaks[tweaks.index(tweak) + 1] - - # Control bones' constraints - if ctrl != ctrls[0]: - con = pb[ctrl].constraints.new('COPY_ROTATION') - con.target = self.obj - con.subtarget = ctrls[ctrls.index(ctrl) - 1] - for i, prop in enumerate(['use_x', 'use_y', 'use_z']): - if self.copy_rotation_axes[i]: - setattr(con, prop, True) - else: - setattr(con, prop, False) - con.use_offset = True - con.target_space = 'LOCAL' - con.owner_space = 'LOCAL' - - def generate(self): - bpy.ops.object.mode_set(mode='EDIT') - eb = self.obj.data.edit_bones - - self.orient_org_bones() - - # Clear all initial parenting - for bone in self.org_bones: - # eb[ bone ].parent = None - eb[bone].use_connect = False - - # Creating all bones - ctrl_chain = self.make_controls() - tweak_chain = self.make_tweaks() - def_chain = self.make_deform() - - all_bones = { - 'control': ctrl_chain, - 'tweak': tweak_chain, - 'deform': def_chain - } - - self.make_constraints(all_bones) - self.parent_bones(all_bones) - - -def add_parameters(params): - """ Add the parameters of this rig type to the - RigifyParameters PropertyGroup - """ - params.copy_rotation_axes = bpy.props.BoolVectorProperty( - size=3, - description="Automation axes", - default=tuple([i == 0 for i in range(0, 3)]) - ) - - # Setting up extra tweak layers - ControlLayersOption.TWEAK.add_parameters(params) + # Setting up extra tweak layers + ControlLayersOption.TWEAK.add_parameters(params) - items = [('automatic', 'Automatic', ''), ('manual', 'Manual', '')] - params.roll_alignment = bpy.props.EnumProperty(items=items, name="Bone roll alignment", default='automatic') + items = [('automatic', 'Automatic', ''), ('manual', 'Manual', '')] + params.roll_alignment = bpy.props.EnumProperty(items=items, name="Bone roll alignment", default='automatic') -def parameters_ui(layout, params): - """ Create the ui for the rig parameters. - """ + @classmethod + def parameters_ui(self, layout, params): + """ Create the ui for the rig parameters. + """ - r = layout.row() - r.prop(params, "roll_alignment") + r = layout.row() + r.prop(params, "roll_alignment") - row = layout.row(align=True) - for i, axis in enumerate(['x', 'y', 'z']): - row.prop(params, "copy_rotation_axes", index=i, toggle=True, text=axis) + row = layout.row(align=True) + for i, axis in enumerate(['x', 'y', 'z']): + row.prop(params, "copy_rotation_axes", index=i, toggle=True, text=axis) - ControlLayersOption.TWEAK.parameters_ui(layout, params) + ControlLayersOption.TWEAK.parameters_ui(layout, params) def create_sample(obj): diff --git a/rigs/limbs/super_finger.py b/rigs/limbs/super_finger.py index 77a8376..f3daa52 100644 --- a/rigs/limbs/super_finger.py +++ b/rigs/limbs/super_finger.py @@ -1,304 +1,63 @@ import bpy -from ...utils import copy_bone, flip_bone -from ...utils import strip_org, make_deformer_name, connected_children_names, make_mechanism_name -from ...utils import create_circle_widget, create_widget -from ...utils import MetarigError, align_bone_x_axis -from rna_prop_ui import rna_idprop_ui_prop_get +import re -script = """ -controls = [%s] -master_name = '%s' -if is_selected(controls): - layout.prop(pose_bones[master_name], '["%s"]', text="Curvature", slider=True) -""" +from itertools import count, repeat +from ...utils.errors import MetarigError +from ...utils.rig import connected_children_names +from ...utils.bones import flip_bone, align_chain_x_axis +from ...utils.naming import strip_org, make_mechanism_name, make_deformer_name +from ...utils.widgets import create_widget +from ...utils.widgets_basic import create_circle_widget +from ...utils.misc import map_list, map_apply -class Rig: +from ...base_rig import * +from ..chain_rigs import SimpleChainRig - def __init__(self, obj, bone_name, params): - self.obj = obj - self.org_bones = [bone_name] + connected_children_names(obj, bone_name) - self.params = params - if len(self.org_bones) <= 1: - raise MetarigError("RIGIFY ERROR: Bone '%s': listen bro, that finger rig jusaint put tugetha rite. A little hint, use more than one bone!!" % (strip_org(bone_name))) +class Rig(SimpleChainRig): + """A finger rig with master control.""" + def initialize(self): + super(Rig,self).initialize() - def orient_org_bones(self): - - bpy.ops.object.mode_set(mode='EDIT') - eb = self.obj.data.edit_bones + self.bbone_segments = 8 + self.first_parent = self.get_bone_parent(self.bones.org[0]) + def prepare_bones(self): if self.params.primary_rotation_axis == 'automatic': + align_chain_x_axis(self.obj, self.bones.org) - first_bone = eb[self.org_bones[0]] - last_bone = eb[self.org_bones[-1]] - - # Orient uarm farm bones - chain_y_axis = last_bone.tail - first_bone.head - chain_rot_axis = first_bone.y_axis.cross(chain_y_axis) # ik-plane normal axis (rotation) - if chain_rot_axis.length < first_bone.length/100: - chain_rot_axis = first_bone.x_axis.normalized() - else: - chain_rot_axis = chain_rot_axis.normalized() + ############################## + # Master Control - for bone in self.org_bones: - align_bone_x_axis(self.obj, bone, chain_rot_axis) + @stage_generate_bones + def make_master_control_bone(self): + orgs = self.bones.org + name = self.copy_bone(orgs[0], self.master_control_name(orgs[0]), parent=True) + self.bones.ctrl.master = name - def generate(self): - org_bones = self.org_bones + first_bone = self.get_bone(orgs[0]) + last_bone = self.get_bone(orgs[-1]) + self.get_bone(name).tail += (last_bone.tail - first_bone.head) * 1.25 - bpy.ops.object.mode_set(mode='EDIT') - eb = self.obj.data.edit_bones + def master_control_name(self, org_name): + # Compute master bone name: inherit .LR suffix, but strip trailing digits + name_parts = re.match(r'^(.*?)(?:([._-])?\d+)?((?:[._-][LlRr])?)(?:\.\d+)?$', strip_org(org_name)) + name_base, name_sep, name_suffix = name_parts.groups() + name_base += name_sep if name_sep else '_' + return name_base + 'master' + name_suffix - self.orient_org_bones() + @stage_configure_bones + def configure_master_control_bone(self): + master = self.bones.ctrl.master - # Bone name lists - ctrl_chain = [] - def_chain = [] - mch_chain = [] - mch_drv_chain = [] + bone = self.get_bone(master) + bone.lock_scale = True, False, True - # Create ctrl master bone - org_name = self.org_bones[0] - temp_name = strip_org(self.org_bones[0]) - - if temp_name[-2:] == '.L' or temp_name[-2:] == '.R': - suffix = temp_name[-2:] - master_name = temp_name[:-2] + "_master" + suffix - else: - master_name = temp_name + "_master" - master_name = copy_bone(self.obj, org_name, master_name) - ctrl_bone_master = eb[master_name] - - # Parenting bug fix ?? - ctrl_bone_master.use_connect = False - ctrl_bone_master.parent = None - - ctrl_bone_master.tail += (eb[org_bones[-1]].tail - eb[org_name].head) * 1.25 - - for bone in org_bones: - eb[bone].use_connect = False - if org_bones.index(bone) != 0: - eb[bone].parent = None - - # Creating the bone chains - for i in range(len(self.org_bones)): - - name = self.org_bones[i] - ctrl_name = strip_org(name) - - # Create control bones - ctrl_bone = copy_bone(self.obj, name, ctrl_name) - ctrl_bone_e = eb[ctrl_name] - - # Create deformation bones - def_name = make_deformer_name(ctrl_name) - def_bone = copy_bone(self.obj, name, def_name) - - # Create mechanism bones - mch_name = make_mechanism_name(ctrl_name) - mch_bone = copy_bone(self.obj, name, mch_name) - - # Create mechanism driver bones - drv_name = make_mechanism_name(ctrl_name) + "_drv" - mch_bone_drv = copy_bone(self.obj, name, drv_name) - - # Adding to lists - ctrl_chain += [ctrl_bone] - def_chain += [def_bone] - mch_chain += [mch_bone] - mch_drv_chain += [mch_bone_drv] - - # Restoring org chain parenting - for bone in org_bones[1:]: - eb[bone].parent = eb[org_bones[org_bones.index(bone) - 1]] - - # Parenting the master bone to the first org - ctrl_bone_master = eb[master_name] - ctrl_bone_master.parent = eb[org_bones[0]] - - # Parenting chain bones - for i in range(len(self.org_bones)): - # Edit bone references - def_bone_e = eb[def_chain[i]] - ctrl_bone_e = eb[ctrl_chain[i]] - mch_bone_e = eb[mch_chain[i]] - mch_bone_drv_e = eb[mch_drv_chain[i]] - - if i == 0: - # First ctl bone - ctrl_bone_e.parent = mch_bone_drv_e - ctrl_bone_e.use_connect = False - # First def bone - def_bone_e.parent = eb[self.org_bones[i]].parent - def_bone_e.use_connect = False - # First mch bone - mch_bone_e.parent = eb[self.org_bones[i]].parent - mch_bone_e.use_connect = False - # First mch driver bone - mch_bone_drv_e.parent = eb[self.org_bones[i]].parent - mch_bone_drv_e.use_connect = False - else: - # The rest - ctrl_bone_e.parent = mch_bone_drv_e - ctrl_bone_e.use_connect = False - - def_bone_e.parent = eb[def_chain[i-1]] - def_bone_e.use_connect = True - - mch_bone_drv_e.parent = eb[ctrl_chain[i-1]] - mch_bone_drv_e.use_connect = False - - # Parenting mch bone - mch_bone_e.parent = ctrl_bone_e - mch_bone_e.use_connect = False - - # Creating tip control bone - tip_name = copy_bone(self.obj, org_bones[-1], temp_name) - ctrl_bone_tip = eb[tip_name] - flip_bone(self.obj, tip_name) - ctrl_bone_tip.length /= 2 - - ctrl_bone_tip.parent = eb[ctrl_chain[-1]] - - bpy.ops.object.mode_set(mode='OBJECT') - - pb = self.obj.pose.bones - - # Setting pose bones locks - pb_master = pb[master_name] - pb_master.lock_scale = True, False, True - - pb[tip_name].lock_scale = True, True, True - pb[tip_name].lock_rotation = True, True, True - pb[tip_name].lock_rotation_w = True - - pb_master['finger_curve'] = 0.0 - prop = rna_idprop_ui_prop_get(pb_master, 'finger_curve') - prop["min"] = 0.0 - prop["max"] = 1.0 - prop["soft_min"] = 0.0 - prop["soft_max"] = 1.0 - prop["description"] = "Rubber hose finger cartoon effect" - - # Pose settings - for org, ctrl, deform, mch, mch_drv in zip(self.org_bones, ctrl_chain, def_chain, mch_chain, mch_drv_chain): - - # Constraining the deform bones - con = pb[deform].constraints.new('COPY_TRANSFORMS') - con.target = self.obj - con.subtarget = mch - - # Constraining the mch bones - if mch_chain.index(mch) == 0: - con = pb[mch].constraints.new('COPY_LOCATION') - con.target = self.obj - con.subtarget = ctrl - - con = pb[mch].constraints.new('COPY_SCALE') - con.target = self.obj - con.subtarget = ctrl - - con = pb[mch].constraints.new('DAMPED_TRACK') - con.target = self.obj - con.subtarget = ctrl_chain[ctrl_chain.index(ctrl)+1] - - con = pb[mch].constraints.new('STRETCH_TO') - con.target = self.obj - con.subtarget = ctrl_chain[ctrl_chain.index(ctrl)+1] - con.volume = 'NO_VOLUME' - - elif mch_chain.index(mch) == len(mch_chain) - 1: - con = pb[mch].constraints.new('DAMPED_TRACK') - con.target = self.obj - con.subtarget = tip_name - - con = pb[mch].constraints.new('STRETCH_TO') - con.target = self.obj - con.subtarget = tip_name - con.volume = 'NO_VOLUME' - else: - con = pb[mch].constraints.new('DAMPED_TRACK') - con.target = self.obj - con.subtarget = ctrl_chain[ctrl_chain.index(ctrl)+1] - - con = pb[mch].constraints.new('STRETCH_TO') - con.target = self.obj - con.subtarget = ctrl_chain[ctrl_chain.index(ctrl)+1] - con.volume = 'NO_VOLUME' - - # Constraining and driving mch driver bones - pb[mch_drv].rotation_mode = 'YZX' - - if mch_drv_chain.index(mch_drv) == 0: - # Constraining to master bone - con = pb[mch_drv].constraints.new('COPY_LOCATION') - con.target = self.obj - con.subtarget = master_name - - con = pb[mch_drv].constraints.new('COPY_ROTATION') - con.target = self.obj - con.subtarget = master_name - con.target_space = 'LOCAL' - con.owner_space = 'LOCAL' - - else: - # Match axis to expression - options = { - "automatic": {"axis": 0, - "expr": '(1-sy)*pi'}, - "X": {"axis": 0, - "expr": '(1-sy)*pi'}, - "-X": {"axis": 0, - "expr": '-((1-sy)*pi)'}, - "Y": {"axis": 1, - "expr": '(1-sy)*pi'}, - "-Y": {"axis": 1, - "expr": '-((1-sy)*pi)'}, - "Z": {"axis": 2, - "expr": '(1-sy)*pi'}, - "-Z": {"axis": 2, - "expr": '-((1-sy)*pi)'} - } - - axis = self.params.primary_rotation_axis - - # Drivers - drv = pb[mch_drv].driver_add("rotation_euler", options[axis]["axis"]).driver - drv.type = 'SCRIPTED' - drv.expression = options[axis]["expr"] - drv_var = drv.variables.new() - drv_var.name = 'sy' - drv_var.type = "SINGLE_PROP" - drv_var.targets[0].id = self.obj - drv_var.targets[0].data_path = pb[master_name].path_from_id() + '.scale.y' - - # Setting bone curvature setting, custom property, and drivers - def_bone = self.obj.data.bones[deform] - - def_bone.bbone_segments = 8 - drv = def_bone.driver_add("bbone_in").driver # Ease in - - drv.type='SUM' - drv_var = drv.variables.new() - drv_var.name = "curvature" - drv_var.type = "SINGLE_PROP" - drv_var.targets[0].id = self.obj - drv_var.targets[0].data_path = pb_master.path_from_id() + '["finger_curve"]' - - drv = def_bone.driver_add("bbone_out").driver # Ease out - - drv.type='SUM' - drv_var = drv.variables.new() - drv_var.name = "curvature" - drv_var.type = "SINGLE_PROP" - drv_var.targets[0].id = self.obj - drv_var.targets[0].data_path = pb_master.path_from_id() + '["finger_curve"]' - - # Assigning shapes to control bones - create_circle_widget(self.obj, ctrl, radius=0.3, head_tail=0.5) + @stage_generate_widgets + def make_master_control_widget(self): + master_name = self.bones.ctrl.master - # Create ctrl master widget w = create_widget(self.obj, master_name) if w is not None: mesh = w.data @@ -313,31 +72,182 @@ def generate(self): mesh.from_pydata(verts, edges, []) mesh.update() - # Create tip control widget - create_circle_widget(self.obj, tip_name, radius=0.3, head_tail=0.0) + ############################## + # Control chain + + @stage_generate_bones + def make_control_chain(self): + orgs = self.bones.org + self.bones.ctrl.main = map_list(self.make_control_bone, orgs) + self.bones.ctrl.main += [self.make_tip_control_bone(orgs[-1], orgs[0])] + + def make_control_bone(self, org): + return self.copy_bone(org, strip_org(org), parent=False) + + def make_tip_control_bone(self, org, name_org): + name = self.copy_bone(org, strip_org(name_org), parent=False) + + flip_bone(self.obj, name) + self.get_bone(name).length /= 2 + + return name + + @stage_parent_bones + def parent_control_chain(self): + ctrls = self.bones.ctrl.main + map_apply(self.set_bone_parent, ctrls, self.bones.mch.bend + ctrls[-2:]) + + @stage_configure_bones + def configure_control_chain(self): + map_apply(self.configure_control_bone, self.bones.org + [None], self.bones.ctrl.main) + + def configure_control_bone(self, org, ctrl): + if org: + self.copy_bone_properties(org, ctrl) + else: + bone = self.get_bone(ctrl) + bone.lock_rotation_w = True + bone.lock_rotation = (True, True, True) + bone.lock_scale = (True, True, True) + + def make_control_widget(self, ctrl): + if ctrl == self.bones.ctrl.main[-1]: + # Tip control + create_circle_widget(self.obj, ctrl, radius=0.3, head_tail=0.0) + else: + create_circle_widget(self.obj, ctrl, radius=0.3, head_tail=0.5) + + ############################## + # MCH bend chain + + @stage_generate_bones + def make_mch_bend_chain(self): + self.bones.mch.bend = map_list(self.make_mch_bend_bone, self.bones.org) + + def make_mch_bend_bone(self, org): + return self.copy_bone(org, make_mechanism_name(strip_org(org)) + "_drv", parent=False) + + @stage_parent_bones + def parent_mch_bend_chain(self): + ctrls = self.bones.ctrl.main + map_apply(self.set_bone_parent, self.bones.mch.bend, [self.first_parent] + ctrls) + + # Match axis to expression + axis_options = { + "automatic": {"axis": 0, + "expr": '(1-sy)*pi'}, + "X": {"axis": 0, + "expr": '(1-sy)*pi'}, + "-X": {"axis": 0, + "expr": '-((1-sy)*pi)'}, + "Y": {"axis": 1, + "expr": '(1-sy)*pi'}, + "-Y": {"axis": 1, + "expr": '-((1-sy)*pi)'}, + "Z": {"axis": 2, + "expr": '(1-sy)*pi'}, + "-Z": {"axis": 2, + "expr": '-((1-sy)*pi)'} + } + + @stage_rig_bones + def rig_mch_bend_chain(self): + map_apply(self.rig_mch_bend_bone, count(0), self.bones.mch.bend) + + def rig_mch_bend_bone(self, i, mch): + master = self.bones.ctrl.master + if i == 0: + self.make_constraint(mch, 'COPY_LOCATION', master) + self.make_constraint(mch, 'COPY_ROTATION', master, space='LOCAL') + else: + axis = self.params.primary_rotation_axis + options = self.axis_options[axis] + + bone = self.get_bone(mch) + bone.rotation_mode = 'YZX' + + self.make_driver( + bone, 'rotation_euler', index=options['axis'], + expression=options['expr'], + variables={'sy': (master, '.scale.y')} + ) + + ############################## + # MCH stretch chain + + @stage_generate_bones + def make_mch_stretch_chain(self): + self.bones.mch.stretch = map_list(self.make_mch_stretch_bone, self.bones.org) + + def make_mch_stretch_bone(self, org): + return self.copy_bone(org, make_mechanism_name(strip_org(org)), parent=False) + + @stage_parent_bones + def parent_mch_stretch_chain(self): + ctrls = self.bones.ctrl.main + map_apply(self.set_bone_parent, self.bones.mch.stretch, [self.first_parent] + ctrls[1:]) + + @stage_rig_bones + def rig_mch_stretch_chain(self): + ctrls = self.bones.ctrl.main + map_apply(self.rig_mch_stretch_bone, count(0), self.bones.mch.stretch, ctrls, ctrls[1:]) + + def rig_mch_stretch_bone(self, i, mch, ctrl, ctrl_next): + if i == 0: + self.make_constraint(mch, 'COPY_LOCATION', ctrl) + self.make_constraint(mch, 'COPY_SCALE', ctrl) + + self.make_constraint(mch, 'DAMPED_TRACK', ctrl_next) + self.make_constraint(mch, 'STRETCH_TO', ctrl_next, volume='NO_VOLUME') + + ############################## + # ORG chain + + @stage_rig_bones + def rig_org_chain(self): + map_apply(self.rig_org_bone, self.bones.org, self.bones.mch.stretch) + + ############################## + # Deform chain + + @stage_configure_bones + def configure_master_properties(self): + master = self.bones.ctrl.master + + self.make_property(master, 'finger_curve', description="Rubber hose finger cartoon effect") # Create UI - controls_string = ", ".join( - ["'" + x + "'" for x in ctrl_chain] - ) + ", " + "'" + master_name + "'" - return [script % (controls_string, master_name, 'finger_curve')] - - -def add_parameters(params): - """ Add the parameters of this rig type to the - RigifyParameters PropertyGroup - """ - items = [('automatic', 'Automatic', ''), ('X', 'X manual', ''), ('Y', 'Y manual', ''), ('Z', 'Z manual', ''), - ('-X', '-X manual', ''), ('-Y', '-Y manual', ''), ('-Z', '-Z manual', '')] - params.primary_rotation_axis = bpy.props.EnumProperty(items=items, name="Primary Rotation Axis", default='automatic') - - -def parameters_ui(layout, params): - """ Create the ui for the rig parameters. - """ - r = layout.row() - r.label(text="Bend rotation axis:") - r.prop(params, "primary_rotation_axis", text="") + self.script.add_panel_selected_check(self.bones.ctrl.flatten()) + self.script.add_panel_custom_prop(master, 'finger_curve', text="Curvature", slider=True) + + def rig_deform_bone(self, org, deform): + master = self.bones.ctrl.master + bone = self.get_bone(deform) + + self.make_constraint(deform, 'COPY_TRANSFORMS', org) + + self.make_driver(bone.bone, 'bbone_in', variables=[(master, 'finger_curve')]) + self.make_driver(bone.bone, 'bbone_out', variables=[(master, 'finger_curve')]) + + ############### + # OPTIONS + + @classmethod + def add_parameters(self, params): + """ Add the parameters of this rig type to the + RigifyParameters PropertyGroup + """ + items = [('automatic', 'Automatic', ''), ('X', 'X manual', ''), ('Y', 'Y manual', ''), ('Z', 'Z manual', ''), + ('-X', '-X manual', ''), ('-Y', '-Y manual', ''), ('-Z', '-Z manual', '')] + params.primary_rotation_axis = bpy.props.EnumProperty(items=items, name="Primary Rotation Axis", default='automatic') + + @classmethod + def parameters_ui(self, layout, params): + """ Create the ui for the rig parameters. + """ + r = layout.row() + r.label(text="Bend rotation axis:") + r.prop(params, "primary_rotation_axis", text="") def create_sample(obj): From 8ddd922c9b434eab6de5828b1e44a7e04e7b73d2 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Sun, 9 Dec 2018 22:06:43 +0300 Subject: [PATCH 4/4] Split and convert the super_spine rig to the new format. Combining spine, head and tail in the same rig leads to overly complicated and inflexible code. The only reason to do that is to be able to join the B-Bone chains together, and with the new rig class structure it can be done via rig interaction. Thus the rig is split into a basic spine rig, and a head rig that can optionally attach its deform chain to the parent. --- rigs/spines/basic_spine.py | 332 +++++++++++++++++++++ rigs/spines/super_head.py | 593 +++++++++++++++++++++++++++++++++++++ 2 files changed, 925 insertions(+) create mode 100644 rigs/spines/basic_spine.py create mode 100644 rigs/spines/super_head.py diff --git a/rigs/spines/basic_spine.py b/rigs/spines/basic_spine.py new file mode 100644 index 0000000..053a3c1 --- /dev/null +++ b/rigs/spines/basic_spine.py @@ -0,0 +1,332 @@ +#====================== 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 ======================== + +# + +import bpy + +from itertools import count, repeat + +from ..chain_rigs import TweakChainRig + +from ...utils.errors import MetarigError +from ...utils.layers import ControlLayersOption +from ...utils.naming import strip_org, make_deformer_name, make_mechanism_name +from ...utils.bones import BoneDict, put_bone, align_bone_to_axis +from ...utils.widgets_basic import create_circle_widget, create_cube_widget +from ...utils.misc import map_list, map_apply + +from ...base_rig import * + + +class Rig(TweakChainRig): + """ + Spine rig with fixed pivot, hip/chest controls and tweaks. + """ + + bbone_segments = 8 + + def initialize(self): + if len(self.bones.org) < 3: + raise MetarigError("RIGIFY ERROR: Bone '%s': input to rig type must be a chain of 3 or more bones" % (strip_org(self.base_bone))) + + # Check if user provided the pivot position + self.pivot_pos = self.params.pivot_pos + + if not (0 < self.pivot_pos < len(self.bones.org)): + raise MetarigError( + "RIGIFY ERROR: please specify a valid pivot bone position" + ) + + self.length = sum([self.get_bone(b).length for b in self.bones.org]) + + #################################################### + # Main control bones + + @stage_generate_bones + def make_control_chain(self): + orgs = self.bones.org + ctrl = self.bones.ctrl + pivot = self.pivot_pos + + ctrl.master = self.make_torso_control_bone(orgs[pivot], 'torso') + ctrl.hips = self.make_hips_control_bone(orgs[pivot-1], 'hips') + ctrl.chest = self.make_chest_control_bone(orgs[pivot], 'chest') + + def make_torso_control_bone(self, org, name): + name = self.copy_bone(org, name, parent=False) + align_bone_to_axis(self.obj, name, 'y', self.length * 0.6) + + # Put the main control in the middle of the hip bone + base_bone = self.get_bone(self.bones.org[0]) + put_bone(self.obj, name, (base_bone.head + base_bone.tail) / 2) + + return name + + def make_hips_control_bone(self, org, name): + name = self.copy_bone(org, name, parent=False) + align_bone_to_axis(self.obj, name, 'y', self.length / 4, flip=True) + return name + + def make_chest_control_bone(self, org, name): + name = self.copy_bone(org, name, parent=False) + align_bone_to_axis(self.obj, name, 'y', self.length / 3) + return name + + @stage_parent_bones + def parent_control_chain(self): + ctrl = self.bones.ctrl + org_parent = self.get_bone_parent(self.bones.org[0]) + self.set_bone_parent(ctrl.master, org_parent) + self.set_bone_parent(ctrl.hips, ctrl.master) + self.set_bone_parent(ctrl.chest, ctrl.master) + + @stage_configure_bones + def configure_control_chain(self): + pass + + @stage_generate_widgets + def make_control_widgets(self): + ctrl = self.bones.ctrl + mch = self.bones.mch + self.make_master_widget(ctrl.master) + self.make_control_widget(ctrl.hips, mch.wgt_hips) + self.make_control_widget(ctrl.chest, mch.wgt_chest) + + def make_master_widget(self, ctrl): + create_cube_widget( + self.obj, ctrl, + radius=0.5, + bone_transform_name=None + ) + + def make_control_widget(self, ctrl, wgt_mch): + self.get_bone(ctrl).custom_shape_transform = self.get_bone(wgt_mch) + + create_circle_widget( + self.obj, ctrl, + radius=1.0, + head_tail=0.75, + with_line=False, + bone_transform_name=wgt_mch + ) + + #################################################### + # MCH bones associated with main controls + + @stage_generate_bones + def make_mch_control_bones(self): + orgs = self.bones.org + mch = self.bones.mch + + mch.pivot = self.make_mch_pivot_bone(orgs[self.pivot_pos], 'pivot') + mch.wgt_hips = self.make_mch_widget_bone(orgs[0], 'WGT-hips') + mch.wgt_chest = self.make_mch_widget_bone(orgs[-1], 'WGT-chest') + + def make_mch_pivot_bone(self, org, name): + name = self.copy_bone(org, make_mechanism_name(name), parent=False) + align_bone_to_axis(self.obj, name, 'y', self.length * 0.6 / 4) + return name + + def make_mch_widget_bone(self, org, name): + return self.copy_bone(org, make_mechanism_name(name), parent=False) + + @stage_parent_bones + def parent_mch_control_bones(self): + mch = self.bones.mch + self.set_bone_parent(mch.pivot, mch.chain.chest[0]) + self.set_bone_parent(mch.wgt_hips, mch.chain.hips[0]) + self.set_bone_parent(mch.wgt_chest, mch.chain.chest[-1]) + + @stage_rig_bones + def rig_mch_control_bones(self): + mch = self.bones.mch + # Is it actually intending to compute average of these, or is this really intentional? + # This effectively adds rotations of the hip and chest controls. + self.make_constraint(mch.pivot, 'COPY_TRANSFORMS', mch.chain.hips[-1], space='LOCAL') + + #################################################### + # MCH chain for distributing hip & chest transform + + @stage_generate_bones + def make_mch_chain(self): + orgs = self.bones.org + self.bones.mch.chain = BoneDict( + hips = map_list(self.make_mch_bone, orgs[0:self.pivot_pos], repeat(True)), + chest = map_list(self.make_mch_bone, orgs[self.pivot_pos:], repeat(False)), + ) + + def make_mch_bone(self, org, is_hip): + name = self.copy_bone(org, make_mechanism_name(strip_org(org)), parent=False) + align_bone_to_axis(self.obj, name, 'y', self.length / 10, flip=is_hip) + return name + + @stage_parent_bones + def parent_mch_chain(self): + master = self.bones.ctrl.master + chain = self.bones.mch.chain + self.parent_bone_chain([master, *reversed(chain.hips)]) + self.parent_bone_chain([master, *chain.chest]) + + @stage_rig_bones + def rig_mch_chain(self): + ctrl = self.bones.ctrl + chain = self.bones.mch.chain + map_apply(self.rig_mch_bone, chain.hips, repeat(ctrl.hips), repeat(len(chain.hips))) + map_apply(self.rig_mch_bone, chain.chest, repeat(ctrl.chest), repeat(len(chain.chest))) + + def rig_mch_bone(self, mch, control, count): + self.make_constraint(mch, 'COPY_TRANSFORMS', control, space='LOCAL', influence=1/count) + + #################################################### + # Tweak bones + + @stage_parent_bones + def parent_tweak_chain(self): + mch = self.bones.mch + chain = mch.chain + parents = [chain.hips[0], *chain.hips[0:-1], mch.pivot, *chain.chest[1:], chain.chest[-1]] + map_apply(self.set_bone_parent, self.bones.ctrl.tweak, parents) + + @stage_configure_bones + def configure_tweak_chain(self): + super(Rig,self).configure_tweak_chain() + + ControlLayersOption.TWEAK.assign(self.params, self.obj, self.bones.ctrl.tweak) + + #################################################### + # Deform bones + + @stage_configure_bones + def configure_bbone_chain(self): + self.get_bone(self.bones.deform[0]).bone.bbone_in = 0.0 + + #################################################### + # SETTINGS + + @classmethod + def add_parameters(self, params): + """ Add the parameters of this rig type to the + RigifyParameters PropertyGroup + """ + + params.pivot_pos = bpy.props.IntProperty( + name='pivot_position', + default=2, + min=0, + description='Position of the torso control and pivot point' + ) + + # Setting up extra layers for the FK and tweak + ControlLayersOption.TWEAK.add_parameters(params) + + @classmethod + def parameters_ui(self, layout, params): + """ Create the ui for the rig parameters.""" + + r = layout.row() + r.prop(params, "pivot_pos") + + ControlLayersOption.TWEAK.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('spine') + bone.head[:] = 0.0000, 0.0552, 1.0099 + bone.tail[:] = 0.0000, 0.0172, 1.1573 + bone.roll = 0.0000 + bone.use_connect = False + bones['spine'] = bone.name + + bone = arm.edit_bones.new('spine.001') + bone.head[:] = 0.0000, 0.0172, 1.1573 + bone.tail[:] = 0.0000, 0.0004, 1.2929 + bone.roll = 0.0000 + bone.use_connect = True + bone.parent = arm.edit_bones[bones['spine']] + bones['spine.001'] = bone.name + + bone = arm.edit_bones.new('spine.002') + bone.head[:] = 0.0000, 0.0004, 1.2929 + bone.tail[:] = 0.0000, 0.0059, 1.4657 + bone.roll = 0.0000 + bone.use_connect = True + bone.parent = arm.edit_bones[bones['spine.001']] + bones['spine.002'] = bone.name + + bone = arm.edit_bones.new('spine.003') + bone.head[:] = 0.0000, 0.0059, 1.4657 + bone.tail[:] = 0.0000, 0.0114, 1.6582 + bone.roll = 0.0000 + bone.use_connect = True + bone.parent = arm.edit_bones[bones['spine.002']] + bones['spine.003'] = bone.name + + + bpy.ops.object.mode_set(mode='OBJECT') + pbone = obj.pose.bones[bones['spine']] + pbone.rigify_type = 'spines.basic_spine' + 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.tweak_layers = [False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False] + except AttributeError: + pass + pbone = obj.pose.bones[bones['spine.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['spine.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' + pbone = obj.pose.bones[bones['spine.003']] + 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 + arm.edit_bones.active = bone diff --git a/rigs/spines/super_head.py b/rigs/spines/super_head.py new file mode 100644 index 0000000..e79067e --- /dev/null +++ b/rigs/spines/super_head.py @@ -0,0 +1,593 @@ +#====================== 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 ======================== + +# + +import bpy + +from itertools import count, repeat + +from ..chain_rigs import TweakChainRig, SimpleChainRig + +from ...utils.errors import MetarigError +from ...utils.layers import ControlLayersOption +from ...utils.naming import strip_org, make_deformer_name, make_mechanism_name +from ...utils.bones import BoneDict, put_bone, align_bone_to_axis, align_bone_orientation +from ...utils.bones import is_same_position, is_connected_position +from ...utils.widgets_basic import create_circle_widget, create_cube_widget +from ...utils.widgets_special import create_neck_bend_widget, create_neck_tweak_widget +from ...utils.misc import map_list, map_apply + +from ...base_rig import * + + +class Rig(TweakChainRig): + """ + Head rig with long neck support and connect option. + """ + + bbone_segments = 8 + + def initialize(self): + if len(self.bones.org) < 2: + raise MetarigError("RIGIFY ERROR: Bone '%s': input to rig type must be a chain of 3 or more bones" % (strip_org(self.base_bone))) + + self.use_connect = self.params.connect_chain + self.use_connect_master = False + self.use_connect_tweak = False + + self.long_neck = len(self.bones.org) > 3 + self.length = sum([self.get_bone(b).length for b in self.bones.org]) + + if self.use_connect: + parent = self.rigify_parent + + if not isinstance(parent, SimpleChainRig): + raise MetarigError("RIGIFY ERROR: Bone '%s': cannot connect to non-chain parent rig." % (strip_org(self.base_bone))) + + if not is_connected_position(self.obj, parent.bones.org[-1], self.bones.org[0]): + raise MetarigError("RIGIFY ERROR: Bone '%s': cannot connect chain - bone position is disjoint." % (strip_org(self.base_bone))) + + + def prepare_bones(self): + orgs = self.bones.org + + if self.use_connect: + # Exactly match bone position to parent + self.get_bone(orgs[0]).head = self.get_bone(self.rigify_parent.bones.org[-1]).tail + + #################################################### + # Main control bones + + @stage_generate_bones + def make_control_chain(self): + orgs = self.bones.org + ctrl = self.bones.ctrl + + ctrl.neck = self.make_neck_control_bone(orgs[0], 'neck', orgs[-1]) + ctrl.head = self.make_head_control_bone(orgs[-1], 'head') + if self.long_neck: + ctrl.neck_bend = self.make_neck_bend_control_bone(orgs[0], 'neck_bend', ctrl.neck) + + def make_neck_control_bone(self, org, name, org_head): + name = self.copy_bone(org, name, parent=False) + + # Neck spans all neck bones (except head) + self.get_bone(name).tail = self.get_bone(org_head).head + + return name + + def make_neck_bend_control_bone(self, org, name, neck): + name = self.copy_bone(org, name, parent=False) + neck_bend_eb = self.get_bone(name) + + # Neck pivot position + neck_bones = self.bones.org + if (len(neck_bones)-1) % 2: # odd num of neck bones (head excluded) + center_bone = self.get_bone(neck_bones[int((len(neck_bones))/2) - 1]) + neck_bend_eb.head = (center_bone.head + center_bone.tail)/2 + else: + center_bone = self.get_bone(neck_bones[int((len(neck_bones)-1)/2) - 1]) + neck_bend_eb.head = center_bone.tail + + align_bone_orientation(self.obj, name, neck) + neck_bend_eb.length = self.get_bone(neck).length / 2 + + return name + + def make_head_control_bone(self, org, name): + return self.copy_bone(org, name, parent=False) + + @stage_parent_bones + def parent_control_chain(self): + bones = self.bones + self.set_bone_parent(bones.ctrl.neck, bones.mch.rot_neck) + self.set_bone_parent(bones.ctrl.head, bones.mch.rot_head) + if self.long_neck: + self.set_bone_parent(bones.ctrl.neck_bend, bones.mch.stretch) + + @stage_configure_bones + def configure_control_chain(self): + bones = self.bones + self.configure_control_bone(bones.org[0], bones.ctrl.neck) + self.configure_control_bone(bones.org[-1], bones.ctrl.head) + if self.long_neck: + self.configure_control_bone(bones.org[0], bones.ctrl.neck_bend) + + @stage_generate_widgets + def make_control_widgets(self): + ctrl = self.bones.ctrl + self.make_neck_widget(ctrl.neck) + self.make_head_widget(ctrl.head) + if self.long_neck: + self.make_neck_bend_widget(ctrl.neck_bend) + + def make_neck_widget(self, ctrl): + radius = 1/max(1, len(self.bones.mch.chain)) + + create_circle_widget( + self.obj, ctrl, + radius=radius, + head_tail=0.5, + bone_transform_name=None + ) + + def make_neck_bend_widget(self, ctrl): + radius = 1/max(1, len(self.bones.mch.chain)) + + create_neck_bend_widget( + self.obj, ctrl, + radius=radius/2, + head_tail=0.0, + bone_transform_name=None + ) + + def make_head_widget(self, ctrl): + # place wgt @ middle of head bone for long necks + if self.long_neck: + head_tail = 0.5 + else: + head_tail = 1.0 + + create_circle_widget( + self.obj, ctrl, + radius = 0.5, + head_tail = head_tail, + with_line = False, + bone_transform_name = None + ) + + #################################################### + # MCH bones associated with main controls + + @stage_generate_bones + def make_mch_control_bones(self): + orgs = self.bones.org + mch = self.bones.mch + + mch.rot_neck = self.make_mch_rotation_bone(orgs[0], 'ROT-neck') + mch.rot_head = self.make_mch_rotation_bone(orgs[-1], 'ROT-head') + mch.stretch = self.make_mch_stretch_bone(orgs[0], 'STR-neck', orgs[-1]) + + def make_mch_rotation_bone(self, org, name): + name = self.copy_bone(org, make_mechanism_name(name), parent=True) + self.get_bone(name).length = self.length / 5 + return name + + def make_mch_stretch_bone(self, org, name, org_head): + name = self.copy_bone(org, make_mechanism_name(name), parent=False) + self.get_bone(name).tail = self.get_bone(org_head).head + return name + + @stage_parent_bones + def align_mch_rotation_bones(self): + # Choose which bone to follow + mch = self.bones.mch + parent = self.rigify_parent + + if self.use_connect and 'master' in parent.bones.ctrl: + self.use_connect_master = True + self.follow_bone = parent.bones.ctrl.master + else: + self.follow_bone = 'root' + + # Align rot bone orientations to it + align_bone_orientation(self.obj, mch.rot_neck, self.follow_bone) + align_bone_orientation(self.obj, mch.rot_head, self.follow_bone) + + @stage_parent_bones + def parent_mch_control_bones(self): + bones = self.bones + self.set_bone_parent(bones.mch.rot_head, bones.ctrl.neck) + self.set_bone_parent(bones.mch.stretch, bones.ctrl.neck) + + @stage_configure_bones + def configure_follow_properties(self): + # Choose where to put custom properties + controls = self.bones.ctrl.flatten() + + if self.use_connect_master: + self.prop_bone = self.follow_bone + controls += self.rigify_parent.bones.ctrl.flatten() + else: + self.prop_bone = self.ctrl.head + + # Generate properties and script + self.make_property(self.prop_bone, 'neck_follow', default=0.5) + self.make_property(self.prop_bone, 'head_follow', default=0.0) + + self.script.add_panel_selected_check(controls) + self.script.add_panel_custom_prop(self.prop_bone, 'neck_follow', slider=True) + self.script.add_panel_custom_prop(self.prop_bone, 'head_follow', slider=True) + + @stage_rig_bones + def rig_mch_control_bones(self): + mch = self.bones.mch + self.rig_mch_rotation_bone(mch.rot_neck, 'neck_follow') + self.rig_mch_rotation_bone(mch.rot_head, 'head_follow') + self.rig_mch_stretch_bone(mch.stretch, self.bones.ctrl.head) + + def rig_mch_rotation_bone(self, mch, prop_name): + con = self.make_constraint(mch, 'COPY_ROTATION', self.follow_bone) + self.make_constraint(mch, 'COPY_SCALE', self.follow_bone) + + self.make_driver(con, 'influence', variables=[(self.prop_bone, prop_name)], polynomial=[1,-1]) + + def rig_mch_stretch_bone(self, mch, head): + self.make_constraint(mch, 'DAMPED_TRACK', head) + self.make_constraint(mch, 'STRETCH_TO', head) + + #################################################### + # MCH IK chain for the long neck + + @stage_generate_bones + def make_mch_ik_chain(self): + orgs = self.bones.org + if self.long_neck: + self.bones.mch.ik = map_list(self.make_mch_ik_bone, orgs[0:-1]) + + def make_mch_ik_bone(self, org): + return self.copy_bone(org, make_mechanism_name('IK-'+strip_org(org)), parent=False) + + @stage_parent_bones + def parent_mch_ik_chain(self): + if self.long_neck: + ik = self.bones.mch.ik + self.set_bone_parent(ik[0], self.bones.ctrl.tweak[0]) + self.parent_bone_chain(ik, use_connect=True) + + @stage_rig_bones + def rig_mch_ik_chain(self): + if self.long_neck: + ik = self.bones.mch.ik + head = self.bones.ctrl.head + map_apply(self.rig_mch_ik_bone, count(0), ik, repeat(len(ik)), repeat(head)) + + def rig_mch_ik_bone(self, i, mch, ik_len, head): + if i == ik_len - 1: + self.make_constraint(mch, 'IK', head, chain_count=ik_len) + + self.get_bone(mch).ik_stretch = 0.1 + + #################################################### + # MCH chain for the middle of the neck + + @stage_generate_bones + def make_mch_chain(self): + orgs = self.bones.org + self.bones.mch.chain = map_list(self.make_mch_bone, orgs[1:-1]) + + def make_mch_bone(self, org): + name = self.copy_bone(org, make_mechanism_name(strip_org(org)), parent=False) + bone = self.get_bone(name) + bone.length /= 4 + bone.use_inherit_scale = False + return name + + @stage_parent_bones + def align_mch_chain(self): + for mch in self.bones.mch.chain: + align_bone_orientation(self.obj, mch, self.bones.ctrl.neck) + + @stage_parent_bones + def parent_mch_chain(self): + mch = self.bones.mch + map_apply(self.set_bone_parent, mch.chain, repeat(mch.stretch)) + + @stage_rig_bones + def rig_mch_chain(self): + chain = self.bones.mch.chain + if self.long_neck: + ik = self.bones.mch.ik + map_apply(self.rig_mch_bone_long, count(0), chain, repeat(len(chain)), ik[1:]) + else: + map_apply(self.rig_mch_bone, count(0), chain, repeat(len(chain))) + + def rig_mch_bone_long(self, i, mch, len_mch, ik): + ctrl = self.bones.ctrl + + self.make_constraint(mch, 'COPY_LOCATION', ik) + + step = 2/(len_mch+1) + xval = (i+1)*step + influence = 2*xval - xval**2 #parabolic influence of pivot + + self.make_constraint( + mch, 'COPY_LOCATION', ctrl.neck_bend, + influence=influence, use_offset=True, space='LOCAL' + ) + + self.make_constraint(mch, 'COPY_SCALE', ctrl.neck) + + def rig_mch_bone(self, i, mch, len_mch): + ctrl = self.bones.ctrl + + nfactor = float((i + 1) / (len_mch + 1)) + self.make_constraint( + mch, 'COPY_ROTATION', ctrl.head, + influence=nfactor, space='LOCAL' + ) + + #################################################### + # Tweak bones + + @stage_generate_bones + def make_tweak_chain(self): + orgs = self.bones.org + self.bones.ctrl.tweak = map_list(self.make_tweak_bone, count(0), orgs[0:-1]) + + def make_tweak_bone(self, i, org): + if i == 0 and self.use_connect and isinstance(self.rigify_parent, TweakChainRig): + # Share the last tweak bone of the parent rig + parent_ctrl = self.rigify_parent.bones.ctrl + name = parent_ctrl.tweak[-1] + + if not is_same_position(self.obj, name, org): + raise MetarigError("RIGIFY ERROR: Bone '%s': cannot connect tweaks - position mismatch" % (strip_org(self.base_bone))) + + self.use_connect_tweak = True + + bone = self.get_bone(name) + org_bone = self.get_bone(org) + + bone.head = org_bone.head + bone.tail = org_bone.tail + bone.roll = org_bone.roll + bone.length /= 2 + + name = self.rename_bone(name, 'tweak_' + strip_org(org)) + parent_ctrl.tweak[-1] = name + return name + + else: + return super(Rig,self).make_tweak_bone(i, org) + + @stage_parent_bones + def parent_tweak_chain(self): + ctrl = self.bones.ctrl + mch = self.bones.mch + + if self.use_connect_tweak: + # Steal the parent of the shared tweak bone before overriding it + first_tweak_parent = self.get_bone_parent(ctrl.tweak[0]) + if first_tweak_parent: + self.set_bone_parent(mch.rot_neck, first_tweak_parent) + + map_apply(self.set_bone_parent, ctrl.tweak, [ctrl.neck, *mch.chain]) + + @stage_configure_bones + def configure_tweak_chain(self): + super(Rig,self).configure_tweak_chain() + + ControlLayersOption.TWEAK.assign(self.params, self.obj, self.bones.ctrl.tweak) + + @stage_rig_bones + def generate_neck_tweak_widget(self): + # Generate the widget early to override connected parent + if self.long_neck: + bone = self.bones.ctrl.tweak[0] + create_neck_tweak_widget(self.obj, bone, size=1.0) + + #################################################### + # ORG and DEF bones + + @stage_parent_bones + def parent_deform_chain(self): + super(Rig,self).parent_deform_chain() + + if self.use_connect: + last_parent_deform = self.rigify_parent.bones.deform[-1] + self.set_bone_parent(self.bones.deform[0], last_parent_deform, use_connect=True) + + @stage_configure_bones + def configure_bbone_chain(self): + self.get_bone(self.bones.deform[-1]).bone.bbone_segments = 1 + + @stage_rig_bones + def rig_org_chain(self): + tweaks = self.bones.ctrl.tweak + [self.bones.ctrl.head] + map_apply(self.rig_org_bone, self.bones.org, tweaks, tweaks[1:] + [None]) + + + #################################################### + # SETTINGS + + @classmethod + def add_parameters(self, params): + """ Add the parameters of this rig type to the + RigifyParameters PropertyGroup + """ + + params.connect_chain = bpy.props.BoolProperty( + name='Connect chain', + default=False, + description='Connect the B-Bone chain to the parent rig' + ) + + # Setting up extra layers for the FK and tweak + ControlLayersOption.TWEAK.add_parameters(params) + + @classmethod + def parameters_ui(self, layout, params): + """ Create the ui for the rig parameters.""" + + r = layout.row() + r.prop(params, "connect_chain") + + ControlLayersOption.TWEAK.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('spine') + bone.head[:] = 0.0000, 0.0552, 1.0099 + bone.tail[:] = 0.0000, 0.0172, 1.1573 + bone.roll = 0.0000 + bone.use_connect = False + bones['spine'] = bone.name + + bone = arm.edit_bones.new('spine.001') + bone.head[:] = 0.0000, 0.0172, 1.1573 + bone.tail[:] = 0.0000, 0.0004, 1.2929 + bone.roll = 0.0000 + bone.use_connect = True + bone.parent = arm.edit_bones[bones['spine']] + bones['spine.001'] = bone.name + + bone = arm.edit_bones.new('spine.002') + bone.head[:] = 0.0000, 0.0004, 1.2929 + bone.tail[:] = 0.0000, 0.0059, 1.4657 + bone.roll = 0.0000 + bone.use_connect = True + bone.parent = arm.edit_bones[bones['spine.001']] + bones['spine.002'] = bone.name + + bone = arm.edit_bones.new('spine.003') + bone.head[:] = 0.0000, 0.0059, 1.4657 + bone.tail[:] = 0.0000, 0.0114, 1.6582 + bone.roll = 0.0000 + bone.use_connect = True + bone.parent = arm.edit_bones[bones['spine.002']] + bones['spine.003'] = bone.name + + bone = arm.edit_bones.new('spine.004') + bone.head[:] = 0.0000, 0.0114, 1.6582 + bone.tail[:] = 0.0000, -0.013, 1.7197 + bone.roll = 0.0000 + bone.use_connect = False + bone.parent = arm.edit_bones[bones['spine.003']] + bones['spine.004'] = bone.name + + bone = arm.edit_bones.new('spine.005') + bone.head[:] = 0.0000, -0.013, 1.7197 + bone.tail[:] = 0.0000, -0.0247, 1.7813 + bone.roll = 0.0000 + bone.use_connect = True + bone.parent = arm.edit_bones[bones['spine.004']] + bones['spine.005'] = bone.name + + bone = arm.edit_bones.new('spine.006') + bone.head[:] = 0.0000, -0.0247, 1.7813 + bone.tail[:] = 0.0000, -0.0247, 1.9796 + bone.roll = 0.0000 + bone.use_connect = True + bone.parent = arm.edit_bones[bones['spine.005']] + bones['spine.006'] = bone.name + + + bpy.ops.object.mode_set(mode='OBJECT') + pbone = obj.pose.bones[bones['spine']] + pbone.rigify_type = 'spines.basic_spine' + 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.tweak_layers = [False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False] + except AttributeError: + pass + pbone = obj.pose.bones[bones['spine.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['spine.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' + pbone = obj.pose.bones[bones['spine.003']] + 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['spine.004']] + pbone.rigify_type = 'spines.super_head' + 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.connect_chain = True + except AttributeError: + pass + try: + pbone.rigify_parameters.tweak_layers = [False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False] + except AttributeError: + pass + pbone = obj.pose.bones[bones['spine.005']] + 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['spine.006']] + 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 + arm.edit_bones.active = bone