Lightwave Plugin Tutorials - Lesson Three

Using bitmaps and callbacks in your options box.

So were do we want to go today? We are going to look into callbacks. A callback is a function that is called by Lightwave to respond to a certain event. In our example we use them to respond to drawing the options box and clicking a button. We also make our options box look much more professional by adding a picture and adjusting the layout so that it now looks like this.

Adding a picture

We want our plugin to stay self contained and not be dependent on external files. So the biggest problem with using bitmaps in our plugin is getting the data in a bitmap embedded into the actual plugin. Lightwave plugin guru, Bob Hood, recommends creating the data as a header file. That is the approach we take. You will find a TGA to C header file convertor in the tutorial files. Run the program Tga2Header.exe. Start by selecting a TGA picture file then choose a destination save name and the program creates a header file from the pixel data in the picture.

You should now have a file that begins something like this where IMAGE1 is replaced by the file name of the header file.

#define IMAGE1WIDTH     319
#define IMAGE1HEIGHT    83

unsigned char image1_data[] = {
	120,120,112,
	122,122,117,
	124,124,121,
	125,125,123,
	126,126,125,
	127,127,126,

Add this file to the same folder as your project files and amend your includes to use it:

#include <splug.h>
#include <moni.h>
#include <lwran.h>
#include <lwpanel.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include "image1.h"
  • If you are using the tutorial files then you should rename "Gravity3.c" as "Gravity.c" before opening the project file. If you do this then you should be looking at the right source code.

Now we have the data, but we have to do some extensive changes to our options box code to use it. Firstly, we need a function that Lightwave can use when the options box needs redrawing. This function can have any name you choose, but the parameters in the function call must be a

anyName(LWPanelID id, void * userdata, DrMode dr).

The void * can be cast to something more useful, we will point to our instance. When we define our options box we will tell Lightwave to use this function when redrawing. Firstly we get a pointer to the panels drawing functions. Then we use the define value for height and width of our picture, this was added to the start of the image1.h file, to paint the picture a single pixel at a time. For this we use the DrawFuncs function drawRGBPixel.

drawRGBPixel(LWPanelID id, int r, int g, int b, int x, int y)

In the header file the pixel data is red, green, blue, red, green, blue etc. So we simply work through it, pixel by pixel, until we get to the end.

void drawPic(LWPanelID panID, IMINST *im, DrMode dm){
    int x,y,i,j,index,w;
    DrawFuncs *df;
    LWControl *ctrl=NULL;

    panl->get(panID,PAN_W,&w);  // Get the panel width
    df=panl->drawFuncs;
    index = 0;
    y = IMAGE1HEIGHT;
    for(j=0;j<IMAGE1HEIGHT;j++){
        x = (w-IMAGE1WIDTH)/2;
        for(i=0;i<IMAGE1WIDTH;i++){
            df->drawRGBPixel(panID,image1_data[index],
                 image1_data[index+1],image1_data[index+2],x++,y);
            index+=3;
        }
        y--;
    }

The drawPic function has one other feature; we want to draw a dividing line between the upper and lower sections of the box. The dividing line is to be just above the 8th control. So we iterate up the controls to find this one. Then we use the macro CON_Y(ctrl) to get the y position of this control. CON_Y is part of a set of macros that recieve data about a control. Look in the LWPanels.h file to find the others, they are near to the end of the file. Finally, we use another DrawFuncs function

drawLine(LWPanelID id, int col, int x1, int y1, int x2, int y2)

That covers the user defined drawing function. That wasn't too bad, was it?

//Find usenulls control
for (i=0;i<8;i++) ctrl=panl->nextControl(panID,ctrl);
//Get the y pos of the control
y=CON_Y(ctrl);
//Draw a dividing line
df->drawLine(panID,COLOR_DK_GREY,0,y-5,320,y-5);
df->drawLine(panID,COLOR_LT_GREY,0,y-4,320,y-4);
}

Changes to the Interface function

All the changes to the layout involve changes to the Interface function, these begin when the controls are added. With the first control we move it down to a y position of 100 so that we have room for our bitmap. MOVE_CON(ctrl,x,y) is the macro used;

panID = panl->create("Lightwave Plugin Tutorial - 3", im);
//Add and set controls now.
TEXT_CTL(panl, panID, "", msg);
ctrl = panl->nextControl(panID, NULL);
MOVE_CON(ctrl,10,100);

The next control is the widest, having 3 boxes to display the start position. We save its width using the macro CON_W(ctrl);

FVECRO_CTL(panl, panID, "Start Position");
ctrl = panl->nextControl(panID, ctrl);
SETV_FVEC(ctrl, im->startpos);
w=CON_W(ctrl);

We want the other controls to be positioned so that their right extent aligns to the start position control's right extent. First we get the y value which we are not altering, but we do need to pass it back later. Then we get the width of the control. Finally we move the control to the master width minus the current width. Hey presto, right aligned;

INTRO_CTL(panl, panID, "Duration");
ctrl = panl->nextControl(panID, ctrl);
SET_INT(ctrl, im->duration);
y=CON_Y(ctrl); x=CON_W(ctrl); MOVE_CON(ctrl,w-x,y);

The other controls use the same method, until we get to the check box, (which in Lightwave are known as BOOL controls). We want to grey out the next two controls if the check box is not checked. How do we tell Lightwave? We again use a call back. This is set using the CON_SETEVENT macro. The first parameter is the control, then a function name, and finally user data in the form of a pointer. Now whenever that control is clicked your specified function will be called.

BOOL_CTL(panl, panID, "Use nulls");
ctrl = panl->nextControl(panID, ctrl);
SET_INT(ctrl, im->usenulls);
y=CON_Y(ctrl); x=CON_X(ctrl); MOVE_CON(ctrl,x,y+10);
CON_SETEVENT(ctrl,useNullsEvent,im);

The last two controls are item controls. They will display a list of objects, bones or lights. They are created using 5 parameters. The usual panel functions, the id and a title. In additions they take a Global *, you've got this automatically in your Interface function, the final parameter indicates the type of list you want. We want these controls to be greyed out if the new parameter in the instance structure "usenulls" is not 1. The macro GHOST_CON does this for you.

ITEM_CTL(panl, panID, "Null 1",global,LWI_OBJECT);
ctrl = panl->nextControl(panID, ctrl);
SET_INT(ctrl,(int)im->null1);
if (!im->usenulls) GHOST_CON(ctrl);

ITEM_CTL(panl, panID, "Null 2",global,LWI_OBJECT);
ctrl = panl->nextControl(panID, ctrl);
SET_INT(ctrl,(int)im->null2);
if (!im->usenulls) GHOST_CON(ctrl); 

The last thing to do is tell Lightwave to use the draw function we have defined

( *panl->set )( panID, PAN_USERDRAW, drawPic );

The user defined event

A user defined event gets a pointer to the control that triggered it and the data that was passed. We get the current state of the control and use this information to set the ghosting for the two item controls

void useNullsEvent(LWControl *ctrl, IMINST *im){
	int i;
	LWPanelID panID;
	panID=(LWPanelID)CON_PAN(ctrl);
	GET_INT(ctrl,i);
	ctrl=panl->nextControl(panID,ctrl);
	if (i){ UNGHOST_CON(ctrl); }else{ GHOST_CON(ctrl); }
	ctrl=panl->nextControl(panID,ctrl);
	if (i){ UNGHOST_CON(ctrl); }else{ GHOST_CON(ctrl); }
}

Changes to the instance, loader and saver

The instance now has three additional features; an integer to define whether to use nulls, ( we will indicate why soon), and two LWItemID's for nulls.

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

These additions alter our saver and loader. So that older scenes will still load we use the version parameter to switch between savers and loaders.

	XCALL_(static LWError)
load(LWInstance inst,const LWLoadState *ls)
{
    IMINST *im;
	char line[128];

    XCALL_INIT;
    im = (IMINST*) inst;
    (*ls->read)(ls->readData,line,80);
	im->version=atoi(line);
	switch(im->version){
	case 3:
		ls->read(ls->readData,line,80);
		im->usenulls=atoi(line);
		ls->read(ls->readData,line,80);
		im->null1=(void*)atoi(line);
		ls->read(ls->readData,line,80);
		im->null2=(void*)atoi(line);
	case 2:
		ls->read(ls->readData,line,80);
		im->launchspeed=atof(line);
		ls->read(ls->readData,line,80);
		im->launchangle=atof(line);
		ls->read(ls->readData,line,80);
		im->bouncecoeff=atof(line);
		ls->read(ls->readData,line,80);
		im->gravity=atof(line);
		break;
	}
	im->mode=2; //Set flag to say just loaded
    return (NULL);
}

	XCALL_(static LWError)
save(LWInstance inst,const LWSaveState *ss)
{
    IMINST *im;
	char line[80];

    XCALL_INIT;
    im = (IMINST*) inst;
    sprintf(line,"%i",im->version);
    ss->write(ss->writeData,line,80);
    sprintf(line,"%i",im->usenulls);
    ss->write(ss->writeData,line,80);
    sprintf(line,"%i",im->null1);
    ss->write(ss->writeData,line,80);
    sprintf(line,"%i",im->null2);
    ss->write(ss->writeData,line,80);
    sprintf(line,"%f",im->launchspeed);
    ss->write(ss->writeData,line,80);
    sprintf(line,"%f",im->launchangle);
    ss->write(ss->writeData,line,80);
    sprintf(line,"%f",im->bouncecoeff);
    ss->write(ss->writeData,line,80);
    sprintf(line,"%f",im->gravity);
    ss->write(ss->writeData,line,80);
    return (NULL);
}

Using the Nulls

We use the nulls to get our ball bouncing at a different height to y=0. Null1 defines the left point of a slope, Null2 the right. We get the x,y information from the nulls and calculate the slope (y2-y1)/(x2-x1). For any x positions we can now find the y value from x*slope + y1. This is used to test the y value for our ball, if the ball is below this value then we start a new bounce.

// Global functions===============================================
void CreatePath(IMINST *im){
    double x,y,z,theta,speed,t=0.0,a,b,nully,slope,null1[3],null2[3];
    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];
    if (im->usenulls){
        //Get frame zero positions of nulls
        ii->param(im->null1,LWIP_POSITION,0.0,null1);
        ii->param(im->null2,LWIP_POSITION,0.0,null2);
        a=null2[0]-null1[0];
        if (a==0.0) a=1.0;//Test against div by zero
        slope=(null2[1]-null1[1])/a;
    }

    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*im->gravity*t*t;
        if (im->usenulls){
            nully=slope*x+null1[1];
            if (y<nully){
                y=nully;
                startpos.x=x;
                startpos.y=nully;
                //Calculate new velocity
                a=(speed*cos(theta));
                b=(speed*sin(theta)-im->gravity*t);
                speed=sqrt(a*a+b*b)*im->bouncecoeff;
                //Restart time
                t=0.0;
                }
             }else{
             if (y<0){
                 y=0.0;
                 startpos.x=x;
                 startpos.y=0.0;
                 //Calculate new velocity
                 a=(speed*cos(theta));
                 b=(speed*sin(theta)-im->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;
}

Now we are ready to use the plugin, the tutorial scene "bounce3.lws" goes with this lesson.

Summary

So we have now created a plugin, added an options box and improved the look and functionality of our options box. In the next lesson we will look at how we can use a displacement map to deform our ball when it bounces. Stay tuned.

Lesson 1 - Introduction, compiling your first plugin.
Lesson 2 - Loading, saving and options boxes.
Lesson 4 - Your first Displacement Map.