The Stitch plugin might not be a tool for everyday use but it’s better to have it and not need it, than need it and not have it.

I’ve spent last few months building car interiors in Maya. Because I mostly made models for initial sketch phase, I never cared about stitches. One day my colleague Alex showed me his model of a car seat. It had split lines and stitches all over it created with his own MEL script and my eyes almost popped out. I’ve immediately decided to take it to the next level and create something a bit more powerfull and elegant – the Stitch plugin. For those who’re looking for something reliable there’s Stitch from Ticket01. For those who want to save 249€ and have some fun there’s this article covering few challenges I had to face during the project.

### Introduction

The Stitch plugin is basicaly a bit more advaced tool for duplicating objects along a curve. Although I could simply model a single stitch and duplicate it along a curve I’ve decided to define the stitch parametricaly and use combination of mesh normal and curve tangent to define the stitch orientation. I also added the posibility to use an actual maya geometry instead of my predefined stitch. I’m planing to add more features in the future, so stay tuned.

### Project Curve on Mesh

If we want the stitches to follow a curve on a surface, we first have to figure out how to project a nurbs curve on a mesh surface. Maya built-in function *Project Curve on Mesh* is out of question since it is based on unsmoothed mesh and also not available in API (except through MEL command). Let’s do it the hard way then.

Lets first smooth the base mesh to ensure the projected curve is going to follow it nicely. We’ll get the base mesh from a node’s input attribute and smooth it in-place with the following helper function.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// Simple in-place mesh smoothing function MStatus StitchNode::generateSmoothMesh(MObject& mesh, int divisions) { MStatus status(MStatus::kSuccess); MFnMesh meshFn(mesh); MMeshSmoothOptions smoothOptions; smoothOptions.setDivisions(divisions); smoothOptions.setKeepBorderEdge(false); meshFn.generateSmoothMesh(mesh, &smoothOptions, &status); CHECK_MSTATUS_AND_RETURN_IT(status); return status; } |

As for the curve projection, it is actually pretty easy to project a single point on mesh via MFnMesh::getClosestPoint() function. So we’ll simply divide the curve and find the closest point on mesh to each sample point.

### But…

Unfortunately this only works when the mesh surface doesn’t have any gaps/holes and is larger than the original curve. To check whether the sampled point actually lies above the surface we first take the surface normal of the closest point and shoot a ray from the original point back to the surface.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Find closest point on mesh, returns true if the original point lies over the mesh ////////////// bool StitchNode::closestPointOnMesh(MObject& mesh, MPoint& point, MVector& normal){ MFnMesh meshFn(mesh); // First get the closest point on the surface and mesh normal MPoint closestPoint; meshFn.getClosestPointAndNormal(point, closestPoint, normal, MSpace::kObject); // Shoot a ray from original point and look for a collision MFloatPoint fClosestPoint; MMeshIsectAccelParams accelParams = meshFn.autoUniformGridParams(); int hitFace, hitTriangle; float hitBary1, hitBary2, hitRayParam; bool overMesh = meshFn.closestIntersection(point, normal, NULL, NULL, false, MSpace::kObject, 99999.9f, true, &accelParams, fClosestPoint, &hitRayParam, &hitFace, &hitTriangle, &hitBary1, &hitBary2, (float)1e-6); point = closestPoint; return overMesh; } |

If we ignore all the points that lie outside the surface, we end up with a projected curve that doesn’t start on the boundary of the mesh even though the original curve is crossing it. That’s why we keep the points that come immediately before or after the points lying inside the surface. This solution isn’t completely accurate though – points on the boundary are not exact projection of the curve.

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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// Project curve on mesh MStatus StitchNode::projectCurveOnMesh(MObject& mesh, MObject& curve, int samples, MObjectArray& projectedCurves) { MStatus status(MStatus::kSuccess); projectedCurves.clear(); // Distribute samples along the curve MFnNurbsCurve curveFn(curve, &status); double domainStart = 0; double domainEnd = curveFn.length(); double distance = (domainEnd - domainStart) / samples; MPointArray curvePoints; bool onSrf = false; for (int i = 0; i < (samples + 1); i++) { MPoint point; MVector normal; status = findPointAtLength(curve, (domainStart + distance*i), point); // Get the closest point and info about its position bool prevOnSrf = onSrf; onSrf = closestPointOnMesh(mesh, point, normal); // If two points in the row lie outside the surface, delete the first one if (!prevOnSrf && !onSrf) curvePoints.clear(); curvePoints.append(point); // If the previous point lies inside the surface and the current one outside, // finish the point list and create curve. Repeat. if ((prevOnSrf && !onSrf) || (onSrf && i == samples)) { if (curvePoints.length() < 2) continue; MObject projectedCurve; status = generateNurbsCurve(curvePoints, projectedCurve); projectedCurves.append(projectedCurve); curvePoints.clear(); } } return status; } |

