Rigify rig API redesign #63138

Closed
opened 2019-03-30 19:57:46 +01:00 by Alexander Gavrilov · 7 comments

Current Limitations

After adding custom rig (feature set) support, Rigify has effectively become a framework for rig generation rather than just a simple add-on. However the current API that rigs have to implement basically consists of a single generate() method and is thus very limited.

No support for rig interaction

The only information rigs receive about the environment is a reference to the base bone of the rig within the armature. The only way they can affect generation apart from changing the armature itself is returning certain values from generate().

Due to these limitations, implementing a set of semi-independent rigs that are intended to cooperate in certain aspects is impossible. This may for example be a valid approach in implementing a flexible face rig: instead of making one monolithic rig, or trying to separate it into completely independent units, it may be best to make a set of rigs that manage their own areas, but cooperate to create some global controls like the eye target etc.

Inefficient mode switching

Switching modes is actually a very expensive operation, especially in 2.8. However the single callback design forces mode switching to be done by individual rigs, and this encourages inefficient mode switching - investigation showed that even utilities did it, e.g. copy_bone. The result of that is that according to profiling results, most of the time spent generating a rig is mode switching.

The most efficient approach to mode switching is obviously to split one generate() method into multiple callbacks and do switching between the calls in the main generation code.

Rig code style isn't designed for subclassing

User-defined rigs provide a potential way for users to slightly tweak a standard rig by subclassing it and overriding methods. However the way all currently existing rigs are coded makes that basically impossible.

In order to allow meaningful subclassing, it is necessary to split the rig code into many simple methods that each do one logical thing, instead of creating enormous chunks of code that handle everything. One generate() method in fact encourages the latter.

Badly organized generate.py code

Most of the core generation management code is one huge function that directly handles all kind of things, and keeps various references in many local variables.


New Design

This describes the new API design implemented by the accompanying patch.

Generator class

The main generation driver code in generate.py is converted into a class. The code is split into multiple methods as appropriate, and data is stored in fields of the class instance.

Rigs receive a reference to the instance of the class, and can thus access the data and call methods to affect the generation process, for example:

  • Data about the generation context: scene, view_layer, collection, metarig, rig_id etc.
  • Lists of rig instances and mapping of bones to owning rigs: rig_list, root_rigs, bone_owners.
  • Current generation stage.
  • disable_auto_parent(bone) method to suppress auto-parenting to root bone for a specific bone (previously handled by generate() return value).

BaseRig class

In order to provide more structure to the rigs, the new API requires them to inherit from a new BaseRig class.

The base rig defines a number of standard fields, that contain e.g:

  • Link to the generator instance, armature obj, script generator instance, base_bone name, rig params.
  • A nested BoneDict bones for use in the rig code to keep bone names between callbacks.
  • Rig hierarchy derived from the hierarchy of their bones: rigify_parent, rigify_children.
  • Bones owned by the rig: rigify_org_bones, rigify_child_bones, rigify_new_bones.

