MPxHwShaderNode's - Adding real time shaders to Maya

 

 

 

Maya 4.0 was the first version of Maya to come with the ability to overide the rendering within the viewport. In my opinion, the first implimentation was a nasty hack rushed out to make maya comparable to max and xsi's shader systems (which are better).

Since version 4, the Hardware Shaders in Maya have been greatly improved, however the interface is still lacking in some areas (specifically textures). Therefore expect the MPxHwShaderNode class to undergo another revision before too long...

So then, the main aim of a HW shader is to allow you to customise the drawing of surfaces within the viewport so that it can use the most up to date OpenGL extensions and shading languages. These shaders can be used for Hardware renders, however they cannot be used for normal maya or mental ray renders.

 

 

 

   

 

 

Defining the node

To make use of a hard ware shader, you must first derive a class from MPxHwShaderNode. As a simple example, the following shader aims to recreate the standard openGL lighting model as a maya shader.

There is a lot of code here, trust me; it's not as complicated as it looks and will be explained in detail shortly....

 

 

 


	#include <maya/MString.h>
#include <maya/MPlug.h>
#include <maya/MDataBlock.h>
#include <maya/MDataHandle.h>
#include <maya/MFnNumericAttribute.h>
#include <maya/MFloatVector.h>
#include <maya/MPxHwShaderNode.h>
#include <GL/gl.h>
#include <GL/glu.h> //---------------------------------------------------------------------------------------------
/// \brief This shader aims to re-create the very basic OpenGL material properties, ie
/// we want to support the following params for glMaterial*()
///
/// GL_AMBIENT
/// GL_DIFFUSE
/// GL_SPECULAR
/// GL_EMISSION
/// GL_SHININESS
///
///
///

class BasicHwShader

