Working with Shapes and Splines

3DS Max Plug-In SDK

Working with Shapes and Splines

See Also: Class SplineShape, Class BezierShape.

Overview

This section presents information about working with shapes and splines. It covers the main classes used, provides an overview of creating splines, and discusses the capping of shapes with meshes and patches.

A good example shape for study is the Donut plug-in. This shape has two circles, an inner and an outer that make up the shape. The code for this plug-in is in \MAXSDK\SAMPLES\OBJECTS\DONUTS.CPP.

Overview of the Principal Classes

The following are the main classes used when working with shapes and splines:

Class ShapeObject

These are open or closed hierarchical shape objects. This is the base class that SimpleSpline, SimpleShape, SplineShape, and LinearShape are derived from. This class is defined in \MAXSDK\INCLUDE\OBJECT.H.

Class SplineShape

The SplineShape is the shape object flows down the geometry pipeline of MAX. The SplineShape contains a BezierShape. A SplineShape and its contained BezierShape are analogous to a TriObject which flows down the pipeline and its Mesh. This class is defined in \MAXSDK\INCLUDE\SPLSHAPE.H.

Class BezierShape

The BezierShape is effectively a collection of Bezier Splines. For example, the 3ds max Donut object has two splines in a hierarchy to make a shape. The BezierShape contains these splines. The BezierShape is analogous to the Mesh of the TriObject. This class is defined in \MAXSDK\INCLUDE\SHAPE.H.

Class Spline3D

This is a general 3D spline class. The BezierShape class has a list of these splines that make up the bezier shape. This class is defined in \MAXSDK\INCLUDE\SPLINE3D.H.

Class PolyShape

This class is used in the caching of bezier shapes. This is used for doing a one time interpolation of a bezier shape into a form that is the same shape but doesn't require any further interpolation. In this way the system can do the complex calculations once, store the shape into this PolyShape representation, and not have to go through the cubic spline calculations to figure out where the points are in the future. This class maintains an array of PolyLines. This class is defined in \MAXSDK\INCLUDE\POLYSHP.H.

Class PolyLine

This class describes a single polygon in a PolyShape using linear segments. This class is defined in \MAXSDK\INCLUDE\POLYSHP.H.

Class SimpleSpline

This is a class used in the creation of shape plug-ins. Most of the 3ds max shapes and splines are derived from this class. For example, Line, Arc, Circle, Ellipse and Star are all SimpleSplines. This class is defined in \MAXSDK\INCLUDE\SIMPSPL.H.

Class SimpleShape

This class is used to make procedural shape primitives easier to create. The 3ds max Helix procedural shape is derived from this class. It's defined in \MAXSDK\INCLUDE\SIMPSHP.H.

Class LinearShape

This class is similar to a SplineShape except this class uses a PolyShape as its data while a SplineShape uses a BezierShape as its data. Therefore this is a shape made up of entirely linear segments. This class is defined in \MAXSDK\INCLUDE\LINSHAPE.H.

Main Methods in Creating a Spline

Below is a section of code from the Circle plug-in. This plug-in is derived from class SimpleSpline. This code demonstrates the key methods used in building a shape. The method SimpleSpline::BuildShape() is called to build the shape at the specified time and store the results in ashape.

