## Developing a modeling script – Start to finish

March 18th, 2006 by Helge Mathee - Viewed 41759 times -Hey all.

In this article I want to discuss the full development process of a custom modeling tool, called “EdgeExtrudePro”.

What you need:

I going to describe the full process, so be warned, it is going to be a long trip. The article is meant for the experienced scripted as well as the start-up scripting TD, but to follow the descriptions, you’ll have to know at least the fundamentals of JScript, Arrays (the multiple usages of them, look-up tables etc) and some basic vector math. I am going to explain the math used for this tool briefly, but this is not a math primer. ;-)

What you will get out of it:

This article will get you an understanding of selection checking and user-error handling. Moreover you can learn some of the basic relationships inside of 3D geometry, such as edge – polygon adjacency, polygon-island boundaries, edge-to-edge connections and some more.

Ok – so let’s start.

The name of the tool was not my idea, so I am just going to keep it that way. What the tool does really, is using a curve to cut a hole into geometry. Here’s a link to the original tool (For Maxon’s Cinema 4D) http://www.vreel-3d.de/plugins/EEPro/EElinks.html

Here’s an animated gif from the webpage above, demonstrating the functionality. We are focusing on the part with the red circle.

Looking the animated gif, I can see the following steps to reach our goal:

- Get a polygonmesh and a curve
- Put the curve onto the surface of the mesh
- Delete all polygons “below” the curve’s shape
- Extrude the resulting new boundary to fit the curve

What’s really important when developing a custom tool, is to focus on what really has to be done. As you don’t know all of the steps before you start working on them, it is still good to partition the huge task into smaller tasks, and partition them again, until you have a task you can easily solve. For now, I am just going to rephrase our tasks a little bit.

- Check the selection for a polygonmesh and a nurbscurvelist
- ShrinkWrap the nurbscurvelist onto the polygonmesh.
- Delete all polygons “below” the nurbscurvelist’s shape
- Extrude the resulting new boundary to fit the curve.

Ok – sounds better. Let’s start with the first one.

**1. Check the selection for a polygonmesh and a nurbscurvelist**

I don’t want to use picking sessions or anything similar right now, I will simply force the user to have two objects selected. Moreover, there are certain conditions I want to be checked. They look like this:

- The number of objects selected has to be 2
- The first object has to be a polygonmesh
- The second object has to be a nurbscurvelist
- The nurbscurvelist has to be closed
- The nurbscurvelist has to contain only one curve

Allright, putting all of this into a simple JScript function, it looks like this:

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 | // checks if the selection is correct function js_checkSelection() { // check if we have two objects if(selection.count!=2) { logmessage("Please select a polygonmesh and a curvelist!"); return false; } // check if the first one is a polygonmesh if(selection(0).type!="polymsh") { logmessage("The first object you selected is not a polygonmesh!") return false; } // check if the second object is a curvelist if(selection(1).type!="crvlist") { logmessage("The second object you selected is not a curvelist!"); return false; } // check if the curve is closed if(!selection(1).ActivePrimitive.Geometry.Closed) { logmessage("The curve you selected is not closed!"); return false; } // check if the curvelist contains only one curve if(selection(1).ActivePrimitive.Geometry.Curves.count!=1) { logmessage("The curve you selected contains more or less curves than 1!"); } // if everything went well - just return true return true; } |

Allright – let’s go to number 2.

**2. ShrinkWrap the nurbscurvelist onto the polygonmesh.**

For the projection I am just going to use XSI’s ShrinkWrap. There’s the option to implement the projection myself, but I try to rely on existing functionality as much as possible – to speed up the implementation process. Just ShrinkWrapping is not going to do the full job though. So these are the considerations:

- The two objects have to be in the same reference space
- After projection / shrinking the curve onto the surface we want to freeze it, because we might change the topology the curve was shrinked onto

The only thing I really did is manually shrink-wrapping a curve onto a polygonmesh, copy & pasting the code out of the script editor history and adding some more steps. Here’s my shrinking function:

1 2 3 4 5 6 7 8 9 10 11 12 13 | //apply the shrinkwrap function js_applyShrinkWrap(curve,mesh) { // if the curve is not parented to the mesh yet // parent it in... // we have to do this to simplify the reference frames if(curve.parent.fullname!=mesh.fullname) ParentObj(mesh,curve); ResetTransform(curve, siCtr, siSRT, siXYZ); var op = ApplyOp("ShrinkWrap", curve+";"+mesh, 3, siPersistentOperation); SetValue(op+".proj", 6); FreezeObj(op); } |

