Lightwave Plugin Tutorials - Lesson One

Introduction compiling your first plugin

Having been lurking around receiving the Lightwave Plugins mailing list for a couple of years now, I get the impression that its not just me that thinks the documentation for creating plugins leaves something to be desired. Its for this reason that I have started this series of tutorials. If you like them or loath them I still like getting email so send me a message.

I got into writing Lightwave plugins after reading Bob Hood's introduction to his wonderful LScript language, in Inside Lightwave 3D. I cannot stress how important both Bob and Ernie Wright are to the community of plugin developers, thanks guys.

When I’m doing paid work I’m an artist, so if I can write plugins, so can you. So lets get started on our first tutorial. The first step is to download the SDK (Software Development Kit) from the Newtek site. Unzip it to some sensible folder making sure to take the directory information. From now on everything will be totally VC++ orientated, because this is my development platform and I am strictly an IDE (Integrated Development Environment) man. I can't be doing with writing in a text editor and command-line compilng. So we need to tell VC++ where to find the include files and the library. This is done using options/directories, add the path for the includes and the library files. You will find both the includes and the library files in the SDK that you unzipped. If you are working on Intel then you will find ready made versions of serv_w.obj and server.lib, which you will need later, in the tutorial files. Place these files in DevStudio\Vc\Lib. If you are on another platform then you will need to compile your platform specific version.


An Overview of Layout Plugins

Lighwave plugins currently come in 18 flavours. These are:

  • anim saver
  • image saver
  • color picker
  • item motion
  • command sequence
  • Layout generic
  • displacement
  • mesh edit
  • file requester
  • object import
  • frame buffer
  • object replacement
  • global
  • pixel filter
  • image filter
  • scene converter
  • image loader
  • shader

Lightwave 6 will add another 8 to these

  • anim loader
  • master
  • channel handler
  • texture
  • custom object
  • tool handler
  • environment
  • volumetric

If you add a fancy interface to a Layout plugin then you also need to know about how to create all those input boxes that are supplied by

  • LWPanels

So there is a lot to learn!

But, for now we are going to create a groovy little Item motion plugin that will bounce a ball when it hits a Y value of zero. I am absolutely useless at remembering all the bits of syntax in all the calls so I have created a blank version of each of the different kinds of plugins and this is where we will start.

Firstly the standard headers that you will need to include.

// ItemMotion.C -- Blank Item Motion Plug-in
// by Nik Lever
// last revision 2/3/99

#include <splug.h>
#include <moni.h>
#include <lwran.h>
#include <lwpanel.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>

// Disable warnings for various conversions.
#ifdef _WIN32
#pragma warning(disable:4244)
#endif

//Typedefs
typedef unsigned short int USHORT;

Now here are some useful global variables that we will use later

// Global -- Item Info functions.
static LWSceneInfo *si;
static LWItemInfo *ii;
static LWObjectInfo *oi;
static GlobalFunc *glb;
LWPanelFuncs *panl;
static LWPanControlDesc desc;
static LWValue ival={LWT_INTEGER},ivecval={LWT_VINT},
fval={LWT_FLOAT},fvecval={LWT_VFLOAT},sval={LWT_STRING};

Lightwave can have lots of objects using the services of a plugin. It identifies each one by passing an instance structure. This is just a void pointer that you cast into something that contains useful data for your plugin. Create your structure as a typedef and then the casting is easily achieved.

// Item Motion instance structure.
typedef struct stIMINST{
   LWItemID id;
   int mode;
   char desc[80];
}IMINST;

Now we have the standard functions that Lightwave uses for a Item Motion plugin. More about this in a minute. Take a look at them and be confused be very confused. Actually, they make far more sense than at first seems apparent and after the next 4 lessons you will understand them clearly.

// Standard instance handler functions.
XCALL_(static LWInstance)
create(LWError *err,LWItemID id)
{
   IMINST *im;

   XCALL_INIT;
   im = calloc(sizeof(IMINST),1);
   im->id = id;
   im->mode=0;
   strcpy(im->desc,"Blank Item Motion");
   return ((LWInstance) IMINST);
}


XCALL_(static void)
destroy(LWInstance inst)
{
   IMINST *im;
   XCALL_INIT;
   im = (IMINST*) inst;
   if (inst) free(inst);
}

XCALL_(static LWError)
copy(LWInstance from,LWInstance to,LWItemID id)
{
   IMINST *old,*cpy;

   XCALL_INIT;
   old = (IMINST*) from;
   cpy = (IMINST*) to;
   *cpy = *old;
   cpy->id = id;
   return (NULL);
}


XCALL_(static LWError)
load(LWInstance inst,const LWLoadState *ls)
{
   IMINST *im;

   XCALL_INIT;
   im = (IMINST*) inst;
   return (NULL);
}

XCALL_(static LWError)
save(LWInstance inst,const LWSaveState *ss)
{
   IMINST *im;

   XCALL_INIT;
   im = (IMINST*) inst;
   return (NULL);
}

XCALL_(static void)
process(LWInstance inst,ItemMotionAccess *ia)
{
   IMINST *im;

   XCALL_INIT;
   IM = (IMINST*) inst;

   switch (lp->mode){
   case 0://Not initialiased
      break;
   case 1://Ready to work with
      break;
   case 2://Just loaded
      break;
   }
}

XCALL_(static const char *)
descln(LWInstance inst)
{
   IMINST *im;

   XCALL_INIT;
   im = (IMINST *) inst;
   return (im->desc);
}

The location of all the preceeding functions is passed to Lightwave with the activation function. This is the first thing that any plugin does, it tells Lightwave where to find the instructions it needs to run the plugin. We are really only interested in LW5 or above so the version number will be higher than 1. If it is not then we bomb out. We also use the activation function to fill in the global variables we declared earlier. The actual details of these functions varies with the different plugin types. But, in a later tutorial you will learn how to use the LWSDK include files to give you the information you need to create the other types of plugins.

// Activation function. Remembers required globals and sets up the
// handler structure. This will work in 5.*.
XCALL_(static int)
blankim (long version,GlobalFunc *global,
   ItemMotionHandler *local,void *serverData)
{

   XCALL_INIT;
   if (version == 2) {
      local->create = create;
      local->destroy = destroy;
      local->copy = copy;
      local->load = load;
      local->save = save;
      local->evaluate = process;
      local->descln = descln;
   } else
      return (AFUNC_BADVERSION);

   glb=global;
   ii = (LWItemInfo *) (*global)("LW Item Info",GFUSE_TRANSIENT);
   if (!ii) return (AFUNC_BADGLOBAL);
   oi = (LWObjectInfo *)(*global)("LW Object Info",GFUSE_TRANSIENT);
   if (!oi) return (AFUNC_BADGLOBAL);
   si = (LWSceneInfo *)(*global)("LW Scene Info",GFUSE_TRANSIENT);
   if (!si) return (AFUNC_BADGLOBAL);

   return (AFUNC_OK);
}

Finally, we have to tell Lightwave where to find your plugin and its details. This is the data that will be stored in the LW.cfg file when you use AddPlugin from the options panel of Lightwave Layout.

ServerRecord ServerDesc[] = {
   { "ItemMotionHandler",	"BlankIM",	blankim},
   { NULL }
};

So that is the overview of a Blank Item Motion Plugin that does absolutely nothing!


Creating your first plugin

To make it even easier download the tutorial files. The tutorial files include a Visual C++ project, but you'll want to know how to roll your own so this is how it was created.

  1. Create a new Workspace as a Windows 32 bit DLL. Call it Gravity.
  2. Now Add the blank file, renamed as gravity.c
  3. Find a copy of serv.def in the LWSDK folders you unzipped earlier. Copy this file to the gravity folder and add it to the project.
  4. Now in the Project/Settings add the following link libraries, serv_w.obj server.lib libc.lib kernel32.lib and check the box that excludes default libraries.
  5. Rename the compiled file in the Debug tab to gravity.p. Once you are happy with your Plugin you might want to transfer it to your usual Plugin directory. Remember to change the path in your Lw.cfg file so that it points to the new location of the plugin.

Now we will amend our instance structure. So that we can keep a check on the status of our plugin. To make the code easier to read we will create a new type, VECTOR. In this plugin we are applying Newton’s Second Law to a projectile. The only force working on a projectile is gravity. The maths is standard stuff, if you know the launch speed and angle then you can calculate the position at any time. We’re going to calculate all these positions for an entire scene and store it in an array. This is both convenient and is the only way to work when we get to Displacement Maps so its useful to get into the habit. Because everything is stored in and array, we need a pointer to the array and how big the array is, this kept in the duration parameter. We want the user to be able to alter the starting position so we store that in an array of double values, where [0] is x, [1] is y and [3] is z. The final parameter in our instance structure is the variable mode. We use this to inform our plugin when it should perform different actions. We also define a couple of useful constants, gravity is defined as 9.82 ms2 and DEG2RAD converts between degrees and radians. There are 360 degrees and 2PI ( PI is approx 3.1415) radians in a circle. When programming in C all sin and cos functions expect radian value.

typedef struct stVECTOR{
   double x,y,z;
}VECTOR;

// Item Motion instance structure.
typedef struct stIMINST{
   LWItemID id;
   char desc[80];
   VECTOR *pos;
   double startpos[3];
   double launchspeed;
   double launchangle;
   double bouncecoeff;
   int duration;
   int mode;
}IMINST;

//Constants
#define GRAVITY 9.82
#define DEG2RAD 0.01745329251994

For our Activation function we simply rename our Blank Item Motion to gravity. Other than that no changes are required. Now we look at the changes to the standard handlers.

Create

Here we allocate memory for our instance structure and assign some default values.

XCALL_(static LWInstance)
create(LWError *err,LWItemID id)
{
   IMINST *im;

   XCALL_INIT;
   im = calloc(sizeof(IMINST),1);
   im->id = id;
   strcpy(im->desc,"Gravity Version 1 by Nik Lever");
   im->pos = NULL;
   im->duration = 0;
   im->mode = 0;
   im->startpos[0]=0.0;
   im->startpos[1]=0.0;
   im->startpos[2]=0.0;
   im->launchspeed=5.0;//Metres per second
   im->launchangle=70.0;//Degrees
   im->bouncecoeff=0.8;//How much height is lost on a bounce
   return ((LWInstance) im);
}

destroy

If we created the memory for an instance structure then we must destroy the memory allocated to it, including the array of position values.

XCALL_(static void)
destroy(LWInstance inst)
{
   IMINST *im;
   XCALL_INIT;
   im = (IMINST*) inst;
   if (im->pos) free(im->pos);
   if (inst) free(inst);
}

descln

The title that appears when you select Motion Plugins from the Graph Editor in Layout is defined using the descln handler. We keep the information in our instance structure. This allows use to create data specific to this instance of the plugin. For now we just use this to give the current duration:

XCALL_(static const char *)
descln(LWInstance inst)
{
   IMINST *im;

   XCALL_INIT;
   im = (IMINST *) inst;
   sprintf(im->desc,"Gravity Version 1 by Nik Lever (%i)",im->duration);
   return (im->desc);
}

copy, load and save

The only change here is that we set the mode parameter in the load handler to 2. This can be used later to inform the process handler that the plugin has just loaded. Creation, normal running and loading normally involve different types of process. This is just planning ahead.

XCALL_(static LWError)
load(LWInstance inst,const LWLoadState *ls)
{
   IMINST *im;

   XCALL_INIT;
   im = (IMINST*) inst;
   im->mode=2; //Set flag to say just loaded
   return (NULL);
}

process

This is where most of the work is done. But before we look at the code lets take a look at the ItemMotionAccess structure that is passed to the process function by Lightwave. In the include file it is defined like this

//This is the syntax for access and handling
typedef struct st_ItemMotionAccess {
	LWItemID item;
	LWFrame  frame;
	LWTime   time;
	void     (*getParam) (LWItemParam, LWTime, double vector[3]);
	void     (*setParam) (LWItemParam, const double vector[3]);
} ItemMotionAccess;

A LWItemID is simply the number of that object in the list of objects in Layout. It is not a pointer to the object. The frame value is obvious, time is in seconds but is a floating point value, a decimal. You can get at any of these in the standard way you use a structure in C. That is you use the following syntax

myframe=ia->frame;