void CircleObject::BuildShape(TimeValue t, BezierShape& ashape) {

 // Start the validity interval at forever and whittle it down.

 ivalid = FOREVER;

 float radius;

 pblock->GetValue(PB_RADIUS, t, radius, ivalid);

 LimitValue( radius, MIN_RADIUS, MAX_RADIUS );

The first thing to notice is the call to NewShape(). This ensures the shape is flushed out and emptied.

Next this method calls MakeCircle(). See the code for this method below.

 ashape.NewShape();

 // Get parameters from SimpleSpline and place them in the BezierShape

 int steps;

 BOOL optimize,adaptive;

 ipblock->GetValue(IPB_STEPS, t, steps, ivalid);

 ipblock->GetValue(IPB_OPTIMIZE, t, optimize, ivalid);

 ipblock->GetValue(IPB_ADAPTIVE, t, adaptive, ivalid);

 ashape.steps = adaptive ? -1 : steps;

 ashape.optimize = optimize;

Next this method calls MakeCircle(). See the code for this method below.

 MakeCircle(ashape,radius);

After MakeCircle() has returned, another important call is made. This is UpdateSels(). This should be called when you are done adding all the polygons to the shape. This method updates the selection set information maintained by the shape. It is vital to call this before you are done.

 ashape.UpdateSels(); // Make sure it readies the selection set info

Finally we clear any caches from the shape.

 ashape.InvalidateGeomCache();

 }

static void MakeCircle(BezierShape& ashape, float radius) {

 float vector = CIRCLE_VECTOR_LENGTH * radius;

First create a new spline. This is done by calling the NewSpline() method on the shape. The shape adds a polygon to itself and returns a pointer to it.

 Spline3D *spline = ashape.NewSpline();

 // Now add all the necessary points

 for(int ix=0; ix<4; ++ix) {

  float angle = 6.2831853f * (float)ix / 4.0f;

  float sinfac = (float)sin(angle), cosfac = (float)cos(angle);

  Point3 p(cosfac * radius, sinfac * radius, 0.0f);

  Point3 rotvec = Point3(sinfac * vector, -cosfac * vector, 0.0f);

Next points or knots are added to the spline by calling AddKnot(). This allows you to add different types of knots and line segments.

  spline->AddKnot(SplineKnot(KTYPE_BEZIER,LTYPE_CURVE,

   p,p + rotvec,p - rotvec));

  }

After adding four knots, the SetClosed() method is called to make sure it's a closed circle.

 spline->SetClosed();

Next, an important call is made. The spline has a cached set of bezier points inside it. This needs to be called if you change the points of the spline. This methods updates the information internal to the spline.

 spline->ComputeBezPoints();

 }

Capping a Shape with a Mesh

This section discusses the capping of shapes using a mesh. The SDK provides tools for easily creating these mesh caps for shapes.

Typically for an extruded object, the capping on the front of the object is exactly the same as the capping on the back of the object. For example, if you extrude the letter 'M'. It's redundant to have to cap this separately for the front and back (as they are the same). When dealing with complex shapes, having to go back and do the second cap is very time consuming. So the capping system tries to cache information to speed up the process.

Sample code for capping can be found in the extrude and lathe modifiers. These plug-ins are in \MAXSDK\SAMPLES\MODIFIERS\SURFREV.CPP and \MAXSDK\SAMPLES\MODIFIERS\EXTRUDE.CPP.

The following code fragments are taken from EXTRUDE.CPP. This demonstrates the basic process for capping a shape. This code is part of the method ExtrudeMod::BuildMeshFromShape().

For this example, we are starting of with a ShapeObject shape that we want capped with a mesh. The first thing to do is convert the shape to a PolyShape. This greatly simplifies the mesh conversion. The code below shows how this is done.

// Make the shape convert itself to a PolyShape.

// This makes our mesh conversion MUCH easier!

PolyShape pShape;

shape->MakePolyShape(t, pShape);

Next, we organize the curves into a hierarchy. This automatically figures out the shape nesting and directions for proper capping.

ShapeHierarchy hier = pShape.OrganizeCurves(t);

Next we need to reverse the shapes whose directions are incorrect for the hierarchy. The hierarchy calculated above contains a BitArray that describes which shapes need to be reversed. This is passed into the Reverse() method of the PolyShape to tell it which shapes to reverse.

// Need to flip the reversed curves in the shape!

pShape.Reverse(hier.reverse);

At this point the PolyShape is all set up with the proper clockwise/counter-clockwise ordering on all the polygons so all that needs to be done is to generate the faces. You may refer to the full source of EXTRUDE.CPP to see how this is done.