**3. Delete all polygons “below” the nurbscurvelist’s shape**

Okay – so now this is where it gets complicated. I am going to explain how we find what needs to be deleted by partitioning the task again:

- Find all edges which are crossing the curve
- Find all adjacent polygons to these edges
- Find the two boundaries of this resulting polygon island (inner and outer boundary)
- Find out which boundary is shorter and call it the inner boundary
- Get all of the polygons inside of the inner boundary + the polygons from the crossing edges
- Delete them

I need a function to figure out if an edge crosses a curve. This seems to be a hard one, it’s not really though. Let’s look at it:

To find out if an edge is crossing a curve, we need to find out if the two points of the edge are on the “opposite” sides of the curve. If they are, the edge crosses the curve. To do that, I will find the closest points on the curve for the two edge-points, find their center, and the center’s point on the curve. Then I take the tangent of the curve at this position and build a plane which cuts the edge in 3D, or not. If it cuts the edge, it crosses the curve.

In the illustration, the blue line is the curve, the green grid marks the plane used for the intersection calculation and the red line is an example edge.

And here’s the javascript function:

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 66 67 | // check if an edge crosses a curve function js_doesEdgeCrossCurve(edge,curve) { // get the two positions var edgePos1 = edge.points(0).position; var edgePos2 = edge.points(1).position; // get the U value of the closest points on the curve var edgeU1 = curve.GetClosestCurvePosition2(edgePos1).toArray()[2]; var edgeU2 = curve.GetClosestCurvePosition2(edgePos2).toArray()[2]; // get the two positions on the curve of the closest points var edgeOnCurve1 = curve.curves(0).EvaluatePosition(edgeU1).toArray()[0]; var edgeOnCurve2 = curve.curves(0).EvaluatePosition(edgeU2).toArray()[0]; // get the position in the center of the two edge on curves var edgePos = XSIMath.CreateVector3(); edgePos.Add(edgeOnCurve1,edgeOnCurve2); edgePos.ScaleInPlace(0.5); // get the U value of the closest point on the curve to this one var edgeU = curve.GetClosestCurvePosition2(edgePos).toArray()[2]; // get the position of that point var edgeOnCurve = curve.curves(0).EvaluatePosition(edgeU).toArray()[0]; // get the tangent of the curve at that position var tangent = curve.curves(0).EvaluatePosition(edgeU).toArray()[1]; // get the U value of the closest point on the curve to this one var normal = XSIMath.CreateVector3(); // make the normal the connection of the first point // and the position on the curve normal.Sub(edgePos1,edgeOnCurve); // cross this result with the tangent to retrieve the bi-normal normal.Cross(normal,tangent); normal.NormalizeInPlace(); // cross this result with the tangent again to retrieve the normal normal.Cross(normal,tangent); normal.NormalizeInPlace(); // define two connection vectors var con1 = XSIMath.CreateVector3(); var con2 = XSIMath.CreateVector3(); // the first is the connection between the edgepos1 and the position on the curve // resp. for the second con1.Sub(edgePos1,edgeOnCurve); con2.Sub(edgePos2,edgeOnCurve); // get the distance of the two connection vectors to the plane defined by the normal var dist1 = con1.Dot(normal); var dist2 = con2.Dot(normal); // if the points are on different sides of the plane // (if one distance is larger and the other one less than 0) if( (dist1>=0 && dist2< =0) || (dist1<=0 && dist2>=0)) { // the edge crosses the curve return true; } else { // nope - it doesn't cross return false; } } |

To find all of the edges, I create a helper function which simply calles *js_doesEdgeCrossCurve* a couple of times, like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // returns an array of indices of the edge crossing the curve function js_getAllCrossingEdges(meshObj,curveObj) { // define an array for all of the edges crossing the curve var result = new Array(); // get the geometry of both objects var mesh = meshObj.ActivePrimitive.Geometry; var curve = curveObj.ActivePrimitive.Geometry; // loop through for(var i=0;i<mesh .edges.count;i++) { // get the edge var edge = mesh.edges(i); // if it crosses, put in the result array if(js_doesEdgeCrossCurve(edge,curve)) { result[result.length] = i; } } return result; } |

