Mesh Operation - Example
Overview
This sample plugin will demonstrate how to create a very simple mesh operation that bevels some selected polygons. The evaluation function will handle the creation of the geometry, and the re-evaluation function will handle subsequent evaluations that simply offset the beveled geometry along its normal.
Bevel
The evaluation of the bevel is split into two separate operations, geometry creation and geometry offset. First, the new geometry is created in the same position as the original geometry, then the beveled polygons are offset along their normal. The point positions are cached after the geometry creation, so that in subsequent reevaluations we can easily apply a new offset to the original geometry position.
Visitor
The creation of the beveled geometry is handled by a visitor that is evaluated for each "selected" polygon. The visitor performs a simple bevel operation by duplicating the polygon points, updating the polygon to use the new points, and then creating "side" polygons to connect the new points to the old points. The new point positions are cached, so that they can easily be enumerated when performing a incremental update.
Class Variables
These three variables are initialised by the Mesh Operation when it instantiates the Visitor. The Polygon and Point objects are COM accessors that provide access to the elements, these types are automatically updated to point at the current element. The Vector is a cache of points that we create, so they can easily be enumerated by the incremental update.
CLxUser_Polygon _polygon; CLxUser_Point _point; std::vector <PointData> *_cache;
Evaluate
This function is called for every "selected" polygon. Loop over the points on the polygon. For each point, we store the Position and Normal, then duplicate the point by calling the Copy method. The old and new points are stored in temporary lists, so that we can easily enumerate over them to generate the side polygons. The normal, position and new point is also stored in the incremental data cache. The position is cached before an offset is applied, as we always want the offset to be calculated as an absolute distance from its original position.
_polygon.VertexCount (&pointCount); for (unsigned int i = 0; i < pointCount; i++) { PointData pointData; LXtPointID oldPoint = NULL; LXtFVector position; if (LXx_OK (_polygon.VertexByIndex (i, &oldPoint))) { _point.Select (oldPoint); _point.Normal (polygon, pointData.normal); _point.Pos (position); LXx_VCPY (pointData.position, position); _point.Copy (&pointData.id); _cache->push_back (pointData); newPoints.push_back (pointData.id); oldPoints.push_back (oldPoint); } }
Now the point has been copied and we've cached the point information, the polygon is updated to use the newly created points.
_polygon.SetVertexList (&newPoints[0], newPoints.size (), 0);
We should have an identical number of new points and old points, so we loop over the points and create polygons from the old ones to the new ones. Note the use of NewProto to create the polygon, this ensures that the new polygon inherits properties such as material tags from the original polygon.
if (oldPoints.size () == newPoints.size ()) { pointCount = oldPoints.size (); for (unsigned int i = 0; i < pointCount; i++) { LXtPointID wallPoints[4] = {NULL}; LXtPolygonID wallPolygon = NULL; wallPoints[0] = oldPoints[i]; wallPoints[1] = newPoints[i]; wallPoints[2] = newPoints[(i+1)%pointCount]; wallPoints[3] = oldPoints[(i+1)%pointCount]; _polygon.NewProto (polygonType, wallPoints, 4, 1, &wallPolygon); } }
Offset
The offset calculation is really simple, we loop over the cached points and for each one, we multiply the normal by the offset and then add that back to the cached position, this then becomes the new position for the point.
for (i = 0; i < _points.size (); i++) { PointData *pointData = NULL; LXtVector pos; pointData = &_points[i]; if (LXx_OK (point.Select (pointData->id))) { LXx_VCPY (pos, pointData->normal); LXx_VSCL (pos, _offset); LXx_VADD (pos, pointData->position); point.SetPos (pos); } }
Mesh Operation
The evaluation of this mesh operation is split into two sections, Evaluate and ReEvaluate. Evaluate will create some initial beveled geometry, and for subsequent compatible evaluations, ReEvaluate will be called to simply offset the original beveled geometry along it's normal. When operating on many elements, this separate ReEvaluation pass can improve performance significantly.
Class Variables
The offset amount and cache of newly created points are stored as public class variables. When we convert an old compatible mesh operation to a new mesh operation, we must copy the variables from the old mesh operation to the new one.
double _offset; std::vector <PointData> _points;
Constructor
In the Mesh Operation constructor, a single attribute that is used to control the offset distance is defined using DynamicAttributes. A default value of 0.0 is set, this will be used as the default channel value when the mesh operation server is converted into an item.
dyna_Add ("offset", LXsTYPE_DISTANCE); attr_SetFlt (0, 0.0);
Evaluate
Evaluation is relatively lightweight, as it simply wraps the visitor and offset functions.
Grab the offset attribute and cache it. This will be used to later on to test if the mesh operation is compatible. If the offset is 0.0, we early out without performing the bevel.
_offset = dyna_Float (0); if (lx::Compare (_offset, 0.0) == LXi_EQUAL_TO) return LXe_OK;
Perform the bevel by instantiating the visitor, initialising its class variables, then calling the Enumerate function to touch every selected polygon. The mode argument that is passed to Enumerate function allows us to easily enumerate over the selected elements and skip unselected polygons. Once the polygons are beveled, OffsetPositions is called to perform an offset on the newly created points that were cached in the Visitor. Finally, SetMeshEdits is called to inform the system of the geometry change. The entire evaluation is wrapped in a batch, which can provide improved performance in some cases.
if (LXx_OK (mesh.BeginEditBatch ())) { visitor._polygon.fromMesh (mesh); visitor._point.fromMesh (mesh); visitor._cache = &_points; visitor._polygon.Enumerate (mode, visitor, NULL); OffsetPositions (mesh); mesh.SetMeshEdits (LXf_MESHEDIT_GEOMETRY); mesh.EndEditBatch (); }
Compare
In the compare function, the old mesh operation from a previous evaluation is passed in, and it should be tested to see if it's cached state is compatible with the current properties of the mesh operation. A mesh operation is deemed compatible if the cached elements from the previous evaluation can simply be deformed to achieve an identical result to performing a full evaluation of the mesh operation.
lx::CastServer casts the old mesh operation COM interface into our internal implementation.
lx::CastServer (SERVER_NAME, other_obj, other);
If the current offset and previous offset are the same, we return LXiMESHOP_IDENTICAL to skip evaluation altogether. If either offset value was 0.0, then geometry in one of the evaluations will exist that won't exist in the other, so we return LXiMESHOP_DIFFERENT to perform a full evaluation, otherwise we return LXiMESHOP_COMPATIBLE to perform an incremental update.
if (other) { if (lx::Compare (other->_offset, _offset) == LXi_EQUAL_TO) return LXiMESHOP_IDENTICAL; if (lx::Compare (other->_offset, 0.0) != LXi_EQUAL_TO && lx::Compare (_offset, 0.0) != LXi_EQUAL_TO) return LXiMESHOP_COMPATIBLE; } return LXiMESHOP_DIFFERENT;
Convert
If the previous evaluation was compatible, convert will be called to copy the point cache from the old mesh operation to the new one. We just copy values from one vector to another.
lx::CastServer (SERVER_NAME, other_obj, other); if (other) { _points.clear (); for (unsigned int i = 0; i < other->_points.size (); i++) { _points.push_back (other->_points[i]); } }
ReEvaluate
ReEvaluate is really simple, it just calls the OffsetPositions function which updates the position of the cached points.
if (LXx_OK (OffsetPositions (mesh))) return mesh.SetMeshEdits (LXf_MESHEDIT_POSITION); return LXe_FAILED;
Server Tags
There are two server tags for this mesh operation, one specifies that the mesh operation should automatically be converted into an item, and the second specifies that the mesh operation supports polygon selections only.
LXtTagInfoDesc MeshOperation::descInfo[] = { { LXsMESHOP_PMODEL, "." }, { LXsPMODEL_SELECTIONTYPES, LXsSELOP_TYPE_POLYGON }, { 0 } };
User Interface
Like the majority of the MODO user interface, the UI for mesh operations is defined through XML config files. Configs are explained in some depth.
Command Help
The command help section is used to define the username of both the item and the items channels. Despite the server name being called "sample.bevel", the item automatically generated from the mesh operation server has ".item" appended to the end, so all configs refer to the auto generated item as "sample.bevel.item".
<atom type="CommandHelp"> <hash type="Item" key="sample.bevel.item@en_US"> <atom type="UserName">Bevel Sample</atom> <hash type="Channel" key="enable"> <atom type="UserName">Enable</atom> </hash> <hash type="Channel" key="offset"> <atom type="UserName">Offset</atom> </hash> </hash> </atom>
Properties Form
The properties form is defined using the "Attributes" block. The properties form contains a single control for displaying the offset channel, but also includes some common sheets to provide things like enable controls. The filter atom determines when the properties form should be displayed and is defined below.
<atom type="Attributes"> <hash type="Sheet" key="sample.bevel.item:sheet"> <atom type="Label">Bevel Sample</atom> <atom type="Filter">sample.bevel.item:filterPreset</atom> <hash type="InCategory" key="itemprops:general#head"> <atom type="Ordinal">110</atom> </hash> <list type="Control" val="ref item-common:sheet"> <atom type="StartCollapsed">0</atom> <atom type="Hash">#0</atom> </list> <list type="Control" val="ref meshoperation:sheet"> <atom type="StartCollapsed">0</atom> <atom type="Hash">#1</atom> </list> <list type="Control" val="cmd item.channel sample.bevel.item$offset ?"> <atom type="Label">Offset</atom> <atom type="StartCollapsed">0</atom> <atom type="Hash">sample.bevel.item.offset.ctrl:control</atom> </list> </hash> </atom>
The filter can be used to control when a form is displayed. They are defined by name, and have various nodes which dictate the rules for when a form should be displayed, in this case it uses the itemtype filter. The itemtype filter checks if the sample.bevel.item is selected.
<atom type="Filters"> <hash type="Preset" key="sample.bevel.item:filterPreset"> <atom type="Name">Bevel Sample</atom> <atom type="Description"></atom> <atom type="Category">pmodel:filterCat</atom> <atom type="Enable">1</atom> <list type="Node">1 .group 0 ""</list> <list type="Node">1 itemtype 0 1 "sample.bevel.item"</list> <list type="Node">-1 .endgroup </list> </hash> </atom>
Categories
When the user adds the mesh operation, the mesh operation items are displayed in a browser that is sorted by categories. If no category is defined by the plugin, it will appear in a category called "Other", however this can be overridden with the following config fragment. The "MeshOperations" category is the master category that shows all mesh operations, and "polygon" is a sub-category under "MeshOperations". Various selection types are also provided for Vertex and Edge mesh operations, if the mesh operation supports multiple selection types, the category will need to be defined for each type.
<atom type="Categories"> <hash type="Category" key="MeshOperations"> <hash type="C" key="sample.bevel.item">polygon</hash> </hash> <hash type="Category" key="MeshOperationsPolygons"> <hash type="C" key="sample.bevel.item">polygon</hash> </hash> </atom>
The Code
Plugin
#include <lxsdk/lxidef.h> #include <lxsdk/lx_mesh.hpp> #include <lxsdk/lx_pmodel.hpp> #include <lxsdk/lxu_attributes.hpp> #include <lxsdk/lxu_math.hpp> #include <vector> #define SERVER_NAME "sample.bevel" /* * The PointData structure is used to cache the incremental data for each point. * It stores the element pointer, the non-offset position, as well as the normal. */ struct PointData { LXtPointID id; LXtVector position; LXtVector normal; }; /* * The polygon visitor performs the bevel operation for a single polygon. It * creates new points at the same position as the points on the polygon it is * beveling. The polygon is then updated to use the new points and "wall" polygons * are added bridging the old and new points. The new point information is cached * to that it can work with the incremental re-evaluation. */ class PolygonVisitor : public CLxVisitor { public: PolygonVisitor () : _cache (NULL) { } void evaluate () LXx_OVERRIDE { std::vector <LXtPointID> newPoints; std::vector <LXtPointID> oldPoints; LXtPolygonID polygon = NULL; LXtID4 polygonType = 0; unsigned int pointCount = 0; polygon = _polygon.ID (); /* * For each point on the polygon, create a new point that * matches it's position. Add the point information to * the cache. */ _polygon.VertexCount (&pointCount); for (unsigned int i = 0; i < pointCount; i++) { PointData pointData; LXtPointID oldPoint = NULL; LXtFVector position; if (LXx_OK (_polygon.VertexByIndex (i, &oldPoint))) { _point.Select (oldPoint); /* * Get the normal and position of the * original point. */ _point.Normal (polygon, pointData.normal); _point.Pos (position); LXx_VCPY (pointData.position, position); /* * Copy the new point and store it in the * cache. */ _point.Copy (&pointData.id); _cache->push_back (pointData); /* * Add the new and old points to a list * so they can be enumerated to create * the wall polygons. */ newPoints.push_back (pointData.id); oldPoints.push_back (oldPoint); } } /* * Update the polygon to use the new points. */ _polygon.SetVertexList (&newPoints[0], newPoints.size (), 0); /* * Create "wall" polygons linking the old and new points. */ _polygon.Type (&polygonType); if (oldPoints.size () == newPoints.size ()) { pointCount = oldPoints.size (); for (unsigned int i = 0; i < pointCount; i++) { LXtPointID wallPoints[4] = {NULL}; LXtPolygonID wallPolygon = NULL; wallPoints[0] = oldPoints[i]; wallPoints[1] = newPoints[i]; wallPoints[2] = newPoints[(i+1)%pointCount]; wallPoints[3] = oldPoints[(i+1)%pointCount]; _polygon.NewProto (polygonType, wallPoints, 4, 1, &wallPolygon); } } } CLxUser_Polygon _polygon; CLxUser_Point _point; std::vector <PointData> *_cache; }; class MeshOperation : public CLxImpl_MeshOperation, public CLxDynamicAttributes { public: MeshOperation () { /* * A single attribute is added to the mesh operation to * control the bevel offset. This attribute is converted * to a channel on the automatically generated item. */ dyna_Add ("offset", LXsTYPE_DISTANCE); attr_SetFlt (0, 0.0); } LxResult mop_Evaluate ( ILxUnknownID mesh_obj, LXtID4 type, LXtMarkMode mode) LXx_OVERRIDE { CLxUser_Mesh mesh (mesh_obj); PolygonVisitor visitor; LxResult result = LXe_FAILED; /* * If the offset amount is 0.0, then we want to do nothing. */ _offset = dyna_Float (0); if (lx::Compare (_offset, 0.0) == LXi_EQUAL_TO) return LXe_OK; if (LXx_OK (mesh.BeginEditBatch ())) { /* * Enumerate over the polygons and bevel each one. */ visitor._polygon.fromMesh (mesh); visitor._point.fromMesh (mesh); visitor._cache = &_points; visitor._polygon.Enumerate (mode, visitor, NULL); /* * Apply an offset to the beveled polygons. */ OffsetPositions (mesh); mesh.SetMeshEdits (LXf_MESHEDIT_GEOMETRY); mesh.EndEditBatch (); result = LXe_OK; } return result; } int mop_Compare ( ILxUnknownID other_obj) LXx_OVERRIDE { MeshOperation *other = NULL; _offset = dyna_Float (0); /* * Cast the other interface into our implementation, and * then compare the offset attribute. */ lx::CastServer (SERVER_NAME, other_obj, other); if (other) { /* * If the offset is identical, we don't want to * do anything. */ if (lx::Compare (other->_offset, _offset) == LXi_EQUAL_TO) return LXiMESHOP_IDENTICAL; /* * As long as neither offset is 0.0, the previous * operation is compatible. */ if (lx::Compare (other->_offset, 0.0) != LXi_EQUAL_TO && lx::Compare (_offset, 0.0) != LXi_EQUAL_TO) return LXiMESHOP_COMPATIBLE; } return LXiMESHOP_DIFFERENT; } LxResult mop_Convert ( ILxUnknownID other_obj) LXx_OVERRIDE { MeshOperation *other = NULL; _offset = dyna_Float (0); /* * Cast the other interface into our implementation, and * then copy the cached points that want to offset. */ lx::CastServer (SERVER_NAME, other_obj, other); if (other) { _points.clear (); for (unsigned int i = 0; i < other->_points.size (); i++) { _points.push_back (other->_points[i]); } return LXe_OK; } return LXe_FAILED; } LxResult mop_ReEvaluate ( ILxUnknownID mesh_obj, LXtID4 type) LXx_OVERRIDE { CLxUser_Mesh mesh (mesh_obj); _offset = dyna_Float (0); /* * Deform the cached points by reapplying the offset. */ if (LXx_OK (OffsetPositions (mesh))) return mesh.SetMeshEdits (LXf_MESHEDIT_POSITION); return LXe_FAILED; } LxResult OffsetPositions ( CLxUser_Mesh &mesh) { CLxUser_Point point; LxResult result = LXe_FAILED; int i = 0; if (mesh.test ()) { point.fromMesh (mesh); for (i = 0; i < _points.size (); i++) { PointData *pointData = NULL; LXtVector pos; pointData = &_points[i]; if (LXx_OK (point.Select (pointData->id))) { LXx_VCPY (pos, pointData->normal); LXx_VSCL (pos, _offset); LXx_VADD (pos, pointData->position); point.SetPos (pos); } } result = LXe_OK; } return result; } double _offset; std::vector <PointData> _points; static LXtTagInfoDesc descInfo[]; }; LXtTagInfoDesc MeshOperation::descInfo[] = { { LXsMESHOP_PMODEL, "." }, { LXsPMODEL_SELECTIONTYPES, LXsSELOP_TYPE_POLYGON }, { 0 } }; void initialize () { CLxGenericPolymorph *srv = NULL; srv = new CLxPolymorph <MeshOperation>; srv->AddInterface (new CLxIfc_MeshOperation <MeshOperation>); srv->AddInterface (new CLxIfc_Attributes <MeshOperation>); srv->AddInterface (new CLxIfc_StaticDesc <MeshOperation>); lx::AddServer (SERVER_NAME, srv); }
Config
<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- The CommandHelp block is used to define the item and channel usernames. --> <atom type="CommandHelp"> <hash type="Item" key="sample.bevel.item@en_US"> <atom type="UserName">Bevel Sample</atom> <hash type="Channel" key="enable"> <atom type="UserName">Enable</atom> </hash> <hash type="Channel" key="offset"> <atom type="UserName">Offset</atom> </hash> </hash> </atom> <!-- The properties form displays a single property to control the offset. The common mesh operation sheet is also included to add the enable checkbox to the top of the form. --> <atom type="Attributes"> <hash type="Sheet" key="sample.bevel.item:sheet"> <atom type="Label">Bevel Sample</atom> <atom type="Filter">sample.bevel.item:filterPreset</atom> <hash type="InCategory" key="itemprops:general#head"> <atom type="Ordinal">110</atom> </hash> <list type="Control" val="ref item-common:sheet"> <atom type="StartCollapsed">0</atom> <atom type="Hash">#0</atom> </list> <list type="Control" val="ref meshoperation:sheet"> <atom type="StartCollapsed">0</atom> <atom type="Hash">#1</atom> </list> <list type="Control" val="cmd item.channel sample.bevel.item$offset ?"> <atom type="Label">Offset</atom> <atom type="StartCollapsed">0</atom> <atom type="Hash">sample.bevel.item.offset.ctrl:control</atom> </list> </hash> </atom> <!-- A filter determines when the properties form should be displayed. In this case the properties form should be displayed when the sample.bevel.item item type is selected. --> <atom type="Filters"> <hash type="Preset" key="sample.bevel.item:filterPreset"> <atom type="Name">Bevel Sample</atom> <atom type="Description"></atom> <atom type="Category">pmodel:filterCat</atom> <atom type="Enable">1</atom> <list type="Node">1 .group 0 ""</list> <list type="Node">1 itemtype 0 1 "sample.bevel.item"</list> <list type="Node">-1 .endgroup </list> </hash> </atom> <!-- The categories define which sub-category the mesh operation will appear in. Two categories have to be set, one that shows all Mesh Operations and another which is filtered to show only operations that support a specific selection type. --> <atom type="Categories"> <hash type="Category" key="MeshOperations"> <hash type="C" key="sample.bevel.item">polygon</hash> </hash> <hash type="Category" key="MeshOperationsPolygons"> <hash type="C" key="sample.bevel.item">polygon</hash> </hash> </atom> </configuration>