Realsoft 3D deformers, such as skeletons radial deformers and 3d lattice deformers, to name a few, define special purpose geometry to define their deformation effects. The generic deformer differs from these classes in that it doesn't define any geometric properties itself. Instead, it uses other geometric objects to define its geometric deformation effect. For example, a rectangle and an analytic sphere can be used for defining a deformation which maps a flat surface to a spherical surface.
Generic deformer also supports a weight attribute, which can be used for animating deformation effects. The stronger the weight, the stronger the generic deformer affects its target objects.
Having a minimal but working plugin library is a good way of starting plugin development. So create a new folder 'mydeformer' and copy the files from the 'samples/plugins/dummy' folder in it. Then change the library name from dummy to geomdeform build the plugin.
If you now start Realsoft 3D, you should see the message "My plugin works", as we demonstrated in the A Minimal Plugin tutorial.
The best super class for our geometric deformer is 'inc/real/objects/r3constructor.h', from which all geometric deformers, such as the skeleton, radial deformers and bend are derived.
Let's take a closer look at the super class chain:
inc/oops/r3root.h - This is the base class for everything
inc/oops/r3model.h - Base class for model objects. Classes in Realsoft can be divided into two groups: models implementing functionality and views implementing user interface. See Model-View concept.
inc/real/code/r3ttag.h - Implements animation support and dynamic attributes
inc/real/objects/r3prim.h - Base class for all geometric objects, implements great deal of functionality behind all geometric objects
inc/real/objects/r3level.h - Implements hierarchy for geometric objects, objects can have sub objects
inc/real/objects/r3constructor.h - Base class for construction stack based objects
gendeform.h - Implements weight attribute and deformation based on other geometric objects.
Our class will inherit properties from all the above classes. These classes take care of great deal of functionality we need. In fact, we only need to implement one new attribute - weight.
Our public class header would look as follows:
#ifndef GENDEFORMER_H #define GENDEFORMER_H #include <real/objects/r3constructor.h> /* super class */ #define R3CLID_GENDEFORMER 2373 /* ask a class id from realsoft, or use testids */ /* method ids - none */ /* attribute ids */ enum { R3GENDEFA_Weight = R3CL_ATTRBASE(R3CLID_GENDEFORMER) /* R3FLOAT, level of deformation */ }; /* registration function */ int RegisterGenericDeformerClass(R3APP *apphnd); #endif
The above class header file defines all the public properties for our class.
100 class ids are reserved for testing purposes, which can be used as follows:
#define R3CLID_GENDEFORMER (R3CLID_TESTBASE + x)
where 'x' can be anything between 0 ... 99. However, we already have an official class id for this class so we use it.
It is a good idea to use test ids for developing plugins and when you are ready to release the plugin, ask official class ids from Realsoft.
The actual implementation file can be found from 'sdk/samples/plugins/geometrics/deformer/gendeform.c'. Let's see what it does.
All implementation files must include at least one header: our own public class header. However, we need more because we are goin to implement fully functional 100% industry proof tool set.
We (gendeform.c) need the following headers:
#include "gendeform.h" // we always need our own public class header #include <oops/r3file.h> // file class for saving / loading our weight #include <oops/r3matrix.h> // our deformation effect needs matrix math #include <oops/r3vect4.h> // vector 4 (nurbs control points) #include <real/code/r3acpoint.h> // choreography class #include <oops/r3locale.h> // we support localization #include <oops/r3icon.h> // icon class #include "r3icons/gendeform.h" // our icon
Our instance data has only one item - weight.
typedef struct r3idata { R3FLOAT weight; }R3IDATA;
The user must be able to animate our weight attribute. He/she might want to create an animation where a flat mesh turns smoohtly into a sphere.
Therefore, we must support the public attribute interface by defining the following static array in the inplementation file:
static R3TAGNAME public_tags[] = { {"Weight", R3GENDEFA_Weight, R3TID_FLOAT, R3TN_ANIMATEABLE, }, {NULL} };
And by catching and handling the following methods:
R3RM_MAKETAGLIST R3RM_FREETAGLIST R3RM_NAMEFORTAG R3RM_FINDTAGDESCRBYNAME
There is nothing special with these. In most cases you can cut the source code of these methods from an existing implementation file and paste the code into your new class. These methods just provide an interface to the R3TAGNAME structure so that the animation system, and scripting, to name a few, can talk to the class.
Only if you geometric attribute defines dynamic attributes, you'll have pay more attention to these methods. Instead of defining attributes statically using R3TAGNAME array, as we do above, you must build the array dynamically, to match the available attributes in your class.
For example, skeleton may have any number of joints. Correspondingly, it cannot define a static array to specify properties for the joints.
We don't define too many strings, just the default object name and class description. They can be localized as follows:
#include <oops/r3locale.h> enum { R3S_DEFAULTNAME, R3S_CLASSNAME }; static R3LOCALE *l, locale[] = { {"DefaultName", "Generic deformer"}, {"ClassDescription", "Deformer"} };
Note, keep each string and its value in a single line. Realsoft extracts these strings into a string files which the user can then localize. Remember that user doesn't understand anything about C or C++ so keep these string as human readable as possible.
In the registration function, you must open the database by calling:
/* open the database */ l = R3AppOpenLocale(app, R3MODULENAME, R3CLASSNAME, locale, R3NumArrayElements(locale));
You can then use R3STR() macro to fetch any of the defined strings. If there is a localized version available, the system will return it to you. If not, the above defined built-in string will be returned.
For example, you could print the class name to the user as follows:
R3Info(R3STR(l, R3S_CLASSNAME));
And when you no longer need the strings, you must close the database via call to:
/* close the database when you no longer need it */ R3AppCloseLocale(app, l, R3NumArrayElements(locale));
Many interfaces in the program, such as the select window, are interested to show an icon associated with your class.
Because the icon is the same for all generic deformers, it can be defined as a class attribute rather than object attribute. Class attributes are defined as static variables:
#include <oops/r3icon.h> #include "r3icons/gendef.h" static R3OBJ *class_icon;
The 'oops/r3icon.h' header defines realsoft 3D icon class. The "r3icon/gendef.h" defines the properties of the actual icon instance.
You don't write this gendefor.h icon header manually. Instead, you generate it automatically from the actual images. Currently the SDK can use Microsoft's ICO images only.
When you enter nmake (or r3make) in the r3icons folder, a file named 'gendform.h' is generated from 'gendeform.ico' file. By including this header file into your source file you can create an icon, as follows:
/* r3rm_get */ case R3RA_Icon: if(!class_icon) { /* create an icon */ class_icon = R3New(R3CLID_ICON, R3ICONA_Data, gendeform_bits, R3ICONA_Width, gendeform_width, R3ICONA_Height, gendeform_height, R3ICONA_Depth, gendeform_depth, R3TAG_END); if (class_icon) R3DoA(class_icon, R3RM_REF, NULL); } *(R3OBJ **)tag->value = class_icon; break;
Note that gendeform_bits, gendform_width etc. are symbols that you can pass to R3CLID_ICON constructor. However, newer make any assumptions about what these symbols refer to. It is totally platform specific issue. On X the gendeform_bits refer to a bit map data. On Windows it refers to a resource name etc.
Because the created icon is a class attribute rather than object attribute, we must not delete it in R3RM_DELETE method, which is used for deleting objects.
Instead we must catch the R3RCM_FREECACHE method. This method is sent to the class before any of the classes is actually deleted.
case R3RCM_FREECACHE: if(cl == obj) { if(class_icon) { R3DoA(class_icon, R3RM_UNREF, NULL); class_icon = NULL; } } return (void *)TRUE;
Note: create the icon only when somebody asks it to not waste resources for something that is newer needed.
/* r3rm_get method */ case R3GENDEFA_Weight: *(R3FLOAT *)tag->value = self->weight; // push our weight to the caller break; /* r3rm_set method */ case R3GENDEFA_Weight: self->weight = *(R3FLOAT *)tag->value; // pull weight from the caller R3MChangedA(cl, obj, tag, create); break;
The R3MChangedA() is a simple macro that actually calls
if(!create) R3Do(obj, R3MM_CHANGED, tag->ident, NULL, R3TAG_END);
All classes that are derived from the inc/oops/r3model.h base class must call R3MM_CHANGED whenever the value of an attribute is changed. This generates R3RM_UPDATE event to those who have connected to the object using Model-View concept.
Because the weight attribute must be animateable, we need to call two additional methods: R3TTM_PREPANIMTAG and R3TTM_CHANGEANIMATEDTAGVALUE.
/* r3rm_set */ case R3GENDEFA_Weight: R3Do(obj, R3TTM_PREPANIMTAG, R3TTA_AniTag, tag->ident, R3TAG_END); self->weight = *(R3FLOAT *)tag->value; R3DoA(obj, R3TTM_CHANGEANIMATEDTAGVALUE, (void*)tag->ident); R3MChangedA(cl, obj, tag, create); break;
R3TTM_PREPANIMTAG creates a key framer and make the attribute animateable.
Then we change the attribute and call R3TTM_CHANGEANIMATEDTAGVALUE. This informs the animation system that the attribute has been changed. For example, the key framer choreography object creates a new key from this call.
Because we defined a new attribute, we must catch R3RM_READ and R3RM_WRITE methods so that if the weight attribute gets saved / loaded with projects.
You must first pass these methods to the super class and then write your own attributes.
Below you can find complete implementations of these methods. Let's pretent that we didn't have this weight attribute untill in class version 2. So we study the class version to make sure we don't try to read the attribute from projects written with the old version.
#include <oops/r3file.h> static void *r3rm_write(R3CLASS *cl, R3OBJ *obj, R3TAG *tags) { R3IDATA *self = R3CL_IADDR(cl, obj); R3OBJ *file = NULL; int *error = 0; R3TagGet(tags, R3RA_FileObject, &file, R3RA_Error, &error, R3TAG_END); if (!file) return (void*)FALSE; if (!R3DoSuperA3(cl, obj, R3RM_WRITE, 0, 0, tags)) return (void*)FALSE; if(!R3Do(obj, R3FM_WRITEFLOAT, &self->weight)) return (void*)FALSE; return (void*)TRUE; } static void *r3rm_read(R3CLASS *cl, R3OBJ *obj, R3TAG *tags) { R3IDATA *self = R3CL_IADDR(cl, obj); R3OBJ *file = NULL; int *error = 0, version; R3TagGet(tags, R3RA_FileObject, &file, R3RA_Error, &error, R3TAG_END); version = R3VersionForClid(R3CLID_GENDEFORMER, tags); if (!file) return (void*)FALSE; if (!R3DoSuperA3(cl, obj, R3RM_READ, 0, 0, tags)) return (void*)FALSE; if(version > 1) { if(!R3Do(obj, R3FM_READFLOAT, &self->weight)) return (void*)FALSE; } return (void*)TRUE; }
So far we have explained properties needed by any geometric classes. However, we are implementing a geometric deformer object that is used for modifying other geometric objects. Hence we need to provide functionality that allows the user to bind our object to desired target objects.
This is done by creating a choreography object for the target objects. The purpose of the choreography object is to link the target object to us so that when the target object is updated, our object will be called. So the choreography object tells us when we should apply our deformation effect to the target object.
To do this, we need to catch the R3CONSTRUCTORM_NEWCHOR method in which we will create a choreography object to the target object:
#include <real/code/r3acpoint.h> static void *r3constructorm_newchor(R3CLASS *cl, R3OBJ *obj, void *p1, void *p2, R3OBJ *target) { R3IDATA *self = R3CL_IADDR(cl, obj); R3Do(target, R3TTM_MANAGETAG, R3TTA_AniTag, R3PRIMA_Points, R3TTA_AniClid, R3CLID_ACPOINT, R3TTA_AniName, "generic deformer", R3TTA_KiObj, obj, R3TTA_KiMth, R3CONSTRUCTORM_TRANSFORMATTR, R3TAG_END); return (void*)TRUE; }
When the user clicks the 'Bind' tool this method is called once per target object.
R3TTM_MANAGETAG method makes the given attribute (R3PRIMA_Points) animateable and attached one R3CLID_ACPOINT choreography object to manage the attribute. Then it makes the choreography to sensitive to given input object, as specified by R3TTA_KiObj parameter.
Whenever the target object needs to be updated, this choreography will call input object with given R3TA_KiMth method.
The actual deformation function will then be called once per each attribute associated with the choreography. Note that even if we asked the system to manage one attribute - R3PRIMA_Points - the user can change the attribute set associated with our choreography. For example, the user can delete the R3PRIMA_Points attribute and associated our choreography with R3PRIMA_Color attribute.
If possible, we should be prepared to handle any attribute the choreography object may pass to us. However, geometric deformations are defined in cartesian space and applying geometric deformations to color attributes maybe a bit far fetched.
static void *r3constructorm_transformpoints(R3CLASS *cl, R3OBJ *obj, R3OBJ *target, R3ACPOINTCONTEXT *apc, char *points) { R3IDATA *self = R3CL_IADDR(cl, obj); int i; R3OBJ *fromobj, *toobj; R3VECTOR lp, uvw, p; if(!(apc->tag == R3PRIMA_Points)) { if((fromobj = R3DoA(obj, R3LEVM_GETSUBBYORDNUM, (void*)0)) && (toobj = R3DoA(obj, R3LEVM_GETSUBBYORDNUM, (void*)1))) { for(i = 0; i < apc->numvals; i++) { lp = *(R3VECTOR *)(points + i * apc->psize); /* map target point to abs space */ R3DoA(target, R3PRIMM_POINTTOABSSPACE, &lp); /* map target point to the param space of the first sub object */ R3DoA2(fromobj, R3PRIMM_MAPTOPARAMSPACE, &lp, &uvw); /* evaluate out through the param space of the second sub object */ R3DoA3(toobj, R3PRIMM_EVALUATEEXT, (void*)R3SPACE_ABSOLUTE, &uvw, &p); /* map back to the target's space */ R3DoA(target, R3PRIMM_POINTTOOBJSPACE, &p); /* and blend to the actual data channel with weight */ VInterpolate(points + i * apc->psize, apc->weight, points + i * apc->psize, &p); } } return (void *)TRUE; }
We first map the target point to abs space. Then we map the point to the parameter space of the first sub object. And then we use these uvw coordinates to evaluate the second object. Finally, we map the evaluated point back to the target's space.
We have now implemented all the methods we need. It is really up to you implemented them using a 'C', or a C++, or whatever other language you might like to use. The dispatcher function provides the interface between Realsoft 3D and your code.
Whenever Realsoft 3D needs to call a method of our geometric object, this dispatcher function gets called:
static void *R3Dispatch(R3CLASS *cl, R3OBJ *obj, int mth, void *p1, void *p2, void *msg) { switch(mth) { case R3RM_CREATE: return R3Create(cl, obj, msg); case R3RM_DELETE: return R3Delete(cl, obj, msg); case R3RM_WRITE: return R3WriteMth(cl, obj, msg); case R3RM_COPY: return R3Copy(cl, obj); case R3RM_READ: return R3ReadMth(cl, obj, msg); case R3RM_GET: return R3Get(cl, obj, msg); case R3RM_SET: return R3Set(cl, obj, msg, FALSE); case R3RM_MAKETAGLIST: R3TagList(cl, obj, msg); return R3DoSuperA3(cl, obj, mth, p1, p2, msg); case R3RM_FREETAGLIST: R3FreeTagList(cl, obj, msg); return R3DoSuperA3(cl, obj, mth, p1, p2, msg); case R3RM_NAMEFORTAG: return R3NameForTag(cl, obj, (int)p1, msg); case R3RM_FINDTAGDESCRBYNAME: return R3FindTagDescrByName(cl, obj, p1, msg); case R3CONSTRUCTORM_TRANSFORMATTR: return r3constructorm_transformpoints(cl, obj, p1, p2, msg); case R3CONSTRUCTORM_NEWCHOR: return r3constructorm_newchor(cl, obj, p1, p2, msg); case R3RCM_FREECLASS: if((R3OBJ*)cl == obj) { R3AppCloseLocale(app, l, R3NumArrayElements(locale)); } break; case R3RCM_FREECACHE: if((R3OBJ*)cl == obj) { if(class_icon) { R3DoA(class_icon, R3RM_UNREF, NULL); class_icon = NULL; } } break; default: return (void *)R3DoSuperA3(cl, obj, mth, p1, p2, msg); } return NULL; }
Registration function plugs your class into Realsoft 3D. It should register all the other classes it needs, so that whoever uses it will not have to worry about the internal structure of the class.
int RegisterGenericDeformerClass(R3APP *apphnd) { if(app) return TRUE; /* already registered */ app = apphnd; /* make sure our super class is there for us */ if(!R3RegisterConstructorClass(app)) return FALSE); if(!R3RegisterIconClass(app)) return FALSE); if(!R3RegisterAcPointClass(app)) return FALSE); /* open locale database */ l = R3AppOpenLocale(app, R3MODULENAME, R3CLASSNAME, locale, R3NumArrayElements(locale)); /* create our class */ if(R3ClassCreate(R3CLID_CONSTRUCTOR, R3CLID_GENDEFORMER, R3Dispatch, sizeof(R3IDATA), R3DEFAULTCLASSTAGS, R3RCA_Description, R3STR(l, R3S_CLASSNAME), R3RCA_Version, 1, R3TAG_END) == NULL) { R3AppCloseLocale(app, l, R3NumArrayElements(locale)); return FALSE; } return TRUE; }
Realsoft 3D supports symmetric multi processing in which any number of threads can run the same piece of code at the same time.
![]() |
Important |
---|---|
Only the program class creation / cleanup and R3LibraryInit() / R3LibraryFree() code is run single threaded by the main thread. So static variables can only be used as 'class attributes', to manage class specific data. You may newer use static variables for any instance specific data. |
Newer call class registration functions from other than the main thread.
To keep this tutorial simple, lets allow the user to create geometric deformers from the Select Window's new pop-up menu.
#include <oops/r3locale.h> /* let's localize the 'New/Generic Deformer popup item */ #include <real/gadget/r3sel.h> /* select windows' clas header */ #include "gendeform.h" /* our class header */ enum { R3S_GENDEFORMER }; static R3LOCALE locale[] = { {"GenericDeformer", "Generic deformer"}, }, *l; int R3LibraryInit(R3APP *app) { /* if the application has the select window class, register 'new' pop-up menu for it */ if(R3ClassFind(R3CLID_SELECT)) { l = R3AppOpenLocale(apphnd, R3MODULENAME, R3CLASSNAME, locale, R3NumArrayElements(locale)); if(RegisterGenericDeformerClass(apphnd)) R3DoClass3(R3CLID_SELECT, R3SELCM_REGISTERNEWOBJTYPE, (void *)R3CLID_GENDEFORMER, R3STR(l, R3S_GENDEFORMER), R3TAG_END); } return TRUE; } int R3LibraryFree(R3APP *apphnd) { if(l) R3AppCloseLocale(apphnd, l, R3NumArrayElements(locale)); return 0; }
The select window class defines the method R3SELCM_REGISTERNEWOBJTYPE that allows you to expand the class specification of the select window. When the select window will be created (instanced) your new popup menu item will be instanced with it.
However, before you can call this method you must study whether the application has a select window. This is done by calling R3ClassFind() function. If you forget to don't this, your plug-in may work fine with the actual Realsoft 3D application, but crash with other Realsoft 3D applications, such as batchren or rendd.
Another common mistake is that you avoid crashes by registering the select window. This will save your plugin from crashing, but it will turn all Realsoft 3D applications to more or less full featured Realsoft 3D applications, which is equally bad.