Mesh Operation - Example¶
__TOC__
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.
1 2 3 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | _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.
1 | _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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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.
1 2 | 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.
1 2 | 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.
1 2 3 4 | _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.
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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.
1 | 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.
1 2 3 4 5 6 7 8 9 10 | 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.
1 2 3 4 5 6 7 8 9 10 11 | 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.
1 2 3 4 | 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.
1 2 3 4 5 6 | 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 ./Category:Configs.
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”.
1 2 3 4 5 6 7 8 9 10 11 | <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <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.
1 2 3 4 5 6 7 8 9 10 11 | <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.
1 2 3 4 5 6 7 8 9 | <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¶
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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 | #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¶
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 | <?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>
|