Unlike the legacy API, rigs normally shouldnt override init. Instead there is a find_org_bonesmethod that should just return which bones are exclusively owned by the rig, and stage callbacks that replacegenerate()`.

New style rigs also should define add_parameters and parameters_ui as @classmethod instead of a separate function in the module.

Rig classes that don't inherit from BaseRig are assumed to follow the legacy API and are automatically wrapped in a LegacyRig class that translates between the interfaces. The only existing rig that didn't work without fixing was the face rig, because it relied on its constructor being called in Edit mode for no good reason; the fix was trivial.

Generation stages

Instead of one generate callback, rigs define a whole sequence of callbacks. Each of the stages in the sequence is executed for all rigs before proceeding to the next, and mode switches are performed between each stage as appropriate. Note that for python code that directly works with bone data structures there is no need to use Pose mode over Object mode, so the latter is used.

initialize: called in Object mode for collecting information about the initial state of the rig.

  • This stage effectively replaces the function of the constructor in the old rig interface, but is called after all rig instances are constructed. It is forbidden to actually modify the armature here, so that all rigs can access the true initial state.

prepare_bones: called in Edit mode to prepare bones for generation.

  • This can be used to align bones and auto-correct other minor positioning errors before bones are actually copied in the next stage. This stage is forbidden from adding bones.

generate_bones: called in Edit mode to actually generate all required bones.

  • This is the only stage that is allowed to add new bones; for this reason the generate() method of rigs using the old API is called here.

parent_bones: called in Edit mode to assign bone parents, B-Bone custom handles etc.

  • This stage is also forbidden from adding bones.

configure_bones: called in Object mode to configure pose mode properties.

  • This is the intended stage for assigning transform locks, bone layers etc. It may also be convenient to create custom properties and generate script code here.
  • rig_bones: called in Object mode to create constraints and drivers.
  • generate_widgets: called in Object mode to create widgets.

finalize: called in Object mode at the very end of the generation process.

  • This stage is mostly intended for use in rigs that interact with others; ordinary self-contained ones shouldn't need it.

Some reasons why there are more stages than might seem necessary just because of mode switching:

  • Mode switching may be necessary not just to change mode, but to force some data in the structures to update. For this reason, a mode switch is always triggered between stages, even if the mode isn't actually changed.
  • Staging helps in rig interaction: each stage can assume that the effects of the previous one have been applied by all rigs, i.e. act similar to barriers in multihreading.
  • Big selection of stage callbacks helps in promoting the 'small methods that each do one thing' approach to coding in the rigs.

Stage decorators

To further aid in promoting the small method approach, there are two ways to get a rig method called in a certain stage. The most obvious one is overriding a method with the desired stage name:

def initialize(self):
  ... initialization code ...

This is simple and easy to understand, and actually turns out to be the best method for the initialize stage specifically, but for other stages in a more complex rig it runs into a problem. Specifically, keeping to the idea of small methods, it is desirable to split code dealing with different sub-parts of the rig into their separate methods; but it results in the main callback just calling other methods:

def generate_bones(self):
  self.generate_deform_bones()
  self.generate_control_bones()
  self.generate_tweak_bones()

Such redirect-only callbacks look bad and are error-prone, as it is easy to forget to add one of the methods - especially with inheritance in the picture. For this reason, it is possible to mark any method to be called during a stage:

@stage_generate_bones
def generate_deform_bones(self):
  ...

@stage_generate_bones
def generate_control_bones(self):
  ...

These methods will then be called before the main generate_bones() method in the order they were first declared (and base class before subclass). However it is recommended not to rely on the order within the stage unless absolutely unavoidable.

Utility methods

The base rig class also inherits from certain mixins to provide utility methods for use in generating the rig. Among other things, these methods have the benefit of allowing the use of bone names in arguments, instead of having to look up the actual edit or pose bones.

BoneUtilityMixin

This class provides methods for creating and modifying bones. The methods automatically take care of managing the above mentioned tables that map bones to their owning rigs.

  • self.new_bone(name): creates a new bone with the specified name, and returs the actual name of the new bone.
  • self.copy_bone(old_name, new_name, parent=False, bbone=False): copies the specified bone (optionally including parent and b-bone attributes); returns new name.
  • self.copy_bone_properties(source_name, target_name): copies rotation mode, locks and custom properties from source to target.
  • The legacy utils.copy_bone function effectively consists of copy_bone + copy_bone_properties, but to achieve that results it has to mode switch. Thus it is deprecated.
  • self.rename_bone(old_name, new_name): renames the specified bone; returns the actual new name.
  • self.get_bone(name): looks up the edit or pose bone reference based on current mode. If the name is None, returns None.
  • self.get_bone_parent(name): retrieves the name of the parent bone, or None.
  • self.set_bone_parent(name, parent_name, use_connect=False): sets the parent of the given bone and the connect flag.
  • use_connect=None preserves the current state of the connect flag.
  • self.parent_bone_chain(bone_names, use_connect=None): connects the listed bones into a chain via set_bone_parent.

For more information see utils/bones.py

MechanismUtilityMixin

This class provides methods for creating the mechanism of the rig.

  • self.make_property(bone_name, prop_name, default_value, ...): creates a custom property with limits etc.
  • self.make_constraint(bone_name, type, subtarget=None ...): creates a constraint that connects two bones.
  • self.make_driver(owner, property, ...): creates a driver; the armature object is supplied as default for variable lookup.

For more information see utils/mechanism.py

Generator plugins

In order to allow hooking into the generation stage system by objects other than rigs, a GeneratorPlugin class is defined. Its subclasses are singletons keyed by their constructor arguments (which include the generator instance), and their stage callbacks are called after all the rigs.

Currently the only such plugin is the script generator.

ScriptGenerator class

Generating the python script is a specialized task, and shouldn't be mixed with the main generation driver code, so it is moved into a separate class. It interfaces with the generation process by being a generator plugin.

Instead of providing data for script generation by returning values from generate(), new rigs should directly call methods of the script generator. Some of them replicate the functions of the old values:

  • script.add_panel_code(str_list): adds raw code lines to the panel.
  • script.add_imports(str_list): adds lines to the import section of the script.
  • script.add_utilities(str_list): adds code to the utility function section of the script.
  • script.register_classes(str_list): adds class names to register.
  • script.register_driver_functions(str_list): adds driver function names to register.
  • script.register_property(name, definition): adds a custom RNA property definition.

However, the new interface also allowed creating higher level functions:

  • script.add_panel_selected_check(control_names): generates panel code that checks if one of the listed controls is selected.
  • This method keeps track of calls and won't generate redundant code if called multiple times with the same argument. This state is reset to unknown by add_panel_code.
  • script.add_panel_custom_prop(bone_name, prop_name, ...): generates a layout.prop call for the given bone and custom property.
  • The rest of the arguments are directly passed to layout.prop. Must be preceeded by a call to add_panel_selected_check.
  • script.add_panel_operator(operator_name, properties={...}, ...): generates a layout.operator call.
  • Trailing arguments are passed to layout.operator, while the properties map is used to initialize properties of the operator itself. Must be preceeded by a call to add_panel_selected_check.

Example:

self.script.add_panel_selected_check(self.bones.ctrl.flatten())
self.script.add_panel_custom_prop(master, 'finger_curve', text="Curvature", slider=True)
# Current Limitations After adding custom rig (feature set) support, Rigify has effectively become a framework for rig generation rather than just a simple add-on. However the current API that rigs have to implement basically consists of a single generate() method and is thus very limited. ## No support for rig interaction The only information rigs receive about the environment is a reference to the base bone of the rig within the armature. The only way they can affect generation apart from changing the armature itself is returning certain values from generate(). Due to these limitations, implementing a set of semi-independent rigs that are intended to cooperate in certain aspects is impossible. This may for example be a valid approach in implementing a flexible face rig: instead of making one monolithic rig, or trying to separate it into completely independent units, it may be best to make a set of rigs that manage their own areas, but cooperate to create some global controls like the eye target etc. ## Inefficient mode switching Switching modes is actually a very expensive operation, especially in 2.8. However the single callback design forces mode switching to be done by individual rigs, and this encourages inefficient mode switching - investigation showed that even utilities did it, e.g. copy_bone. The result of that is that according to profiling results, most of the time spent generating a rig is mode switching. The most efficient approach to mode switching is obviously to split one generate() method into multiple callbacks and do switching between the calls in the main generation code. ## Rig code style isn't designed for subclassing User-defined rigs provide a potential way for users to slightly tweak a standard rig by subclassing it and overriding methods. However the way all currently existing rigs are coded makes that basically impossible. In order to allow meaningful subclassing, it is necessary to split the rig code into many simple methods that each do one logical thing, instead of creating enormous chunks of code that handle everything. One generate() method in fact encourages the latter. ## Badly organized generate.py code Most of the core generation management code is one huge function that directly handles all kind of things, and keeps various references in many local variables. ---- # New Design This describes the new API design implemented by the accompanying patch. ## Generator class The main generation driver code in generate.py is converted into a class. The code is split into multiple methods as appropriate, and data is stored in fields of the class instance. Rigs receive a reference to the instance of the class, and can thus access the data and call methods to affect the generation process, for example: * Data about the generation context: `scene`, `view_layer`, `collection`, `metarig`, `rig_id` etc. * Lists of rig instances and mapping of bones to owning rigs: `rig_list`, `root_rigs`, `bone_owners`. * Current generation stage. * `disable_auto_parent(bone)` method to suppress auto-parenting to root bone for a specific bone (previously handled by `generate()` return value). ## BaseRig class In order to provide more structure to the rigs, the new API requires them to inherit from a new BaseRig class. The base rig defines a number of standard fields, that contain e.g: * Link to the `generator` instance, armature `obj`, `script` generator instance, `base_bone` name, rig `params`. * A nested `BoneDict` `bones` for use in the rig code to keep bone names between callbacks. * Rig hierarchy derived from the hierarchy of their bones: `rigify_parent`, `rigify_children`. * Bones owned by the rig: `rigify_org_bones`, `rigify_child_bones`, `rigify_new_bones`. Unlike the legacy API, rigs normally shouldn`t override `__init__`. Instead there is a `find_org_bones` method that should just return which bones are exclusively owned by the rig, and stage callbacks that replace `generate()`. New style rigs also should define `add_parameters` and `parameters_ui` as `@classmethod` instead of a separate function in the module. Rig classes that don't inherit from BaseRig are assumed to follow the legacy API and are automatically wrapped in a LegacyRig class that translates between the interfaces. The only existing rig that didn't work without fixing was the face rig, because it relied on its constructor being called in Edit mode for no good reason; the fix was trivial. ## Generation stages Instead of one generate callback, rigs define a whole sequence of callbacks. Each of the stages in the sequence is executed for all rigs before proceeding to the next, and mode switches are performed between each stage as appropriate. Note that for python code that directly works with bone data structures there is no need to use Pose mode over Object mode, so the latter is used. # `initialize`: called in Object mode for collecting information about the initial state of the rig. - This stage effectively replaces the function of the constructor in the old rig interface, but is called after all rig instances are constructed. It is forbidden to actually modify the armature here, so that all rigs can access the true initial state. # `prepare_bones`: called in Edit mode to prepare bones for generation. - This can be used to align bones and auto-correct other minor positioning errors before bones are actually copied in the next stage. This stage is forbidden from adding bones. # `generate_bones`: called in Edit mode to actually generate all required bones. - This is the only stage that is allowed to add new bones; for this reason the `generate()` method of rigs using the old API is called here. # `parent_bones`: called in Edit mode to assign bone parents, B-Bone custom handles etc. - This stage is also forbidden from adding bones. # `configure_bones`: called in Object mode to configure pose mode properties. - This is the intended stage for assigning transform locks, bone layers etc. It may also be convenient to create custom properties and generate script code here. - `rig_bones`: called in Object mode to create constraints and drivers. - `generate_widgets`: called in Object mode to create widgets. # `finalize`: called in Object mode at the very end of the generation process. - This stage is mostly intended for use in rigs that interact with others; ordinary self-contained ones shouldn't need it. Some reasons why there are more stages than might seem necessary just because of mode switching: * Mode switching may be necessary not just to change mode, but to force some data in the structures to update. For this reason, a mode switch is always triggered between stages, even if the mode isn't actually changed. * Staging helps in rig interaction: each stage can assume that the effects of the previous one have been applied by all rigs, i.e. act similar to barriers in multihreading. * Big selection of stage callbacks helps in promoting the 'small methods that each do one thing' approach to coding in the rigs. ### Stage decorators To further aid in promoting the small method approach, there are two ways to get a rig method called in a certain stage. The most obvious one is overriding a method with the desired stage name: ``` def initialize(self): ... initialization code ... ``` This is simple and easy to understand, and actually turns out to be the best method for the initialize stage specifically, but for other stages in a more complex rig it runs into a problem. Specifically, keeping to the idea of small methods, it is desirable to split code dealing with different sub-parts of the rig into their separate methods; but it results in the main callback just calling other methods: ``` def generate_bones(self): self.generate_deform_bones() self.generate_control_bones() self.generate_tweak_bones() ``` Such redirect-only callbacks look bad and are error-prone, as it is easy to forget to add one of the methods - especially with inheritance in the picture. For this reason, it is possible to mark any method to be called during a stage: ``` @stage_generate_bones def generate_deform_bones(self): ... @stage_generate_bones def generate_control_bones(self): ... ``` These methods will then be called before the main `generate_bones()` method in the order they were first declared (and base class before subclass). However it is recommended not to rely on the order within the stage unless absolutely unavoidable. ## Utility methods The base rig class also inherits from certain mixins to provide utility methods for use in generating the rig. Among other things, these methods have the benefit of allowing the use of bone names in arguments, instead of having to look up the actual edit or pose bones. ### BoneUtilityMixin This class provides methods for creating and modifying bones. The methods automatically take care of managing the above mentioned tables that map bones to their owning rigs. * `self.new_bone(name)`: creates a new bone with the specified name, and returs the actual name of the new bone. * `self.copy_bone(old_name, new_name, parent=False, bbone=False)`: copies the specified bone (optionally including parent and b-bone attributes); returns new name. * `self.copy_bone_properties(source_name, target_name)`: copies rotation mode, locks and custom properties from source to target. - The legacy utils.copy_bone function effectively consists of copy_bone + copy_bone_properties, but to achieve that results it has to mode switch. Thus it is deprecated. * `self.rename_bone(old_name, new_name)`: renames the specified bone; returns the actual new name. * `self.get_bone(name)`: looks up the edit or pose bone reference based on current mode. If the name is None, returns None. * `self.get_bone_parent(name)`: retrieves the name of the parent bone, or None. * `self.set_bone_parent(name, parent_name, use_connect=False)`: sets the parent of the given bone and the connect flag. - `use_connect=None` preserves the current state of the connect flag. * `self.parent_bone_chain(bone_names, use_connect=None)`: connects the listed bones into a chain via `set_bone_parent`. For more information see utils/bones.py ### MechanismUtilityMixin This class provides methods for creating the mechanism of the rig. * `self.make_property(bone_name, prop_name, default_value, ...)`: creates a custom property with limits etc. * `self.make_constraint(bone_name, type, subtarget=None ...)`: creates a constraint that connects two bones. * `self.make_driver(owner, property, ...)`: creates a driver; the armature object is supplied as default for variable lookup. For more information see utils/mechanism.py ## Generator plugins In order to allow hooking into the generation stage system by objects other than rigs, a GeneratorPlugin class is defined. Its subclasses are singletons keyed by their constructor arguments (which include the generator instance), and their stage callbacks are called after all the rigs. Currently the only such plugin is the script generator. ### ScriptGenerator class Generating the python script is a specialized task, and shouldn't be mixed with the main generation driver code, so it is moved into a separate class. It interfaces with the generation process by being a generator plugin. Instead of providing data for script generation by returning values from `generate()`, new rigs should directly call methods of the script generator. Some of them replicate the functions of the old values: * `script.add_panel_code(str_list)`: adds raw code lines to the panel. * `script.add_imports(str_list)`: adds lines to the import section of the script. * `script.add_utilities(str_list)`: adds code to the utility function section of the script. * `script.register_classes(str_list)`: adds class names to register. * `script.register_driver_functions(str_list)`: adds driver function names to register. * `script.register_property(name, definition)`: adds a custom RNA property definition. However, the new interface also allowed creating higher level functions: * `script.add_panel_selected_check(control_names)`: generates panel code that checks if one of the listed controls is selected. - This method keeps track of calls and won't generate redundant code if called multiple times with the same argument. This state is reset to unknown by `add_panel_code`. * `script.add_panel_custom_prop(bone_name, prop_name, ...)`: generates a `layout.prop` call for the given bone and custom property. - The rest of the arguments are directly passed to layout.prop. Must be preceeded by a call to `add_panel_selected_check`. * `script.add_panel_operator(operator_name, properties={...}, ...)`: generates a `layout.operator` call. - Trailing arguments are passed to `layout.operator`, while the `properties` map is used to initialize properties of the operator itself. Must be preceeded by a call to `add_panel_selected_check`. Example: ``` self.script.add_panel_selected_check(self.bones.ctrl.flatten()) self.script.add_panel_custom_prop(master, 'finger_curve', text="Curvature", slider=True)
Alexander Gavrilov self-assigned this 2019-03-30 19:57:46 +01:00
Author
Member

Added subscribers: @angavrilov, @cessen, @icappiello, @LucioRossi, @pioverfour

Added subscribers: @angavrilov, @cessen, @icappiello, @LucioRossi, @pioverfour
Member

Added subscriber: @JulienDuroure

Added subscriber: @JulienDuroure

Added subscriber: @TakeshiFunahashi

Added subscriber: @TakeshiFunahashi

Added subscriber: @ChristopheSwolfs

Added subscriber: @ChristopheSwolfs
Member

Added subscriber: @Mets

Added subscriber: @Mets
Author
Member

Changed status from 'Open' to: 'Resolved'

Changed status from 'Open' to: 'Resolved'

Added subscriber: @T.R.O.Nunes

Added subscriber: @T.R.O.Nunes
Sign in to join this conversation.
No Milestone
No project
6 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: blender/blender-addons#63138
No description provided.