Custom Shapes in the Maya API

A
user-defined surface shape is one of the most complex types of nodes that can
be implemented within Maya. A number of supporting classes are required, and a
number of virtual methods must be properly implemented to get your class
working right with the various parts of Maya. The goal of this article is to
break down the process of creating the shape into manageable chunks, and to
provide details of each step of the process so that you can make good design
decisions as you decide how and what to implement for your custom shape.

We will start by
discussing the motivation for user-defined shapes. And continue step-by-step by
adding the methods used to support drawing, selection, components,
manipulation, history, sets and deformation. At each step we will show the minimum
needed to implement each level of functionality so that you can decide which
level of complexity is appropriate for the particular type of shape you want to
implement. As we add virtual methods to our shape, we will discuss the purpose
and the circumstances under which the methods are called. This paper also
covers some related areas such as deformers, sets (groupParts, groupIds), and
tweaks. See this previous article for more details.

In conjunction
with the post, I have provided a number of example plug-ins. These plug-ins
demonstrate the shape implementation at various stages: draw/select, settable
components, manipulatable components, etc… The shape represented in all of
these plug-ins is extremely simple à a 4-point line segment sometimes
drawn as a hermite curve. The goal of using such a simple, immediately
understandable shape is that we didn’t want the details of the shape type to
obscure details of the implementation of the node itself.

MPxLocatorNode vs. MPxSurfaceShape

Two classes
can be used to implement a shape within Maya. The most basic is the
MPxLocatorNode class. If your shape needs to support only drawing and
selection, it is best to derive from the MPxLocatorNode class. The
MPxLocatorNode class is derived from the same nodes as Maya’s internal locator
(crosshair) node.

The Maya
Developer’s Toolkit includes an example MPxLocatorNode called
footPrintNode.cpp. This example shows that deriving from MPxLocatorNode is
quite straightforward. Typically, the only virtual methods that need to be
overridden are:

  • MPxLocatorNode::draw()
  • MPxLocatorNode::boundingBox()
  • MPxLocatorNode::isBounded()

The draw
implementation typically would contain OpenGL calls to draw the shape. The
boundingBox and isBounded methods allow Maya to determine whether or not your
object is within the camera’s view. When the object is not within the view, the
draw method will not be invoked.

Selection of
nodes derived from MPxLocatorNode works automatically based on the
implementation of the above three routines. When the mouse click/drag falls
within the bounding box returned by your shape, Maya invokes the draw method
using OpenGL selection to determine whether the shape has been selected.

If you want
your shape to include any of the following, more advanced capabilities, you
must instead derive from MPxSurfaceShape:

  • Component
    selection or manipulation
  • History
    operations
  • Sets
  • Deformers

Introduction to MPxSurfaceShape

As a learning
tool, we begin our look at the MPxSurfaceShape class by implementing the same
functionality as an MPxLocatorNode using the MPxSurfaceShape class. In other
words, this section covers what is needed to implement drawing and selection
without component
s for an MPxSurfaceShape.

An example plug-in implementing
this level of functionality has been provided with the post in the folder
called curveDrawSel. This example plug-in simply draws a
non-editable 4-point line segment, and is extremely similar to the
footPrintNode in its capabilities.

The
Developer’s Toolkit for Maya has an example MPxSurfaceShape example plug-in
called apiMeshShape. One improvement to note in the curveDrawSel plug-in over
the apiMeshShape is that we use OpenGL selection rather than simply saying that
the shape is selected any time a selection occurs within its bounding box.

The UI class: MPxSurfaceShapeUI

An MPxSurfaceShape cannot be
implemented as a single class in isolation. Instead, for any MPxSurfaceShape
that is implemented, a corresponding MPxSurfaceShapeUI class must also be
implemented. The duties of the UI class are drawing and selection. The
MPxSurfaceShape node class performs all other shape-related operations.

The MPxSurfaceShapeUI
methods that must be implemented are:

  • MPxSurfaceShapeUI::getDrawRequests()
  • MPxSurfaceShapeUI::draw()
  • MPxSurfaceShapeUI::select()

