Developing an Animation Effect plug-in for OpenFX

Introduction

The plug-in animation effect is defined in the Animator as part of   a costume for a ModelActor. The effect provides the capability to reposition the vertices in the model. It is requred in the Animator and in the Renderer modules.  In the Animator it is required to provide a function to set the parameters that relate to the effect, and a second function to provide a preview of the effect in the Preview window. In the Renderer it must provide a function that generates the same vertex coordinate movements.

Note: The animator uses both integer and DOUBLE data types to represent the vertex coordinates, the Renderer uses both integer and FLOAT data types. In the renderer the DOUBLE data type is reassigned to FLOAT to save on memory usage.

Storing and using effect settings

Plug-in effects (and shaders/image processors) all have user configurable parameters. The number and type of the configurable parameters necessarily support different numbers of and different types of data.  To be able to store these in the .MFX and .OFX files OpenFX stores these parameters as a text string - each effect plug-in is expected to begin by reading its parameters from the parameter text string (if it exists, or create it from defaults, if it does not exist.) In that part of the plug-in used by the Designer or Animator, before it terminates it is expected to write into the text string its parameters. This can normally be done using the C functions sscanf() and sprintf().  By adopting this strategy OpenFX imposes no constraints on the type or number of parameters.

Building and Files

Each animation effect is built as a Windows DLL, it will have one or more C or C++ language source file, one or more project specific header files, a Visual Studio project file, a resource file (.RC)  and a file with filename extension .EFX.  The project will build to a .DLL of the same name as the EFX file.  The build files can have any location, but the EFX and DLL files must be placed in the "EFFECTS" folder.

The plugin source files need to include one esential header file.   the "pstruct.h" header is located in the "effects" folder and it included a number of other header files defining structures used by both the Animator and the Renderer. Two of the headers define the memory allocation functions used by the current OpenFX, these are located in the "animate" folder. The "common\mem.h" file provides system speciifc memory allocation function defines.

The animation structures open to the effects to use are presented in header "sfxinfo.h"  (Located in the effects folder) and the rendere structures that will be requried in an animation effect are defined in folder "render2", file ..\render2\vertex_face.h"

The resource file (.RC) will contain a dialog box description to allow the effect's parameters to be set by the user.  To get a feel for a plugin animation effect we shall look at a plugin for a simple wave effect that deforms the vertices of the object it is applied two in a wave like manner.

The user loads an effect and applied it to an object using the Model Keyfram editor and adds an Effect to the object as shown here:

effect.png (203422 bytes)

The files associated with the wave effect are:

these are located in the "effects" folder. The build project  file is also located in the "effects" folder and there is a SLN file that includes all the effects projects.

The build process will create a DLL file called wave.dll in the effects folder.  The wave.efx is a dummy file containing nothing, it is used for the external  texture selection box to provide a list of all effects, it can be regarded as a simple place holder.   If you want to start writing a new effect  plugin then we suggest you copy the files from the example plugin to a new set.  It's possible to copy the .VCPROJ files and modify it using notepad  replacing all instances of the string  "wave" with the name of your new texture.

This concludes all we have to say about building. Most of the other plugins in OpenFX follow a similar strategy.

The plugin-code

The plugin code follows the basic design of a Windows DLL . In this section we will examine the parts of the source file that are common to all effects plugins. The most important items to be aware of are the names of the functions called by OpenFX's modules.  These functions use pointers to data structures through which access to the OpenFX main module's (designer/animator/renderer) memory management, and other functions may be made.    

The OpenFX method of accessing global variables and main program functions   (VERY IMPORTANT)

OpenFX uses a pointer mechanism for allowing the plugins to access functions in the main program and program global variables. Since all OpenFX plugins are DLLs that are loaded into the main applications address space. The plugin DLLs are NOT loaded when the main application starts (many of the built-in actions are also implemented as DLLs that ARE loaded when the program starts) they are loaded when required and unloaded when they have finished.
In the case of plugins that are used in the Designer and Animator AND are needed by the Renderer (e.g. shaders) then the Renderer loads all the needed plugins when it reads the script file during initialisation. The Renderer unloads the plugins when it is finished with the script.

When the Animator requires to configure an effect  it calls  the function _SetExternalParameters(), with  arguments:

char *_SetExternalParameterss(char *Op, ,HWND hWnd, long ruler, char *name, X__MEMORY_MANAGER *lpevi);

typedef struct tagX__MEMORY_MANAGER {
void *(*fpMalloc)(long size);
void (*fpFree)(void *buffer);
void *lpAni;
void *lpMod;
} X__MEMORY_MANAGER;