Result:

Now as we have all edges crossing the curve, we need to find all of the adjacent polygons. As adjacency is a simple task, I am not going to use XSI’s command for selecting adjacent components, but implement a JScript function to do the job. It is simply looping over all edges and finding all polygons adjacent to each edge.

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 | // gets all adjacent polygons of an edge-index-array function js_getAdjacentPolygons(meshObj,indices) { // get the geometry of the meshObj var mesh = meshObj.ActivePrimitive.Geometry; // an array which is used as a hash-table // for all of the polyons used // example: if polyUsed[4] == true, that means // the polygon with the index 4 is adjacent var polyUsed = new Array(); // loop through all edge indices for(var i=0;i<indices .length;i++) { // get the edge var edge = mesh.edges(indices[i]); // get the collection of neighborpolygons of this edge // note: an edge can have only one polygon linked to it // that means it is a boundary edge... if that happens, // just return false to tell the calling function that // we have to stop now. var nbPolies = edge.NeighborPolygons(); if(nbPolies.count<2) { logmessage("We cannot cut a boundary!"); return false; } for(var j=0;j<nbPolies.count;j++) { polyUsed[nbPolies(j).index] = true; } } // define the result array var result = new Array(); for(var index in polyUsed) { result[result.length] = index; } return result; } |

Result:

To find the two boundaries and determine which one is shorter, I loop through all edges of the polygons I just got from the adjacency function and count their occurences. If I find an edge which is used only once, it is a boundary of the polygon-island. To find the two different boundaries, I start with one edge, mark it as non-boundary and find the next connected edge which is a boundary. I repeat that step until I reach the edge I started with. Those edges all together are one boundary (either the inner or the outer one – I don’t know that yet). Now I can just take all of the edges and define them as “the other” boundary. By comparing the number of elements in both boundaries, I can find the shorter one and define it as “the inner boundary”.

First – I need a function which tells me if two edges are connected or not:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // returns true if two edges are connected function js_areEdgesConnected(edge1,edge2) { // if the edges are the same, we don't call them "connected" if(edge1.index==edge2.index) { return false; } // compare all points of the one edge with // all points of the other one // if there is any match, the edges are connected if( (edge1.points(0).index==edge2.points(0).index) || (edge1.points(0).index==edge2.points(1).index) || (edge1.points(1).index==edge2.points(0).index) || (edge1.points(1).index==edge2.points(1).index)) { return true; } return false; } |

Second – I need a function which does what I described above: Find the shorter (inner boundary) and its elements:

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 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | // returns the shortest edge boundary of a given polygon index array function js_getShortestEdgeBoundary(meshObj,indices) { // get the geometry of the mesh var mesh = meshObj.ActivePrimitive.Geometry; // this is a hash-table which counts the usage of edges // if the counter is one, the edge is a boundary(!) var edgeCount = new Array(); // loop through all given polygons by index for(var i=0;i<indices .length;i++) { // get the polygon (called facet here) var facet = mesh.facets(indices[i]); // get the polygons edges var edges = facet.edges; for(var j=0;j<edges.count;j++) { // put the index of the edge into the edgeCount array // if it is not there yet, otherwise increment the number there if(!edgeCount[edges(j).index]) edgeCount[edges(j).index] = 1; else edgeCount[edges(j).index]++; } } // define two arrays for the boundaries var boundary1 = new Array(); var boundary2 = new Array(); // now we start by putting the first boundary edge into the boundary1 array for(var index in edgeCount) { // if the edge is a boundary if(edgeCount[index]==1) { // put the edge as the first element in the boundary1 array boundary1[boundary1.length] = index; // set the edge to be nothing / so it looks like it is not a boundary anymore // because we want to use it only once edgeCount[index] = 0; // and leave the loop afterwards... break; } } // check again if we have an element if(boundary1.length!=1) { logmessage("This should never happen... something really bad."); return false; } // now we try to add edges to the boundary until we hit the start again // we definitively have to leave if we have more edges than exist // this is to make sure we don't loop infinitely for some reason while(boundary1.length<mesh.edges.count) { // get the last edge in the boundary var lastEdge = mesh.edges(boundary1[boundary1.length-1]); // check if the boundary is alreay closed // the shortest possible boundary has 3 edges, // so only check if we have at least 3 if(boundary1.length>2) { // the first edge in the boundary var firstEdge = mesh.edges(boundary1[0]); // if the edges are connected, the boundary is closed. if(js_areEdgesConnected(firstEdge,lastEdge)) { // yeah - closed - we leave this loop. break; } } // try to find the next edge which is connected to the last one // in the boundary... init it with -1, so we can check later if we really found one var newIndex = -1; // loop through all edges we counted for(var index in edgeCount) { // if the edge is a boundary // note that we set used edges to 0, so we don't use them more than once if(edgeCount[index]==1) { // get this edge var currentEdge = mesh.edges(index); // if these two edges are connected, we add it to the boundary if(js_areEdgesConnected(currentEdge,lastEdge)) { // set the newindex to the current index newIndex = index; // and leave this loop! break; } } } // if the newIndex is still -1, we didn't find a new edge, // which is really bad - so we leave the whole function and give up if(newIndex==-1) { logmessage("Something bad happened. I give up."); return false; } // add the newIndex to the boundary boundary1[boundary1.length] = newIndex; // remove it from the edgeCount array, // so we don't use it again edgeCount[newIndex] = 0; } // so now we have the first boundary - the other boundary simply // consists of all of the boundary edges left, so: // loop through the edgeCount array and get all edges with count1 // and put them in boundary2 for(var index in edgeCount) { // is it a boundary? if(edgeCount[index]==1) { boundary2[boundary2.length] = index; } } // do a last check if both boundaries are valid if(boundary1.length<3 || boundary2.length<3) { logmessage("The boundaries are too short - that's bad - I give up"); return false; } // so now we know both boundaries, we check which one contains less edges if(boundary1.length<boundary2 .length) return boundary1; else return boundary2; } |

Result:

Now we have the polygons crossing the curve and the inner boundary. Another function will now get all polygons to be deleted. It basically does a “grow selection”, but with the following rule: We can only grow the selection over edges which are on the inner boundary. That way we get all polygons “inside” the boundary – not the ones outside of the polygons crossing 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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | // adds all polygons inside of a polygonselection to a given array // by using a given edgeboundary function js_addAllInsidePolygons(meshObj,polyIndices,edgeIndices) { // get the geometry of the meshObj var mesh = meshObj.ActivePrimitive.Geometry; // put all polygons in a "used" hashtable var polyUsed = new Array(); for(var i=0;i<polyindices .length;i++) { polyUsed[polyIndices[i]] = true; } // put all the edges in a "used" hashtable var edgeUsed = new Array(); for(var i=0;i<edgeIndices.length;i++) { edgeUsed[edgeIndices[i]] = true; } // now loop through all polygons and // find all neighbor polygons, but(!) // only the one which are connected by edges // edgeIndices array // so we get only the ones on the right side for(var i=0;i<polyIndices.length;i++) { // get the polygon (called facet) var facet = mesh.facets(polyIndices[i]); // get the edges of the polygon var edges = facet.edges // loop through all edges and checked if they are used for(var j=0;j<edges.count;j++) { if(edgeUsed[edges(j).index]) { // get the two polygons of the edge var nbFacets = edges(j).neighborPolygons(); // if the edge has more than one polygon // (if not - we only counted it, otherwise how would we // be here, huh?) if(nbFacets.count>1) { // for each nb-facet for(var l=0;l<2;l++) { // if the facet is not used yet if(!polyUsed[nbFacets(l).index]) { // put it as used and add it to the polyIndices array polyUsed[nbFacets(l).index] = true; polyIndices[polyIndices.length] = nbFacets(l).index; // afterwards put all edges of the new polygon into the edgeUsed array var nbFacetEdges = nbFacets(l).edges; for(var k=0;k<nbfacetedges .count;k++) { edgeUsed[nbFacetEdges(k).index] = true; } } } } } } } // now get all used polygons and return them as an array var result = new Array(); for(var index in polyUsed) { result[result.length] = index; } return result; } |

Result:

**4. Move the resulting new points onto the curve**

The extrude is going to be done by the main function, more about that later. For now – I just write a function which takes all of the points above a specific index and moves them onto the curve by closest point. For this it is really important that we froze the curve earlier, just in case we need to delete the polygons we shrunk onto.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // moves all points of a mesh starting with the given index onto the curve function js_movePointsOntoCurve(meshObj,curveObj,startIndex) { // get the geometries of the objects var mesh = meshObj.ActivePrimitive.Geometry; var curve = curveObj.ActivePrimitive.Geometry; // loop through all points starting with the given index for(var i=startIndex;i<mesh .points.count;i++) { var oldPos = mesh.points(i).position; // get the U value of the closest point on the curve var newPos = curve.GetClosestCurvePosition2(oldPos).toArray()[3]; // now move the point Translate(meshObj+".pnt["+i+"]", newPos.x,newPos.y,newPos.z, siAbsolute, siParent, siObj, siXYZ); } } |

To finish up the tool, we end by creating a main function which calls all of our helper functions one after another.

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 | // the main function to run the tool function js_main() { // check the selection if(!js_checkSelection()) { return false; } // define our variables (we can because the selection was checked before) var mesh = selection(0); var curve = selection(1); // apply the shrink wrap... js_applyShrinkWrap(curve,mesh); // get the crossing edges.. var edgesCrossing = js_getAllCrossingEdges(mesh,curve); if(edgesCrossing.length==0) { logmessage("There are no crossing edges!"); return false; } // get the adjacent polygons var poliesOnCurve = js_getAdjacentPolygons(mesh,edgesCrossing); if(!poliesOnCurve) { // something didn't work - we have to abort. return false; } // now get the shortest edge boundary var edgeBoundary = js_getShortestEdgeBoundary(mesh,poliesOnCurve) // now get all polygons inside that boundary as well var polygonsToDelete = js_addAllInsidePolygons(mesh,poliesOnCurve,edgeBoundary); // select the adjacent edges of the polygons SelectAdjacent(mesh+".poly["+polygonsToDelete+"]", "Edge", false); // delete all polygons... finally something's happenin' ! ApplyTopoOp("DeleteComponent", mesh+".poly["+polygonsToDelete+"]", siUnspecified, siPersistentOperation, null); // now we have the edges still selected, as we selected the adjacent ones before, // what we do now is remember the pointcount - as the points we have to move are the // new ones after the extrude operation var pointCount = mesh.ActivePrimitive.Geometry.Points.Count; // now duplicate the edges (if we don't specify anything it is going to be the selection DuplicateMeshComponent(null, siPersistentOperation); // now move all points on the mesh onto the curve starting with the given index js_movePointsOntoCurve(mesh,curve,pointCount); } |

Result:

**Ressources**

Here’s the final script: Edge Extrude Pro

Moreover, here is a camtasia capture of working with it: Camtasia Demo (Techsmith, 9 MB)

cheers!

Great stuff Helge. Thanks.

Great explanation .. Great Tool .. Thank you

Very nice walkthrough, and a nice tool too have. I just wish in-viewport interface was more easily programmable than using CDH. I think this would make more interactive tools possible.

Amazing to see the logic and steps behind. Thanks

Great article, thanks a lot!

That”s just i was looking for !!! 10x

Great stuff Helge! Is there going to be a PART II of “Getting Started with Scripting in XSI” DVD?

you talked about it in the first one… I hope you”ll find the time to make it.

Thanks Helge,

Great explanation and break down. Great read.

PJ

Oz: Hopefully there will be. I can”t tell any details yet though…

Many thanks for each single line of code ( and comment )

Great stuff.

FL

Bravo!!

Very usefull and interesting! :D

Thanks!!

Great stuff. Thx !! :)

This was an awesome tutorial. I really enjoyed stepping through the logic process here.

Very clear and I think very helpful for people interested in cg problem solving and the XSI SDK.

Opens the door to all kinds of interesting procedural geometry experiments for many people I”m sure…

Michael

awesome.. since im an early stage usin xsi.. hope someday those scripts really a meaningful use to me :)

Cheers..~

Awesome article! Thanks!

~Joel

Helge,

Did you get bored at work again? :P

Fantastic tutorial. Thanks for teaching us.

peace,

Lu

Great tute! The detailed comenting is very illuminative.

Usefull tool too as icing on the cake.

Thank you, Helge.

It was a pleasure to read.

Great, man! Need more XSI scripting tuts! (*Especially* with tha lack of plug-ins for XSI :( )

10x, again

Awesome tutorial. Thanks for share!

Helge,

It looks like that the shape changes when the shrinkwrap applies. What if the shape is converted to polymesh, that polymesh is shrinkwrapped with follow vertex normal bidirectional, and the border edges would be converted into spline again?

Just a small idea