: public MPxHwShaderNode
{
public:
/// \brief The post constructor must be used to set any additional connections between
/// nodes. During the constructor, maya will not have yet added the node into the
/// scenegraph. This means that any attempt to use a function set on the node will
/// fail.
///

virtual void postConstructor(); /// \brief The compute function is called to calculate the value of a specific
/// output attribute. This usually only happens when an input value changes
/// on which the output was dependant on; or when the node is forced to be
/// recomputed because it has been flagged as dirty.
/// \param plug - the output attribute to calculate
/// \param data - the data from which you MUST read the attributes back from.
/// \return Did it succeed or fail?
///
/// \note YOU MUST ONLY EVER READ THE NODE'S ATTRIBUTES BACK FROM THE DATA BLOCK
/// GIVEN TO YOU. YOU MUST NOT ATTEMPT TO USE ANY FUNCTION SETS, AND YOU MUST
/// NOT ACCESS ANY OTHER NODES WHILST IN THE COMPUTE FUNCTION.
///
/// THE COMPUTE FUNCTION IS THE ONLY FUNCTION ON OUR NODE, THAT IS LIKELY TO
/// BE USED DURING RENDERING. BY ACCESSING ONLY THE ATTRIBUTES VIA THE DATA
/// BLOCK, MAYA ENSURES THAT RENDERING REMAINS THREAD SAFE. DUPLICATE
/// DATA BLOCKS CAN BE PASSED TO TWO OR MORE PROCESSORS. THIS MEANS THAT NO
/// SHARED DATA WILL BE ACCESSED BY DIFFERENT THREADS. ie, EACH THREAD RECIEVES
/// IT'S OWN COPY OF THE DATA.
///
/// IF YOU DO START ACCESSING GLOBAL DATA, OR DATA FROM OTHER NODES WITHIN THE
/// COMPUTE FUNCTION - IT WILL PROBABLY WORK WHILST YOU'RE WORKING INSIDE MAYA,
/// HOWEVER - IT IS LIABLE TO CRASH WHEN RENDERING, ie WHEN MULTIPLE PROCESSORS
/// ARE USED.
///

virtual MStatus compute( const MPlug&, MDataBlock& ); /// \brief We overload this function to provide the real time openGL rendering for
/// our objects. This allows us to therefore render the geometry in any way
/// we wanted. For example, we might want to make use of CG shading, bump
/// mapping etc.
/// All geometry to be rendered by this method will be provided to you
/// in openGL vertex array format.
/// \param path - DAG path to the object, get any transform info here...
/// \param prim - the primitive type to be rendered.
/// \param writable - This is a mask that tells us which arrays can be safely
/// overwritten. For example, the Dot3 bump shader requires
/// us to place the texture space light vectors into the colour
/// array for rendering.
/// \param indexCount - the number of vertex indices in the indexArray
/// \param indexArray - the index array
/// \param vertexCount - the number of items in the vertexArray, normalArray and
/// each colour and tex coord array provided.
/// \param vertexIDs - This array is only provided if you overload the provideVertexIDs()
/// so that it returns true.
/// \param vertexArray - the vertex array data. Each vertex will have 3 float values
/// \param normalCount - The number of normal arrays provided.
/// \param normalArrays- normalArrays[0] = the normals
/// normalArrays[1] = the tangents
/// normalArrays[2] = the bi-normals
/// \param colorCount - the number of colour arrays provided
/// \param colorArrays - colourArrays[0] = first colour array
/// colourArrays[1] = second colour array, etc etc
/// \param texCoordCount- The number of texture coord arrays provided.
/// \param texCoordArrays- The tex coord arrays provided for rendering. Used to support
/// multi-texturing.
/// \return Did it succeed or fail?
///

virtual MStatus glGeometry( const MDagPath&,
int prim,
unsigned int writable,
int indexCount,
const unsigned int * indexArray,
int vertexCount,
const int * vertexIDs,
const float * vertexArray,
int normalCount,
const float ** normalArrays,
int colorCount,
const float ** colorArrays,
int texCoordCount,
const float ** texCoordArrays); /// \brief You need to overload this function to tell maya how many colours you
/// want Maya to provide you with.
/// \return The number of colour arrays you want maya to give you
///

virtual int colorsPerVertex(); /// \brief This tells maya how many sets of Normals you want passed to the normalsArray
/// parameter of glGeometry(). put simply if you return....
/// 0 - no normals
/// 1 - normalsArray[0] will hold normals, use this if you have no bump mapping
/// 2 - normalsArray[1] will also hold tangents. Use this if you are going to /// calculate the bi-normals in a vertex shader
/// 3 - normalsArray[2] will also hold the bi-normals. Use this if you can't /// calculate the binormal in a vertex shader
/// \return 0 to 3
///

virtual int normalsPerVertex(); /// \brief This tells maya how many uv coord arrays you would like for your shader.
/// \return the num of UV coord arrays you would like
///

virtual int texCoordsPerVertex(); /// \brief Used to tell maya if your shader has transparency. This is needed so that maya
/// knows that the poly data should be sorted from front to back.
/// \return true if has a transparency
///

virtual bool hasTransparency(); /// \brief If this function returns true, then the vertexIDs parameter of glGeometry will
/// hold an array of integers that provide mappings to the original vertex id's
/// of the points array.
/// \return true or false
///

virtual bool provideVertexIds(); public:
/// \brief This function returns a new instance of our shader
/// \return A new shader instance
///

static void * creator(); /// \brief This function sets up the attributes on the shader node
/// \return ok or fail
///

static MStatus initialize(); public: // ------------ The Type Info ------------ /// The specific ID for this node type
const static MTypeId m_TypeId; /// The type name for this new node type
const static MString m_TypeName; private: // ------------ Internal Funcs ------------ /// \brief An internal utility function to retrieve a float attribute from this
/// node.
/// \param oAttr - one of the static MObject attribute references on the node
/// \param val - the returned float value
///

void GetFloat(MObject oAttr, float& val) const; /// \brief An internal utility function to retrieve a colour attribute from this
/// node.
/// \param oAttr - one of the static MObject attribute references on the node
/// \param val - the returned colour value, RGB only!
///

void GetColour(MObject oAttr, float val[]) const; // ------------ The attributes ------------
/// The ambient Colour of the material

static MObject m_aAmbientColor; /// The diffuse colour of the material
static MObject m_aDiffuseColor; /// The specular colour for the material
static MObject m_aSpecularColor; /// The emission colour for the material
static MObject m_aEmissionColor; /// The shininess of the material
static MObject m_aShininess;
};

 

 

 