Maya documentation covers
the motivation and usage of these methods, and can be found under Developer
Resources: API Guide -> Shapes -> Drawing and Refresh
.

The shape node class: MPxSurfaceShape

Now that the
UI class is handling drawing and selection, the only required methods within
MPxSurfaceShape are:

  • MPxSurfaceShape::boundingBox()
  • MPxSurfaceShape::isBounded()

These methods
are called by Maya to determine whether or not to invoke your node’s selection
and drawing methods based on the view frustum of the camera.

Summary

If you simply
want a shape to draw itself and be selectable, use the MPxLocatorShape as your
base class. Doing the same thing with the surfaceShape class wasn’t a whole lot
more difficult, but we had to use 2 classes instead of one, and we had to
implement 2 new methods (select and getDrawRequests) to implement functionality
that is handled automatically for the locator shape.

On the plus
side, we have more flexibility. If we wanted to do some alternative tricky
implementation of select rather than OpenGL select, the surfaceShape class does
allow this sort of customization while the locatorShape does not. But the main
reason to derive from MPxSurfaceShape is so that we can expand our shape to add
component handling.

Component Handling

In this
section, we add basic component handling to the shape. The example plug-in for
this section is provided in the folder called compCurveDrawSel.
Here we add support for customizing the shape of the curve by specifying the
component locations using the setAttr command. We do not yet allow the user to
select or move the components, only to specify their locations using setAttr or
connections.

The component attribute

The first
decision to be made is how we want to represent the components within the node.
In this example, like the existing apiMeshShape.cpp example, we will store the
components in the same attribute as some existing Maya shapes: the
“controlPoints” attribute. The controlPoints attribute is inherited from the
“controlPoint” node type – one of the base classes of MPxSurfaceShape. The
controlPoint node is also a base class of Maya’s nurbsSurface, mesh,
subdivSurface, nurbsCurve and lattice shapes. The nurbsSurface, nurbsCurve and
lattice types all make use of the “controlPoints” attribute. Meshes use their
own attribute to store vertices because of an architectural decision to use
floating point rather than double storage.

 
ControlPoints
Attribute documentation from the
controlPoint node’s controlPoints attribute.

The reason
for storing the control points in an attribute is threefold. First, because the
individual xyz components are numeric attributes, the attribute can be animated
and connected to other numeric attributes. Second, the Maya file format is
organized around attributes, and the controlPoints attribute data is saved to
file. The third reason is simply consistency. All the other shape nodes within
Maya store their points in a similar manner.

Internal shape data

We could
implement our shape by storing the controlPoint data solely in the datablock of
the plug-in node. The setAttr and getAttr commands will get and set data
directly into the datablock by default. If we did store the data solely in the
datablock, each time the draw method asked for the points, our code would use
an MArrayDataHandle to get the value of the controlPoints attribute from the
MDataBlock. For such a small shape as the curve in our example plug-in, these
operations would not be very expensive. However for a large shape with many
controlPoints, accessing points from the MArrayDataHandle class can be rather
tedious.

In addition,
many shapes have more information per component than solely the position. You
may want to store per-vertex tangency or connectivity information. For this
reason, many shapes within Maya choose to store their control point position
data separately from the datablock (for example in a geometry data attribute or
in an internal class member).

We chose to
store our shape data as an internal member of the node class. At this stage of
our example, our shape data can consist simply of an MPointArray. We call it
fCurvePoints and store it as a member of our node class.

setInternalValue

In order to
transfer the values that the user sets or connects to the controlPoints
attribute into our MPointArray class member, we implement the virtual method setInternalValue.
We take the data passed in and place it into our internal array. We also call

MPxSurfaceShape::setInternalValue (plug, handle);