The void structure members "lpAni" and "lpMod"   are used to pass pointers to the internal structures ANI_STRUCTURE(Animator)   and X__STRUCTURE(Designer) when appropriate!  (When not used these pointers are set to NULL.)

The Designer, Animator and Renderer  keep only one copy of this structure, it is initialised at start-up and passed to all the plugins.  By using the members of this structure any plugin can access the memory functions used internally.   This method is very simple and very flexible for extension and allowing plug-ins to access global data and functions in the calling programs.

In order that the contents of structures like this can be made available to all functions and files in a plugin the pointers passed in to the functions should normally  be copied into the global variable:

Accessing variables and functions in a DLL module through direct use of a pointer to the X__MEMORY_MANAGER  is very tiresome.  For example to use the Designer's number of selected  vertices variable (called "NvertSelect")  it would be necessary to use the code (  (*(lpevi->NvertSelect))    )  to  gain access to this variable.   This is very messy!  To overcome this issue and provide an interface to the Reindeer's  functions that make them look (in the code) just like normal function calls, a number of #defines are included in the header file "defines.h" , which is one of the ".h" files that every plugin must include.

Using these defines, calls to functions and use of global variables can be used as if they were part of the module code.

Keeping this very important information in mind we can return to considering the Checker texture plugin and look at the key code constructs.

Animation effects need to provide functions that show the action of the effect in the Animator and render the effect in the Renderer. There is an additional complication in the case of animation effects: Local or Global action.

To allow the effect to access the animator's internal variables two data structures are defined in the "sfxinfo.h" header. An instance of these structures is populated by the Animator and pointer sto thes are passed to the effect.

typedef float Svertex[3]; /* structure for preview vertex positions Animator*/ 
typedef struct {
long nvert; /* number of vertices that apply to the model */
long ltime; /* integer equivalent of member "time" */
long vmin[3];
long vmax[3];
long origin[3];
float time; /* proportion of elapsed time on actor channel */
long Nfaces;
void *Faces;
long version;
} sfxinfo; 
typedef struct {
long size, /* size of this structure */
  version, /* version of this structure */
  pos[3], /* position of the object (in the scene) */
  ground_pos[3],
  camera_pos[3],
  internal_axis;
BOOL ground, /* 1 if ground is present in animation */
  extras;
float phi,  theta,alpha,
      scale_x,scale_y,scale_z,
       internal_angle;
} sfxdata;

To allow the effect to access the Renderer's data structures

#define double float /* The renderer uses FLOAT real numbers */
typedef double vector[3]; 
#define UNSIGNED long 
typedef  long  ivector[3];
// Access is also provided to the Renderer's faces and vertices through the inclusion of the 
// Renderer's main data structures
#inlcude "../render2/vertex_face.h"

 

The required elements in a plugin effect are:

Essential headers, defines and global variables
#include <math.h>
#include <windows.h>
static HINSTANCE hDLLinstance=NULL; 
#include "pstruct.h"   // essential definitions 
#include "paint.c"   // provided funcitons to paint a window background (optional)
DLL standard entry code for Visual Studio compiler
BOOL WINAPI DllMain(HANDLE hDLL, DWORD dwReason, LPVOID lpReserved){
switch (dwReason) {
case DLL_PROCESS_ATTACH:
  break;
case DLL_PROCESS_DETACH:
  break;
}
return (int)TRUE;
}
Function called by the Animator to allow the user to set the effect's parameters,
char * _SetExternalParameters(
char *Op, /* string for the parameters */
HWND hWnd, /* parent window */
long ruler, /* ruler scale value to facilitate scaling */
char *name, /* name of DLL file with the effect */
X__MEMORY_MANAGER *lpEVI /* pointer to structure with memory functions */
){
char buffer[128];
if(Op != NULL){ /* parameters exist so read them off the list */
  /* the name of the DLL file is always the first string */
  sscanf(Op,"%s % ...",buffer,   ...);
}
  if(DialogBox(hDLLinstance,MAKEINTRESOURCE( ....),hWnd, (DLGPROC)DlgProc) == FALSE)return Op;
  if(Op != NULL)CALL_FREE(Op); /* free the old string */
 sprintf(buffer," ... ", ...);
  if((Op=(char *)CALL_MALLOC(strlen(buffer)+1)) == NULL){
    return NULL;
  }
  strcpy(Op,buffer);  // copy parameter string from local buffer to the new string.
  return Op;   // return parameter string 
}
Exported functions called by Renderer  to generate the effect on the object in the scene.
long _RenderExternalEffect(char *parameters, sfxinfo *SFXinfo, vertex *v){
 //  Renderer function to implement the  local effect in the 
 return 1;
}
long _RenderGlobalEffect (char *oarameters, sfxinfo *SFXinfo, sfxdata *SFXdata, vertex *v){
 // Renderer function to implement the effect if it is global.
 return 1;
}
Exported functions called by Animator to generate the effect in the camera view window. This will essentially be the same as the Renderer's functions.
void _PreviewExternalEffect(char *parameters, sfxinfo *SFXinfo, Svertex *v){
  // modify the vertex positions for rendering in the Animator camera view 
}
void _PreviewGlobalEffect(char *PrmList, sfxinfo *SFXinfo, sfxdata *SFXdata, Svertex *Vlist){
  // modify the vertex positions for rendering in the Animator camera view 
return;
}

The Wave effect example

To return to the wave example, we can  look at how the effect is generated. The wave effect is a local effect so only the local effect functions are used. With the exception of the format of the vertex data passed to the effect,  the preview (used by the animator) and the final rendering (the effect is called by the Renderer) are exactly the same.

In the case of the preview the vertex data is a 3 element vector of floats, to set the value of vertex "i" and coordinate "j" the statement:

v[i][j] += displacement; 

moves the vertex. In the case of the renderer, the statement:

v[i].p[j] += displacement;

move the vertex. (In the renderer the vertex structure is a more complex data structure that the simple list of vectors used by the animation. With this execption the code that calcuates the effect (in preview and final rendering) is basically identical so we need only  examine the Preview:

// FUNCITON ENTRY
void _PreviewExternalEffect(char *parameters, sfxinfo *SFXinfo, Svertex *v){

// LOCAL VARIABLES 
double amplitude,wavelength,displacement,PRMamp,PRMlngth,PRMfreq,d,maxd;
char buffer[256];
int type, PRMaxis, axis, fade, paxis, paxis2;
long i;

// READ THE PARAMETERS INTO LOCAL VARIABLES
sscanf(parameters,"%s %d %d %f %f %f %d",buffer,&type,&PRMaxis,&PRMamp, &PRMlngth,&PRMfreq,&fade);

// CHOOSE WHICH AXIS TO WAVE AND SET IDENTIFIERS ACCORDINGLY
if(PRMaxis == 3) axis=1; /* F / B */
else if(PRMaxis == 1) axis=2; /* U / D */
else if(PRMaxis == 2) axis=0; /* L / R */
if(axis == 0){ paxis = 1; paxis2 = 2; }
else if(axis == 2){ paxis = 1; paxis2 = 0; }
else if(axis == 1){ paxis = 0; paxis2 = 2; }

// GET THE OTHER PARAMETERS AND SCALE THEM SO THAT THEY MATCH
// THE OBJECTS DIMENSIONS (1 WAVE CYCLE ACROSS THE OBJECT
// PICK THESE UP FROM THE OBJECT'S DATA STRUCTURE
wavelength=max((SFXinfo->vmax[paxis]-SFXinfo->vmin[paxis]) * PRMlngth,1.0);
amplitude=(PRMamp/25.0) * (wavelength/8);
if(type == 2){ /* Radial */
   maxd=0.00;
   for(i=0;i<SFXinfo->nvert;i++){
     d=sqrt(v[i][paxis]*v[i][paxis] + v[i][paxis2]*v[i][paxis2]);
      maxd=max(maxd,d);
   }
}

// CALCULATE AND APPLY DISPLACEMENT TO ALL VERTICES
for(i=0;i<SFXinfo->nvert;i++){
   if(type == 1) d=v[i][paxis]; /* Linear */
   else if(type == 2){ /* Radial */
      d=sqrt(v[i][paxis]*v[i][paxis] + v[i][paxis2]*v[i][paxis2]);
   }
  displacement=amplitude*cos(PI*2*((SFXinfo->time*PRMfreq)-d/wavelength));
   if(type == 1){ /* Linear */
      d = v[i][paxis]-SFXinfo->vmin[paxis];
      maxd = SFXinfo->vmax[paxis]-SFXinfo->vmin[paxis];
   }
   if(fade == 2) displacement *= d / maxd; /* fade up */
   else if(fade == 3) displacement *= 1 - d / maxd; /* fade down */

  // UPDATE THE VERTEX COODINATES
   v[i][axis] += displacement;
}

}

This completes our discussion of the "Wave"  plug-in efffect as part of the material.

Go back to the developer page...