CreateVertex Mesh Operation Example¶
Overview¶
This example shows how to implement a simple mesh operation to create a new vertex in a mesh. We need four objects: a Package object to describe the item representing our mesh operation, a Modifier Server and a Modifer to hook into the procedural modelling system, and a Mesh Operation to perform the operation.
Package class¶
The Package
class implements the ILxPackage
interface to
describe our mesh operation item. Since our item is just a ‘container’
for the channels that we’d like our mesh operation to have - it doesn’t
have any extra behaviour itself - our Package
class inherits from
the CLxDefaultPackage
user class. This saves us the trouble of
implementing an ILxPackageInstance
class ourselves and simply
provides a generic package instance for us instead.
A new instance of the item is created when the user selects ‘Create Vertex’ from the mesh operation UI.
On initialization when the plugin is loaded, we add a server to create a
new instance of our Package
object which implements the
ILxPackage
interface, and also adds a CLxIfc_StaticDesc
interface to indicate it has a descInfo
server tag table. The items
described by the package will have type pmodel.createVertex
.
1 2 3 4 5 6 7 8 9 10 11 | // Create a new Package server that describes the channels our modifier needs
void Package::initialize()
{
CLxGenericPolymorph *srv = new CLxPolymorph<Package>;
srv->AddInterface(new CLxIfc_Package<Package>);
srv->AddInterface(new CLxIfc_StaticDesc<Package>);
lx::AddServer(CREATEVERTEX_MESHOP, srv);
}
|
To add the required extra channels to our Package, we implement the
SetupChannels
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // Add our channels
LxResult Package::pkg_SetupChannels (ILxUnknownID addChan)
{
CLxUser_AddChannel ac(addChan);
// Add a single position channel to receive the position for the new vertex
if (LXx_OK(ac.NewChannel(POSITION_CHAN, LXsTYPE_DISTANCE)))
{
ac.SetVector(LXsCHANVEC_XYZ);
LXtVector v;
LXx_V3SET(v, 0.0, 0.0, 0.0);
ac.SetDefaultVec(v);
}
return LXe_OK;
}
|
This adds a new channel called position
with type
LXsTYPE_DISTANCE
(so it can handle all the usual unit conversions
supported by Modo). Having added the channel, we can then mark it as
really being a vector and set it’s default value. By setting it to be a
vector, Modo creates X
, Y
and Z
sub-channels to hold the
value of each component of the position.
Finally, we give the item a type of LXsITYPE_MESHOP
in the server
tags table so that it has the standard channels required for a mesh
operation. We also add a couple of extra tags to the table,
LXsPMODEL_SELECTIONTYPES
to indicate that we’re not interested in
selections, and LXsPMODEL_NOTRANSFORM
to tell Modo that we don’t
need to know the mesh transformation so Modo can avoid evaluating it
unnecessarily.
1 2 3 4 5 6 7 8 9 | // Package information describing what type of plugin this is and a few other options
LXtTagInfoDesc Package::descInfo[] =
{
{ LXsPKG_SUPERTYPE, LXsITYPE_MESHOP }, // This is a MeshOp plugin
{ LXsPMODEL_SELECTIONTYPES, LXsSELOP_TYPE_NONE }, // We ignore selections
{ LXsPMODEL_NOTRANSFORM, "." }, // We don't care about the mesh transform
{ 0 }
};
|
ModifierServer class¶
The mesh operation item defined by our Package
class by itself does
nothing. To actually do something we need a modifier, and to create a
modifier we need a modifier server.
We create a new server at start up in the initialize
function. The
modifiers that the server creates will have a server ID of
pmodel.createVertex.mod
.
1 2 3 4 5 6 | // Register a Modifier server that can create our modifier
void ModifierServer::initialize()
{
CLxExport_ItemModifierServer<ModifierServer>(CREATEVERTEX_MESHOP ".mod");
}
|
The server implements the ItemType
function to return
pmodel.createVertex
. This means that for every instance of our mesh
operation item in the scene, the server will be called to create a new
modifier.
1 2 3 4 | const char* ModifierServer::ItemType()
{
return CREATEVERTEX_MESHOP;
}
|
To allocate a new modifier instance, the server’s Alloc
function is
called. The item
parameter is the mesh operation item to which the
modifier will apply, and the eval
parameter allows it to access the
channels data on the item.
Note that the object we’re creating is just a normal C++ object rather
than a COM object. This is because our ModifierServer
class derives
from CLxItemModifierServer
rather than implementing the
ILxEvalModifier
COM interface directly. This is a helper class in
the SDK library that handles the common case where we want a single
modifier for each instance of a specific item type, implementing the
necessary COM interfaces for us so that we only need to return a simple
C++ class that handles the actual modifier behaviour.
1 2 3 4 | CLxItemModifierElement* ModifierServer::Alloc(CLxUser_Evaluation &eval, ILxUnknownID item)
{
return new ModifierElement(eval, item);
}
|
ModfierElement class¶
The modifier is responsible for querying channel inputs and using them
to create a new object that implements ILxMeshOperation
. This new
object is then stored on the mesh operation item by the modifier and
used by the procedural modelling system within Modo to modify the mesh.
In the modifier constructor, we use the supplied eval
object to mark
which channels on the item we’re going to read and write, and save their
index within the attribute block so that we can use it later to read and
write the channel values when evaluating the modifier.
1 2 3 4 5 6 7 8 9 10 11 12 | // Initialise the modifier
ModifierElement::ModifierElement(CLxUser_Evaluation &eval, ILxUnknownID item)
{
// Save the indices of all the channels we need so that we can use them
// to query and return values when evaluating the modifier
mPositionXIndex = eval.AddChan(item, POSITION_CHAN ".X", LXfECHAN_READ);
mPositionYIndex = eval.AddChan(item, POSITION_CHAN ".Y", LXfECHAN_READ);
mPositionZIndex = eval.AddChan(item, POSITION_CHAN ".Z", LXfECHAN_READ);
mOutputIndex = eval.AddChan(item, LXsICHAN_MESHOP_OBJ, LXfECHAN_WRITE);
}
|
The modifier is evaluated to update the mesh op object whenever the
input channels change. The Eval
function reads the current position
values from the input channels, calls the Spawn
function to create a
new instance of our MeshOp
object, and then stores it in the output
channel.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // Evaluate the modifier to produce a new instance of the MeshOp
void ModifierElement::Eval(CLxUser_Evaluation &eval, CLxUser_Attributes &attr)
{
// Read the vertex position from the position channel
LXtVector pos;
pos[0] = attr.Float(mPositionXIndex);
pos[1] = attr.Float(mPositionYIndex);
pos[2] = attr.Float(mPositionZIndex);
// Create a new instance of our MeshOp
ILxUnknownID obj = NULL;
if (MeshOp::Spawn(pos, (void**)&obj))
{
// Store it on the output channel
attr.SetRef(mOutputIndex, obj);
lx::UnkRelease(obj);
}
}
|
MeshOp class¶
The MeshOp
class is a COM object that implements the
ILxMeshOperation
interface to allow us to modify a mesh.
The initialize
function registers a spawner that we can use to
create our MeshOp
objects. We use a spawner here rather than a
server since we need to perform some initialisation on the object after
it’s been created. The MeshOp
objects have a spawner ID of
pmodel.createVertex.op
.
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Add a spawner that will allow us to create new instances of the MeshOp class
//
// We don't want a server here since we want to create the MeshOp instances ourself
// so we can initialise them with the vertex position
void MeshOp::initialize()
{
CLxGenericPolymorph *srv = new CLxPolymorph<MeshOp>;
srv->AddInterface(new CLxIfc_MeshOperation<MeshOp>);
lx::AddSpawner(CREATEVERTEX_MESHOP ".op", srv);
}
|
The ModfierElement
object calls the Spawn
function to create a
new MeshOp
instance. This function calls the spawner to create the
object and then stores the supplied position on it so that it can be
used when the mesh operation is evaluated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // Create a new instance of the MeshOp class from the spawner
//
// This is effectively a constructor, but we need to create the object via the spawner
// rather than directly as it's really a COM object, not just a plain C++ class
MeshOp* MeshOp::Spawn(LXtVector pos, void** ppvObj)
{
static CLxSpawner<MeshOp> spawner(CREATEVERTEX_MESHOP ".op");
MeshOp* meshop = spawner.Alloc(ppvObj);
if (meshop)
{
LXx_VCPY(meshop->mPosition, pos);
}
return meshop;
}
|
The Evaluate
function is called by the procedural modelling system
to allow the mesh operation to apply its changes to the mesh. Our
implementation simply queries a point accessor from the mesh and then
uses it to create a new vertex at the position we stored when the
MeshOp
object was created.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // Evaluate the MeshOp by modifying the input mesh
LxResult MeshOp::mop_Evaluate(ILxUnknownID meshObj, LXtID4 type, LXtMarkMode mode)
{
// Get the mesh interface from the input mesh object
CLxUser_Mesh mesh(meshObj);
// Query a point accessor for the mesh that will allow us to add a new vertex
CLxUser_Point pointAccessor;
if (!pointAccessor.fromMesh(mesh))
return LXe_FAILED;
// Add the new vertex at the requested position
LXtPointID pointID;
if (!LXx_OK(pointAccessor.New(mPosition, &pointID)))
return LXe_FAILED;
return LXe_OK;
}
|
UI Config¶
The pmodel_createVertex.cfg
describes the UI for the plugin to Modo.
The CommandHelp
section of the config file provides user readable
names for various items that will be displayed in the UI. In our case,
we just need a name for the mesh operation itself, and a name for its
position channel.
1 2 3 4 5 6 7 8 | <atom type="CommandHelp">
<hash type="Item" key="pmodel.createVertex@en_US">
<atom type="UserName">Create Vertex</atom>
<hash type="Channel" key="position">
<atom type="UserName">Position</atom>
</hash>
</hash>
</atom>
|
The Filters
section describes when to make our UI visible. We can treat
this as just boilerplate that’s required to make the UI work, but that
we don’t really need to worry about. Note that the quotes in the
Node
values need to be escaped as "
for the config file to
function correctly.
1 2 3 4 5 6 7 8 9 10 11 | <atom type="Filters">
<hash type="Preset" key="pmodel.createVertex:filterPreset">
<atom type="Name">pmodel.createVertex</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 "pmodel.createVertex"</list>
<list type="Node">-1 .endgroup </list>
</hash>
</atom>
|
The Attributes
section defines the main properties panel for the
mesh operation. In our case we just want to display controls for the
three components of the position vector. However, in order for the three
components to be grouped correctly, we need to define them as a separate
sheet which is referenced as a Sheet
control from the main sheet.
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 | <atom type="Attributes">
<hash type="Sheet" key="pmodel.createVertex:sheet">
<atom type="Label">Create Vertex</atom>
<atom type="Filter">pmodel.createVertex: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="sub pmodel.createVertex.position:sheet">
<atom type="Label">Position</atom>
<atom type="ShowLabel">0</atom>
<atom type="Style">gang</atom>
<atom type="Hash">pmodel.createVertex.position.ctrl:control</atom>
</list>
</hash>
<hash type="Sheet" key="pmodel.createVertex.position:sheet">
<atom type="Label">Position</atom>
<atom type="ShowLabel">0</atom>
<atom type="Style">gang</atom>
<list type="Control" val="cmd item.channel pmodel.createVertex$position.X ?">
<atom type="Label">Position X</atom>
<atom type="Hash">pmodel.createVertex.position.X.ctrl:control</atom>
</list>
<list type="Control" val="cmd item.channel pmodel.createVertex$position.Y ?">
<atom type="Label">Y</atom>
<atom type="Hash">pmodel.createVertex.position.Y.ctrl:control</atom>
</list>
<list type="Control" val="cmd item.channel pmodel.createVertex$position.Z ?">
<atom type="Label">Z</atom>
<atom type="Hash">pmodel.createVertex.position.Z.ctrl:control</atom>
</list>
</hash>
</atom>
|
The position controls obtain the value to display from the
cmd item.channel ...
string in their val tag. For example,
cmd item.channel pmodel.createVertex$position.X ?
queries the value
of the X component of the position channel.
Finally, to make our plugin appear in the Mesh Operations preset
browser, we need to add it to the mesh operations category. The
create
value means it will appear under the ‘Create’ category in the
list of mesh operations.
1 2 3 4 5 | <atom type="Categories">
<hash type="Category" key="MeshOperations">
<hash type="C" key="pmodel.createVertex">create</hash>
</hash>
</atom>
|