In the source files I’m also using function *MFnNurbsCurve::findLengthFromParam()*, which is not available in versions older than Maya 2016 extension 2. If you’re writing your code for older version, you can get away with using parameter instead of length. This might result in uneven spacing of samples, which can be fixed by reparametrizing the input curve.

### Stitch placement and orientation

Now the fun part…

For each stitch we need 3 vectors that will define orientation matrix. I learned this from a post from Chad Vernon (yes, THE Chad Vernon) in Aligning an object’s rotation to that of a vector discussion. For the vector T we use *MFnNurbsCurve::tangent()* or create a vector from two neighboring points. That way the stitch geometry will follow the direction of the curve. The vector N comes from *MFnMesh::getClosestPointAndNormal()* which will orient the stitch normal to the mesh surface. Finally the last vector is a simple cross product of the two.

Here you can see how it works in the code. First we have to get position of our samples on the projected curve. Then we find the closest point on mesh, surface normal and define the tangent vector. Finally we construct an orientation matrix and call helper function which builds the oriented stitch geometry.

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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
MStatus StitchNode::distributeStitches(MObject& curve, MObject& mesh, MPointArray& points, MIntArray& counts, MIntArray& indices) { MStatus status(MStatus::kSucces); MFnNurbsCurve curveFn(curve); MFnMesh meshFn(mesh); // Calculate distribution of stitches int count = int(round(curveFn.length() / m_distance)); double distance = curveFn.length() / count; for (int i = 0; i < count; i++) { // Get endpoints of the stitch MPoint pointA, pointB; status = findPointAtLength(curve, (distance*i), pointA); status = findPointAtLength(curve, (distance*(i+1)), pointB); // Get surface normal for each point MVector normal, normalB; if (!closestPointOnMesh(m_mesh, pointA, normal)) continue; else if (!closestPointOnMesh(m_mesh, pointB, normalB)) continue; // Get vector T and crossproduct MVector tangent = pointB - pointA; double length = tangent.length(); MPoint position = pointA + tangent / 2; tangent.normalize(); MVector cross = tangent^normal; // Feed XYZ values of each vector in 4x4 matrix double orientation[4][4] = {{ tangent.x, tangent.y, tangent.z, 0 }, { cross.x, cross.y, cross.z, 0 }, { normal.x, normal.y, normal.z, 0 }, { 0, 0, 0, 1 } }; MMatrix orientationMatrix(orientation); // Generate stitch generateStitch(length, points, counts, indices, position, orientationMatrix); } return status; } |

### Generating stitch geometry

This is the function that creates lists of points, vertex counts and vertex indices we need to to feed in *MFnMesh::create()* function. It looks a bit complicated but only because we’re defining a mesh from scratch. If you want to duplicate model you built in Maya, you simply call *MFnMesh::getPoints()* and *MFnMesh::getVertices()* to get these values. Note how we “multiply” the *geoPoints* with the orientation matrix and move them to the new position.

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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
MStatus StitchNode::generateStitch(double length, MPointArray& points, MIntArray& counts, MIntArray& indices, MPoint& position, MMatrix& orientation){ MStatus status(MStatus::kSuccess); // Calculate correct dimensions of a stitch // Variables starting with m_ come from node input attributes double radius = m_thickness / 2; double outside = length / 2; double inside = outside - m_thickness; // Mesh points double polyPoints[16][4] = {{ -outside, -radius+m_skew, -m_thickness, 0 }, { outside, -radius-m_skew, -m_thickness, 0 }, { -outside, radius+m_skew, -m_thickness, 0 }, { outside, radius-m_skew, -m_thickness, 0 }, { -inside, radius+m_skew/2, 0, 0 }, { inside, radius-m_skew/2, 0, 0 }, { -inside, -radius+m_skew/2, 0, 0 }, { inside, -radius-m_skew/2, 0, 0 }, { inside, -radius, m_thickness, 0 }, { outside, -radius, m_thickness, 0 }, { inside, radius, m_thickness, 0 }, { outside, radius, m_thickness, 0 }, { -inside, -radius, m_thickness, 0 }, { -outside, -radius, m_thickness, 0 }, { -outside, radius, m_thickness, 0 }, { -inside, radius, m_thickness, 0 } }; // Number of vertices in each polygon int polyCounts[12] = { 4,4,4,4,4,4,4,4,4,4,4,4 }; // Indices of vertices forming each polygon int polyIndices[48] = { 15, 12, 6, 4, 14, 15, 4, 2, 13, 14, 2, 0, 12, 13, 0, 6, 11, 9, 1, 3, 10, 11, 3, 5, 8, 10, 5, 7, 9, 8, 7, 1, 0, 1, 7, 6, 6, 7, 5, 4, 4, 5, 3, 2, 2, 3, 1, 0 }; MPointArray geoPoints(polyPoints, 16); MIntArray geoCounts(polyCounts, 12), geoIndices(polyIndices, 48); unsigned firstIndex = points.length(); // Append points for (unsigned i = 0; i < geoPoints.length(); i++) { geoPoints[i] *= orientation; // Apply orientation matrix geoPoints[i] += position; // Move to correct position points.append(geoPoints[i]); } // Append counts for (unsigned i = 0; i < geoCounts.length(); i++) counts.append(geoCounts[i]); // Append indices for (unsigned i = 0; i < geoIndices.length(); i++) indices.append(firstIndex + geoIndices[i]); return status; } |

### Conclusion

The Stitch plugin is a great tool to add detail to your model. Although the projection method works quite nicely, it is somewhat primitive and definitely needs improvements. Let me know if you have any questions or suggestions. As always the displayed code is just a scoup from the plugin. For the complete code check the source files.

Good day!!! \~/

I’m wildly sorry!

StitchNode does not work!!! (Maya 2018.3)

C:\Users\username\Documents\maya\2018\plug-ins\stitchNode.mll

C:\Users\username\Documents\maya\2018\scripts\AEstitchNodeTemplate.mel

C:\Users\username\Documents\maya\2018\prefs\icons\stitchNode.png

https://yadi.sk/i/C7jIq6A93WfLTy

SEAMS EASY works fine!!! (Maya 2018.3)

Will you advise something?

Thank you!!!

Hi,

I’m glad the SeamsEasy works. As for the StitchNode, it unfortunately doesn’t work in 2018 at the moment. I’ll see if I can at some point transfer the functionality of StitchNode in the SeamsEasy plugin.

Stepan

That would be wonderful !!!

And thank you very much for the tool !!! \ ~ /

Hi Stepan,

I have a beginner question. Which part of the Shelfbutton.mel has to be copied into the script editor ?

The content of the mel is:

// The stitch command can be called with following flags

// -d or -distance, takes double

// -c or -count, takes int

// -l or -length, takes double

// -th or -thickness, takes double

// -sk or -skew, takes double

// -t or -translate, takes double double double

// -r or -rotate, takes double double double

// -s or -scale, takes double double double

if(

`pluginInfo -q -l "stitchNode.mll"`

==0)loadPlugin “stitchNode”;

stitch -d 5 -th 0.5 -sk 0.2;

source AEstitchNodeTemplate;

refreshEditorTemplates;

Sorry for another noob question 😉

Hi Simon,

You can copy the whole thing. But only the line with “stitch” is actually important.

Best regards

Stepan

Thanks very much for your super quick answer 😉 Work perfectly. Tool is amazing.

Hi again,

Any chance you could just say where to install the parts of the download? (Noob question, sorry!)

Thanks,

Seb

Hi Seb,

here’s example:

C:\Users\username\Documents\maya\2017\plug-ins\

stitchNode.mllC:\Users\username\Documents\maya\2017\scripts\

AEstitchNodeTemplate.melC:\Users\username\Documents\maya\2017\prefs\icons\

stitchNode.pngLoad the plugin through Plug-in manager, simply copy content of

shelfButton.melin script editor and drag on your shelf and assign icon if you want. shelfButton.mel also contains short list of flags u can use with “stitch” command.Perfect, thanks very much!

This looks great man, thanks for sharing 🙂

Is it only for Maya2017?

Hi Sascha,

I can compile it for 2016 EXT 2 if that helps, but as I’m using some functions from the latest API I’d have to rewrite the code to make it work in 2016 or lower. In few days I expect to release another plugin with stitches distributed along edges rather that projected curve, that one will be for 2016, 2016.5, 2017

Many thanks for giving this away for free, it looks great! I’m looking forward to checking out your seam plug-in; I do quite a lot of modelling of seats for aircraft, and this will save SO much time.

Hi Seb. I’m currently working on a car seat project and its already a huge time and ass saver. I still want to optimize few things, but it shoudn’t take long.

Great work! Thank you for sharing all your excellent scripts.