Effect Overrides

MPxShaderOverride is the API entry point to override all shading and lighting for a plug-in surface shader in Viewport 2.0. Similar to MPxGeometryOverride, this class does not define the Maya node for the shader. Since this class overrides the full draw, the override is completely responsible for defining and binding resources such as textures, lights and geometry. An instance of MDrawContext (draw context) is provided at appropriate points to allow access to device information to facilitate these tasks. Since this class requires raw draw calls, it is not draw API agnostic. Separate code paths must be created for DirectX and OpenGL if support for both APIs is desired. Viewport 2.0 support for the Maya CgFX and dx11Shader plug-ins is implemented using this interface.

Implementations of MPxShaderOverride must be associated with specific types of shading nodes. In most cases, a plug-in defines a shading node and a separate MPxShaderOverride is written to provide draw code for objects using the shader. The CgFX plug-in defines the CgFX node using the MPxHwShaderNode interface and a separate MPxShaderOverride exists to support drawing in Viewport 2.0. MPxShaderOverride implementations must be registered with MDrawRegistry using a classification string. Shaders with classification strings that satisfy the override classification are drawn using the override. The classification string must begin with "drawdb/shader" to be recognized by the system. Maya creates one instance of the registered shader override for each instance of the associated shader type that is actively used in the scene.

Figure 40

At a high level, MPxShaderOverride has two main tasks. It specifies the geometry streams it needs to draw (geometry requirements) and then it draws objects to which it has been assigned. This demonstrates the producer-consumer relationship of the new rendering model. This class produces geometry requirements (MGeometryRequirements), which are consumed by the geometry system (internal class or plug-in implementations of MPxGeometryOverride) in order to produce geometry streams. Then, the draw method of this class consumes the geometry streams in order to draw.

Figure 41: A sample configuration of a shader override assigned to two different DAG objects. One DAG object is using a geometry override. The other is using an internal geometry updater. This configuration can be an example of a NURBS surface updater. The shader override “produces” requirements for each updater. Each object “produces” new geometry to render. This geometry is passed down the pipeline in the associated render items until it is “consumed” by the shader override.

The three main phases that the override must implement are initialization, update and draw.

The phases of MPxShaderOverride are triggered for execution when an instance of the associated shader type is bound to an object or when the input attributes of the shader itself change. No special logic needs to be added to trigger an update. The update methods that are called depend on the type of change that occurs. New assignments of the shader trigger full rebuilds. Similarly, if rebuildAlways() returns true, then an attribute change also triggers a full rebuild. In all other cases, initialization is skipped and only update will occur. The draw phase happens on every refresh where the shader needs to draw an object.

During the initialization phase, the geometric stream requirements can be specified using addGeometryRequirement(). The requirements specify the geometric streams that are required from objects to which the given shading effect is assigned. If no requirements are specified, then a single position stream requirement is used. The initialize() method must return a string representing the shader key. It often happens that different instances of the MPxShaderOverride represent essentially the same shader, but with different shader parameters. The shader key is used to identify the MPxShaderOverride instances representing the same shader. To optimize rendering, the renderer makes an effort to consolidate the rendering of objects based on the properties returned from the MPxShaderOverride. This includes the shader key, but can also include other factors. Refer to Consolidation considerations below for further details about consolidation and geometry handling. This allows the plug-in to perform its setup only once for the entire sequence. It is the responsibility of each plug-in to decide on the meaning of representing the same shader.

Figure 42: The shader key provided by the shader override can be the same for two render items. In this case, consolidation may “merge” these items into a single render item before drawing occurs.

During initialization, if the current display mode is non-textured display mode, an internally defined static shader instance is used for all render items that use the shading node associated with a given shader override. This is a performance optimization to avoid any additional node monitoring, as well as to allow render items that share this shader instance to be consolidated.

To override this behavior, the MPxShaderOverride::nonTexturedShaderInstance() method can be overridden to return a custom shader instance instead of the default shared instance. A return parameter can be set to indicate if this shader instance needs to be updated upon node attribute changes. If no monitoring is required, then Maya will attempt to skip the update phase while in non-textured mode. It is possible that an update is still required for the shader that is used for textured mode display. For example, if MPxShaderOverride::rebuildAlways() returns true, then the update phase would be called regardless of the options set with this method.

