3.4 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 36

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 37: 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 with the same MPxShaderOverride shader key. 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 38: 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 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:

NoteThe 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().

Draw Context

MDrawContext provides read-only access to the device state at draw time.

The primary advantages of using this class are that state is cached to avoiding recomputation, and data queries will in general not access the GPU device. This helps to avoid expensive pipeline stalls. For example a plug-in may make a call to get the model view and projection matrices in OpenGL:

float tmp[4][4];
glGetFloatv(GL_MODELVIEW_MATRIX, &tmp[0][0]);
glGetFloatv(GL_PROJECTION_MATRIX, &tmp[0][0]);

The following calls can be made instead:

HWRender::MDrawContext& context;
MStatus status;
MMatrix transform = context.getMatrix(MHWRender::MDrawContext::kWorldViewMtx, &status);
MMatrix projection = context.getMatrix(MHWRender::MDrawContext::kProjectionMtx, &status);

Data that is accessible may update at different frequencies. Viewport size is an example of per frame data. A modeling matrix is an example of data per render item.

The state manager (MStateManager) is only accessible from this class, as only when an MDrawContext is active can state is modified.

Additional semantic information is provided in the form of a “pass context” (MPassContext). Currently an identifier for a pass and a list of semantics can be queried. The pass identifier can be an internally generated one or one which is specified by a render loop override. See 3.6 Render Loop Overrides for more information.

Items in the semantic list provide a progressively refined set of information as to the state or location within a pipeline. Some examples include: