Tutorial: Box3

LightWave

Part 2 Part 4 Articles Table of Contents

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.