During the update phase, all data values required for shading are updated. The interface has an explicit split between the point at which the dependency graph can be accessed (updateDG()), and the point at which the draw API (OpenGL or DirectX) can be accessed (updateDevice()). Any intermediate data can be cleaned up when endUpdate() is called. As an example, the override may require input from an attribute on a given node.

The override can provide a hint as to whether shading involves semi-transparency. This hint can be provided by overriding the isTransparent() method which gets called between updateDevice() and endUpdate().

Some advanced shading effects, like displacement, may alter the size of the object being shaded. In this case it is desirable to adjust the bounding box of the object to prevent it from being frustum culled at the wrong time. This can be accomplished by overriding the boundingBoxExtraScale() method. Note that this does not allow the override to provide an absolute bounding box for an object. Instead the override provides a scale factor which will be applied to the computed bounding box. The reason for this is because many shaders may affect the same object and each shader cannot know the requirements of all the others.

The draw phase is performed by the pure virtual draw() method. This method returns true if it is able to draw successfully. If it returns false, then drawing is done using the default shader used for unsupported materials. Drawing is deliberately not intermixed with data update. At the point when drawing is called, all evaluation should have been completed. If there is user data that needs to be passed between the update and drawing phases, the override must cache that data itself. It is an error to access the Maya dependency graph during draw, and attempts to do so may result in instability. Although it is possible for implementations of the draw() method to handle all shader setup and geometry draw, the expected use of the draw() method is for shader setup only. Then, drawGeometry() is called to allow Maya to handle the geometry drawing. If manual geometry binding is required, it is possible to query the hardware resource handles through the geometry on each render item in the render item list passed into the draw() method.

The activateKey() and terminateKey() method are also invoked in the draw phase each time a render item is drawn with a different shader key. The activateKey() and terminateKey() methods can be used to optimize rendering by configuring the rendering state only once for a batch of draw() calls that are sharing the same shader key. For three shader overrides (A,B and C) that all return the same shader key, a sample sequence of invocations is as follows:

NOTE:The terminateKey() callback is always invoked on the same MPxShaderOverride instance as the one used to invoked the activateKey() callback.

All draw methods have access to the draw context through the MDrawContext parameter. This, along with the texture manager, should be used to manage state and textures wherever possible. Using these interfaces (as opposed to making raw draw API calls) ensures better performance and avoids problems with device state corruption.

The handlesDraw() method is called before activateKey(), draw() and terminateKey(). This method allows override for the determination of whether the override will handle the drawing based on the current draw context. For instance, it may choose to handle drawing for a color pass but not a shadow map creation pass. If false is returned from this method then activateKey(), draw() and terminateKey() will not be called.

The Maya SDK example hwPhongShader provides a simple example of how to use of MPxShaderOverride. The plug-in defines the node using MPxHwShaderNode and implements MPxShaderOverride in the class "hwPhongShaderOverride" to provide Viewport 2.0 support. The override's draw() method shows both methods of drawing (controlled by a compile time constant). The first case sets up the shader and then calls drawGeometry() to perform the draw. The second case manually performs all the geometry binding after the shader set up and manually makes the OpenGL call glDrawElements() to perform the draw.

For more detailed examples of using MPxShaderOverride, please see the CgFX plug-in (cgFxShaderNode.h/.cpp) and the dx11Shader plug-in (dx11ShaderOverride.h/.cpp). dxShaderOverride contains an example of using handlesDraw().

Consolidation considerations

Usage of MShaderInstance

If the override uses shader instances, then a unique global set should be used instead of keeping a shader instance per override. Refer to Consolidation considerations for shader instances for more details.


If consolidation is not desired, this can be indicated by overriding the:

MPxShaderOverride::handlesConsolidatedGeometry() virtual method

If this is not set, then the shader options are used to determine if items can be consolidated.

Refer to the dx11Shader, glslShader, and hwPhongShader developer kit plug-ins. It is also exposed as a semantic in the first two examples. See Semantics and annotations supported by the dx11Shader and glslShader plug-ins in Viewport 2.0).

Beyond shader key uniqueness, usage of custom data also affect render item consolidation, as this data is used to determine MRenderItem uniqueness. That is, render items with different data pointers indicate that the items cannot be consolidated. This should be taken into account if support for consolidation is desired.

Extracting Consolidation Information

A shader may be required to extract per Maya object information at draw time.