The other two options are pointers to functions. Here you pass the type of parameter you want, it could be LWIP_POSITION as we will use here or

  • LWIP_RIGHT Gets the left column of the rotation matrix
    LWIP_UP Gets the middle column of the rotation matrix
    LWIP_FORWARD Gets the right column of the rotation matrix
    LWIP_ROTATION Gets the rotation hpb of a LWItem at certain time
    LWIP_SCALING Gets the scaling sx,sy,sz of a LWItem at certain time
    LWIP_PIVOT Gets the pivot point px,py,pz
    LWIP_W_POSITION Gets the world position of a LWItem
    LWIP_W_RIGHT Gets the left column of the world rotation matrix
    LWIP_W_UP Gets the middle column of the world rotation matrix
    LWIP_W_FORWARD Gets the right column of the world rotation matrix
  • If you are confused about the matrix options then maybe this will help

    |R1 U1 F1||x| |R1*x + U1*y + F1*z|
    |R2 U2 F2||y|=|R2*x + U2*y + F2*z|
    |R3 U3 F3||z| |R3*x + U3*y + F3*z|

    Here R represents LWIP_RIGHT and U and F are LWIP_UP and LWIP_FORWARD respectively. When any point is multiplied by these it gives its new location, you can see from the matirx operation above that R affects only the x value of the point, U the y and F the Z.

    OK, so we are now ready to put all this to work. First we check if we are on frame zero. If we are then we return so the user can alter the position of the object on frame zero. Then we find out which mode we are currently using. 0 means the plugin has just been created so we better create the data for the path. 2 means we are just loaded so we also need to Create the path. If the mode is 1 then we have a path but we need to check if it needs amending, we do this by check the current position for frame 0 against our stored value. If its changed then we need to update our path. If not then we amend the current position to our calculated value.

    XCALL_(static void)
    process(LWInstance inst,const ItemMotionAccess *ia)
    {
       IMINST *im;
       double pos[3];
       XCALL_INIT;
       im = (IMINST*) inst;
       if (ia->frame==0) return; //Ignore frame zero
    
       switch (im->mode){
       case 0://Not initialiased
          ia->getParam(LWIP_POSITION,0.0,im->startpos);
          CreatePath(im);
          break;
       case 1://Ready to work with
          if (ia->frame==1){
             //Check any changes on frame 1
             ia->getParam(LWIP_POSITION,0.0,pos);
             if (im->startpos[0]!=pos[0] || im->startpos[1]!=pos[1] ||
                 im->startpos[2]!=pos[2]){
                im->startpos[0]=pos[0];
                im->startpos[1]=pos[1];
                im->startpos[2]=pos[2];
                CreatePath(im);
             }
          }
          if (ia->frame<im->duration){
             pos[0]=im->pos[ia->frame].x;
             pos[1]=im->pos[ia->frame].y;
             pos[2]=im->pos[ia->frame].z;
             ia->setParam(LWIP_POSITION,pos);
          }
          break;
       case 2://Just loaded
          ia->getParam(LWIP_POSITION,0.0,im->startpos);
          CreatePath(im);
          break;
       }
    }
    
    

    So you probably want to know whats in CreatePath. Well its not terribly complicated but it does contain a bit of maths. Many plugins require only basic maths so don't be put off if you feel it is a little confusing. First we check if there has been a path before and free any memory that was created if there was. Then we create some more memory for our path. Then frame by frame we create the path, check for y<0. If it is then we update the speed parameter and restart the clock and continue our calculation.

    void CreatePath(IMINST *im){
    	double x,y,z,theta,speed,t=0.0,a,b;
    	int i;
    	VECTOR startpos;
    
    	im->duration=si->frameEnd;
    	if (im->pos) free(im->pos);
    	im->pos=(VECTOR*)malloc(sizeof(VECTOR)*im->duration);
    	theta=im->launchangle*DEG2RAD;
    	speed=im->launchspeed;
    	startpos.x=im->startpos[0];
    	startpos.y=im->startpos[1];
    	startpos.z=im->startpos[2];
    	for (i=0;i<im->duration;i++){
    		t+=0.04;//Add a frame to the time value
    		x=startpos.x + speed*t*cos(theta);
    		y=startpos.y + speed*t*sin(theta)-0.5*GRAVITY*t*t;
    		if (y<0){
    			y=0.0;
    			startpos.x=x;
    			startpos.y=0.0;
    			//Calculate new velocity
    			a=(speed*cos(theta));
    			b=(speed*sin(theta)-GRAVITY*t);
    			speed=sqrt(a*a+b*b)*im->bouncecoeff;
    			//Restart time
    			t=0.0;
    		}
    		z=startpos.z;
    		im->pos[i].x=x; im->pos[i].y=y; im->pos[i].z=z;
    	}
    	im->mode=1;
    }
    

    So that's it. Now compile it and run in Debug mode, Visual C++ will ask for an executable to use browse to Newtek\Programs\Lightwav.exe. Ignore the no debug information in executable messages. Add your plugin, you only need to do this once, after that Lightwave saves the path in its Lw.cfg file. Either create a scene with an object and from the Graph Editor, Motion Plugins button, scroll down to find Gravity and add it; or use the tutorial files scene "bounce1.lws". Now run the scene. Try changing the y value of the object on frame zero.


    Summary

    Hopefully this first tutorial wasn't to over facing. In the next lesson we will add an options box so that we can change the value of gravity, launch speed and angle, and bounce coefficients. Lesson 3 we will use a NULL to define the bounce point and improve our options box to include a bitmap. Lesson 4 we will look at using a Displacement map to alter an object. Watch out for these further tutorials in the coming weeks.

    Lesson 2 - Loading, saving and options boxes.
    Lesson 3 - Using bitmaps and callbacks in your options box.
    Lesson 4 - Your first Displacement Map.

     

     

    Millennium 3D Textures, models,tutorials and lots of other free stuff.