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