One way to accomplish this is to force the render items using the shader to not be consolidated. The result is a set of shader setup and draw calls per object. This behaviour can be achieved by marking items as not to be consolidated based on: the shader key, the usage of custom data, or the MPxShaderOverride::handlesConsolidatedGeometry() override.

An alternative to forcing separation at consolidation time is to allow render items to be consolidated and wait until draw time to extract path and geometry information.

For passes that do not require per object extraction, such as a shadow map pass, the default behaviour of drawing the entire consolidated geometry can be preserved.

For passes that do require extraction, the method MRenderItem::isConsolidated() should first be checked to see if a render item contains consolidated geometry. If so, the method MRenderItem::sourceIndexMapping() can be used to get a list of Maya DAG paths and associated index ranges. The index ranges indicate where the per-path geometry resides within the consolidated geometry data buffers available at draw time.

As the method MRenderItem::sourceDagPath() can only return one path, it is not suitable for extracting consolidated geometry paths.

The following sample code from the hwPhongShader plug-in shows the basic logic:

const MHWRender::MRenderItem* renderItem = renderItemList.itemAt(renderItemIdx);
const MHWRender::MGeometry* geometry = renderItem->geometry();

MHWRender::MGeometryIndexMapping geometryIndexMapping;
if (renderItem->isConsolidated())

    // Extract information if the geometry was consolidated
    for (int i=0; i<geometryIndexMapping.geometryCount(); i++)
        // Pull out the dag path, any component, and the start and end   
        // indexing into the geometry buffers.
        MDagPath path = geometryIndexMapping.dagPath(i);
        MObject comp = geometryIndexMapping.component(i);
        int indexStart = geometryIndexMapping.indexStart(i);
        int indexLength = geometryIndexMapping.indexLength(i);
    // Have unconsolidated geometry. The indexing thus spans the entire
    // range of the buffers returned.
    MDagPath sourceDagPath = renderItem->sourceDagPath();

For a concrete example, consider a scene with two objects: pPlaneShape1 and pPlaneShape2, which are assigned the same plug-in shader (hwPhong). The image shows the topology (vertex and faces ids) for each object. Triangulation is shown with dotted lines.

In order to access the geometric data per object, the indexing information must first be extracted. The indexing information can be returned by examining the geometry (MGeometry) for the returned render item (MRenderItem) at draw time:

MRenderItem renderItem; // Passed at draw time.

// Get the geometry 
const MHWRender::MGeometry* geometry = renderItem->geometry();

// Get the index type
MHWRender::MGeometry::Primitive indexPrimType = renderItem->primitive();
MString indexPrimTypeName = MHWRender::MGeometry::primitiveString(indexPrimType);
const MHWRender::MIndexBuffer* buffer = geometry->indexBuffer(0);

// Get the data type
MString dataType = MHWRender::MGeometry::dataTypeString(buffer->dataType()); 

// Get the size of the index buffer
int indexCount = geometry->indexBufferCount();  

// Dump the indexing for debugging purposes. Note that when geometry is consolidated, the
// index can be unsigned short.
MHWRender::MIndexBuffer* nonConstIB = const_cast<MHWRender::MIndexBuffer*>(buffer);
if (buffer->dataType() == MHWRender::MGeometry::kUnsignedInt32)
    const unsigned int *ptr = (const unsigned int*)nonConstIB->map();
    for (unsigned int i=0; i<indexBufferCount; i++)
        const unsigned int index = ptr[i];
        fprintf(stderr, "-- index[%d] = %d\n", i, index);
    const unsigned short *ptr = (const unsigned short*)nonConstIB->map();
    for (unsigned int i=0; i<indexBufferCount; i++)
        const unsigned short index = ptr[i];
        fprintf(stderr, "-- index[%d] = %d\n", i, index);

In the case where consolidation is possible between the two objects, the returned information is shown below. Plain text and bold are used to show how the indices are related to the original objects. Bold is used to indicate pPlaneShape1.

Indexing Primitive Type = Triangles,

Index type = Unsigned Int 16,

Index count = 18

Index array:

index[0] = 0

index[1] = 1

index[2] = 3

index[3] = 3

index[4] = 1

index[5] = 2

index[6] = 4

index[7] = 5

index[8] = 7

index[9] = 7

index[10] = 5

index[11] = 6

index[12] = 8

index[13] = 9

index[14] = 11

index[15] = 11

index[16] = 9

index[17] = 10