from our
override of setInternalValue. This call puts the controlPoint
data into the node’s datablock in addition to storing it in our MPointArray
class, and means that we do not need to implement getInternalValue. Instead, if
anyone (such as the getAttr command) asks for the value of the controlPoints
attribute, it will be handled by the DG, and will retrieve the value from the
datablock. Alternatively, if storage space was an issue, we would not need to
place the controlPoints into the datablock at all. In this case, we would
implement getInternalValue to retrieve and return the appropriate value from
our internal MPointArray.

One other
detail is necessary to keep our shape on screen up-to-date when the
controlPoints attribute is keyed (or is given some other input connection). We
need to change the method called from our UI class to retrieve the points. We
add the 2 lines shown below in bold:

void compCurveDrawSel::points(MPointArray points) {
MDataBlock block =forceCache ();
block.inputArrayValue (mControlPoints);
points.setLength (sCurvePointCount);
for (unsigned ii =0; ii < sCurvePointCount; ii++)
points.set (fCurvePoints [ii], ii);
}

This is only
necessary when there is an input connection to the controlPoints attribute
because of how dirty propagation and evaluation work within Maya. In other
words, when the user does a “setAttr” MEL command on the controlPoints
attribute, the data will be set immediately into the node and setInternalValue will be called. However, when there is
a connection to the controlPoints attribute, our node will be marked dirty
dirty, but setInternalValue does not get called unless our node
asks for the controlPoints value. When the controlPoints value is requested, it
will be dirty and will trigger evaluation of the connection. The addition of
the above 2 lines to the points method accomplishes the evaluation, and will
cause setInternalValue to be invoked, thereby updating the
fCurvePoints to the proper value.

The bounding box, an optional performance improvement

Our shape is
now controllable by the user, so its bounding box is variable. That means we
need to improve our bounding box handling. The boundingBox
method is called quite frequently — each time the window refreshes. Therefore,
we don’t want to have an expensive calculation inside the boundingBox method.

To address
this, we will add 2 attributes to our shape representing the 2 corners of the
bounding box. We make these attributes affected by the controlPoints attributes
so that they will be marked dirty when the controlPoints are modified. These
are output attributes, so we implement a compute method that calculates the boundingBox based on the location of the controlPoints.

Now, when the
boundingBox method is called, it retrieves the
bounding corners from the datablock of the node. If the bounding corners are
not dirty, this is a very fast operation that simply references into the
datablock. If the corners are dirty, compute will be invoked to recalculate and
set the bounding box.

Summary

In this
section, we simply added support for user-settable and animatable control point
locations. We didn’t have to change our UI class at all. However, we did end up
implementing 2 new virtual methods (setInternalValue
and compute) and adding attributes to our node to improve the performance
of our bounding box calculation.

Manipulation of components

In this
section, we will add the ability to select and move individual components or
sets of components using both the manipulator and the move/rotate/scale
commands. The example plug-in demonstrating this additional capability is in
the folder called compCurveMovable.

Adding
support for component motion is a lot of work. This section covers the methods
and the additional class required, their purpose and when they are called from
Maya.

The transformUsing() method

The transformUsing method is called from the transformation MEL commands
such as move, rotate and scale. It is also called from the transform-related
manipulators such as translate, rotate and scale manips.

virtual void transformUsing (
const MMatrix& mat,
const MObjectArray& componentList,
MVertexCachingMode cachingMode,
MPointArray* pointCache
);

Part of the
complexity of the transformUsing method is related to undo. The transformUsing method is called during both the original transform
command and during undo. During the initial command, the cachingMode will be
set to “save”. In undo, the cachingMode will be restored.

During
manipulator use, the cachingMode will be set to “save” initially, and “update”
subsequently. Undo of manipulations will have the cachingMode set to “restore”.
These settings can be better understood if we consider the motivation. Imagine
the user starts moving an object using the translate manipulator. In a single
mouse drag, the screen will refresh multiple times and the object will be
transformed in small increments by the many calls to transformUsing method. If the user then hits “Undo”, Maya wants to
restore the object to its original location in one operation, not multiple
calls to the transformUsing method. 

