This demonstration shows a few reflection, light map and bump mapping tricks which can greatly enhance the quality of a rendered scene.
Infinite Reality supports the dynamic resize of video as it is scanned from the framebuffer with a bilinear filter. This resize factor can be modified from frame to frame to control the pixel fill load on the graphics pipe, or it can simply be set once to resize the video from a smaller screen region to a high resolution video. Performer allows the easy use of this resize capability via the pfPipeVideoChannel class. What happens in rendering terms is the effective viewport size is reduced and this smaller framebuffer region is resized back up to the higher resolution video format.

Switching the video resolution is used in the demonstration depending on the command line input and is used to maintain frame rate when there are fewer Raster Managers configured in the system, firstly a pfPipeVideoChannel is obtained for the channel in question, then the DVR mode is set to manual adjustment, next the channel size is read and used as a basis for selecting a resolution at some fraction of the original:
Finally the 2D cursor does not exist in the resized screen region space, so it will be scanned out at the wrong place and size as a result of the resize, the solution is to turn off the standard cursor and have Performer render a 2D cursor in the viewport each frame, this means the size and position of the cursor will also be affected by the video resize parameters.
Due to some reflection effects in the scene which are described later there is more than one channel to be resized, in fact two performer channels overlap exactly the same screen region, each must be resized in order that the viewports specified by Performer are adjusted correctly.
Joystick i/o is supported by the bumplogo demo using serial communications and all the code required is contained in the Bg.C module, the BgPub.h file declares the functions used to cleanly access the BG Systems Flybox. The purpose here is not to describe the details of serial i/o, but to offer this joystick interface which wraps the i/o code as a useful and easy means of supporting BG Systems Flybox interface on an SGI based application.
These functions are self explanatory, the initialization call should be made once, the exit call should be made when done, once initialized the read call may be made whenever the application needs to poll the joystick x, y and twist positions in addition to the two lever positions. The buttons are not supported by these calls although the Bg.c file includes the code for this and a suitable function could be exported.
Planar reflections are relatively easy to simulate, hence their popularity. An object reflected in a plane can be transformed with standard matrix operations (a negative scaling factor through the normal of the plane). This results in the reflected object representation which unfortunately has it's face winding reversed, you can use OpenGL to reverse the face culling glCullFace or in Performer parlance pfCullFace state to switch the face culling for reflected geometry.
Another method of supporting reflections is to model the reflected geometry in a modelling package and load this into the scene graph. This is often an easier approach because it simplifies the state changes involved because the face winding can be reversed in the tool and the matrix work is no longer required. Diagramatically you can see below that when an object is transformed through a reflecting plane then viewed from one side of that plane, the plane appears to act as a mirror, the effect is enhanced if some textured geometry is drawn in the plane of the reflecting surface to make it seem as if the plane is also being illuminated.

You will often hear that you need stencil operations to support the masking of planar reflections to the bounds of the reflector, this is completely untrue as the example shows. Using simple zbuffering is is possible to adequately mask geometry to the bounds of a reflecting surface. Stencil operations are rarely essential although they may assist with implementation details. The diagram below demonstrates the scene geometry setup where depth buffering is used to mask the visibility of a reflection to the aperture of a reflecting surface withour recourse to stencil functionality.

Understanding this we can then model the room with several reflected versions of the room geometry for the various reflecting planes like the walls and the floor in anticipation of realistic reflection results in the final rendered scene:

In addition to the geometric reflection the transmission of light through a reflecting surface may also be modelled, this makes the contribution of the reflected light a little more physically based. The water bowl in addition to modelling the refraction of the submerged bowl surface also reflects the room and the logo in the water. The logo brightness is attenuated as a function of the angle of incidence the eye makes with the surface to help approximate Fresnels law, this is done by drawing the reflection first, then the plane of the water surface. The water plane is black with per vertex alpha information which modulates the brightness of the reflected light already drawn. Each frame the alpha at each vertex is computed based on the eye incidence. After modulating the reflection brightness the rest of the scene is drawn including the non reflected bowl. A separate channel and scene are used to draw the scene contents reflected in the water prior to the rest of the scene being drawn. The refraction of the bowl is accomplished using a scaling operation in the MultiGen tool. The image here shows the gradual attenuation of the reflection with angle of incidence while the refracted bowl brightness remains unattenuated.

The code in the function UpdateWaterReflective in the module water.C is invoked each frame with the eyepoint position to update the pfGeoSet per vertex color attributes which apply the light attenuation for this effect. The per vertex computation looks something like this:
Lighting calculations which are more sophisticated than the simple per vertex computations supported in OpenGL can greatly enhance the realism of your rendered scene, one method is to use a multipass approach and use texture information to modulate the intensity of an already textured surface. This is neccesarily a multipass approach because most implementations of the gl only support a single texture per polygon, so if the base polygon is textured with scene content and a light map texture is to be applied to this then that texture must be applied in a second rendering pass.
The multipass approach we're aiming for here is:

As you can see the wall texture and images are being multiplied by the spotlight intensity in the monochrome light texture. This quality of lighting is not possible using per vertex computations or vanilla OpenGL implementations. The modulation of the framebuffer by the luminance of a light map is straightforward and is accomplished by the applying a blend function which multiplies the destination fragment colour by the source fragment colour. In Performer this has to work in conjunction with the Performer state in addition to avoiding zbuffer visibility fighting so a draw callback to support this type of multipass would look something like this:
In addition to the draw callbacks we also need two representations of texture information in the scene, I have chosen to create two sets of geometry using the MultiGen modelling tool, the first set describes the base textured walls and pictures, the second set is geometrically simpler and casts the light map modulation textures over the same areas.

Simply using the fragment multiplication described above would result in more complex but generally uninteresting lighting in the room, to add some dynamic lighting effects and a richer lighting scheme a GL_ADD texturing environment is also used. This allows the lobe light intensity to be added to the base polygon intensity, instead of replacing or modulating it, in the demonstration the base polygon intensity is determined by a local lighting calculation for the single light source moving in the room. Any other two pass approach could not achieve this degree of complexity since the GL_ADD is essential for adding the light map and the per vertex calculation and the light map multiplies the destination fragment color. If the destination color held the lightmap by, for example, modulating in the first pass, this would in turn be modulated by the light map which isn't even close to our requirements. So, by changing the texture environment to GL_ADD the lobe intensity is added to a per vertex local light calculation prior to the destination source multiplication. The difference is illustrated below, this time the center image represents the fragments of the lobe texture plus the vertex lighting.

In addition to this the blend color is used to modulate the lobe texture color fading the lights up and down and introduce some dynamic lighting to the lobes. To change the intensity and color in different phases the lobe light MultiGen OpenFlight file is split into different portions which allows us to apply different blend parameters in the draw callbacks which set the blend color. There are two implementation details to this trick, the first is the modification of the Performer state to use a GL_ADD texture environment:
The second is the variation of the blend colour to dim the lobe light intensity, this modifies the callbackabove to look something like this:
The biasparams are always zero for our purposes. Note that if Performer were well behaved w.r.t. the texture blend information then the texture state calls would not be required if the pfTexEnv were updated in the application using the setBlendColor method, unfortunately at least in the Performer 2.1 release the blend information is only used correctly for a GL_BLEND texture environment.
Currently OpenGL implementations do not support bump mapping in hardware, but there is a two pass approach to creating a close approximation of bump mapping which is shown in this demonstration. This code is the manifestation of an algorithm and sample IrisGL code shown to me in mid 1995 by Brian Cabral, and owes it's existence entirely to Brian's patience in explaining his ideas in great detail to me, his original work was infact more accurate and included a renormalization term which is not implemented here. The principal is to render a Lambertian diffuse lighting calculation to the framebuffer then vary the brightness of this result using texture blending operations to simulate the peturbation of the surface normal. So the diffuse lighting term is the cosine of the angle of the surface normal and light vector which is computed by taking the dot product of the two normalized vectors, we'll call this L·N, this is the same as the vertical component of the normalized light vector in tangent space, which we'll call Lz. So Lz is the same as L·N as illustrated in the diagram below, taking L·N projects L onto N and gives the component length of L w.r.t N, and vice versa.