The next thing is just to create the caps. This begins by instantiating a MeshCapInfo class and asking the PolyShape to make a cap. This fills up the MeshCapInfo class with all the information it needs to create a cap. The type can be morph or grid capping. Morph capping only uses the existing vertices in the PolyShape to generate the cap. The capping code does the best job it can given this constraint, however it is possible to wind up with long sliver-like faces on the cap. This is referred to as a morph cap because if you cap a shape using this method it does not generate any new vertices and you can then morph between shapes with the same number of vertices. A Grid cap generates new vertices in the interior of the shape in a grid pattern. This helps to break up the shape and helps reduce slivering. Grid capping will generate different number of vertices based on the shape and thus the shapes are not morphable.

MeshCapInfo capInfo;

pShape.MakeCap(t, capInfo, capType);

After this is done, the MeshCapInfo is cached within the shape. Therefore if this is needed again, no work needs to be done.

Next, a MeshCapper object is created. This is done by passing the PolyShape as an argument to the constructor. This gets the MeshCapper ready for the topology of the shape. Developers don't need to understand the inner workings of the MeshCapper, it's just a tool used to aide in capping.

// Build information for capping

MeshCapper capper(pShape);

Below is the code where the start of the extrusion is capped. Inside the capper is a MeshCapPoly. There is one for each polygon in the shape. The MeshCapPoly needs to know the corresponding mesh vertex for each vertex in the PolyLine. This is done by calling SetVert() on the MeshCapPoly. For example, this might associate vertex 0 in the PolyLine with vertex 200 in the mesh, vertex 1 with mesh vertex 220, etc.