One thing to
note about our implementation of the transformUsing method is that we have implemented it
to transfer the motions directly into our class’s internal shape data
(fCurvePoints). One by-product of this is that it is now possible for our
internal data to be out of sync with the controlPoints attribute. The
manipulator links itself to the controlPoints attribute, and it needs to be
able to query the controlPoints attribute for the proper location of the point.
Therefore, we need to implement getInternalValue for our node.

The getInternalValue() method

Typically
you’ll implement getInternalValue for your node at the same time as you
implement setInternalValue. In the last chapter, we explained
why we were able to avoid implementing getInternalValue. Now there is no way to avoid it.
With the addition of transformation support, getInternalValue
is required to keep our node working right. The reason is that since transformUsing does not go through setInternalValue,
the transformation data never goes into the datablock. We implement getInternalValue so that it returns the requested
values from our internal fCurvePoints into the supplied handle argument.

The componentToPlugs() method

When a
transform manipulator is used on selected components, they call the componentToPlugs method to learn which plugs on your
shape node correspond to the selected components. The manipulator will query
these attribute values when it is determining where to draw itself.

This method is
also called when a command such as the setKeyframe command needs to determine
where to connect new animationCurves when a selected component is keyframed.

Iterator Support

Manipulators
in Maya require iterator support on shapes. This means that you need to add an
additional class, derived from MPxGeometryIterator. The following three virtual
methods also need to be added:

  • geometryIteratorSetup
  • acceptsGeometryIterator(bool)
  • acceptsGeometryIterator(MObject&,
    bool, bool)

If you are
writing multiple user-defined shapes, you may well be able to share the same
iterator class amongst your shapes, although that will depend somewhat on how
you store the data within your shape class. If each shape has a very unique
data structure, it will probably make more sense to have a separate iterator
for each type of data.

How you
design your iterator will depend on your class itself, and what it needs to
support. As the seminar continues, we will be enhancing the iterator. For this
example, it is satisfactory to simply make the iterator store a pointer to our
shape, and loop through the shape’s points. The primary thing to consider when
writing your iterator is that it should not rely on any data that could be
destructed while the iterator is using it. For example, if geometryIteratorSetup is invoked with readOnly = false,
then you can expect the setPoint method to be called more than once.
Be sure that nothing in your implementation of setPoint
could cause the data held onto by the iterator to become stale.

The UI Class

The UI class
needs to be enhanced to support drawing and selection of the individual
components separately from the shape itself. This means expanding the getDrawRequests implementation to handle the case where the 2nd
argument (objectAndActiveOnly) is false.

void compCurveMovableUI::getDrawRequests (
const MDrawInfo & info,
bool objectAndActiveOnly,
MDrawRequestQueue & queue
);

When
objectAndActiveOnly is false, we will create and add an additional drawRequest
to the queue corresponding to the point components of our shape. If any of our
components are active, we also create a drawRequest for them.

When
components are drawn, our draw method will now get called more than once. We
modify our draw method to use the MDrawRequest::token() method to determine what should be
drawn.

When the user
selects in component mode, our select method needs to be able to handle it. In
component select mode, our shape will be hilited, and we use this as a cue to
determine whether or not to check for selected points.

The matchComponent method

Having
implemented all of the above, your node already supports the selection tool and
the transformation manipulators. But what if you manually enter the select or
move command and pass it a string that corresponds to your vertices?  The matchComponent method is used by Maya to create a
component corresponding to a string typed in by the user.

Simple History Support

The next
modification we will make to our shape is to support “simple” history. By
simple history, what we mean is that we are going to modify our shape so that
any node that outputs a data of type “vectorArray” can be provided as input to
our node. The figure below shows an example where we’ve connected the
particleShape’s position attribute into our curve node’s inputSurface
attribute.

History
The particleShape position drives the
location of the curve’s points.

By referring
to this as “simple” history, we are distinguishing it from more advanced types
of history covered in the next chapter such as deformer support and support for
component sets with history (groupParts nodes).

Two forms of input: history and tweaks