Threading Issues

One of the things you see with Nodes within Maya, is that they aim to be safe for use on mulitple processors. To do this, the node must only access data stored within it's own attributes provided to us within a data block.

When writing a HW shader however, we need to access other nodes connected to it, namely fileTexture nodes. This means that we break the rules to have a thread-safe node.

This means that we have to inform Maya that this node is not safe for multi-processing. We can do this by calling setMPSafe(false) within the postConstructor for the node.

Note: we use the postConstructor to do any node initialisation. Do not use the nodes constructor since Maya would not have finished creating the node at that point.

In addition, we also need to provide a creator function for the node

 

 

 



void BasicHwShader
::postConstructor()
{
// tell maya that this node is not safe for multiple processors.

setMPSafe(false);
} void * BasicHwShader::creator()
{
return new BasicHwShader();
}

 

 

 

Adding The Attributes

The next thing we have to do is to add the attributes to the shader node. This is all done within the initialize() function. For this node we simply need the MFnNumericAttribute function set to add some colours and a float.

Having created our 5 attriubtes, we need to add them to the node definition by calling addAttribute(). You will then notice that we set up a set of attribute dependecies with the attribute outColor.

Infact, the MPxHwShaderNode contains 4 static MObject colour attributes,

outColor
outTransparency
outMatteOpacity
outGlowColor

There's not much to be said about them though since this node is not used in a software render....

 

 

 




/// The ambient Colour of the material
MObject BasicHwShader::m_aAmbientColor; /// The diffuse colour of the material
MObject BasicHwShader::m_aDiffuseColor; /// The specular colour for the material
MObject BasicHwShader::m_aSpecularColor; /// The emission colour for the material
MObject BasicHwShader::m_aEmissionColor; /// The shininess of the material
MObject BasicHwShader::m_aShininess;

//

MStatus BasicHwShader::initialize()
{
// we will use a numeric attribute function set to add all of
// the attributes to the node description.

MFnNumericAttribute nAttr; // create the ambient colour attribute
m_aAmbientColor = nAttr.createColor( "ambientColor", "ambc");
nAttr.setStorable(true);
nAttr.setKeyable(true);
nAttr.setDefault(0.3f, 0.3f, 0.3f); // create the diffuse colour attribute
m_aDiffuseColor = nAttr.createColor( "color", "c");
nAttr.setStorable(true);
nAttr.setKeyable(true);
nAttr.setDefault(0.7f, 0.7f, 0.7f); // create the specular colour attribute
m_aSpecularColor = nAttr.createColor( "specularColor", "sc");
nAttr.setStorable(true);
nAttr.setKeyable(true);
nAttr.setDefault(1.0f, 1.0f, 1.0f); // create the emission colour attribute
m_aEmissionColor = nAttr.createColor( "incandescence", "ic");
nAttr.setStorable(true);
nAttr.setKeyable(true);
nAttr.setDefault(0.0f, 0.0f, 0.0f); // create the materials shininess attribute
m_aShininess = nAttr.create( "cosinePower", "cp",
MFnNumericData::kFloat,
10.0f);
nAttr.setStorable(true);
nAttr.setKeyable(true);
nAttr.setMin(0.0f);
nAttr.setMax(90.0f); // add the various attributes to the node
addAttribute(m_aAmbientColor);
addAttribute(m_aDiffuseColor);
addAttribute(m_aSpecularColor);
addAttribute(m_aEmissionColor);
addAttribute(m_aShininess); // set the attribute dependencies
attributeAffects(m_aAmbientColor, outColor);
attributeAffects(m_aDiffuseColor, outColor);
attributeAffects(m_aSpecularColor, outColor);
attributeAffects(m_aEmissionColor, outColor);
attributeAffects(m_aShininess, outColor); // done
return MS::kSuccess;
}

 

 

 

 