if(capStart) {

 vert = 0;

 for(poly = 0; poly < polys; ++poly) {

  PolyLine &line = pShape.lines[poly];

  MeshCapPoly &capline = capper[poly];

  int lverts = line.numPts;

  for(int v = 0; v < lverts; ++v)

   // Gives this vert's location in the mesh!

   capline.SetVert(v, vert++);

  vert += lverts * levels;

  }

The next thing that is done is used only for grid capping. A grid cap generates new vertices inside the shape that make up the grid. In the case of a SurfRev for example, the end cap might be rotated, or scaled in some manner. A matrix is required so the capper knows how to orient the vertices into the correct location. This matrix is ignored for non-grid capping.

 // Create a work matrix for grid capping

 Matrix3 gridMat = TransMatrix(offset1);

The final step is to cap the mesh. This is done using a method of the capper named CapMesh(). This method is passed the output mesh, the MeshCapInfo, a flag that indicates if the cap should be flipped, the smoothing group number for all the faces in the cap, and a pointer to the orientation matrix.

 capper.CapMesh(mesh, capInfo, TRUE, 16, &gridMat);

Once this is done the shape has been capped with a mesh.

Capping a Shape with a Patch

This section discusses the capping of shapes with patches. This is very similar to the mesh capping discussed above.

Again, the following code fragments are taken from EXTRUDE.CPP. This demonstrates the basic process for capping a shape with a patch.

Here we try to use a BezierShape to create the cap. First a BezierShape object is created. Then the shape to be capped is asked if it can make a transformation from itself to a BezierShape. If it can, we just ask it to make a BezierShape. If it cannot, we are forced to use a PolyShape. For patch capping this is much less desirable, but if we can't make a BezierShape, then this is used. The shape is asked to convert to a PolyShape, and then the PolyShape is converted to a BezierShape. This is a very poor conversion, as all it does is make a linear spline out of the PolyShape.

// If the shape can convert itself to a BezierShape, have it do so!

BezierShape bShape;

if (shape->CanMakeBezier())

 shape->MakeBezier(t, bShape);

else {

 PolyShape pShape;

 shape->MakePolyShape(t, pShape);

 bShape = pShape; // UGH -- Convert it from a PolyShape -- not good!

 }

Next, the curves are organized into a hierarchy. This automatically figures out the shape nesting and directions for proper capping.

ShapeHierarchy hier;

bShape.OrganizeCurves(t, &hier);

Next we need to reverse the shapes whose direction is incorrect for the hierarchy. The hierarchy calculated above contains a BitArray that describes which shapes need to be reversed. This is passed into the reverse method of the BezierShape to tell it which shapes to reverse.

// Need to flip the reversed polys...

bShape.Reverse(hier.reverse);

At this point the BezierShape is all set up with the proper clockwise/counter-clockwise ordering on all the polygons. Next the extrude modifier generates the patches. You may refer to the full source of EXTRUDE.CPP to see how this is done.

The next step is to create the caps. This begins by instantiaing a PatchCapInfo class and asking the BezierShape to make a cap. This fills up the PatchCapInfo class with all the information it needs to create a cap.

PatchCapInfo capInfo;

bShape.MakeCap(t, capInfo);

After this is done, the PatchCapInfo is cached within the shape. Therefore if this is needed again, no work needs to be done.

Next, a PatchCapper object is created. This is done by passing the BezierShape as an argument to the constructor. This gets the PatchCapper ready for the topology of the shape. Developers don't need to understand the inner workings of the PatchCapper, it's just a tool used to aide in capping.

// Build information for capping

PatchCapper capper(bShape);

Below is the code where the start of the extrusion is capped. Inside the capper is a PatchCapPoly. There is one for each polygon in the shape. The PatchCapPoly needs to know the vertex number in the patch mesh that corresponds to the given knot in the bezier spline. This is done by calling SetVert() on the PatchCapPoly. After this is done, the same thing happens for each of the vectors when SetVec() is called.

if(capStart) {

 vert = 0;

 int baseVec = 0;

 for(poly = 0; poly < polys; ++poly) {

  Spline3D *spline = bShape.splines[poly];

  PatchCapPoly &capline = capper[poly];

  int lverts = spline->KnotCount();

  for(int v = 0; v < lverts; ++v)

   // Gives this vert's location in the mesh!

   capline.SetVert(v, vert++);

  vert += lverts * levels;

  vec = baseVec;

  int lvecs = spline->Segments() * 2;

  for(v = 0; v < lvecs; ++v)

   // Gives this vec's location in the mesh!

   capline.SetVec(v, vec++);

  baseVec += lvecs * (levels + 1) + spline->KnotCount() * levels * 2;

  }

There are vectors generated inside the patch cap. In the case of a SurfRev for example, the end cap might be rotated, or scaled in some manner. A matrix is required so the capper knows how to orient the vectors into the correct location.

 // Create a work matrix for capping

 Matrix3 mat = TransMatrix(offset1);

The final step is to cap the patch mesh. This is done using a method of the capper named CapPatchMesh(). This method is passed the output patch mesh, the PatchCapInfo, a flag that indicates if the cap should be flipped, the smoothing group number for all the patches in the cap, and a pointer to the orientation matrix.

 capper.CapPatchMesh(pmesh, capInfo, TRUE, 16, &mat);

Once this is done the shape has been capped with a patch.

Modifier Pipeline Note

The ShapeObject class has a special method, CopyBaseData(), which is used by derived classes to make sure that, when they copy themselves, they are also copying the data in the ShapeObject. This is usually used in the assignment operator of derived classes. It is also used in the ShallowCopy method of objects that are passed up the modifier pipeline. In this case, it is VITAL that you wrap the CopyBaseData call in Suspend and Resume calls to the undo mechanism:

theHold.Suspend();

CopyBaseData(*fob);

theHold.Resume();

If this is not done, when certain undo functions are performed, a crash will occur due to the temporary pipeline object being destroyed. Just remember to wrap the call as shown in any code that is used as part of the modifier pipeline evaluation.