The consolidation information provided for the geometries indicates that:

  • The first geometry has path=|pPlane2|pPlaneShape2, with an index start=0, and index length=12. This indexing is longer since we have two quads (or 4 triangles) in this geometry.

  • The second geometry has a path=|pPlane1|pPlaneShape1, with an index start=12, and index length=6. This indexing is for the single quad (2 triangles).

NOTE:16-bit indexing (unsigned short) can be returned when geometry is consolidated, versus 32-bit indexing.

The following is the result of a dump of the vertex and face identifier information:

vertexid[0] = 0.000000

faceid[0] = 0.000000
vertexid[1] = 1.000000 faceid[1] = 0.000000
vertexid[2] = 4.000000 faceid[2] = 0.000000
vertexid[3] = 3.000000 faceid[3] = 0.000000
vertexid[4] = 1.000000 faceid[4] = 1.000000
vertexid[5] = 2.000000 faceid[5] = 1.000000
vertexid[6] = 5.000000 faceid[6] = 1.000000
vertexid[7] = 4.000000 faceid[7] = 1.000000
vertexid[8] = 0.000000 faceid[8] = 0.000000
vertexid[9] = 1.000000 faceid[9] = 0.000000
vertexid[10] = 3.000000 faceid[10] = 0.000000
vertexid[11] = 2.000000 faceid[11] = 0.000000

The first 8 entries for each data buffer is associated with pPlaneShape2, while the last 4 entries for each buffer are for pPlaneShape1.

By following the indexing provided to look up the data buffers, the following vertex ids per triangle are obtained:

[0,1,3], [3,1,4], [1,2,4], [4,2,5], [0,1,2], [2,1,3]

The first 4 triangles are from pPlaneShape2, and the last 2 are from pPlaneShape1.

The corresponding face id lookup would produce results as follows:

[0,0,0],[0,0,0],[1,1,1],[1,1,1], [0,0,0], [0,0,0]

The first 2 triangles from pPlaneShape2 belong to face 0, the second 2 triangles from pPlaneShape2 belong to face 1, and the last 2 triangles belong to face 0 on pPlaneShape1.

NOTE:Face and vertex identifier information is performed by querying vertex streams with the appropriate vertexid and faceid semantics during initialization of the shader.

Drawing logic in Legacy Default Viewport (Viewport 1) versus Viewport 2.0

Viewport 1 does not have an equivalent to geometry consolidation; however, it may attempt to send back a series of geometry to draw for each shader. This is an optimization to avoid always binding and unbinding shaders.

The calling pattern for the MPxHwShaderNode interface would be as follows:

  1. glBind() to bind the shader
  2. glGeometry()
    1. Bind streams for path A
    2. Bind indexing for path A
    3. Draw
  3. glGeometry()
    1. Bind streams for path B
    2. Bind indexing for path B
    3. Draw
  4. More glGeometry() calls per object instance
  5. glUnbind() to unbind the shader

The calling pattern for the Viewport 2.0 MPxShaderOverride interface instead would be as follows:

  1. activateKey() to bind the shader instance
  2. draw()
    1. Bind streams for consolidated geometry
    2. If per index range drawing desired:
      1. Set the index range
      2. Draw
    3. Otherwise choose some custom drawing logic.
  3. terminateKey() to unbind the shader

As there is only one draw with all the information in Viewport 2.0:

  • This results in a single binding of consolidated geometry versus one per object.
  • This provides flexibility for the plug-in to ultimately decide how to draw the geometry, as opposed to the Viewport 1 logic, which calls back to the plug-in a fixed number times with a predetermined set of index buffer and data buffer streams.

Per-Frame Work

It is possible that per-frame work exists that is common to all shaders. In this case, there are a few options available:

  • Within the context of an MPxShaderOverride, calls to obtain the frame stamp can be used (MDrawContext::getFrameStamp()). As an example, to avoid expensive lighting re-computation during multiple calls to the shader, the glslShader and dx11Shader plug-ins only perform the computation upon frame stamp changes.

  • A Viewport 2.0 notification callback can be used at a global level. (MRenderer::addNotification(MHWRender::MPassContext::kBeginRenderSemantic)).

  • The per-viewport MUiMessage callbacks (add3dViewPreRenderMsgCallback()) are still available for Viewport 2.0 at a global level.

Frame and draw context

For information regarding frame and draw context, see Frame and draw contexts.