SSG Auxiliary Libraries. |
by Steve Baker |
PLIB/ssgAux is a suite of auxiliary libraries that build
higher level classes on top of the basic SSG classes.
|
![]() This image was created entirely from ssgAux primitives. |
/usr/include/plib/ssgAux.h /usr/lib/libplibssgaux.a
PLIB/ssgAux functions, classes and constants are all
named with an 'ssga' or 'SSGA' prefix.
eg:
class ssgaShape ; SSGA_TYPE_CUBE
class ssgaShape : public ssgBranch { ssgaShape (void) ; ssgaShape ( int numtris ) ; float *getCenter () { return center ; } float *getSize () { return size ; } int getNumTris () { return ntriangles ; } void setColour ( sgVec4 c ) { sgCopyVec4 ( colour, c ) ; regenerate () ; } void setCenter ( sgVec3 c ) { sgCopyVec3 ( center, c ) ; regenerate () ; } void setSize ( sgVec3 s ) { sgCopyVec3 ( size , s ) ; regenerate () ; } void setSize ( float s ) { sgSetVec3 ( size,s,s,s) ; regenerate () ; } void setNumTris ( int ntri ) { ntriangles = ntri ; regenerate () ; } void setKidState ( ssgState *s ) ; void setKidCallback ( int cb_type, ssgCallback cb ) ; virtual void regenerate () = 0 ; }The constructor allows you to specify the approximate number of triangles you'd like the shape to consume. For some shapes, this will be ignored (eg the Cube has 12 triangles - no matter how many you ask for) - for others, it's honored only approximately (eg the Sphere can only generate certain specific numbers of triangles) - and for others, it may be honored exactly.
You can also set the colour of all the polygons and the ssgState and callbacks that are applied to all of the child nodes.
setCenter allows you to position the center point of the object in 3D space - and setSize lets you determine the overall dimensions of the object (this too may also be approximate - the 'size' of a teapot is hard to specify in any useful way). The version of 'setSize' that takes an sgVec3 allow you to generate cuboids from cubes and sphereoids from spheres...but not all shapes allow this.
The 'regenerate' call allows the complete child structure of the object to be deleted and then recreated. This is occasionaly useful if (for example) you have walked down into the child structures and modified them - and you'd like the shape back into it's pristine conditions.
class ssgaCube : public ssgaShape { ssgaCube (void) ; ssgaCube ( int numtris ) ; virtual void regenerate () ; } ;This is the simplest of ssgaShapes - it defines nothing new over and above the standard ssgaShape.
class ssgaPatch : public ssgaShape { ssgaPatch (void) ; ssgaPatch ( int numtris ) ; void setControlPoint ( int s, int t, sgVec3 xyz, sgVec2 uv, sgVec4 rgba ) ; void setControlPoint ( int s, int t, float x, float y, float z, float u, float v, float r, float g, float b, float a ) ; void getControlPoint ( int s, int t, sgVec3 xyz, sgVec2 uv, sgVec4 rgba ) ; virtual void regenerate () ; } ;This implements a basic spline patch - with a 4x4 grid of 'control points' that define its shape. 'setContolPoint' allows you to set the spatial position, the texture coordinate and the colour at any given (s,t) position. (s and t each must be in the range 0..3).
It's necessary to call the 'regenerate' function whenever you have added or changed one or more control points.
class ssgaTeapot : public ssgaShape { ssgaTeapot (void) ; ssgaTeapot ( int numtris ) ; virtual void regenerate () ; } ;This creates the 'classic' Martin Newell teapot model.
class ssgaSphere : public ssgaShape { ssgaSphere (void) ; ssgaSphere ( int numtris ) ; void setLatLongStyle ( int ll ) ; int isLatLongStyle () ; virtual void regenerate () ; } ;There are two ways to generate a sphere model - one is to produce a fairly uniform grid of triangles that are as nearly equilateral as possible. The alternative is to generate more nearly right-triangles in cylindrical strips that are stacked on top of each other with a circular disk at top and bottom.
The function 'setLatLongStyle(ll)' allows you to choose which you want, call regenerate after you change types!
class ssgaCylinder : public ssgaShape { ssgaCylinder (void) ; ssgaCylinder ( int numtris ) ; void makeCapped ( int c ) ; int isCapped () ; virtual void regenerate () ; } ;Cylinders also come in two forms - capped and uncapped. Once again, if you change the capped versus uncapped status, call regenerate to make the new version.
The simulation uses between one and sixteen "Wave Trains" - each using a class:
class ssgaWaveTrain { ssgaWaveTrain () ; float getSpeed () ; float getLength () ; float getLambda () ; float getHeading () ; float getWaveHeight () ; void setSpeed ( float s ) ; void setLength ( float l ) ; void setLambda ( float l ) ; void setHeading ( float h ) ; void setWaveHeight ( float h ) ; } ;So, you create a bunch of these trains (typically, two or three is plenty to generate 'interesting' motion). Each wave train has a direction in which the waves are travelling, a speed, a wave length (which also affects the shape of the wave), the height of the wave and a 'lambda' term (which controls how much the tops of the waves bend over).
Next, generate the ssgaWaveSystem:
class ssgaWaveSystem : public ssgaShape { ssgaWaveSystem ( int ntri ) ; virtual void regenerate () ; ssgaWSDepthCallback getDepthCallback () ; void setDepthCallback ( ssgaWSDepthCallback cb ) ; ssgaWaveTrain *getWaveTrain ( int i ) ; void setWaveTrain ( int i, ssgaWaveTrain *t ) ; float getWindSpeed () ; float getWindDirn () ; float getEdgeFlatten () ; float getTexScaleU () ; float getTexScaleV () ; void setWindSpeed ( float speed ) ; void setWindDirn ( float dirn ) ; void setEdgeFlatten ( float dist ) ; void setTexScale ( float u, float v ) ; void updateAnimation ( float t ) ; } ;The appearance of waves varies greatly with the depth of the water - and this simulation produces the correct effects. There is an optional user-callback that can be used to feed water depth values into the simulation. The wave system will likely call this function many thousands of times per frame - so make sure your depth function is very efficient. If you don't provide a depth function then the water will be assumed to be infinitely deep.
You can determine the number of polygons used to render the patch of waves, the size of the wave patch and the amount of texture repetition. The 'EdgeFlatten' setting determines to what distance from the edge of the patch the waves are gradually flattened out. This is useful because it allows you to keep all those expensive wave polygons close to the camera - and feather them out over range so that they blend gently into a larger flat ocean.
Designing waves by calling the ssgaWaveSystem API is quite difficult. You are advised to use the wave designer program in the PLIB examples package. This program lets you adjust wave parameters until it looks how you'd like - then hit the 'Write C++ code' to write out a source code snippet that you can cut and paste into your program.
There is a 'README' in the 'water' source directory that explains how to use the program in a little more detail. That's all the wave system documentation I have time to write just now - sorry.
All of that flexibility is evident in the complex constructor function:
class ssgaParticleSystem : public ssgVtxArray { ssgaParticleSystem ( int max , /* Max Number of particles */ int init, /* Number to launch initially */ float add , /* Max number to create per sec */ int turn_to_face, /* Turn to face camera? */ float size, /* Size of particles */ float bsphere_rad, /* Size of bounding sphere */ ssgaParticleCreateFunc create, /* Initial create fn. */ ssgaParticleUpdateFunc update = NULL, /* Update function */ ssgaParticleDeleteFunc del = NULL /* Remove function */ ) ; void setSize ( float sz ) ; float getSize () ; update ( float dt ) ; int getNumActiveParticles () ; } ;There are three callback functions - with the following types:
typedef void (* ssgaParticleCreateFunc) ( ssgaParticleSystem *ps, int index, ssgaParticle *p ) ; typedef void (* ssgaParticleUpdateFunc) ( float deltaTime, ssgaParticleSystem *ps, int index, ssgaParticle *p ) ; typedef void (* ssgaParticleDeleteFunc) ( ssgaParticleSystem *ps, int index, ssgaParticle *p ) ;There is also a public class for a single particle:
class ssgaParticle { public: sgVec4 col ; sgVec3 pos ; sgVec3 vel ; sgVec3 acc ; float size ; float time_to_live ; void *userData ; ssgaParticle () ; } ;This is best explained with an example. Here is an example of a fountain model:
fountain = new ssgaParticleSystem ( 2000, /* Max Number of particles */ 2, /* Number to launch initially */ 200, /* Max number to create per sec */ TRUE, /* Turn to face? */ 0.2, /* Size of particles */ 20.0, /* Size of bounding sphere */ fountain_create, /* Initial create fn. */ fountain_update /* Update function */ ) ;This creates a system that has the capability to draw 2000 particles (each is a quadrilateral). Two particles are launched initially and 200 more are added gradually every second. You can choose to have each particle turn to face the camera - or to be oriented in the X/Z plane, mine are set to turn-to-face. The particles are 0.2 OpenGL units across.
This fountain is a continuous effect - so the inital number of particles to launch is small - and the 'per second' creation rate determines the flow. If you wanted something more like an explosion, have a larger number of initial particles and none launched per-second after that.
One messiness is that we don't know how big the fountain may become over time - and it's VERY inefficient to recompute the bsphere every frame. Hence you have to tell the class what the maximum bounding sphere of the fountain is at the outset so we can field-of-view cull it - this could be a near infinite radius (MAX_FLOAT say) if you truly don't know how big it could get - but that's inefficient because all 2,000 particles must be sent to OpenGL even if the fountain is 20 miles away behind the camera - so do your best to come up with some kind of a reasonable guess.
fountain_create and fountain_update are user-defined callback functions, we could also have defined a 'fountain_delete' function - but many applications won't need this.
These callbacks are passed the address of the particle system, the index number and address of the particle that's being created/updated/deleted - and (for the 'update' function only) - the amount of elapsed time since this particle was last updated.
The callbacks can set, read or change any or all aspects of the particle... it's colour, position, velocity, accelleration, size and 'time to live' (in seconds). There is also a user data pointer - so you can hang your own data onto each individual particle.
The particle system automatically moves each particle according to the usual laws of motion and decrements it's time-to-live - so in many cases, you can just set the initial velocity, the force due to gravity and a reasonable time to live - and let the particle system code do the rest.
My 'fountain_create' function looks like this:
void fountain_create ( ssgaParticleSystem *, int, ssgaParticle *p ) { sgSetVec4 ( p -> col, 1, 1, 1, 1 ) ; /* initially white */ sgSetVec3 ( p -> pos, 0, 0, 0 ) ; /* start off on the ground */ sgSetVec3 ( p -> vel, (rand()%1000-500)/300.0f, (rand()%1000-500)/300.0f, 50.0f ) ; /* Shoot up and out */ sgSetVec3 ( p -> acc, 0, 0, -9.8f ) ; /* Gravity */ p -> time_to_live = 5 ; /* Droplets evaporate after 5 seconds */ }It populates the particle with an initial colour, position, velocity and accelleration and gives it a 'time to live' of five seconds. The velocity is randomised a bit to make a nicer looking fountain.
You can do all sorts of fancy things to the colour, position, velocity and accelleration in your 'update' function.
void fountain_update ( ssgaParticle *p ) { if ( p -> pos [ 2 ] < 0 ) p -> time_to_live = -1 ; p -> col [ 2 ] = (p -> time_to_live > 2) ? 1 : (p -> time_to_live/2.0f) ; }...this one just erases particles that go below zero altitude (by setting their time-to-live variable to -1) and makes them go a pretty shade of yellow for the last two seconds before they die.
The 'delete' function is principally useful for freeing up any user-data you allocated in the 'create' function or during 'update's. Don't delete the particle though - the particle system recycles it to avoid doing too much dynamic memory allocation.
All that remains is to call the ssgaParticleSystem::update function every frame with a parameter that tells the system how much time has elapsed since the last call. This allows you to easily speed up, slow down or pause the particles (or even run them backwards if you want).
I call this every frame:
void updateFountain () { static ulClock ck ; ck . update () ; if ( fountain != NULL ) fountain -> update ( ck.getDeltaTime () ) ; }This function could also check the number of particles currently in flight and delete the fountain when there are none left - that might be useful for an explosion or something where you'd want to save CPU time by not running the explosion particle system when all the pieces have landed and 'gone away'.
The size of each particle is the size field of the individual particle MULTIPLIED by the number you pass into the size parameter of the ssgaParticleSystem constructor (or set with setSize()). This allows you to cheaply set the particle size for all the particles at once - or to tweak the size of each one in turn. The number in the particle structure defaults to 1.0 so you can ignore it and just set the size in the particle system overall.
ssgaParticleSystem is derived from ssgVtxArray - so you can apply textures and other state things using the usual ssgVtxArray::setState(ssgSimpleState*) call. I applied a texture with a fuzzy alpha-blended circle.
While debugging this, I got a bug which caused a picture of Tux to be applied instead of the droplet texture - it was absolutely hilarious to see 2000 tiny penguins shooting up in a fountain and falling gently to earth!
class ssgaFire : public ssgaParticleSystem { ssgaFire ( int num_tris, float radius = 1.0f, float height = 5.0f, float speed = 2.0f ) ; virtual ~ssgaFire () ; virtual void update ( float t ) ; void setUpwardSpeed ( float spd ) void setHeight ( float hgt ) void setRadius ( float rad ) void setHotColour ( sgVec4 col ) } ;In the constructor, you set the number of triangles you wish to generate, the radius and height of the approximately cylindrical fire and the speed at which the flames head up towards the sky.
The number of polygons you use tends to be rather critical - too many and your fire will look too 'smooth' and will be mostly white-hot. Too few and it'll look like a number of detached reddish blobs floating upwards. For the default radius, between 100 and 200 triangles seems to look good - for a 10 meter patch of fire, you may need as many as 2000 triangles to make it look effective. All three parameters (speed, height and radius) can be manipulated in realtime - but since the number of polygons is fixed, there is a limit to the amount of realtime tweaking you can do without making it look silly.
The 'setHotColour' function allows you to set the colour of the hottest flames (at the base of the fire). Since many layers of polygons add up to form the colour, you'll tend to want to use a primary colour with about 10% of one or two of the other primaries. The default colour is (1.0, 0.2, 0.1, 1.0) which produces red flames with yellow and white in the hotter regions.
The ssgaLensFlare object is a special kind of ssgaShape that can be positioned beneath the same transform as the source of the light - and it 'just works'. You don't need to set any parameters - just add it into the SSG scene graph in the right place.
eg:
ssgTransform *myTransform ; ssgBranch *myLightSource ; /* Set up myTransform and myLightsource */ ... myTransform -> addKid ( myLightSource ) ; myTransform -> addKid ( new ssgaLensFlare ) ;It contains a (hard-coded) 256x128 texture map which is compiled into the code and shared between however many lens flares there are in the scene.
![]() | Steve J. Baker. <sjbaker1@airmail.net> |