So the L·N gives us the diffuse illumination term, but this assumes that the polygon is flat, the interpolation of L·N across a polygon is a reasonable approximation, but we need to simulate the variation in the lighting calculation as introduced by the normal changing direction. To do this we first store the change in surface elevation as an intensity map. The intensity representing the elevation is added to the Lz brightness giving the surface brightness plus bump height. The next step is to move the s & t texture coordinate at each vertex as a function of the light position w.r.t the surface and then subtract this brightness from the total in the framebuffer to leave the new Lz term which is varied by the elevation map as a function of Lz (in fact Ls and Lt which we'll come to later). This computation is shown in the diagram below, the left hand side shows the contribution of each term the right hand side shows the total at that stage in the calculation and the sign of the arithmetic operation being applied to get there:

The net result is that the luminance left in the framebuffer is an approximation of a new Lz or L·N for a peturbed surface normal. The key element not detailed yet is the peturbation of the texture coordinates for the subtractive pass. The Lz term as explained is the vertical or 'normal' component of the normalized light vector in tangent space. The texture peturbations should be translations as a function of the lateral components w.r.t. the texture coordinates. By this I mean that texture is mapped across a surface and each s & t term has a derivative or a direction vector in which in moves on that surface. The derivative axes for s and t can be viewed as forming a texture coordinate system in tangent space. The light vector transformed into this space provides Lz, but also provides Ls and Lt terms, i.e. the projected vector lengths w.r.t. the texture derivatives for s & t coordinates. It is the projected Ls & Lt partial derivatives which we use to peturb the texture coordinates and effectively peturb the lighting in the direction of the light vector relative to the surface.

The image sequence below shows the various passes, bear in mind that the first two passes shown here can be drawn in a single pass using a GL_ADD blend function so bump mapping is really a two pass requirement. In addition to the two passes for bump mapping the sequence below shows a texturing pass which multiplies the lighting result by another texture term to give a final bump mapped and textured shading result:




This just leaves a few implementation details, we've covered enough about multipass implementation so far that you'll be familiar with the details now, but the subtractive blend for the displaced bump map is new, this uses an OpenGL extension which subtracts the terms in the glBlendFunc instead of adding them, so a GL_ONE, GL_ONE blendfunc will subtract source from destination if we make the right call to glBlendEquationEXT:
There are some precision considerations, the arithmetic requires that the values in the framebuffer are never clamped so you have to watch the total luminance of the bumps and the Lz term prior to the subtraction of the displaced bump texture. So what's the matter, your surface too dark? Well there's a little used rendering trick which can change all that, normally for multiplying the framebuffer luminance by another texture you multiply source by destination and set destination to zero, or vice versa, but if you do both in the same blendfunction, not only do you modulate the brightness in the framebuffer, you also multiply the result by 2, making the final image in the above sequence double the brightness it appears in this presentation, the blend function required to multiply source by destination and double it looks like this:
It's also worth reviewing the code which performs the per vertex texture peturbation of the texture coordinates in the geoset attributes used for the second pass.First Lz is calculated which is the diffuse term and used for the vertex color, if this is zero or less then no peturbation is applied (the normal is pointing away from the light), otherwize the dot products between the light projected onto the surface and the texture derivatives is used to compute the peturbations. This projection is a simple matter of subtracting the Lz * normal from the light vector, this leaves the light vector projected to the surface. The Pderiv variable stores the texture peturbation components which is subtracted from the texture coordinate stored in QBtex. Qnorms is the surface normal array and Psderiv and Qtderiv stores the normals. One other feature is that as Lz approaches zero the amount of peturbation is reduced slightly to smooth out the bump mapping on the limb of an object. One way of thinking about this is that an object is self shading, one might peturb the surface normal of an object but a surface which is almost back facing can have a normal peturbed towards the light source, unfortunately in reality this still doesn't produce an illumination effect so it makes sense to wind the bump map value down as L·N approaches zero, ie the light is at 90 degrees to the surface normal:
In addition to s & t texture coordinates which can map 2D texture images to a polygon many SGI systems support a 3rd texture dimension and coordinate for the application of 3D texture volumes. The logo demonstrates one use of this third texture dimension. The demo generates the third texture coordinate using pfTexGen state information for the 'r' coordinate, this allows the third coordinate to be automatically created without altering the s & t values stored in the pfGeoSet PFGS_TEXCOORD2 attributes. The first step is the construction of a 3D texture image, this involves feeding a 3D array of image data to glTexImage3DEXT. Performer hides this from us and we need only specify a 3D array of data to the pfTexture setImage method and specifying >1 dimension for the r axis. This is done after a single 3D image is assembled from the four 2D slices read from disk, the getImage method is used to obtain the image data array after using the Performer image loading routines to load the files:

After creating the 3D texture and applying it to the pfGeoState used by the 3D textured pfGeoSets the remaining step is to use texture coordinate generation to produce the r coordinates, bear in ming that the s & t coordinates are specified in the attributes of the geoset so the r coordinate is just used as an interpolator for the image slices assembled in the 3D texture, you can see the effect this has in the texture above, the r coordinate causes the smooth blend between various image levels in a single texturing pass.
The UpdateTexGen function can now be called each frame to modify the plane equation used by the texgen causing the blending between different image slices to vary over time adding some interesting complexity to the rendering of the logo:
|
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 |