The trickiest
part of basic history support on a shape is that there are now two sources of
input for the position of the shape’s components.  Positions may be coming from the history
attribute.  In addition, the user can
make “tweaks” to the points, i.e. select and move them around or animate their
position. This data comes in as before, through the controlPoints attribute. As
a node designer, you must make a decision how you want to combine the history
data with the tweak data. 

In writing
this example shape, we chose to implement tweak handling in the same manner as
Maya’s internal shapes (nurbs, meshes, subds, lattices). These nodes all treat
tweaks as relative when the shape has history, and absolute when
the shape does not have history. The benefit of this approach is that the
tweaks made by the user can be combined with the input history even when the
history is animated.

 
Tweak_radius
Adding tweaks on sphere: radius=1              Modifying history: radius=1.5

An
alternative approach would be to store the tweaks as absolute, regardless of
whether the shape has history. However, that would mean that when the history
changed, the tweaked points would need to either:

  • lose
    their tweaks to follow the history, or
  • stay
    at the position defined by the tweaks, ignoring the new history

Keep in mind that tweaks can be animated/connected
as well, so resetting the tweak attribute values on the fly as the history
changes is not really an option unless you want to prevent connections to them
when there is history. In the long run, how you implement your shape depends on
what you are trying to accomplish.

Shape Input Data

Thus far in
our example plug-ins, we have stored the shape data as a simple type,
MPointArray. In continuing this example, adding support for history might seem
like the appropriate time to switch our data to a more complex type, derived
from MPxGeometryData. Indeed, if we want to support component sets and
deformers, we must store our geometry data as an MPxGeometryData type. However,
at this stage in the example, we want to demonstrate the use of internal Maya
data types as the history attribute, so we will continue storing our data as an
MPointArray, and define our inputSurface attribute to accept data of type
MFnData::kVectorArray.

The example plug-in

The example
plug-in for this chapter is in the folder called compCurveSimpHist.
There is a MEL script in the folder called compCurveSimpSetup.mel which creates
the curve and attaches it to a particle shape. If you run the setup MEL script
and push play, you can see the curve points move along with the particles. You
can select points on the curve and tweak them to see how the tweaks interact
with the history.

Adding the input surface attribute

The most
obvious requirement is that we now need an input surface attribute. As
mentioned before, we are using an input data type of MFnData::kVectorArray for
the inputSurface. The inputSurface affects the boundingBox, and we will make it
an internal attribute, so that any time we evaluate it, we can transfer the
values from the input data into a local data member we’ll call fInputPoints.

When we add
the input attribute, it is also useful to override the virtual method:

MObject compCurveSimpHist::localShapeInAttr() const;

This method
is called from a number of places such as:

  • the attribute editor, when it
    builds its related node tabs
  • the channel box when it builds
    its “Inputs” list
  • the delete construction history
    operation (delete –ch MEL command)

Handling relative vs. absolute tweaks

The other
primary job to be done to support history is to put in our history-dependent
handling for tweaks.

Internal
Data

Our previous
implementation of the shape had only one piece of internal data: the curve
points in absolute space. Now we will instead store 2 pieces of data:

  • fCurvePoints (the history plus
    the tweaks)
  • fInputPoints (the history)

We could
store a third array with the tweaks, but instead we will store the tweak data
directly in the datablock. There is no obvious advantage or disadvantage to
storing it in the datablock, but it does serve as an example of a different way
to store the data. You can decide how you want to store the tweaks in your own
node.

setInternalValue
and
getInternalValue

We modify
these virtual methods. They already had handling for the controlPoints
attribute, but we modify them so that when the shape has history, the data will
go directly to the datablock.

transformUsing

We implemented
transformUsing in the previous chapter. Its
responsibility is to tranfer motion from transform commands/manips into the
shape, and it bypasses setInternalValue. Since we are storing our tweaks in
the datablock when there is history, we need a way to transfer the motion from transformUsing into the datablock. We add a utility method called
updateControlPoints to do this, and call it from transformUsing
when there is history.

Getting the points for drawing/boundingBox

When we are
asked for the bounding box or need to draw the points, we want to make sure we
always have the most up-to-date representation of them. We add the lines shown
below to our boundingBox method and our points
method:

if (fHasHistory) {
block.inputValue (inputSurface);
applyTweaks (block);
}

If the
history has changed, inputSurface will be dirty, and the call to inputValue
will invoke evaluation and a call to setInternalValue to make sure our fInputPoints are
up-to-date. The applyTweaks method will then add all of the tweaks
from the datablock to the fInputPoints.

The mHasHistoryOnCreateAttribute

One of the
attributes we inherit from the base class is the “mHasHistoryOnCreate”
attribute. This is an internal attribute which you can use to your advantage if
you want to know during file IO whether your shape will have history or not.
This is useful because during file IO, connectAttrs always come after setAttrs.
So when setInternalValue gets called for the controlPoints
attribute and you want to have different handling for history vs. non-history,
you can’t simply look for the connection to see if the shape has history or
not. By implementing setInternalValue and getInternalValue
for “mHasHistoryOnCreate” we are able to know during file IO whether there will
be history on our shape.

Because there
are several places in our node where we want to know whether or not there is
history, we also add overrides of the virtual methods

  • connectionMade
  • connectionBroken

This allows
us to set a boolean flag indicating whether or not there is history. This way
of storing whether there is history is optional, but checking this flag is
quite a bit more efficient than checking for input connections to the
inputSurface attribute.

Summary

Until now,
all of our previous examples have been fairly cut and dry. If you wanted to
make a user-defined shape with movable components, you basically have to follow
the basic plan outlined in chapters II-V. Where the creativity and design on
your part really starts coming into play is when you start adding support for
history. The implementation presented here is one way to go, and closely mimics
how shapes inside of Maya are implemented.

Deformer Support – Background Info

The term
deformer refers to any type of node derived from the base class “geometryFilter”.
Examples of deformers are lattices, clusters, nonlinears, and MPxDeformerNodes.
When you add a deformer to a shape, a number of things happen:

  • The shape is copied. This copy of
    the shape is called the “intermediate object” and is placed in the history
    of the shape. (Subsequent deformers will reuse the existing intermediate
    object. New copies will not be added.)
  • a set gets created to store the
    components affected by the deformer
  • a tweak node is created to store
    subsequent tweaks made to the shape
  • the deformer node and any related
    auxiliary objects are created

A typical
dependency graph set-up including deformers is shown below:

 
Deformer_shape
Hypergraph view of deformers in
the history of a shape

At the far
right, pSphereShape is the deformed shape node. At the far left,
pSphereShapeOrig is the intermediate object. The intermediate object is
typically hidden (unless you disable its intermediateObject attribute), and is
at the head of every chain of deformers. It serves as a storage place for the
original undeformed positions of the shape.

In a typical
deformer chain, shape data flows out of the intermediate object and then flows
through each of the deformers. In this example, the deformers that are
traversed are tweak1, cluster1 and ffd1. In this example, only a single shape
flows through ffd1. However, most deformers can operate on any number of shapes
since the input and output geometry attributes of the deformer base class are
multis. The two exceptions to this rule are skinClusters and the wrap node.
Because of implementation details, the skinCluster and the wrap node can
operate on only a single geometry. Additional geometries connected to the
skinCluster and the wrap will be ignored.

Sets on shapes with history

Each deformer
is associated with a single set. It will deform only the points that are
included in its set. If you add points to a deformers using the sets command,
the deformer will automatically be wired up to the construction history of the
shape. Similarly, if you remove points from a set, the deformer will stop
acting upon those points.

Most sets in
Maya are composed of only a single node: the objectSet node. However, component
sets in Maya are different. Component sets are stored within the shape data
rather than within the objectSet node. The set is connected to a groupId node
per shape, and a groupParts node per “construction-historied”
shape.  If you look at the history of a
shape in the hypergraph, by default Maya will hide the groupParts and the groupId
because they clutter the view and are not of interest. If you turn on display
of “auxiliary nodes”, the groupParts and groupId will be drawn.

