A Mesh Edit Box
Author Ernie Wright
Date 11 June 2001
In the previous installment of this tutorial, we created a user
interface and a function that calls Modeler's MAKEBOX
command. In this
installment, we'll leave the MAKEBOX
command behind and instead create our box
from its constituent points and polygons. In LightWave nomenclature, creating, deleting
and modifying points and polygons is called mesh editing, and we'll be using the
functions in a MeshEditOp structure provided by Modeler.
We'll also cover the use of the Surface Functions global to build a menu of surface names on our panel, and I'll introduce command line processing, which allows our box plug-in to be called with arguments by other plug-ins.
We're taking a significant step up in complexity, so I've divided the source into three
separate files. You can find it in sample/boxes/box3/box.c
, ui.c
and cmdline.c
.
Some Data
With the MAKEBOX
command, we didn't need explicit definitions of point
positions and polygon vertices, but we do need these in some form now.
double vert[ 8 ][ 3 ] = { /* a unit cube */ -0.5, -0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, -0.5, 0.5, -0.5, 0.5, -0.5, 0.5, 0.5, -0.5, 0.5, 0.5, 0.5, -0.5, 0.5, 0.5 }; int face[ 6 ][ 4 ] = { /* vertex indexes */ 0, 1, 2, 3, 0, 4, 5, 1, 1, 5, 6, 2, 3, 2, 6, 7, 0, 3, 7, 4, 4, 7, 6, 5 };
The vert
array contains the (x, y, z) coordinates
of the eight corner points of a unit cube, which we'll scale to create the points of the
box. The face
array lists the vertices defining each of the six rectangular faces
of our box. The numbers correspond to indexes into the vert
array.
We're also going to define a UV map for the box. (UV mapping is a texture projection method. It associates specific points in 3D space with specific points on a 2D texture, typically an image.)
float cuv[ 8 ][ 2 ] = { /* continuous UVs (spherical mapping) */ .125f, .304f, .375f, .304f, .625f, .304f, .875f, .304f, .125f, .696f, .375f, .696f, .625f, .696f, .875f, .696f }; float duv[ 2 ][ 2 ] = { /* discontinuous UVs */ -0.125f, 0.304f, -0.125f, 0.696f };
This is the UV mapping Modeler generates when it uses spherical mapping to initialize a new vertex map.
Vertex Map: Associates a set of vectors with a set of points. UV vmaps contain two floats for each point, the u and v coordinates. Color vmaps are made up of RGB or RGBA vectors. Weight maps have a single value per point. Point selection sets are implemented as vmaps with no value at all. Type codes for the most common vmap types are defined in lwmeshes.h, but you can also define your own custom vmaps.
Mesh Editing
The mesh edit version of our makebox
function uses the vert
, face
,
cuv
and duv
arrays to create the points and polygons that comprise our
box.
void makebox( MeshEditOp *edit, double *size, double *center, char *surfname, char *vmapname ) { LWDVector pos; LWPntID pt[ 8 ], vt[ 4 ]; LWPolID pol[ 6 ]; int i, j; for ( i = 0; i < 8; i++ ) { for ( j = 0; j < 3; j++ ) pos[ j ] = size[ j ] * vert[ i ][ j ] + center[ j ]; pt[ i ] = edit->addPoint( edit->state, pos ); edit->pntVMap( edit->state, pt[ i ], LWVMAP_TXUV, vmapname, 2, cuv[ i ] ); } for ( i = 0; i < 6; i++ ) { for ( j = 0; j < 4; j++ ) vt[ j ] = pt[ face[ i ][ j ]]; pol[ i ] = edit->addFace( edit->state, surfname, 4, vt ); } edit->pntVPMap( edit->state, pt[ 3 ], pol[ 4 ], LWVMAP_TXUV, vmapname, 2, duv[ 0 ] ); edit->pntVPMap( edit->state, pt[ 7 ], pol[ 4 ], LWVMAP_TXUV, vmapname, 2, duv[ 1 ] ); }
Let's go through it one step at a time.
void makebox( MeshEditOp *edit, double *size, double *center, char *surfname, char *vmapname ) {
Instead of the LWModCommand structure we passed to the previous version of makebox
,
the first argument to this one is a MeshEditOp, which contains all of the mesh editing
functions. We'll be getting this from our activation function. The other arguments control
the size and center of the box, the surface for the box faces, and the name of the vertex
map that will hold our UVs. To simplify this a bit, there's no argument for the number of
segments, nor will we support more than one.
LWDVector pos; LWPntID pt[ 8 ], vt[ 4 ]; LWPolID pol[ 6 ]; int i, j;
The LWPntID and LWPolID types are used to identify points and polygons. They're returned from functions that create these elements, and they're later passed as arguments when you need to refer to them. The LWDVector type is just an array of three doubles.
for ( i = 0; i < 8; i++ ) { for ( j = 0; j < 3; j++ ) pos[ j ] = size[ j ] * vert[ i ][ j ] + center[ j ];
The position of each point is the size multiplied by the coordinates for a unit cube, offset by the center position.
pt[ i ] = edit->addPoint( edit->state, pos );
The addPoint
function creates a point at the specified position. We'll need to
refer to the points we create when we connect them together to form the faces, so we store
the point IDs.
edit->pntVMap( edit->state, pt[ i ], LWVMAP_TXUV, vmapname, 2, cuv[ i ] ); }
While we're in the points loop, we also initialize the UV values for each point. pntVMap
takes a point ID, a vertex map, and a vector of two floats containing the UV coordinates.
Vmaps are defined by a name, a type code and a vector dimension. The name is what the user sees in the interface when vmaps are listed. Type codes for common vmap types like texture UV maps are defined in lwmeshes.h, but it's also possible to create custom vmap types. The vector is an array of floats associated with a point, and the dimension is just the number of elements in the vector. UV vmaps contain two floats for each point, the u and v coordinates.
If the vmap doesn't exist at the time of the call, pntVMap
creates it.
for ( i = 0; i < 6; i++ ) { for ( j = 0; j < 4; j++ ) vt[ j ] = pt[ face[ i ][ j ]];
The vt
array contains four point IDs, one for each vertex of a box face. The
direction of the polygon normal depends on the order in which the points are listed. The
point indexes in the face
array are listed in clockwise order as seen from the
polygon's visible side.
pol[ i ] = edit->addFace( edit->state, surfname, 4, vt ); }
The addFace
function creates a polygon with the given surface name and vertex
list. If the surface doesn't exist, addFace
creates it.
edit->pntVPMap( edit->state, pt[ 3 ], pol[ 4 ], LWVMAP_TXUV, vmapname, 2, duv[ 0 ] ); edit->pntVPMap( edit->state, pt[ 7 ], pol[ 4 ], LWVMAP_TXUV, vmapname, 2, duv[ 1 ] ); }
Finally, we add two discontinuous UV values to the vmap. Most points have a single UV value. The (u, v) is the same at a given point for all faces that use the point as a vertex. Discontinuous UVs override this value, but only for one of the polygons that shares the point. This fixes the seam problem, where two points are on opposite sides of a discontinuity, or seam, in the texture.
In our case, we're fixing up the -X face of the box, where the left and right sides of
an image map would meet if it used our vmap. Without this fix, the interpolation of u
across this face would be backwards,
the reverse of that across the -Z, +X and
+Z faces.
Surface Name List
In our interface, we need to give the user a way to specify the surface and vmap names. For vmap names, we'll provide a simple text edit field. But to show what else we can do, we'll build a popup menu for the surface names.
The declaration of our Surface popup control and its data description in our get_user
function looks like this.
LWXPanelControl ctl[] = { ... { ID_SURFLIST, "Surface", "iPopChoice" }, ... LWXPanelDataDesc cdata[] = { ... { ID_SURFLIST, "Surface", "integer" }, ...
The value of a popup control is a 0-based integer index into the list of menu items. We
need a way to give XPanels our list of surface names. Although there are other ways to do
it, we'll populate the menu using an XpSTRLIST
hint.
LWXPanelHint hint[] = { ... XpSTRLIST( ID_SURFLIST, surflist ), ...
The second argument to the XpSTRLIST
macro is an array of strings. The last
element of the array is NULL to mark the end of the list.
If we knew the item list in advance, we could simply declare it like this:
char *menulist[] = { "Apples", "Oranges", "Bananas", NULL };
But we don't know in advance what surfaces exist in Modeler, so we have to allocate and initialize one of these string arrays dynamically, when our plug-in is executed.
To build the surface name list, we'll use the first
, next
and name
routines provided by the Surface Functions global. first
and next
walk you through the linked list of surface descriptions in Modeler, and
name
returns the name of a surface, given its LWSurfaceID. The function in our
plug-in that allocates and initializes the surface name list is called init_surflist
.
int init_surflist( LWSurfaceFuncs *surff ) { LWSurfaceID surfid; const char *name; int i, count = 0;
The first thing it does is count the surfaces.
surfid = surff->first(); while ( surfid ) { ++count; surfid = surff->next( surfid ); }
It's possible for the count to be 0. In that case, we create a list with a single
entry, Default
, and return a count of 1.
if ( !count ) { surflist = calloc( 2, sizeof( char * )); surflist[ 0 ] = malloc( 8 ); strcpy( surflist[ 0 ], "Default" ); return 1; }
Otherwise, we allocate an array of count
+ 1 strings. The extra one is the
NULL string that marks the end of the list.
surflist = calloc( count + 1, sizeof( char * )); if ( !surflist ) return 0;
Now we loop through the surface list again using first and next, this time copying the
surface name into our string array. If anything goes wrong while we're doing this, we call
our free_surflist
function, which frees each string and the string array, and
then return a count of 0.
surfid = surff->first(); for ( i = 0; i < count; i++ ) { name = surff->name( surfid ); if ( !name ) { free_surflist(); return 0; } surflist[ i ] = malloc( strlen( name ) + 1 ); if ( !surflist[ i ] ) { free_surflist(); return 0; } strcpy( surflist[ i ], name ); surfid = surff->next( surfid ); }
We're done.
return count; }
This function is fairly typical of the way you'll get and use information from
LightWave. The Surface Functions global doesn't provide a canned getSurfaceNameArray
function, and XPanels doesn't have an iPopSurfaceName
control type. This
arguably places a greater burden on plug-in authors, but it also offers greater
flexibility. Suppose you only want to list green surfaces, or surface names starting with
the letter B?
Doing It Differently
Before we leave the surface list, I want to call attention to things we can and can't
do differently with it. We can't call init_surflist
from within get_user
.
At that point, it's already too late. The (not yet initialized) surflist
has
already been written into the hint array. For the same reason, we can't declare the hint
array static
.
It's also not easy to write the correct value for surflist
into the hint array
after it's been declared, because it's hard to know what its array index will be after the
various Xp
macros have been expanded. For example, our hint array after expansion
looks like the following.
LWXPanelHint hint[] = { (( void * )( 0x3D000B03 )), /* XPTAG_LABEL */ (( void * )( 0 )), (( void * )( "Box Tutorial Part 3" )), (( void * )( 0 )), /* XPTAG_NULL */ (( void * )( 0x3D021481 )), /* XPTAG_DIVADD */ (( void * )( 0x00008001 )), /* ID_SIZE */ (( void * )( 0 )), /* XPTAG_NULL */ (( void * )( 0x3D021481 )), /* XPTAG_DIVADD */ (( void * )( 0x00008002 )), /* ID_CENTER */ (( void * )( 0 )), /* XPTAG_NULL */ (( void * )( 0x3D014503 )), /* XPTAG_STRLIST */ (( void * )( 0x00008003 )), /* ID_SURFLIST */ (( void * )( surflist )), (( void * )( 0 )), /* XPTAG_NULL */ (( void * )( 0 )), /* XPTAG_END */ };
Without expanding it by hand like this, it's not at all obvious that surflist
ends up in hint[12]
.
There's another way to give XPanels the items in a popup, however. Instead of the XpSTRLIST
macro, you can use XpPOPFUNCS
to pass a pair of callbacks that XPanels will call
when it needs to know the item count and the name of each item. Since it doesn't paint you
into a corner the way XpSTRLIST
can, this is the preferred method for item lists
that must be built at runtime. I chose not to use it here because I decided, somewhat
arbitrarily, that an array would be easier to understand than the callbacks would be. But
we will use XpPOPFUNCS
in Part 4.
Command Line Processing
Command sequence plug-ins can call other command sequence plug-ins using Modeler's CMDSEQ
command. CMDSEQ
allows you to pass arguments to the called plug-in.
Our box plug-in, in other words, can be called by other Modeler plug-ins. When used
this way, it becomes just another command! For this to be really useful, we need to
process the command line so that we can accept arguments. Modeler passes the command line
to us in the argument
field of the LWModCommand structure.
Our parameters are the box size and center, the surface name, and the vmap name. The obvious command line for us would be
<size> <center> surfname vmapname
where the size and center arguments are vectors enclosed in angle brackets, and the other two arguments are strings, possibly enclosed in double quotes. For example,
<1.5 2.5 3.5> <0> "Bram Stoker" Dracula
Modeler passes this to us as a single string. It's up to us to divide it into an array
of tokens similar to the argv
array passed to a C console program's main
function. It's a little tricky. We can't just call the C runtime function strtok
,
since spaces are only delimiters if they're not inside double quotes or angle brackets,
and angle brackets are only delimiters if they're not inside double quotes.
We'd also like to support some of the same conventions Modeler itself does for command arguments: Vector components after the first are optional, and if omitted, are assigned the value of the last component present. Strings that don't contain spaces don't have to be enclosed in double quotes.
It might seem like we're making work for ourselves by supporting a more complicated command line. But keep in mind that users can also write a command line for our plug-in when they assign it to a key or a menu, so conforming to Modeler command conventions is usually a good idea. We'll also get some help from LightWave for converting the vectors.
Our get_argv
function breaks the command line into an array of token strings.
It just looks at each character in the command string and decides whether to add it to the
existing token or start a new one. Tokenizing a string is covered in numerous general
programming texts, so I won't go into detail about how get_argv
is implemented.
The function that calls get_argv
is parse_cmdline
.
int parse_cmdline( DynaConvertFunc *convert, const char *cmdline, double *size, double *center, char *surfname, char *vmapname ) { DynaValue from = { DY_STRING }, to = { DY_VDIST }; int argc; char **argv;
The first argument is the function returned by the Dynamic Conversion global. This function takes a DynaValue of one type (in our case, a string) and returns one of a different type (a 3-vector of distance values). We'll use this to convert the size and center vector strings into arrays of three doubles. This gives us automatic support for the default values of missing vector components.
argv = get_argv( cmdline, 4, &argc ); if ( argc == 4 ) { from.str.buf = argv[ 0 ]; to.fvec.defVal = 1.0; convert( &from, &to, NULL ); size[ 0 ] = to.fvec.val[ 0 ]; size[ 1 ] = to.fvec.val[ 1 ]; size[ 2 ] = to.fvec.val[ 2 ]; from.str.buf = argv[ 1 ]; to.fvec.defVal = 0.0; convert( &from, &to, NULL ); center[ 0 ] = to.fvec.val[ 0 ]; center[ 1 ] = to.fvec.val[ 1 ]; center[ 2 ] = to.fvec.val[ 2 ]; strcpy( surfname, argv[ 2 ] ); strcpy( vmapname, argv[ 3 ] ); } free_argv( argc, argv ); return ( argc == 4 ); }
If get_argv
finds four tokens in the command string, the first two are assumed
to be vectors and are assigned to the size and center arrays after conversion. The last
two are assumed to be surface and vmap names. The function returns TRUE if the argument
count is 4.
Activation
The activation function is where we pull all of this together.
XCALL_( int ) Activate( long version, GlobalFunc *global, LWModCommand *local, void *serverData ) { DynaConvertFunc *dynaf; LWXPanelFuncs *xpanf; LWSurfaceFuncs *surff; MeshEditOp *edit;
We'll get these four things by calling functions in Modeler.
double size[ 3 ] = { 1.0, 1.0, 1.0 }; double center[ 3 ] = { 0.0, 0.0, 0.0 }; char surfname[ 128 ]; char vmapname[ 128 ] = "MyUVs"; int ok = 0;
This is where our parameters are kept.
if ( version != LWMODCOMMAND_VERSION ) return AFUNC_BADVERSION;
Like always, the first thing we do is make sure Modeler is calling us with the right version of LWModCommand.
if ( local->argument[ 0 ] ) {
The argument
string is always valid. To decide whether we've received a
command line, we need to see whether the string is empty.
dynaf = global( LWDYNACONVERTFUNC_GLOBAL, GFUSE_TRANSIENT ); if ( !dynaf ) return AFUNC_BADGLOBAL; ok = parse_cmdline( dynaf, local->argument, size, center, surfname, vmapname ); if ( !ok ) return AFUNC_BADLOCAL; }
If it isn't empty, we get our parameters from the command line instead of displaying our interface.
else { xpanf = global( LWXPANELFUNCS_GLOBAL, GFUSE_TRANSIENT ); surff = global( LWSURFACEFUNCS_GLOBAL, GFUSE_TRANSIENT ); if ( !xpanf || !surff ) return AFUNC_BADGLOBAL; if ( !init_surflist( surff )) return AFUNC_BADGLOBAL; ok = get_user( xpanf, size, center, surfname, vmapname ); free_surflist(); }
If we don't have a command line, we display our interface as before.
if ( ok ) { edit = local->editBegin( 0, 0, OPSEL_GLOBAL ); if ( edit ) { makebox( edit, size, center, surfname, vmapname ); edit->done( edit->state, EDERR_NONE, 0 ); } }
If we got parameters from somewhere, either the command line or our interface,
we perform the mesh edit that creates our box. Between the calls to local->editBegin
and edit->done
, we can't call any commands. These calls are the boundaries of
a single undo atom. Mesh edits aren't actually applied until you call done
, so
from the point of view of commands, the geometry database is in an indeterminate state.
We should probably track errors that might occur in makebox
and pass something
other than EDERR_NONE
to done
if something goes wrong, but I left that
out because we had a lot of ground to cover. Don't be lazy like me. Stuff can go
wrong.
return AFUNC_OK; }
But life is good.
What's Next
Up to now, we've been writing imperative code. It marches from beginning to end, pausing only once to allow the user to type some numbers. In the final installment, we'll see how to turn our plug-in into an event-driven tool that allows the user to size and center the box interactively.