Are you one of the people who’d kill for a curvature shader for Maya? Good news, you don’t have to!

Although polygons are no match for mathematically defined NURBS surfaces when it comes to serious design surfacing, in sketch modelling and concept development they’re killing it. To get the best speed to surface quality ratio when dealing with polygons I created this curvature shader for Maya. It is an ultimate tool for highlighting lumps and imperfections in your model as well as achieving better control over surface transitions and radii.

If you found this article while searching for “Maya curvature shader”, there’s quite a big chance you already found HW shader by Byron or Mentalray shader by Tom Cowland. Originally I though I’d do just fine with Byron’s shader, but I ended up with completely new plugin when trying to improve performance and display quality.

### Computing vertex curvature

The both previously mentioned shaders are using very common method of calculating mesh curvature by measuring angle between face normal and vertex normal. The angle is then divided by perimeter of the face which results in higher curvature values in areas with increased density such as radii. Also thanks to this step the displayed curvature stays the same regardless of subdivision level of the mesh.

Since this method measures curvature per-vertex-per-face, with the same vertex the value differs for each adjacent face. This causes somewhat jagged appearance, which doesn’t improve even with increased subdivision levels. The easiest way to fix this problem is to average values of each vertex.

Here’s another problem. The angle between face normal and vertex normal ranges from 0° to 90° without any indication whether we’re dealing with a positive or a negative value. Luckily we can get this information by comparing orientation of angle plane normal and a reference plane normal.

Curvature shader based on vertex-to-face normal angle has one major weakness which shows in areas where for example a tight positive radius crosses a larger negative radius. This issue was the main reason I came up with algorithm which uses adjacent edge instead of face normal.

This method beats the previous one in several ways. The angle between vertex normal and adjacent edge falls between 0° and 180°, where value < 90° indicates negative curvature while value > 90° is a sign of positive curvature. It also handles better the previously mentioned scenario of two radii with different size and orientation since the average curvature is weighted by individual edge lengths.

### Preparation

Although *MPxHwShaderNode::geometry()* method provides points and vectors of the rendered mesh in function parameters, it doesn’t give us any information about connections between vertices, which is crucial for this plugin. Fortunately we can get DAG path of the rendered mesh with *MPxHwShaderNode::currentPath()* which we’re going to call inside *MPxHwShaderNode::bind()*.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
MStatus CurvatureShader::bind(const MDrawRequest& request, M3dView& view) { MStatus status(MStatus::kSuccess); m_path = currentPath(); if (!m_path.hasFn(MFn::kMesh)) return MStatus::kFailure; if (request.displayStyle() == M3dView::kPoints || request.displayStyle() == M3dView::kWireFrame) return MStatus::kFailure; MGLFunctionTable* gl = getGL(status); view.beginGL(); gl->glPushAttrib( GL_ALL_ATTRIB_BITS ); view.endGL(); return status; } |

Before calculating the surface curvature we have to generate a smooth mesh according to its smooth display options.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
MStatus CurvatureShader::generateSmoothMesh(const MDagPath& path, MObject& meshData) { MStatus status(MStatus::kSuccess); MFnMesh meshFn(path, &status); MMeshSmoothOptions smoothOptions; status = meshFn.getSmoothMeshDisplayOptions(smoothOptions); MFnMeshData meshDataCreator; meshData = meshDataCreator.create(&status); MPlug displaySmooth = meshFn.findPlug("displaySmoothMesh", &status); (displaySmooth.asInt() > 0) ? meshFn.generateSmoothMesh(meshData, &smoothOptions, &status) : meshFn.copy(meshFn.object(), meshData, &status); return status; } |

### Implementation

Once we have the smooth mesh, the easiest way to calculate curvature values is using *MItMeshVertex* to query vertex position, normal, and connected points to define adjacent edges.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// Query shader's attributes double scale = MPlug(thisMObject(), aScale).asDouble(); MRampAttribute map(thisMObject(), aColorMap, &status); // Generate and query the rendered mesh MObject meshData; status = generateSmoothMesh(m_path, meshData); MFnMesh meshFn(meshData, &status); MPointArray vtxPoints; status = meshFn.getPoints(vtxPoints, MSpace::kWorld); int numVtx = meshFn.numVertices(); MVectorArray vtxNormals; MColorArray vtxColors; vtxNormals.setLength(numVtx); vtxColors.setLength(numVtx); |

The rest is simple vector math. Using current vertex position and a connected point we construct a vector for each adjacent edge and calculate an angle to vertex normal. Since the angle is in 0-π range we need to offset it so that π/2 (zero curvature) becomes 0 (to achieve symmetric scaling for positive and negative values). After applying edge length and custom scale we calculate an average vertex curvature which needs to be offset back to get correct values from *MRampAttribute.*

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
MItMeshVertex vertexIt(meshData, &status); for (vertexIt.reset(); !vertexIt.isDone(); vertexIt.next()) { int i = vertexIt.index(&status); status = vertexIt.getNormal(vtxNormals[i], MSpace::kWorld); MIntArray connectedVertices; status = vertexIt.getConnectedVertices(connectedVertices); int numConnected = connectedVertices.length(); double vtxCurvature = 0.0; for (int e = 0; e<numConnected; e++) { MVector edge = (vtxPoints[connectedVertices[e]] - vtxPoints[i]); double angle = acos(vtxNormals[i].normal()*edge.normal()); double curvature = (angle / M_PI - 0.5) / edge.length() * scale; vtxCurvature += curvature; } vtxCurvature = vtxCurvature / numConnected + 0.5; map.getColorAtPosition(float(vtxCurvature), vtxColor[i], &status); } |

### Rendering

With previous code we got colors, positions and normals of all mesh vertices which we can now use to draw triangulated mesh with OpenGL

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
MIntArray triCounts; MIntArray triIndices; status = meshFn.getTriangles(triCounts, triIndices); gl->glBegin(GL_TRIANGLES); for (unsigned i = 0; i < numVtx; i++) { int vtxId = triIndices[i]; double position[4]; double normal[3]; float color[3]; vtxPoints[vtxId].get(position); vtxNormals[vtxId].get(normal); vtxColors[vtxId].get(color); gl->glNormal3dv(normal); gl->glColor3fv(color); gl->glVertex3d(position[0], position[1], position[2]); } gl->glEnd(); |

### Conclusion

This article covers the most important part of making a mesh curvature shader such as quering mesh properties, calculating vertex curvature and OpenGL rendering. Since this plugin is computing values for thousands of vertices I used some additional steps to improve performance, which you can check in source files.

The shader only works with Legacy viewport with texturing enabled.

great job

How to install this tool?

Hi Malom,

to be completely honest this version is more for developers as it is not quite complete. I’m planning a release of a proper version soon. Stay tuned.