Deformer_dg_group
Hypergraph view showing the groupParts, groupId and objectSet node for an ffd
deformer. Note that the tweak node was deleted from this shape’s history in order
to make the figure less cluttered.

The groupId
node is simply a means of storing an id that is unique within the scene for
fast look-up of the membership of the set within the shape’s list of sets.

The
groupParts node stores component sets for shapes with construction history.
This is because as the shape data flows through the history, it may change its
numbering. Component lists are stored using the vertex index and some
construction history nodes modify the vertex numbering – for example, “polySplit”,
“deleteComponent”, etc. Because the groupParts is in the construction
history, it remains immune to the vertex numbering changes. It receives the
shape data before the vertices are renumbered. This architecture has proved
useful for certain poly modeling operations.

Tweak Nodes

A tweak is
the name given to translations of vertices on shapes in Maya. Shapes without
construction history are able to add tweak deltas directly into their vertex
position data. However, shapes with construction history need to store the
tweaks as deltas that get added to the shape data coming in through the history
attribute.

When a
deformer is added to a shape in Maya, we do lot of work to prevent tweaks from
existing on the shape. Why? In the previous chapter, we discussed how shapes
store tweaks as relative moves which get added to the history. The example
below shows how such a relative tweak would get added into a smooth skinned
surface.

  Tweak_node
Left: Salty the seal’s
initial state.

Middle: Salty’s nose was
tweaked (think Pinocchio).

Right: The neck joint is
rotated, causing the skin to deform. The tweak does not get deformed since is
added after the deformation.

In the above
picture (left), the user has skinned the seal shape to a skeleton so that
rotating the various joints creates a smooth skinning deformation. In a later
plot twist, Salty is beset by a strange Pinocchio-like spell. The user needed
to make Salty’s nose grow, and accomplished this by translating a few vertices
over in X. For this example, there is no tweak node, so the vertex deltas are
stored on the shape node. It all looks ok until we rotate the neck. Now since
the tweaks get added after the skinning deformation, the deltas move the nose
vertices over in X even though Salty’s nose is no longer aligned with the
X-axis.  In order for Salty to rotate his
head successfully, we need the skinning algorithm to act on Salty’s tweaked
nose, instead of having the tweaks act on Salty’s skinned nose.

To address
this issue, whenever there are deformers in the history of a shape, a tweak
deformer is created automatically and inserted at the front of the deformer
chain. If the shape being deformed had existing tweaks or animated CVs, the
tweaks & animation are moved onto the tweak node. Otherwise, the tweak node
is left empty until the user grabs some cvs and moves them (or keys them). New
tweaks are also added to the tweak node (unless the user manually deletes it,
in which case we revert to the old behavior).

Deformer Support – Implementation

In this
section, we’ll present the changes necessary to upgrade our example plug-in so
that it will support deformers. The example code referred to here is in the
directory called compCurveDeformable. To try out this shape, use
the MEL script compCurveDefSetup to create this shape. Then add some deformers
to your shape, and move some vertices around.

Geometry Data

In order to
support either sets or deformers, it is necessary for the shape-related
attributes on the shape to be geometry data. You could use one of Maya’s
internal geometry data types, such as mesh data. Or you can write your own type
of geometry data derived from MPxGeometryData. For the purposes of this
example, we created our own data type, compCurveData. The data itself contains
a simple MPointArray. When implementing your own type of data, you need to
implement a number of virtual methods to support file io, geometry iteration
over the data, copy, and set creation.

Inside our
example plug-in, you will see that we have gotten rid of all of our internal
data (fInputPoints and fCurvePoints). We are now storing all of our data in the
datablock in the geometry attributes described next.

The change
from storing the data internally as a class member to storing it in a data type
required a lot of small changes to the code. Basically every place where we
were using fInputPoints, we now use the point array that is stored in the data
for the input shape. Every place where we were using fCurvePoints, we now use
the point array stored in the data for the cached shape.

Geometry Attributes

Any
deformable geometry requires at a minimum the following three attributes:

  • input shape
  • cached shape
  • local output shape
  • world output shape

