If you are wondering why your fancy geometry created with MPxSurfaceShape is not working in renderers like Arnold, then this article is for you. Before rendering them, you'll need to create a translator or shader for your renderer.
For Arnold, it can be handled using extensions. To make a 3rd-party plugin work with Arnold, you'll need to write translator extension for it.There are three SDKs you are going to refer to in your project. Arnold SDK, mtoa SDK and Maya SDK.
First, you'll need to download Arnold SDK from here. I am using 184.108.40.206 when writing this sample on windows platform. The SDK might be changed in the future, if you find the sample is not working anymore, please let me know.
Arnold extensions are dynamic libraries. So, we'll need to create a dll project in Visual Studio. Two functions are required to export in your extension. They are initializeExtension and deinitializeExtension. For the apiMeshShape extension, we only need to implement intializeExtention and leave deinitializeExtension blank.
DLLEXPORT void initializeExtension ( CExtension& extension )
DLLEXPORT void deinitializeExtension ( CExtension& extension )
CExtension::RegisterTranslator takes 4 parameters, first is the Maya node type name. For apiMeshShape, it is apiMesh. The second parameter is the Arnold extension name, it can be empty. The third one is a creator function pointer which like Maya creator functions, returning an instance of your translator class. The forth one is optional, which is the node initialize function.
Then we are going to write our translator. You'll need to create Arnold nodes based on your nodes info. There are different kinds of Arnold nodes, like procedural, bump2d, nurbs, etc... We are going to create an Arnold polymesh node for the apiMeshShape and fill the attributes with apiMeshShape's geometry.
class CApiMeshShapeTranslator:public CShapeTranslator
virtual AtNode* CreateArnoldNodes();
virtual void Export(AtNode* polymesh);
virtual void Update(AtNode* polymesh);
void Export(AtNode* polymesh, bool isUpdate);
void ExportRenderParameters(AtNode* polymesh);
void ExportMeshData(AtNode* polymesh);
void ExportMeshShaders(AtNode* polymesh);
static void* creator()
cout << "ApiMeshShapeTranlator loaded." <<endl;
return new CApiMeshShapeTranslator();
We have to implement at least three functions to make apiMeshShape visible in Arnold. They are CreateArnoldNodes, Export and Update.
CreateArnoldNodes will create a basic arnold type node. We are going to create and return a new polymesh here.
Export and Update are related functions, Arnold will call export at the beginning to export attributes which are not going to be changed during the IPR session and calling update later when attributes changes happened. In our scenario, geometry won't be changed, so geometry is going to be exported while lightning, shaders and transforms are going to be always updated. So, I've created an overload function for Export like below:
void CApiMeshShapeTranslator::Export(AtNode* polymesh, bool isUpdate)
//Export rendering parameters
//Export light linking
// We could enable/disable motion deform here, but I'll ignore it and make it never deforming.
m_motionDeform = false;
//If it is an update call, we won't need to export geometry again.
ExportMatrix, ProcessRenderFlags and ExportLightLinking are provided by Arnold itself. If you want your shape to have motion deform, you'll need to set m_motionDeform true and implement ExportMotion. Maya provids source code for xgenTranslator. It is a good reference for how to implement motion in Arnold. In this sample, I am going to ignore it.
During the export, I am going to export shaders first. Just provide the MObject of the dagNode to the Arnold, it will do the querying job for you.
void CApiMeshShapeTranslator::ExportMeshShaders(AtNode* polymesh)
//Get dag node from dagPath
//Get shader node from the dagNode and set it to shader attribute.
auto shadingGroup = GetNodeShadingGroup(fnDagNode.object(), 0);
auto shader = ExportConnectedNode(shadingGroup);
AiNodeSetPtr(polymesh, "shader", shader);
Then, I'll need to export the meshData from apiMeshShape. apiMeshShape uses apiMeshGeom to store the geometry data and I'll get them from it. To make it possible, we'll need to modify apiMeshShape sample a little bit: making meshGeom() exported in the .lib file.
//Modify apiMeshGeom* meshGeom(); in the apiMeshShape.h
__declspec(dllexport) apiMeshGeom* __cdecl meshGeom();
Now we can get the apiMeshGeom easily in our translator. There are normals, vertices, faceCount, faceIds provided with the apiMeshGeom(UVs are not available for the sphere in the sample). So, we are going to fill nsides, vidxs, nidxs, vlist and nlist attribute of the polymesh node.
void CApiMeshShapeTranslator::ExportMeshData(AtNode* polymesh)
// Usually this kind of method should be with an extra parameter, step for motion blur.
// I'll ignore it in here to make the sample as simple as possible.
// Get apiMeshShape node first
auto apiMeshNode = dynamic_cast<apiMesh*>(fnDag.userNode());
if(NULL == apiMeshNode)
MGlobal::displayError("Failed to convert apiMeshShape");
auto geom = apiMeshNode->meshGeom();
auto verticesArray = geom->vertices;
unsigned int numVertices = verticesArray.length();
unsigned int numNormals = geom->normals.length();
unsigned int numPolygons = geom->faceCount;
auto vertices = new float[verticesArray.length() * 3];
auto normals = new float[numNormals * 3];
// Get raw vertices
for (int i = 0; i < numVertices; ++i)
vertices[i*3] = verticesArray[i].x;
vertices[i*3 + 1] = verticesArray[i].y;
vertices[i*3 + 2] = verticesArray[i].z;
// Get raw normals
// We need to export normals if it is smoothShading and doesn't have arnold subdivision properties. For apiMeshShape, export always.
for (int i = 0; i < numNormals; ++i)
normals[i*3] = geom->normals[i].x;
normals[i*3 + 1] = geom->normals[i].y;
normals[i*3 + 2] = geom->normals[i].z;
// We always export the first frame.
// Please check XGen's sample for how to export shapes in motion.
// We are going just export basic geometry infos(vertices, normals),
// there could be other info like references or color sets etc... I'll skip it here.
AtArray* uvs = nullptr;
AtArray* nsides = nullptr;
AtArray* vertexIndices = nullptr;
AtArray* normalIndices = nullptr;
// Component IDs
// Vertices per polygon count
nsides = AiArrayAllocate(numPolygons,1,AI_TYPE_UINT);
unsigned int polygonVertexCount = 0;
for(unsigned int i = 0; i < numPolygons; ++i)
auto numPolygonVertexCount = geom->face_counts[i];
polygonVertexCount += numPolygonVertexCount;
AiArraySetUInt(nsides, i, numPolygonVertexCount);
// Vertex indices and normal indices
vertexIndices = AiArrayAllocate(polygonVertexCount, 1, AI_TYPE_UINT);
normalIndices = AiArrayAllocate(polygonVertexCount, 1, AI_TYPE_UINT);
unsigned int vertexIndex = 0;
for(unsigned int polygon_id =0; polygon_id < geom->face_connects.length(); ++polygon_id)
AiArraySetUInt(vertexIndices, polygon_id, geom->face_connects[polygon_id]);
AiArraySetUInt(normalIndices, polygon_id, geom->face_connects[polygon_id]);
// Set array parameters
AiNodeSetArray(polymesh, "vlist", AiArrayConvert(numVertices * 3, 1, AI_TYPE_FLOAT, vertices));
AiNodeSetArray(polymesh, "nlist", AiArrayConvert(numNormals * 3, 1, AI_TYPE_FLOAT, normals));
AiNodeSetArray(polymesh, "nsides", nsides);
AiNodeSetArray(polymesh, "vidxs", vertexIndices);
Finally, here is Export and Update functions.
void CApiMeshShapeTranslator::Update(AtNode* polymesh)
void CApiMeshShapeTranslator::Export(AtNode* polymesh)
After building it, copy the ArnoldTranslator.dll into mtoadeploy/2017/extension folder. After reloading the mtoa plugin in Maya, you can find the apiMesh is rendered in Arnold Renderer. Hooray! Solidangle also provides a tutorial on their website. You can also use it as a reference.
The full sample code is available on the GitHub, here is the link.