Specifying the Vertex Data

When specifying a hardware shader, we can optionally ask maya for various vertex arrays for colours, normals, tangents, bi-normals, uv sets etc. This data will later be provided to you in the glGeometry function as a series of floating point arrays.

The following functions should be defined :

 

 

 


// use this to return the number of colour arrays your shader expects to recieve. // In this case, the shader will not rewuire any //

int BasicHwShader::colorsPerVertex() {
return 0;
} // This function allows us to recieve the models normals, tangents and bi-normals // 0 = no normal data // 1 = normals only // 2 = normals & tangents. The bi-normal is then usually calculated as the cross product // of the tangent and normal within a vertex shader or program // 3 = normals, tangents and bi-normals //
int BasicHwShader::normalsPerVertex() {
return 1;
} // return the number of uv sets you expect on the model.
int BasicHwShader::texCoordsPerVertex() {
return 1;
}
// return true if the object is transparent

bool BasicHwShader::hasTransparency() {
return false;
} // All Hardware shaders in maya are provided with vertex data in openGL vertex array format. // This format differs from the internal format of Maya's meshes. You may have some per vertex // data assigned to the mesh that you require for you shader. You will however need a way to find // out which vertex was used for a specific vertex array position. By returning true from this // function, you will recieve an array of integers. This array will be the same size as the vertex // array, and each value will be the index into the original meshes points array. //
bool BasicHwShader::provideVertexIds() {
return false;
}

 

 

 

 

Accessing The Attribute Values

Normally with a user defined node, you'd access the data via the datablock within the compute function. Again, since most of our shader work will happen within glGeometry and not the compute, then we need a way of accessing the data.

This can be done by initialising an MPlug to this node and specified attribute. The value can then be queried via the plug. To aid us later, we will therefore define a couple of utility functions to help us...

 

 

 


// retrieves the specific attributes value as a float
void BasicHwShader::GetFloat(MObject oAttr, float & val) const
{
// create a plug to the attribute on this node

MPlug plug(thisMObject(), oAttr); // use the plug to get the float attribute
plug.getValue(val);
} // retrieve the colour for the specified attribute
void BasicHwShader::GetColour(MObject oAttr, float val[]) const
{
// create a plug to the attribute on this node

MPlug plug(thisMObject(), oAttr); // use the plug to get the compound attribute
MObject object;
plug.getValue(object); // attach a numeric attribute function set to the color
MFnNumericData fn(object); // get the data from the color and wang in val
fn.getData(val[0], val[1], val[2]);
}

 

 

 

 

Rendering The Geometry

The actual rendering of the geometry occurs within the glGeometry() function. This will be provided with a set of vertex arrays, the exact numbers of which is defined by the functions colorsPerVertex, normalsPerVertex etc.

We then just need to

 

 

 


// draw me some geometry using vertex arrays...
MStatus BasicHwShader::glGeometry( const MDagPath& path,
int prim,
unsigned int writable,
int indexCount,
const unsigned int* indexArray,
int vertexCount,
const int * vertexIDs,
const float * vertexArray,
int normalCount,
const float ** normalArrays,
int colorCount,
const float ** colorArrays,
int texCoordCount,
const float ** texCoordArrays)
{
// these vars will hold the values stored in the nodes attributes

float Ambient[] = {0.3f, 0.3f, 0.3f,1.0f};
float Diffuse[] = {0.7f, 0.7f, 0.7f,1.0f};
float Specular[] = {1.0f, 1.0f, 1.0f,1.0f};
float Emission[] = {0.0f, 0.0f, 0.0f,0.0f};
float Shininess = 10.0f; // get the colours and shininess from the node
GetColour(m_aAmbientColor, Ambient);
GetColour(m_aDiffuseColor, Diffuse);
GetColour(m_aSpecularColor, Specular);
GetColour(m_aEmissionColor, Emission);
GetFloat(m_aShininess, Shininess); // save the current OpenGL and vertex array settings
glPushAttrib(GL_ALL_ATTRIB_BITS);
glPushClientAttrib(GL_CLIENT_VERTEX_ARRAY_BIT); // set the openGL material properties
glMaterialfv(GL_FRONT_AND_BACK,GL_AMBIENT,Ambient);
glMaterialfv(GL_FRONT_AND_BACK,GL_DIFFUSE,Diffuse);
glMaterialfv(GL_FRONT_AND_BACK,GL_SPECULAR,Specular);
glMaterialfv(GL_FRONT_AND_BACK,GL_EMISSION,Emission);
glMaterialf(GL_FRONT_AND_BACK,GL_SHININESS,Shininess);
// if we have normals, enable them
if(normalCount>0)
{
glEnableClientState(GL_NORMAL_ARRAY);
glNormalPointer( GL_FLOAT, 0, normalArrays[0] );
} // if we have tex coords, enable them
if(texCoordCount>0)
{
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glTexCoordPointer( 2, GL_FLOAT, 0, texCoordArrays[0] );
} // enable the vertex array
glEnableClientState(GL_VERTEX_ARRAY);
glVertexPointer( 3, GL_FLOAT, 0, vertexArray ); // draw the vertex arrays using the indices provided
glDrawElements( prim, indexCount, GL_UNSIGNED_INT, indexArray ); // restore the previous OpenGL settings
glPopClientAttrib();
glPopAttrib(); return MS::kSuccess;
}

 

 

 

 

 

The Compute Function

The compute function for a hardware shader, will only ever be called to evaluate the colour for the Attribute Editors swatch display. There is little point in us trying to make this compute work like a software shader - it will never be used in that context!

The best we can do is just set the outColor to the diffuse colour so that the user has some idea of what the shader looks like....

 

 

 


MStatus BasicHwShader::compute (const MPlug& plug, MDataBlock& data)
{
// Check that the requested recompute is one of the output values
//

if( (plug != outColor) &&
(plug.parent() != outColor) )
return MS::kUnknownParameter; // get the input colour handle
MDataHandle inputData = data.inputValue(m_aDiffuseColor); // use that to get the color value
const MFloatVector & color = inputData.asFloatVector(); // get the outColor handle
MDataHandle outColorHandle = data.outputValue( outColor ); // get a reference to the output colour value
MFloatVector& outColor = outColorHandle.asFloatVector(); // set the output colour to the input colour
outColor = color; // clean the data block
data.setClean( plug );
return MS::kSuccess;
}

 

 

 

Registering The node and User Classification

Registering this node is much the same as any other custom node. One notable difference is that we also present Maya with a user classification string for our node. The string catagorises the node into a set of sections within the creation tabs of the multilister and hypershade.

 

 

 


 #include "BasicHwShader.h"
 #include <maya/MFnPlugin.h>


 // This is a nasty bit of hackyness for compilation under Windows. Under Win32 you need 
 // to compile a dll project and change the extension from "dll" to "mll". One additional
 // thing we have to do is 'export' the initializePlugin and uninitializePlugin functions. 
 // This basically means that when maya loads the dll, it can see the two Functions it needs.
 // If for some reason your plugin fails to load, it may be this thats causing the problems.
 // Under linux we simply need to compile it with the -shared flag.
 // 
 #ifdef WIN32
     #define MLL_EXPORT __declspec(dllexport) 
 #else
     #define MLL_EXPORT
 #endif  
 
 //------------------------------------------------------------------- 
 /// \brief initializePlugin( MObject obj )
 /// \param obj - the plugin handle
 /// \return MS::kSuccess if ok 
 /// \note Registers all of the new commands, file translators and new
 /// node types. 
 /// 
 MLL_EXPORT MStatus initializePlugin(MObject obj ) { 
 	MFnPlugin plugin( obj, "Rob Bateman", "1.0", "Any");  

// a classification for where the node will appear on the create menus
// of the multilister and hypershade.

const MString UserClassify( "shader/surface/utility" ); // register the mel command with the plugin function set. // Do this for each mel command your plugin is going to add into Maya // MStatus status = plugin.registerNode( BasicHwShader::typeName, BasicHwShader::typeId, BasicHwShader::creator , BasicHwShader::initialize, MPxNode::kHwShaderNode, &UserClassify ); if (!status) { status.perror("Failed to register \"basicHwShader\"\n"); return status; } return status; } //------------------------------------------------------------------- /// \brief uninitializePlugin( MObject obj ) /// \param obj - the plugin handle to un-register /// \return MS::kSuccess if ok /// \note un-registers the plugin and destroys itself /// MLL_EXPORT MStatus uninitializePlugin( MObject obj ) { MFnPlugin plugin( obj ); // deregister the mel command with the plugin function set // Do this for each mel command your plugin has added into Maya. // status = plugin.deregisterNode( BasicHwShader::typeId ); if (!status) { status.perror("failed to deregister \"basicHwShader\"\n"); return status; } return status; }

 

 

 

 

 

Adding an Icon

Our node was registered under the name "basicHwShader", to provide an icon for this node, we simply need to create a 32x32 xpm image (gimp can do this). Save the icon as "render_basicHwShader.xpm" and copy it to your ~/maya/6.0/prefs/icons directory.

The icon should automatically be used next time you load the plugin. Easy ;)

 

 

 

 

 

 

The Attribute Editor Template

So then, the good old attribute editor template. This is simply used to define the user interface of the attribute editor when our Hardware Shader is selected.

You will notice the call to AEswatchDisplay with the name of our node. This will render a little sphere with ourr shader applied. This is infact the only time that our compute function will be called.

We also call AEmentalrayPhotonAttrs to place all of the mental ray attributes added by default into a nice little layout.

The rest is pretty much standard, just simply add a control for each of the attributes we want to see, and supress all other attributes we don't want to see....

This script should be saved as AEbasicHwShaderTemplate.mel and placed within your ~/maya/scripts directory.

 

 

 



// file:    ~/maya/scripts/AEbasicHwShaderTemplate.mel
//
global proc AEbasicHwShaderTemplate( string $nodeName )
{
// this creates the little swatch sample at the top of the attribute editor
AEswatchDisplay $nodeName; // the following controls will be in a scrollable layout
editorTemplate -beginScrollLayout; // add a bunch of common properties
editorTemplate -beginLayout "Common Material Attributes" -collapse 0;
editorTemplate -addControl "ambientColor";
editorTemplate -addControl "color";
editorTemplate -addControl "diffuseColor";
editorTemplate -addControl "incandescence";
editorTemplate -endLayout; // create some specular shading options
editorTemplate -beginLayout "Specular Shading" -collapse 0;
editorTemplate -addControl "specularColor";
editorTemplate -addControl "cosinePower";
editorTemplate -endLayout; // hide some output and internal node attributes
editorTemplate -suppress "outColor";
editorTemplate -suppress "outTransparency";
editorTemplate -suppress "outMatteOpacity";
editorTemplate -suppress "outGlowColor";
editorTemplate -suppress "enableHwShading"; // mental ray photon shader attributes
AEmentalrayPhotonAttrs $nodeName; // include/call base class/node attributes
AEdependNodeTemplate $nodeName; // add any extra attributes that have been added
editorTemplate -addExtraControls;
editorTemplate -endScrollLayout;
}