The input
shape is where history gets connected into your node, as in the previous
example. The cached shape is the input shape plus the tweaks – generally
treated to be your node’s private storage place. The local and world output
shape attributes give other nodes the ability to connect downstream from your
shape’s data. The world output shape simply bundles the world matrix of the
shape with the local shape data. The shape data itself is still in local space.

Geometry Attribute Overrides

4 virtual
methods should be implemented to return the attributes corresponding to the
above 4 attributes:

  • localShapeInAttr
  • cachedAttr
  • localShapeOutAttr
  • worldShapeOutAttr

Of these
attributes, 2 are outputs and required us to add to our existing override of
the compute method to handle our 2 new outputs.

These
attributes are used in commands that want to know about history/future shape
attributes on your node. For example, the listHistory command will utilize
these methods to find the history or future (listHistory –future) of your node.
When a deformer is attached to a shape, it connects to the attribute returned
by the worldShapeOutAttr of the intermediate object and to the
localShapeInAttr of the deformed object.

Overrides required for set support

Three methods
must be added to our shape to support the creation of sets of our components:

  • match: This method is used by the set creation code to
    determine whether the selected component type is valid for inclusion in a
    vertex-restricted sets. Since deformer sets must be vertex-restricted,
    your shape must override match to specify that its control point
    components satisfy the vertex restriction.
  • createFullVertexGroup: This method is used by the set
    creation code when it wants to create a set containing all of the control
    points in the entire shape. An example of when this would occur is if you
    select your shape as an object (rather than as in component-mode), and
    create a deformer. The deformer’s set would contain all of the vertex
    components of your shape as defined by the createFullVertexGroup method
    implementation.
  • geometryData: This method is used by the set
    creation and editing code to access the geometry data. Set information for
    components is stored with the geometryData.

Tweak node support

You must
override the tweakUsing method to support movement of
controlPoints on shapes with tweak nodes in their history. The tweakUsing method is fairly similar to the transformUsing method, but it allows the tweaks to be transferred to
the tweak node rather than directly into the shape data itself.

Conclusion

Writing a
custom shapes is not trivial. However, if you start simple, and then add more
details, it’s not so difficult. We recomment that you build up your custom
shape piece by piece and get each part of it solid before you proceed to the
next. Hopefully the example plug-ins and material presented in this article will
provide a good starting point for this process.


Comments

2 responses to “Custom Shapes in the Maya API”

  1. Martin Karlsson Avatar
    Martin Karlsson

    I’m extending OpenMayaMPx.MPxLocatorNode. How do I know when the node is selected, please?
    Goal is to draw lines with different color when selected.

  2. Hi Martin,
    We posted a utility (with source code) recently on http://labs.autodesk.com/utilities/ADN_plugins in case you want to see a full sample (LocatorLib utility).
    All is done in the draw() method, something like this:
    void locatorLibBase::draw (M3dView &view, const MDagPath &path, M3dView::DisplayStyle style, M3dView::DisplayStatus status) {
    view.beginGL () ;
    if ( (style == M3dView::kFlatShaded)
    || (style == M3dView::kGouraudShaded)
    ) {
    // Push the color settings
    glPushAttrib (GL_CURRENT_BIT) ;
    if ( status == M3dView::kActive )
    view.setDrawColor (13, M3dView::kActiveColors);
    else
    view.setDrawColor (13, M3dView::kDormantColors);
    myShadedDraw () ;
    glPopAttrib () ;
    }
    myWireFrameDraw () ; // Draw the outline of the locator
    view.endGL () ;
    }
    The ( status == M3dView::kActive ) will tell you if it is selected or not, and like above you decide what colors you want.
    Now this is for the legacy viewport. If you also want to support the Maya Viewport 2.0, you need another class and do something similar again. So you would need to derive an additional class from the MHWRender::MPxDrawOverride class and use the draw()’ MHWRender::MDrawContext argument.
    Hope that helps,

Leave a Reply

Discover more from Autodesk Developer Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading