SGI

Aqua

This aquatic demonstration shows how to implement many features commonly required by 3D performer applications and a few less common rendering tricks.


Detailed Topics


Motion Model

Motion models are about the most elementary requirement of interactive 3D computer graphics. The idea is to direct the viewing position through the scene under the intuitive direction of the observer to give the illusion of first person immersion. Performer viewing coordinates for a pfChannel are specified as a 4x4 matrix often created from a pfCoord holding position and Euler angles. There are common problems encountered when using Euler angles in motion models for a number of reasons, the most common problem is that interpolations of Euler angles produce non intuitive but predictable results, also different Euler angle representations can lead to the same viewing orientation and the effects of a given Euler angle transformation are difficult to add another Euler angle transformation. While these problems are not insurmountable they have proven themselves to be impossible stumbling blocks for many, and the advantages of other vector based orientation representations both in terms of computational efficiency and ease of use are well known. One popular type of representation is the quaternion which represents changes in orientation as an axis of rotation and an angle. The quaternion is a much overhyped animal but here I'll develop a simple vector based motion model which happens to be stuffed full of quaternion like rotations, but you'll never hear the word again, no doubt the code could be simpler if I dug out Maxwells equations but sticking with vectors is plain easy to understand. The key to the model is that everything is vector based and so hopefully fairly intuitive and as jargon free as a motion model can be. The model transforms the unambiguous forward and up vectors of a vehicle using simple dot and cross products to calculate model dynamics (if you don't understand what the dot and cross products of vectors produces then you may want to skip this section). This model is free of any real physics based calculations other than simple acceleration but as you should quickly realize that it's easy to extend.

Looking in the sub.C module you will find the methods of the sub class. This class provides the functions required to perform the vehicle simulation and a few others we'll get to later. Aside from the constructor there are only two methods here which are significant for our motion model, these are:

void sub::accelerate(float thrust)
void sub::simulate(float joyx, float joyy, float dtime)

The accelerate method is straightforward and allows the speed to be increased within limits using the floating point value supplied as the argument, the vehicle speed is used in the simulate method but the details of the accelerate method aren't of significant interest.

The simulate method is the real business end of the class and performs the updates of the vehicle position using the values of existing class members and the elapsed time and joystick (or mouse) parameters supplied as arguments. The most significant member variables for this calculation are:

pfCoord subcoord holds Euler angles and position
pfVec3 forward holds normalized forward vector
pfVec3 up holds normalized up vector
float speed holds vehicle speed

The motion model needs to do a few simple things, it must move the vehicle forward each frame and it must pitch and roll the vehicle according to the input of the control surfaces.

The forward motion part is really simple so we'll look at that first, the code just needs to multiply the speed by the normalized forward vector to give the velocity vector, this is the distance travelled in a unit of time, the next step is to scale this vector by the elapsed time to provide the displacement this frame, and add this to the displacement to the vehicle position:

subcoord.xyz += forward * (speed * dtime);

The orientation aspects are a little trickier but involve rotating the vehicle vectors around the appropriate axis for the motion commanded by the joystick input. So the first step is to decide how much to rotate the vehicle and around which axis. Commands on joystick x are used to direct the vehicle roll, this is effectively a vehicle rotation about the forward vector member, joystick inputs on y are used to command vehicle pitch which is a rotation about a vector not stored as a member but which can be described as the cross product of the forward and up vectors, this is calculated and stored in the vector called starboard:

pfVec3 starboard;
starboard.cross(forward, up);

So a pitch matrix is calculated using the following code:

pfMatrix calcmat;
calcmat.makeRot(PitchAuthority * (speed+.5f) * dtime * -joyy, starboard[0], starboard[1], starboard[2]);

Note that the axis of rotation is the starboard vector as described above, also note that the commanded pitch is scaled by both the elapsed frame time and a PitchAuthority factor. There is also a scale based on speed to approximate the effect that water flow against the control surfaces is required for the manueverability of the vessel but this is not physically based. The next step is applying this commanded motion to vehicle orientation, this simply requires that we transform the vehicle forward and up vectors through the pitch matrix we have just computed thus:

forward.xformVec(forward, calcmat);
up.xformVec(up, calcmat);

OK, we've now applied a pitching motion to the vehicle, the next step is to apply roll, this is simple enough, we can use the same approach as above but useing the forward vector as the rotation axis for constructing the matrix:

calcmat.makeRot(RollAuthority * (speed+.5f) * dtime * joyx,
forward[0], forward[1], forward[2]);

We only need to transform the up vector this time since the forward vector will be unaffected by the rotation.

up.xformVec(up, calcmat);

After this transformation has been applied to the vehicle some straightforward maintenance is required to avoid cumulative rounding errors which would ultimately corrupt the values held in the forward and up vectors, This is done by reproducing the starboard vector from the new forward and up vectors then computing the up vector based on the cross product of starboard and forward vectors, this ensures the forward and up vectors are at right angles, the next step is to normalize both vectors to ensure they are of unit length.

// ensure forward & up are at right angles by generating the
// cross products & then back
starboard.cross(forward, up);
up.cross(starboard, forward);
// normalise forward & up vectors
forward.normalize();
up.normalize();

Right! The motion model has been successfully updated, the only remaining step is to produce an updated view orientation for the vehicle and the only missing part is the Euler angles derived from the new forward and up vectors. Some simple trig gives us pitch and heading components:

// convert vector representation to Euler
// heading
subcoord.hpr[0] = pfArcTan2(-forward[0], forward[1]);
// pitch
subcoord.hpr[1] = pfArcSin(forward[2]);

To compute the roll I generate a matrix using Euler angles without any roll, I transform an up vector and starboard vector through that matrix and use the dot product of the transformed vectors (without roll) and the up vector from the motion model to ascertain what the Euler angle roll needs to be since this will be the cosine of the angle between the vectors:

calcmat.makeEuler(subcoord.hpr[0], subcoord.hpr[1], 0.0f);
calcvec.set(0.0f, 0.0f, 1.0f);
norolup.xformVec(calcvec, calcmat);
calcvec.set(1.0f, 0.0f, 0.0f);
norolstbd.xformVec(calcvec, calcmat);

So once we have the vectors without roll we can compare them against their equivalents with roll:

dot1 = up.dot(norolup);
if (dot1 > 1.0f) dot1 = 1.0f;
if (dot1 < -1.0f) dot1 = -1.0f;
dot2 = up.dot(norolstbd);
if (dot2 > 1.0f) dot2 = 1.0f;
if (dot2 < -1.0f) dot2 = -1.0f;
roll1 = pfArcCos( dot1 );
roll2 = pfArcCos( dot2 );

A single angle is ambiguous due to symmetry of rotation vector so one angle is used to decide if a quadrant adjustment is required for the other angle which is then used as the Euler roll:

if(roll2 > 90.0f)
roll1 = -roll1;
subcoord.hpr[2] = roll1;

This gives us the Euler angles for the coordinate which can now be used to set a pfDCS position or orient a pfChannel:

if(subpos)
subpos->setCoord(&subcoord);

That's all there is to it, this motion model has no concept of gravity or world up, and is quite usefull as a starting point for adding these concepts. The pov.C module in the BumpLogo demo further extends this motion model to incorporate an additional twist input which simply adds a commanded yaw as a rotation about the up vector. In many respects the motion model in pov.C and the associated class is simpler, but we will go on to explore some of the additional features the sub class adds to the aquatic demo.


Vehicle View Tethering

In addition to first person viewing, it is desirable to track the exterior of a vehicle using a variety of methods depending on circumstances, to show how this is accomplished the view in the demonstration can be set to track the vehicle in a variety of modes, you can review this feature by pressing the 'v' key while running the demo. This should cycle through the range of tethering modes shown below:

The application can choose a number of tether modes defined in sub.h as:

#define SUB_VIEW_POV 0
#define SUB_VIEW_WINGMAN 1
#define SUB_VIEW_TETHERHIGH 2
#define SUB_VIEW_TETHER 3

The sub class provides a method which writes position information to a pfCoord based on the mode supplied, this position is then used to position the tethered pfChannel:

void sub::posview( pfCoord *view, int mode )

An example using this method (taken from various parts of the aqua.C) follows:

pfChannel *centre_chan;
pfMatrix *matey
// Shared->view is a pfCoord
subble->posview(&Shared->view, Shared->view_mode);
matey->makeCoord(&(Shared->view));
centre_chan->setViewMat(*matey);

So all we have to do to support some fairly usefull functionality is calculate different pfCoord contents from the sub class posview method depending on the value of the tether mode.

The simplest option is to just place the eye at the vehicle position to give a first person effect.

view->hpr = subcoord.hpr;
view->xyz = subcoord.xyz;

Another option is to trail the vehicle by a number of frames, for this we store an array of pfCoords representing the recent path history of the vehicle, and just return a position several frames older than the current position. The array of pfCoords called wing is usedto store the path history and the sub simulate method maintains this record.

view->hpr = wing[wingcount].hpr;
view->xyz = wing[wingcount].xyz;

Another approach is to take a plan view of the vehicle, this can be done by elevating the eye position above the vehicle and fixing the pitch of the Euler angle to -90 degrees. Roll is ignored and heading is optional depending on whether you want to rotate with the vehicle or maintain a fixed view orientation, here's the code:

view->hpr.set(subcoord.hpr[0], -90.0f, 0.0f);
view->xyz = subcoord.xyz;

The last method used by the demonstration is to circle the vehicle at a distance, for this purpose an additional heading variable is maintained and used to generate x & y positional offsets based on the sin and cos of this heading multiplied by a tether range, this naturally describes a circle over time as the heading is increased, and the eye just needs to be directed at the vehicle by looking back towards the centre of this circle. This is done by specifying -Thead+180.0f as the Euler angle heading. Just to make things a little more interesting the eye elevation w.r.t the vehicle is modified using the sin term as a z offset and adjusting the Euler angle pitch accordingly.

pfSinCos(Thead, &sval, &cval);
view->hpr.set(-Thead+180.0f, -pfArcSin(sval * TPamp), 0.0f);
view->xyz.set(Trange * sval, Trange * cval, Trange * sval * TPamp);
view->xyz += subcoord.xyz;

Hard Points & Articulations

The attachment of objects such as missiles, or in the case of our example, torpedoes and hydroplane control surfaces, to a known position on a parent vessel is another usefull feature and involves some fairly straightforward procedures, in anycase it's worthwhile reviewing how this is done using the example.

The submarine in the demonstration has hydroplane control surfaces and torpedoes which use two different styles of attachment. Each are controlled from the sub class and pointers to elements of the scene graph are passed to the sub class as arguments in the constructor call. This lets the sub manipulate these portions of the scene graph in order to affect their appearance and position in the scene.

For each hydroplane and the torpedo a pfDCS is passed to the constructor, this pfDCS is then given a position relative to the parent vehicle and attached to the vehicle in the scene graph which is also passed as a pfDCS

submarine->addChild(portplane);
portplane->setRot(0.0f, PlaneAuthority * (-joyy + joyx), 0.0f);

You can see from this example of positioning the port hydroplane that no positional offset relative to the parent is applied to the object, the reason is that the hydroplanes have been modelled with the offset built right in, by happy coincidence the desired point of rotation for these planes is also along the origin of the parent model making their orientation a simple matter. The simulate method called each frame modifies the hydroplane pitch based on the joystick position to make it appear as if the joystick is controlling the plane angles which in turn affect the vehicle motion. The attachment of the child to the vehicle is actually performed once only outside the sub class by the application so only the setRot method is called from the class to modify the plane pitch.

Torpedoes are a different affair, they require an offset to position them on the vehicle hardpoints until they are launched but they do not require regular updates based on joystick inputs. To control torpedo attachment and movement a torp class has been implemented, this class constructor is passed a position and orientation offset (as a pfCoord) which will place it on a hard point, the pfDCS of torpedo and vehicle are also passed in addition to the scene which is required when the torpedo is launched.

hpoint1.xyz.set(0.554f, -0.843f, -1.246f);
hpoint1.hpr.set(-7.0f, -13.0f, 0.0f);
fish[0] = new torp(scn, submarine, torp1, &hpoint1);

In the torpedo constructor the torpedo is positioned at the hardpoint using the setCoord method then attached to the submarine.

fishdcs->setCoord(attachpos);
subdcs->addChild(fishdcs);


Missile Flyout

Once you've attached your arsenal of mass destruction to your vehicle the next significant thing youre going to want to do with it is target it at an enemy and blow that sucker away. Well, here's one way to do just that, selecting a nominal flight vector based on vehicle sighting, releasing the munition from it's cozy position on the vehicle into the real world and then guiding the flight of the munition along it's preordained path.

There are three stages to the missile release, the first step is to figure out a targeting vector along which the missile is to fly. In the example the line of sight from the submarine is taken as the flight vector, this is defined as the vehicle position at the time of release and it's direction of motion. These are passed to the torpedo for the launch stage which is initiated from the sub class fire method, momentum is also passed at this stage since this will depend upon the vehicle velocity.

pfVec3 velvec, firepos, targvec;
// start velocity = sub speed with a little push away from the sub
velvec = forward * speed - up;
firepos = subcoord.xyz
// target vector origin at the sub, vector ahead
targvec = forward;
fish[0]->fire(&velvec, &firepos, &targvec);

Secondly the missile position in the world database must be calculated using the vehicle position and the hardpoint offset, if these are combined we can figure out the release position of the torpedo in world coordinates and use this as a starting point for the missiles independent motion. This computation is performed in the fire method of the torp class. To do this the forward and up vectors of the torp object are transformed through the position matrix of the torpedo hardpoint then through the matrix of the sub world position. The torpedo coordinate is also transformed in the same way but as a point not a vector. This leaves the torpedo with vector based position information similar to that held by the sub class. The Euler angles are then computed in exactly the same manner as explained in the motion model description above. Another way of calculating this would be to multiply the ship and hard point matrices together and transform the vectors through the product. This is expressed in the diagramwhere Ms is the ship martix, Mhp is the hardpoint offset matrix and Mt is the required torp matrix.

subdcs->getMat(submat);
fishdcs->getMat(fishmat);
forward.xformVec(forward, fishmat);
forward.xformVec(forward, submat);
up.xformVec(up, fishmat);
up.xformVec(up, submat);
torpcoord.xyz.set(0.0f, 0.0f, 0.0f);
torpcoord.xyz.xformPt(torpcoord.xyz, fishmat);
torpcoord.xyz.xformPt(torpcoord.xyz, submat);

At the time of release the ships momentum of the torpedo based on the ships modion is imparted to the torpedo, in addition a separation impulse is applied to the torpedo to move it away from the hardpoint at the start of the release. The total velocity applied to the torpedo is actually sent as the first argument to the fire method and the release impulse is added as a downward vector in the ship frame of reference by the sub class before the launch call, this is an easy calculation since down in world space is simply the negative up vector already stored in the class.

velvec = forward * speed - up;

Finally the missile must begin computing it's position each frame, particular attention must be paid to the conservation of momentum during the initial weapon release. The update method for a torp object is called by the sub object to which it belongs. Unlike the ship motion model the torpedo can have a direction of travel which is different from its orientation vectors, this lets us use the momentum at the time of weapon release as the direction vector while steering the weapon using it's direction vectors.

In the update procedure the member variable running_time to track the time in seconds elapsed since weapon release. For the first second after weapon release no steering is done but immediately the weapon is given a spin component by rotating the up vector around the forward vector:

// spin the torpedo 180 deg/sec
calcmat.makeRot(180.0f * dtime, forward[0], forward[1], forward[2]);
up.xformVec(up, calcmat);

When steering begins an aiming point is required, this aiming point is along the sighting vector supplied at launch. To choose this point a calculation of a point along this vector but a few meters in front of the torpedo is made:

// using release point, release vector and distance from release
// derive a aim point 5m in front of torpedo
dist = targorig.distance(torpcoord.xyz) + 5.0f + .5f * running_time;
aim = targorig + (targvec * dist);

So now aim is the point to which we want to steer the torpedo this frame, we can rotate the torpedo towasde aim by rotating around the cross product of the forward vector and the vector to the aim point, the desired angle of rotation is given by the dot products of those vectors, if we place limits on the maximum amount of rotation per frame the torpedo will gradually turn towards this point, we can use the turn rate and the distance to the aiming point to controll how quickly the torpedo aquires even flight along the aiming vector.

// using release point, release vector and distance from release
// derive a aim point 5m in front of torpedo
dist = targorig.distance(torpcoord.xyz) + 5.0f + .5f * running_time;
aim = targorig + (targvec * dist);
aimvec = aim - torpcoord.xyz;
aimvec.normalize();
// use the cross product to get axis of rotation
targrotaxis.cross(forward, aimvec);
// calculate the angle between the axes using the cross product
// rotate about axis
calcmat.makeRot(TurnAuthority*dtime,
targrotaxis[0], targrotaxis[1], targrotaxis[2]);
up.xformVec(up, calcmat);
forward.xformVec(forward, calcmat);

Elsewhere in the torpedo update the thrust is applied along the forward vector of the torpedo which accelerates the weapon towards the target, and a speed limit is introduced which also helps targeting. It would be possible to introduce friction or other hydrodynamics forces for better results but I like the way the weapons gradually sail away oscillating along the tergeting vector and additional forces would adversely affect this.

velocity += forward*accelRate*dtime;
if(velocity.length() > MaxSpeed) // limit speed
{
velocity.normalize();
velocity *= MaxSpeed;
}
torpcoord.xyz += velocity * dtime;

Finally Euler angles are calculated in exactly the same way that the sub motion model computes these.


3D Light Points & Particle Systems

In OpenGL points are 2 dimensional entities in that once projected they are always rasterized as on screen elements consistent with the current glPointSize. For 3 dimensional scenes this is of limited use due to the perception that points have unrealistic dimensions over a range of viewing conditions. For real world objects we need a point description with 3 dimensional properties, some implementations of OpenGL extent the API to support the concept of a 3 dimensional point entity. Performer provides a layer of abstraction from these low level descriptions allowing dimensions for 3D points to be specified in a pfLPointState, 2D primitives are rasterized with appropriate dimensions depending upon their projected screen dimensions using either supported OpenGL extensions or ordinary state changes. So our demonstration uses points to simulate 3D entities in the real world, the first screen shots below illustrates the 3D perspective effects applied to the points by Performer with 3D points being larger in the foreground, the second shot demonstrates the dynamic effects of the points showing buoyant particles being released and floating then fading away over time under dynamic simulation.

The 3D points are easily created using Performer by building a pfGeoSet with points as the primitive type. The geostate associated with this geoset should have a light point state to control the dimensions of these points. Looking at the bubbles.C module you will see this geoset information getting initialized, you can see that the attributes bind per vertex for colour and position, this allows particles to be positioned and coloured independently.

pfGeoSet *bubgset = new pfGeoSet;
bubgset->setAttr(PFGS_COORD3, PFGS_PER_VERTEX, BubVerts, NULL);
bubgset->setAttr(PFGS_COLOR4, PFGS_PER_VERTEX, BubColrs, NULL);
bubgset->setPrimType(PFGS_POINTS);

The geostate information is given a lightpoint state shown below, this gives the minimum and maximum 2D size of the light points and the transparency properties in addition to the actual dimension of the light point in the 3D world from which the 2D projected size gets computed.

pfLPointState *bublpoint = new pfLPointState;
bublpoint->setMode(PFLPS_SIZE_MODE, PFLPS_SIZE_MODE_ON);
bublpoint->setVal(PFLPS_SIZE_MIN_PIXEL, 0.5f);
bublpoint->setVal(PFLPS_SIZE_ACTUAL, 0.07f);
bublpoint->setVal(PFLPS_SIZE_MAX_PIXEL, 10.0f);
bublpoint->setVal(PFLPS_TRANSP_PIXEL_SIZE, 2.0f);
bublpoint->setVal(PFLPS_TRANSP_EXPONENT, 1.0f);
bublpoint->setVal(PFLPS_TRANSP_SCALE, 0.6f);
bublpoint->setVal(PFLPS_TRANSP_CLAMP, 0.1f);
bublpoint->setMode(PFLPS_RANGE_MODE, PFLPS_RANGE_MODE_TRUE);
bublpoint->setMode(PFLPS_TRANSP_MODE, PFLPS_TRANSP_MODE_ALPHA);

The light point state is applied to the geostate which is in turn applied to the geoset.

bubstate->setAttr(PFSTATE_LPOINTSTATE, bublpoint);
bubgset->setGState(bubstate);

Some important geoset modes are also set to give the desired effect, most importantly the PFSTATE_ENLPOINTSTATE is turned on so that the light point state attribute will be used. The anti-aliasing and blending is enabled to improve the shape and colour transparency of the light points on various platforms. Lighting is disabled since we have no normals on the lights and we want to explicitly specify the light colour without any lighting.

bubstate->setMode(PFSTATE_ENLPOINTSTATE, PF_ON);
bubstate->setMode(PFSTATE_ANTIALIAS, PF_ON);
bubstate->setMode(PFSTATE_TRANSPARENCY, PFTR_BLEND_ALPHA);
bubstate->setMode(PFSTATE_ENLIGHTING, PF_OFF);

So that's how lights are created in the scene graph, now all we have to do is update the vertex information in the attribute array to create 3D particles in the scene, and update these particles over time. One word of warning, this system isn't very clever about two things, firstly the light point attributes are not created as a pfFlux buffer or pfCycleBuffer, these provide an MP safe method of passing information downstream to the draw process.This is not required here since the particles I'm simulating have relatively sedate behaviour and there are no meshing issues with dynamic 3D points like the cracking you encounter on 3D meshes. The second problem with my light points is the culling, ideally you'd pack these lightpoints into one or more spatially coherent geosets which could then be culled to the viewing frustum. I didn't do that here so rendering is a little inefficient with a single geoset which always draws the all active lightpoints and uses transparency to turn them off, since culling is based on bound box calculations I don't compute this each frame and have disabled culling of the light points so they are always drawn, visible or not.

bubgeode->setTravMask(PFTRAV_CULL, 0x00, PFTRAV_SELF|PFTRAV_DESCEND, PF_SET);

There are two aspects to the simulation of particles in the demo, the first it the controlled release of these particles from the rear of the vehicle, the second is the update of individual particles each frame. The bulk of the first part is performed by the bubbles class, the bubbles class, the sub constructor creates several bubbles classes with a position relative to the sub class object, and a colour at the time of release, another key constructor argument is the submarine pfDCS which the bubbles use to determine information needed when releasing the bubbles:

thrusters[0] = new bubbles(submarine, -1.312f, -5.350f, -0.211f);
colour.set(0.3f, 0.6f, 0.4f, 1.0f);
thrusters[0]->setColour(&colour);

The individual release positions are held as pfCoord members and several bubbles are created along the rear of the vehicle, you will appreciate that if we take each coord and transform these through the model matrix representing the sub object position in the world we get the release position of the particle in world coordinates, this is done in the bubbles code, a fractional component is used to interpolate the release position of bubbles across frames:

tmpmat = object->getMatPtr();
tmppos.xformPt(position, *tmpmat);
.
.
(*(barry+i)).position =
old_xformpos + ((tmppos - old_xformpos) * fraction);
(*(barry+i)).velocity = velocity;

The bubbles class is also supplied with a release velocity vector and release frequency by the sub object:

velvec.set(0.0f, -speed*2.0f - 1.0f, 0.0f);
if(velvec[1] > -1.0f)
velvec[1] = -1.0f;
thrusters[i]->setFreq(speed + 2.0f + i * .2f);
thrusters[i]->setVelocity(&velvec);

This information determines the initial release direction and release rate of bubbles each frame:

(*(barry+i)).velocity.xformVec(velocity, *tmpmat);

The individual particles have a few properties which determine how they are updated each frame, the most important are velocity, acceleration, and damping. The diagram above shows how these act on a particle to affect it's velocity which is then used to update the particle position, in addition a record of elapsed time is kept to determine how long the particle lasts in the scene and the colour alpha value is reduced before the particle expires:

duration -= dtime;
if(duration < 2.0f) // fade out over last 2 seconds
colour[3] = duration * .5;
if(duration < 0.0f)
{
active = 0;
return;
}
position += velocity * dtime;
velocity -= velocity * ((1.0f - damp) * dtime);
velocity += acceleration * dtime;

As each particle is updated its position and colour are copied into the geoset attribute arrays, once all particles are copied the attribute array length is set, there is an element of risk here in an MP scenario where positions may get updated during the draw for the geoset causing the occasional particle to drop out for a frame when a point drops off or gets added to the active list. As already mentioned, the solution is to use flux buffers for the attribute array memory allocation:

for(i = bcount = 0; i < maxbubs; i++)
{
if( (*(barry+i)).active )
{
(*(barry+i)).update(dtime);
*(BubVerts+bcount) = (*(barry+i)).position;
*(BubColrs+bcount) = (*(barry+i)).colour;
bcount++;
}
}
bubgset->setNumPrims(bcount);

HUD

Head Up Displays are used to present information to pilots without requiring them to glance away from the view "out the window". In computer graphics this often requires the rendering of 2D and 3D symbology over the 3D scene representation. Often this representation includes information to describe the vehicle attitude w.r.t the horizon. The example demonstrates just one way which may be used to represent this HUD 'artificial horizon' information, there are many other ways to accomplish a similar result but this example is presented as a usefull and elegant option withinin Performer which produces results consistent with with a 3D visual without coding for the usual orthographic projection draw callbacks.

The HUD elements are first modelled geometrically, it this case I have used OpenFlight models but you can use any package or create the geometry procedurally:

There are two elements to my simulated HUD display, the first is the artificial horizon meter and the second is the fixed sight ahead of the viewer each item is shown in the image below, the horizon in red and the sight in orange. In order to give our sights a nice fat smoothly filtered effect I have modelled them as textured polygons instead of lines, this also makes them resolution independent, unlike lines which are 2D primitives. In addition to modelling the HUD as polygons, I also create a glowing effect by using a glBlendFunc(GL_ONE, GL_ONE) in a draw callback around the HUD elements, this adds the HUD brightness to the scene instead of blending it as a function of source alpha, it also means I don't need to model alpha in the HUD geometry, just brightness which will be added over the scene, the results can be seen below.

The next step is to track the eye with the HUD elements and clip the HUD display to a smaller region within the display window, for this purpose I create a dedicated HUD pfChannel which guarantees it will be drawn after the out the window visual, I modify the viewport and the field of view parameters of this pfChannel so that it correlates with the out the window visual.

pfChannel *HUDchan = new pfChannel(Shared->p);
Shared->pw->addChan(HUDchan);
HUDchan->setViewport(0.25f, 0.75f, 0.25f, 0.75f)
HUDchan->setFOV(28.5f, 24.0f);

The HUD channel is given it's own scene and unique CULL abd DRAW calbacks, the most notable thing about the HUD channel draw callback is which disables depth buffering and sets the desired blend equation

pfTransparency(PFTR_BLEND_ALPHA);
pfOverride(PFSTATE_TRANSPARENCY, PF_ON);
glBlendFunc(GL_ONE, GL_ONE);
glDepthMask(0);
glDisable(GL_DEPTH_TEST);
pfDraw();
glEnable(GL_DEPTH_TEST);
glDepthMask(1);
glBlendFunc(GL_ONE, GL_ZERO);
pfOverride(PFSTATE_TRANSPARENCY, PF_OFF);
pfTransparency(PFTR_OFF);

The next step is to ensure that the HUD tracks the eye position and the orientation of the horizon while the sight remains fixed in front of the eye. This is done by keeping the channel position at the origin but setting it's hpr to match that of the vehicle, the sight is then also given the same orientation matrix as the channel so that it remains at the center of the frustum.

Shared->view.xyz.set(0.0f,0.0f,0.0f);
matey->makeCoord(&(Shared->view));
HUDchan->setViewMat(*matey);
SIGHTdcs->setMat(*matey);


TexGen & Caustics

No underwater scene is complete without the addition of some caustic lighting effects, there are the illumination patterns caused by light refracting through the uneven water/air boundary, the result is a very uneven and dynamic illumination pattern which we can try and approximate using a multipass rendering technique similar to the application of a light map. The real trick is to apply the light map to the database in such a way as to project throughout in world coordinates and allow our submarine to float past the light map supplying additional motion cues. This sounds simple enough but the OpenGL modelling and viewing matrices are combined into a single representation which makes the task a little more ineresting. The demo implements the less efficient of two options and actually the more difficult to implement, I'll explain the differences and tell you how it should be done.

First the overall objective is to implement the following kind of multipass lighting trick:

Here the textured scene in the framebuffer is multiplied by an animated light pattern representing the caustic lighting effect, the caustic animation sequence is generously provided by Jos Stam, the demo cycles through a periodic animation created by Jos and uses a graphics callback to apply the correct light map modulation function using a glBlendFunc call, the offset polygon function is used to avoid zfighting and pfTransparency is used to guarantee that blending is enabled in OpenGL.

glPolygonOffsetEXT(-0.5, -0.00000001);
glEnable(GL_POLYGON_OFFSET_EXT);
pfTransparency(PFTR_BLEND_ALPHA);
pfOverride(PFSTATE_TRANSPARENCY, PF_ON);
glBlendFunc(GL_ZERO, GL_SRC_COLOR);
glDepthMask(0);

The animation image for each frame is obtained from and MP safe cycle buffer in shared memory and used to apply the texture each frame, the texture state information is then overriden so that the caustic texture is applied to the scene reguardless of geostate information to the contrary:

framecount = *((int *)Shared->cbuf->getData());
pfEnable(PFEN_TEXTURE);
Shared->costy[framecount]->apply();
pfOverride(PFSTATE_ENTEXTURE, PF_ON);
pfOverride(PFSTATE_TEXTURE, PF_ON);

This will apply the texture to the scene, but we still haven't specified suitable texture coordinates. We'd like the caustic pattern to be projected evenly throughout the scene, however we have texture coordinates all over the place in the scene which don't relate to the image mapping we require. Fortunately the gl supplies the ideal mechanism for projectine textures throughout the scene; glTexGen. This is also supported as an attribute in the performer state information. This works by allowing us to generate texture coordinates as a function of distance from a specified plane equation so if you look at the diagram below you can see that a triangle under transformation can produce texture coordinates s & t based on the distance of the vertex x,y,z position from a specified plane equation.

The gl allows this plane equation to be specified in object space or in eye space, the difference is that in object space the texture coordinate generation is performed based on the coordinates sent in the glVertex calls, in eyespace the generation is performed after modelling AND viewing transformation. Immediately should can see a problem, we actually want to generate texture coordinates in world space, i.e. on coordinates which have undergone modelling transformation but before they have undergone viewing transformation. There are two ways to accomplish this, the first is to generate texture coordinates after model viewing transformation in eye space and apply an inverse viewing matrix to the texture matrix stack, the second method is to generate texture coordinates in object space and apply the modelling matrix transformation to the texture matrix. Unfortunately this demonstration does the latter which means that every time a modelling transformation gets applied the texture matrix has to be modified so for example the callbacks around the submarine and its control surfaces must load the texture matrix with new contents.You can make an instant improvement by choosing to modify the texgen method and stick with an inverse viewing texture matrix instead of shadowing the model matrix on the texture stack.

Shared->linear_gen_static = new pfTexGen;
Shared->linear_gen_static->setMode(PF_S, PFTG_OBJECT_LINEAR);
Shared->linear_gen_static->setMode(PF_T, PFTG_OBJECT_LINEAR);
Shared->linear_gen_static->setPlane(PF_S, GENfac, 0.0f, -GENfac, 0.0f);
Shared->linear_gen_static->setPlane(PF_T, 0.0f, GENfac, -GENfac, 0.0f);
.
.
Shared->linear_gen_static->apply();
glGetIntegerv(GL_MATRIX_MODE, &mmode);
glMatrixMode(GL_TEXTURE);
glPushMatrix();
submat = SubPos->getMatPtr();
glScalef(GENfac, GENfac, GENfac);
glMultMatrixf((float *)submat);
glScalef(1.0f/GENfac, 1.0f/GENfac, 1.0f/GENfac);
glMatrixMode(mmode);


Environment, Sky

Moving a hemispherical representation of the sky and horizon around with the viewer is a good way of improving on the pfEarthSky model and any self respecting 3D hacker should be keen to ditch that squealing puppy so here's one example of how it's done, although things don't get very interesting in underwater hemispheres.

The principal is to draw the sky first, and don't write any depth values, the only tricky parts are getting the orientation and position of the sky correct and ensuring it doesn't zbuffer with the rest of the scene or fog out inappropriately, afterall the intention is to see the sky. I can get the orientation easily by just placing the sky geometry beneath a pfDCS which is positioned at the eye without any orientation being introduced, the zbuffering and fog issues are then just a matter of a few state changes in draw callbacks as shown below.

pfDisable(PFEN_FOG);
pfOverride(PFSTATE_ENFOG, PF_ON);
glDepthMask(0);

If the sky covered the entire sphere of visible environment then it would be possible to exploit rendering the environment as a depth buffer clearing operation and save on the overhead of the glClear operation.


Post Texture Alpha Modulated Specular Term

Lots of people have been waxing lyrical about applying the specular term after the texture modulation of the lighting result, conceptually this is trivial but for hardware it can be costly, on most architectures the post texture specular term must be implemented as a second rendering pass so while we're at it we might as well try and do something a little more interesting with the specular calculation. This example shows some of the interesting material effects which can be explored by trying to modulate different lighting terms independently. In this case we use destination alpha written from the texture as a specular term modulator and rgb to modulate the diffuse components. We could have used a second luminance texture for the specular modulation and avoided the need for destination alpha but doing it this way allows us to use environment mapping texture to approximate phong shading for the specular term while using the alpha from another texture (stored in the destination alpha on an earlier pass) to modulate the environment map with just a single additional pass, check out those torpedoes.

RGB(diffuse) -Alpha (specular)

The above images show the rgb components of a texture beside the alpha component of the same texture. When these are used to texture an object it is possible to disable blending yet still write alpha to the framebuffer when the texturing for the diffuse term is applied. This leaves us with usefull hidden information in the framebuffer which can be used by subsequent blending operations to enrich object shading. Now typically with OpenGL implementations the lighting calculation is performed per vertex to produce a colour value which is then interpolated across the polygon, if texture is introduced it cannot affect the results of different terms of the lighting calculation independently, a popular request for example is to have the texture only modulate the diffuse lighting term and leave the specular term at full brightness. This isn't a widely available option today. What can be done is to render the object in question twice, the first time the diffuse term is modulated by texture and the second time the specular term is added to the result in the framebuffer without modulation. During this second pass we have the opportunity to do more than simply apply a per vertex specular term. We can add an environment mapped textured specular illumination term which approximates phong shading, or we can modulate the per vertex shininess term with a separate environment map. Using destination alpha we can even do both in a single pass since a shininess term can be stored in the framebuffer alpha thereby allowing the phong texture map to be multiplied by the alpha. In performer a simple draw callback is required:

The first pass which disables blending but writes alpha need only apply an rgba texture and disable blending by overriding pfTransparency state ensuring it is off. Alpha testing should also be set to always pass since this could still cull fragments causing rendering gaps:

pfTransparency(PFTR_OFF);
pfAlphaFunc(0.0f, PFAF_ALWAYS);
pfOverride(PFSTATE_ALPHAFUNC, PF_ON);
pfOverride(PFSTATE_TRANSPARENCY, PF_ON);

The second pass enables blended transparency and multiplies source fragments by the destination alpha value then adds this to the destination fragment colour.

pfTransparency(PFTR_BLEND_ALPHA);
glBlendFunc(GL_DST_ALPHA, GL_ONE);
pfOverride(PFSTATE_TRANSPARENCY, PF_ON);
glPolygonOffsetEXT(-0.5, -0.00000001);
glEnable(GL_POLYGON_OFFSET_EXT);
glDepthMask(0);

The calls required to implement the texgen spheremap for the phong shading are in the texgen attribute of the geostate information.

Below is another multipass post texture specular pass which also independently textures the specular term. This time the lighting calculations are per vertex instead of the Phong texgen spheremap effect used above. Because it only uses per vertex lighting it is possible to eliminate the requirement for destination alpha and obtain exactly the same effect. The example code uses the same destination alpha approach as explained above but if instead of using destination alpha the software used a glBlendFunc(GL_ONE, GL_ONE) and a modulating intensity map texture for the specular pass instead of relying on alpha from the texture in the first pass then exactly the same results would be obtained as seen below:


Corporate Office
2011 N. Shoreline Boulevard
Mountain View, CA 94043
(650) 960-1980
U.S. 1(800) 800-7441
Europe (44) 118-925.75.00
Asia Pacific (81) 3-54.88.18.11
Latin America 1(650) 933.46.37
Canada 1(905) 625-4747
Australia/New Zealand (61) 2.9879.95.00
SAARC/India (91) 11.621.13.55
Sub-Saharan Africa (27) 11.884.41.47

Copyright © 1998 Silicon Graphics, Inc. All rights reserved.