Shifter Component Development
This guide explains how to write your own Shifter components: the guide the
rigger places, the settings window, and the code that builds the rig. Each
section has snippets you can copy. The last part walks through
eyelid_01, a component that uses the newer features: an engine shared
with a tool, guide validation, and mesh data mirrored with the guide.
What a component is
A component is a Python package with two parts:
The guide (
guide.py): the locators, blades and curves the rigger places, and the settings stored on the guide root.The build (
__init__.py): the code that reads the guide and creates the controls, operators and joints of that part of the rig.
Shifter finds components in:
mgear/shifter_classic_components/(shipped with mGear);mgear/shifter_epic_components/(shipped with mGear);every folder listed in the
MGEAR_SHIFTER_COMPONENT_PATHenvironment variable (your studio or project components).
A component folder holds:
File |
Purpose |
|---|---|
|
|
|
|
|
Settings tab compiled from a Qt Designer |
|
Icon in the component list. Optional. |
|
Right-click menu items for the component controls. Optional, see Right-click menu. |
The folder name is the component type, for example control_01. Keep the
_01 version suffix: when a component changes in a way that breaks
existing guides, add _02 instead of changing _01.
A minimal component
This component places one control and one joint between a root and a tip
locator. Save both files in a folder named simple_ctl_01 inside a
folder of MGEAR_SHIFTER_COMPONENT_PATH.
guide.py:
"""Guide Simple Control 01 module"""
from mgear.shifter.component import guide
from mgear.core import transform
from mgear.core import pyqt
from mgear.vendor.Qt import QtCore
from maya.app.general.mayaMixin import MayaQWidgetDockableMixin
from maya.app.general.mayaMixin import MayaQDockWidget
AUTHOR = "Your Name"
URL = ""
EMAIL = ""
VERSION = [1, 0, 0]
TYPE = "simple_ctl_01"
NAME = "simple"
DESCRIPTION = "One control between a root and a tip locator"
class Guide(guide.ComponentGuide):
"""Component Guide Class"""
compType = TYPE
compName = NAME
description = DESCRIPTION
author = AUTHOR
url = URL
email = EMAIL
version = VERSION
def postInit(self):
"""Names of the guide transforms to save."""
self.save_transform = ["root", "tip"]
def addObjects(self):
"""Draw the guide."""
self.root = self.addRoot()
position = transform.getOffsetPosition(self.root, [0, 1, 0])
self.tip = self.addLoc("tip", self.root, position)
self.dispcrv = self.addDispCurve("crv", [self.root, self.tip])
def addParameters(self):
"""Settings stored on the guide root."""
self.pCtlSize = self.addParam("ctlSize", "double", 1.0, 0.01, None)
self.pUseIndex = self.addParam("useIndex", "bool", False)
self.pParentJointIndex = self.addParam(
"parentJointIndex", "long", -1, None, None
)
class componentSettings(MayaQWidgetDockableMixin, guide.componentMainSettings):
"""Settings window, with only the common tabs."""
def __init__(self, parent=None):
self.toolName = TYPE
pyqt.deleteInstances(self, MayaQDockWidget)
super(componentSettings, self).__init__(parent=parent)
self.setup_componentSettingWindow()
self.create_componentControls()
self.populate_componentControls()
self.create_componentLayout()
self.create_componentConnections()
def setup_componentSettingWindow(self):
self.mayaMainWindow = pyqt.maya_main_window()
self.setObjectName(self.toolName)
self.setWindowFlags(QtCore.Qt.Window)
self.setWindowTitle(TYPE)
self.resize(280, 350)
def create_componentControls(self):
return
def populate_componentControls(self):
return
def create_componentLayout(self):
from mgear.vendor.Qt import QtWidgets
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.tabs)
layout.addWidget(self.close_button)
self.setLayout(layout)
def create_componentConnections(self):
return
def dockCloseEventTriggered(self):
pyqt.deleteInstances(self, MayaQDockWidget)
__init__.py:
"""Component Simple Control 01 module"""
from mgear.shifter import component
from mgear.core import attribute
from mgear.core import primitive
from mgear.core import transform
class Component(component.Main):
"""Shifter component Class"""
def addObjects(self):
"""Create the objects of the component."""
t = transform.getTransformLookingAt(
self.guide.pos["root"],
self.guide.pos["tip"],
self.y_axis,
"yx",
self.negate,
)
self.npo = primitive.addTransform(self.root, self.getName("npo"), t)
self.ctl = self.addCtl(
self.npo,
"ctl",
t,
self.color_fk,
"cube",
w=self.settings["ctlSize"] * self.size,
h=self.settings["ctlSize"] * self.size,
d=self.settings["ctlSize"] * self.size,
tp=self.parentCtlTag,
)
attribute.setKeyableAttributes(self.ctl, self.tr_params)
self.jnt_pos.append(
{"obj": self.ctl, "name": "0", "newActiveJnt": "parent_relative_jnt"}
)
def setRelation(self):
"""Rig objects children components attach to."""
self.relatives["root"] = self.ctl
self.relatives["tip"] = self.ctl
self.controlRelatives["root"] = self.ctl
self.controlRelatives["tip"] = self.ctl
self.jointRelatives["root"] = 0
self.jointRelatives["tip"] = 0
Open the Shifter guide manager. The component is listed with the others; draw it, place the locators and build.
The guide (guide.py)
Guide class attributes
Attribute |
Meaning |
|---|---|
|
Component type, the folder name. |
|
Default component name, e.g. |
|
Shown in the component list. |
|
Extra connector names, see Connectors. |
|
Other component types the guide can be swapped with. |
Guide objects
postInit declares what the guide saves. addObjects draws it. The
names in save_transform must match the names you draw.
def postInit(self):
self.save_transform = ["root", "#_loc"]
self.save_blade = ["blade"]
# between 1 and unlimited "#_loc" locators
self.addMinMax("#_loc", 1, -1)
def addObjects(self):
self.root = self.addRoot()
self.locs = self.addLocMulti("#_loc", self.root)
self.blade = self.addBlade("blade", self.root, self.locs[0])
centers = [self.root] + self.locs
self.dispcrv = self.addDispCurve("crv", centers)
Method |
Draws |
|---|---|
|
The component root. Every setting is written on it as an attribute. |
|
A locator. |
|
A chain of numbered locators.
With |
|
A blade: an orientation, with roll offset and scale. |
|
A display curve, for the eye only. It isn’t read back. |
postDraw runs after the guide is drawn, for extra display only.
Guide settings
addParameters declares the settings. Each one becomes an attribute on the
guide root, saved with the guide and readable in the build as
self.settings[name].
def addParameters(self):
self.pMode = self.addEnumParam("mode", ["FK", "IK"], 0)
self.pDivisions = self.addParam("divisions", "long", 4, 1, 64)
self.pBlend = self.addParam("blend", "double", 0.5, 0, 1)
self.pMesh = self.addParam("mesh", "string", "")
self.pIkRefArray = self.addParam("ikrefarray", "string", "")
self.pUseIndex = self.addParam("useIndex", "bool", False)
self.pParentJointIndex = self.addParam(
"parentJointIndex", "long", -1, None, None
)
Types are
"bool","long","double"and"string"; enums useaddEnumParamand store the index.useIndexandparentJointIndexare read by the joint step of every component (they choose the parent component joint by index). Declare them.A comma separated guide name list (like
ikrefarray) is the usual way to reference other guide elements, for example spaces.
Note
Adding a setting to an existing component makes guides saved before it “not up to date” until they are updated. Prefer a new component version for big changes.
Reading the guide in the build
In the component |
Holds |
|---|---|
|
World position of a guide transform (Vector). |
|
World matrix of a guide transform. |
|
Positions of the |
|
Matrices of the |
|
Blades by name, with |
|
Setting values, by name. |
Mirroring the guide
Duplicate Symmetrical calls symmetrize() on the copy. The base class
mirrors every transform and blade, and renames L/R in string settings. If a
setting holds data that must be mirrored another way, override it. The
eyelid mirrors mesh vertex indices:
def symmetrize(self):
if not super(Guide, self).symmetrize():
return False # center components don't mirror
value = self.values["vertexLoop"]
if value:
mirrored, missing = meshNavigation.mirror_component_string(value)
if missing:
mgear.log(
"{}: no mirror for {}".format(self.fullName, missing),
mgear.sev_warning,
)
else:
self.setParamDefValue("vertexLoop", mirrored)
return True
Validating the guide
A component that needs data the guide only references (a mesh, a vertex
loop, another node) can check it before anything is built. Override
validate() and return the problems as messages:
def validate(self):
"""Check the component can build.
Returns:
list: Problem messages, empty when the component can build.
"""
problems = []
mesh = self.values["mesh"]
if not mesh:
problems.append("Set the mesh in the component settings")
elif not cmds.objExists(mesh):
problems.append("The mesh {} doesn't exist".format(mesh))
return problems
How the build uses it:
It runs at the start of the build, after the pre custom steps, so data a pre step imports (geometry, for example) already exists. It runs for builds from the scene guide and from template files.
With the guide option Stop build on validation fail on (default), each problem is logged as an error, named after the component, and the build stops before creating anything.
With the option off, problems are logged as warnings, those components are skipped and the rest of the rig is built.
Write messages the rigger can act on: what’s wrong and where to fix it. No
tracebacks: keep raise for real bugs.
validate() is also a good place to compute data the build needs, and
keep it on the guide, so the build doesn’t do the work twice. Initialize the
member in postInit:
def postInit(self):
self.save_transform = ["root", "aim"]
self.geometry = None # set by validate, read by the build
The settings window
The settings window class is componentSettings in guide.py. The base
class (guide.componentMainSettings) gives the common tabs (name, side,
index, connector, joints, colors). Add your own tab in
populate_componentControls.
Each widget writes its value to the guide root attribute. The base class has helpers for the usual widgets:
Helper |
Use |
|---|---|
|
Set a check box from the guide. |
|
Check box changed. |
|
Spin box changed. |
|
Combo box changed (enum index). |
|
Line edit changed (cleans the text to a valid name). |
|
Add the selected guides to a name list setting. |
|
Remove from a name list setting. |
def populate_componentControls(self):
self.tabs.insertTab(1, self.settingsTab, "Component Settings")
self.settingsTab.divisions_spinBox.setValue(
self.root.attr("divisions").get()
)
def create_componentConnections(self):
self.settingsTab.divisions_spinBox.valueChanged.connect(
partial(
self.updateSpinBox,
self.settingsTab.divisions_spinBox,
"divisions",
)
)
A tab from a settings list
For components with many settings, describe them once with
mgear.core.settings_schema and let mgear.core.settings_form build the
tab. The same list can define the guide parameters, so they never drift
apart. This is how eyelid_01 does it:
from mgear.core import settings_form
from mgear.core.settings_schema import Param
SCHEMA = [
Param("mesh", "string", "", label="Mesh", kind="mesh",
tooltip="Mesh the component reads"),
Param("divisions", "long", 4, minimum=1, maximum=64,
label="Divisions", tooltip="Number of controls"),
Param("doSkin", "bool", False, label="Autoskin",
tooltip="Skin the joints at the end of the build"),
]
class Guide(guide.ComponentGuide):
def addParameters(self):
for param in SCHEMA:
self.addParam(param.key, param.value_type, param.default,
param.minimum, param.maximum)
class componentSettings(...):
def populate_componentControls(self):
self.form = settings_form.SettingsForm(SCHEMA)
self.tabs.insertTab(1, self.form, "Component Settings")
self.form.set_settings(
{param.key: self.root.attr(param.key).get() for param in SCHEMA}
)
def create_componentConnections(self):
self.form.valueChanged.connect(
lambda key, value: self.root.attr(key).set(value)
)
String settings with a kind (mesh, node, set, edges,
vertices, vertex) get a “<<” button that fills them from the
selection. set_visibility_rule(rule, keys) shows and hides rows from
other settings.
The build (__init__.py)
The Component class derives from mgear.shifter.component.Main. The
rig builds every component step by step: all components run step 1, then all
run step 2, and so on. So a component can use, in a later step, what its
parent created in an earlier one.
Step |
Name |
Methods you override |
|---|---|---|
00 |
Objects |
|
01 |
Properties |
|
02 |
Operators |
|
03 |
Connect |
|
04 |
Joints |
Usually nothing: the joints listed in |
05 |
Finalize |
|
Useful members
Member |
Holds |
|---|---|
|
Guide setting values. |
|
Rig (guide) options, e.g. |
|
Guide size, multiply control shapes by it. |
|
Component colors (index or RGB). |
|
|
|
True on the right side. |
|
-1 on the right side, else 1. |
|
Component root transform. |
|
Controller tag parent of the first control. |
|
Full node name, e.g. |
|
Channel lists for |
Controls
Create controls with self.addCtl. It names them with the rig naming rule,
reuses the shape the rigger stored on the guide, sets the color, the
controller tag, the mirror configuration and the control group.
self.fk_ctl = self.addCtl(
self.fk_npo, # parent
"fk0_ctl", # description, "_ctl" is added by the rule
t, # world matrix
self.color_fk,
"cube", # default shape
w=self.size, h=self.size, d=self.size,
tp=self.parentCtlTag, # controller tag parent
mirrorConf=[0, 0, 0, 0, 1, 1, 0, 0, 0], # invert ry, rz on mirror
guide_loc_ref="root", # guide element this control matches
)
attribute.setKeyableAttributes(self.fk_ctl, self.tr_params)
Name all other nodes with self.getName("description"), so they follow
the component name, side and index.
Attributes
def addAttributes(self):
# on the UI host, for animators
self.blend_att = self.addAnimParam(
"blend", "Fk/Ik Blend", "double", 0, 0, 1
)
self.ref_att = self.addAnimEnumParam(
"ikref", "Ik Ref", 0, ["world", "body"]
)
# on the setup root, for riggers
self.roll_att = self.addSetupParam(
"roll", "Roll", "double", 0, -180, 180
)
Animation attributes go on the component UI host: the guide element set in
the ui_host setting, or the global control when it’s empty. Their name
starts with the component name, for example arm_L0_blend, so components
sharing a host don’t clash (exactName=True keeps the name as given).
Operators
addOperators connects what addObjects created: constraints,
mgear.core.applyop solvers, node networks. Don’t create rig transforms
here, so the hierarchy is complete before the operators run.
Joints
List the joints in addObjects with self.jnt_pos. The joint step
creates them, in order, each driven by its object. Add each joint as a dict
of the addJoint keyword arguments:
self.jnt_pos.append(
{
"obj": self.fk_ctl,
"name": "fk0",
"newActiveJnt": "parent_relative_jnt",
"UniScale": False,
"leaf_joint": self.settings["leafJoints"],
"guide_relative": "root",
}
)
Arguments:
Key |
Meaning |
|---|---|
|
Driver transform: the joint follows it (matrix
constraint). A |
|
Description, solved with the rig joint naming rule,
e.g. |
|
Parent joint, one of:
|
|
Uniform scale: the three scale axes follow the
driver Z scale. The |
|
Segment scale compensate on the joint. The
|
|
|
|
True (default): the rotation goes in the joint orient and the rotate channels are zero at rest. False: the rotation goes in the rotate channels. |
|
Adds a |
|
Guide element the joint comes from, stored on the
joint ( |
|
Node connected to the joint by message
( |
|
String stored on the joint ( |
|
True: drive the joint with Maya nodes instead of the mGear matrix constraint. Kept for old rigs. |
|
With |
The joints are in
self.jointListafter the joint step, in thejnt_posorder.When the rig builds with joints off (
options["joint_rig"]), Shifter makes the joints under their drivers. Only setnewActiveJntwhen joints are on.With the Connect to existing joints rig option, a joint with the same name that already exists in the scene is reused.
Note
Older components add joints as lists: [obj, name, newActiveJnt,
UniScale, gearMulMatrix]. The list form is kept for compatibility with
existing components; use the dict form in new code.
Relations
setRelation tells child components what to attach to. The keys are the
guide element names a child guide can be parented under.
Dict |
Meaning |
|---|---|
|
Rig transform a child component is parented to. |
|
Control used as controller tag parent for children. |
|
Index in |
|
Readable names for spaces ( |
Connectors
The connector is how the component root attaches to its parent. standard
parents the root under the parent relative. Add your own in
addConnection:
def addConnection(self):
self.connections["orient_parent"] = self.connect_orient_parent
def connect_orient_parent(self):
# step 03: self.parent (rig transform) and self.parent_comp
# (parent component) are set
self.parent.addChild(self.root)
pm.orientConstraint(self.parent, self.root, maintainOffset=True)
Then list it in the guide: connectors = ["orient_parent"].
Built-in connections you can reuse: connect_standard,
connect_standardWithSimpleIkRef, connect_orientCns,
connect_positionCns, connect_pointCns, connectRef (spaces).
Tip
Name a connector after a component type, for example eye_01: when the
guide is drawn under that component, the connector is picked
automatically.
postConnect runs after every component of the step is connected, for
work that needs the final parenting (for example following a parent
component control).
Subclassing a component
A component can extend another one: same guide and build, plus your
changes. Import the base component with importComponent and
importComponentGuide. They find a component type wherever it is
installed (mGear or MGEAR_SHIFTER_COMPONENT_PATH), so the base can be a
studio component too.
guide.py of control_ext_01, a control_01 with an extra setting:
from mgear.shifter import importComponentGuide
base = importComponentGuide("control_01")
AUTHOR = "Your Name"
URL = ""
EMAIL = ""
VERSION = [1, 0, 0]
TYPE = "control_ext_01"
NAME = "ctlExt"
DESCRIPTION = "control_01 with an extra animation attribute"
class Guide(base.Guide):
"""Component Guide Class"""
compType = TYPE
compName = NAME
description = DESCRIPTION
author = AUTHOR
url = URL
email = EMAIL
version = VERSION
def addParameters(self):
"""The control_01 settings, plus ours."""
super(Guide, self).addParameters()
self.pExtra = self.addParam("addExtra", "bool", True)
class componentSettings(base.componentSettings):
"""The control_01 settings window, for this component type."""
def __init__(self, parent=None):
super(componentSettings, self).__init__(parent)
self.toolName = TYPE
self.setObjectName(TYPE)
self.setWindowTitle(TYPE)
__init__.py:
from mgear.shifter import importComponent
base = importComponent("control_01")
class Component(base.Component):
"""control_01 with an extra animation attribute."""
def addAttributes(self):
"""The control_01 attributes, plus ours."""
super(Component, self).addAttributes()
if self.settings["addExtra"]:
self.extra_att = self.addAnimParam(
"extra", "Extra", "double", 0, 0, 1
)
Things to know:
Set
compTypeand the other guide class attributes again: they are the identity of the new component.Call
superin the methods you override, unless you replace the base behavior on purpose.New settings appear on the guide; to edit them in the settings window, add your own tab (see The settings window).
A subclass depends on the base component’s code. When the base changes version (
control_02), the subclass keeps using the version it imports.
Rules of thumb
Create nodes in
addObjects, connect them inaddOperators.Name everything with
addCtlorgetName; never hard-code names.Scale control shapes with
self.size.For mirrored behavior, build the right side from mirrored matrices (
transform.getSymmetricalTransform) and keepmirrorConfat zeros; for controls that don’t mirror, set the inverted channels inmirrorConf.Check missing data in
Guide.validate, not by raising in the build.Never open UI during a build: builds also run in mayapy. Use
pyqt.can_show_ui()if a component must decide.Put reusable logic in
mgear.core, not in the component, so tools and other components can use it. See the eyelid case below.
Case study: eyelid_01
eyelid_01 builds the Eye Rigger 3.0 eyelid rig. It shows how to share a
rig between a standalone tool and a component, and how a component can rely
on data the guide only points to (a mesh loop).
Guide locators instead of settings
The guide has a root (eye center) and an aim locator. Instead of
settings, they give the eye center, the eye direction and the distance of
the controls in front of the lid. The component side gives the side. Tilted
and side facing eyes need no extra setting, and Duplicate Symmetrical
mirrors the locators for free.
Mesh data in the guide
The eyelid loop is a component string setting, for example
"head.vtx[120:139]":
the settings tab fills it from the selected vertices;
symmetrizemirrors the indices on the mesh;validatesolves the eyelid, so a missing loop or mesh is reported before the build, and keeps the geometry for the build.
The mesh must exist when the rig builds: import it in a pre custom step.
Eye connector
The connectors eye_01 and MCS_eye_01 are named after the eye
components, so they are picked when the eyelid guide is drawn under an eye.
The connector parents the eyelids under the eye’s look relative (it
doesn’t spin with the aim) and postConnect follows the eye’s root
relative (the aimed control).
Testing components
New components ship with tests. They run in mayapy with pytest (see the
tests folder of the mGear repository):
mayapy -m pytest tests/test_mgear/test_shifter -v
A component test draws a guide, builds it and checks the result:
def test_build(rig_plugins, setup_path):
from maya import cmds
import mgear.pymaya as pm
from mgear import shifter
from mgear.shifter import guide
cmds.file(new=True, force=True)
guide.Rig().drawNewComponent(None, "simple_ctl_01", showUI=False)
root = pm.PyNode("simple_C0_root")
# side and index
guide.Rig().updateProperties(root, "simple", "L", 0)
cmds.setAttr("simple_L0_root.ctlSize", 2.0)
cmds.select("guide")
shifter.Rig().buildFromSelection()
assert cmds.objExists("simple_L0_ctl")
assert cmds.objExists("simple_L0_0_jnt")
Fixtures from tests/conftest.py:
rig_pluginsloads the mGear solvers, or skips the test.count_calls(owner, name, record)records calls, for example the build log:count_calls(mgear, "log", lambda args, kwargs: args[0]).eye_facegives a symmetric face mesh with eyelid loops, used by the eyelid tests.
Things to know:
drawNewComponent(None, ...)with nothing selected starts a new guide. To add a second component to the same guide, pass theguidenode, or a guide locator, as the parent.The guide root and the rig root have the same name; use long names to tell them apart.
Duplicate Symmetrical in a test:
guide.Rig().duplicate(root, True).The settings window can’t be shown in mayapy, but it can be created and driven: select the guide root, create
componentSettings()and set its widgets.A Shifter build in a mayapy script needs a GUI
QApplicationcreated beforemaya.standalone.initialize().
Checklist
guide.py:GuidewithpostInit,addObjects,addParameters(withuseIndexandparentJointIndex) and acomponentSettingswindow.__init__.py:ComponentwithaddObjects(controls throughaddCtl, joints injnt_posas dicts),setRelationfor every saved transform, and operators inaddOperators.validate()for any data the guide only references.symmetrize()if a setting must mirror in a special way.Tests that build the component and check values, not only that nodes exist.