Mesh Operation - Example

From The Foundry MODO SDK wiki
Jump to: navigation, search

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 &quot;&quot;</list>
		<list type="Node">1 itemtype 0 1 &quot;sample.bevel.item&quot;</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 &quot;&quot;</list>
			<list type="Node">1 itemtype 0 1 &quot;sample.bevel.item&quot;</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>