Local Info

Topics

j3d.org

Fog Example

In this example, we show how to make use of fog in the scene graph. Fog is an effect to get depth-cueing for relatively cheap cost and makes for some nice visuals when used properly. In Aviatrix3D, there are a couple of different ways of using fog in the scene graph to get various effects. This example will show each of the different ways of doing that.

 

Understanding the basics of Aviatrix3D Fog

In OpenGL fog forms part of the lighting equations. As part of the final pixel colour, a computation is performed that works out the depth of the fragment and then applies a fractional amount of the desired fog colour to the object. Turning this around and looking from the other direction - fog only is applied to items that make it through the rendering pipeline. If there is nothing to render, then fog is not applied. If you have objects in the scene, and no background geometry, then you won't see any fog colour. This is the first limitation of the OpenGL model, before you even get to the Aviatrix3D interpretation over the top of that. If you would like to have your fogged objects blend into the background, then you must set the background colour to match the fog colour.

To Aviatrix3D, fog is a node in the scene graph - specifically a Leaf node like Shape3D or Light. That implies it can be placed anywhere in the scene graph, under any grouping node. Since lights are scoped to the grouping node you place them under and effect all the children from that group downwards, then you can expect fog to do the same thing. This allows for some interesting effects where you can have different coloured fog effects in separate parts of the scene graph. However, unlike lights, there can only ever be one set of fog values active at any given time. If you construct a scene graph where, going from the root to a leaf, you encounter more than a single fog node, then we'll make use of the one closest to the leaf. This allows you to create nested grouping effects where you can apply some fog in a large part and have a local override with a different colour for a smaller section of the scene graph.

Local fog effects are useful, but many applications require something far simpler - they just want to have a global distance-based effect. If this is more your requirements, we cater for that too. As part of the ViewEnvironemt class you can set the global Fog. There is also a parameter on the Fog node itself that indicates whether that instance should only participate in global effects and ignore the local settings. This allows you to put a number of Fog instances into the scene graph and yet not have them produce local effects if this is not desired. By default, all Fog instances are only used in global rendering. If you want local effects to override the global, you'll need to explicitly enable it per-instance.

Setting up the application

For the first three examples we are going to use the same application framework and just vary the details. Because fog is something that is applied on the basis of the object's distance from the camera, these demos make use of animation so that you can see how the fog changes with depth. To do this, we create a number of primitives with a single colour and have them animate around a circular path - or, more correctly, have a single parent TransformGroup that we change the rotation matrix on and have all the children underneath that.

The first part of the demo application is the observer needed to animate the primitives. This is a standard ApplicationUpdateObserver that you've seen in many other examples. The code it contains is relatively straight forward - for each frame increment the angle a small amount and apply that to a transformation matrix. Starting with the class outline:

public class FogObjectAnimation
    implements ApplicationUpdateObserver, NodeUpdateListener
{
    private Matrix4f matrix;
    private TransformGroup transform;
    private float angle;

    public FogObjectAnimation(TransformGroup tx)
    {
        matrix = new Matrix4f();
        matrix.setIdentity();
        transform = tx;
    }

As you can see, it takes an instance of the TransformGroup that will be animated, and sets up a local matrix to use for doing that animation.

The per-frame tick just tells Aviatrix3D that you'd like to mess with that same TransformGroup instance:

public void updateSceneGraph()
{
    transform.boundsChanged(this);
}

The rest of the work is done in the callback when the transform is ready to update:

public void updateNodeBoundsChanges(Object src)
{
    angle += Math.PI / 500;
    matrix.rotY(angle);
    transform.setTransform(matrix);
}

As you can see, nothing too unusual about the code here. The interesting parts are in the main application.

In the main application, the basic framework is the same as the other examples - pipeline, surface, Swing setup etc. Setting up the scene graph is almost identical too. To aid in the visualisation of the fog, a convenience method is created that will take our parent TransformGroup instance and place the children in that. It will also take as a parameter the number of primitives to create. From there, it just creates the number of primitives requested and places them evenly around a circular path of diameter 1.0. To make it more visually appealing, a little variation in the shapes is implemented too:

private void createPrimitives(int num, Group parent)
{
    double angle_inc = 2 * Math.PI / num;
    double angle = 0;
    Vector3f translation = new Vector3f();

    Matrix4f matrix = new Matrix4f();
    matrix.setIdentity();

    Material material = new Material();
    material.setDiffuseColor(new float[] { 1, 0, 0 });
    material.setSpecularColor(new float[] { 0.4f, 0.4f, 0.4f });
    material.setLightingEnabled(true);

    Appearance app = new Appearance();
    app.setMaterial(material);

    for(int i = 0; i < num; i++)
    {
        float x = 0.5f * (float)Math.sin(angle);
        float y = 0.5f * (float)Math.cos(angle);

        angle += angle_inc;

        translation.x = x;
        translation.z = y;

        matrix.setTranslation(translation);

        TransformGroup tg = new TransformGroup();
        tg.setTransform(matrix);

        parent.addChild(tg);

        switch(i % 4)
        {
            case 0:
                Box box = new Box(0.125f, 0.125f, 0.125f, app);
                tg.addChild(box);
                break;

            case 1:
                Cone cone = new Cone(0.25f, 0.125f, app);
                tg.addChild(cone);
                break;

            case 2:
                Cylinder cyl = new Cylinder(0.25f, 0.125f, app);
                tg.addChild(cyl);
                break;

            case 3:
                Sphere sphere = new Sphere(0.125f, app);
                tg.addChild(sphere);

        }
    }

Now, to place the primitives into the scene graph, you'll just replace the normal geometry setup of the basic example with this snippet of code.

    Group scene_root = new Group();
    scene_root.addChild(tx);

    TransformGroup shape_transform = new TransformGroup();

    createPrimitives(4, shape_transform);

    scene_root.addChild(shape_transform);

And complete the base scene setup later in the method by registering our animator:

    Scene scene = new Scene();
    scene.setRenderedGeometry(scene_root);
    scene.setActiveView(vp);
    scene.setActiveFog(fog);

    sceneManager.setScene(scene);

    FogObjectAnimation anim = new FogObjectAnimation(shape_transform);
    sceneManager.setApplicationObserver(anim);

That's it for the application framework that we'll use for these demos.

Global Fog

To use a global fog, you will first need to create an instance of a Fog node. When doing that there are a number of different parameters to provide. The most useful one is the colour to make the fog. Since we're also going to want to have the background blend with the fog, a global constant has been declared with the desired colour:

    private static final float[] FOG_COLOUR = { 0, 0, 0.5f };

Next, we want to have the background clear to that all the time, so tell the surface about the colour it should clear to. This line of code should be inserted into the standard setupAviatrix() method that is common to these demo applications.

    surface.setClearColor(FOG_COLOUR[0], FOG_COLOUR[1], FOG_COLOUR[2], 1);

Next we need to create an instance of the Fog node and set it's colour. This code will go into the setupSceneGraph() method, before the creation of the Scene as above.

    Fog fog = new Fog(FOG_COLOUR);
    fog.setLinearDistance(0.1f, 3f);
    scene_root.addChild(fog);

As you can see, there is a call to setLinearDistance(). Fog has have a number of different ways of calculating just how much to obscure the object colour with fog colour. These equations determine the fall-off rate between being visible and not visible. By default, the node uses the linear equation. (This can be changed via the constructor or method calls). Linear fog requires the setting of the distances where the fog starts to take effect and where it is at full opacity (nothing is visible except the fog colour). The default values are 0 and 0, so this method call is used to set some more realistic values. In this case 1 and 3 are used because they represent roughly the extents of the path that the primitives rotate through. The distance is distance from the current camera location and is always relative to the camera. If you're aiming to achieve objects fading out in certain parts of the world more than others regardless of camera distance, then you'll need to make use of the volumetric fog techniques discussed later.

To make the fog into a global fog, then you need to register it with the Scene instance. This will ensure that it is used. A further requirement for it to be used is that the Fog instance is also part of a scene graph (that will eventually be live). That's the reason for the last line of the previous code snippet that adds the instance to scene_root

    ...
    scene.setActiveView(vp);
    scene.setActiveFog(fog);

That's all there is to it for global fog. If you run the BasicFogDemo you'll see the following on screen:

Local Fog

To use local fog effects, the process is very similar. You'll still need to create a Fog node and place it in the scene graph, just there will be a slightly different setup. Since want the fog to be local, we don't include the step to register it with the scene instance. That won't be enough, we also need to instruct the fog that it is to be used for local effect too. This is done by calling the setGlobalOnly() method with a value of false. Alternatively, you can set the same flag in the constructor. In this demo, that's what we do, and the code now looks like this:

    fog = new Fog(FOG_COLOUR3, false);
    fog.setLinearDistance(2, 3);
    tg.addChild(fog);

Once that is done, the node will use local fogging effects.

To make the demo more appealing and better illustrating the effects, we've changed where that fog node instance is placed in the scene graph. This time, the instance is registered in the same group as the primitive itself so that you can see the individual fog effects. To accomplish that, we modify the switch statement in createPrimitives to now look like this:

    switch(i % 4)
    {
        case 0:
            Fog fog = new Fog(FOG_COLOUR1, false);
            fog.setLinearDistance(1.5f, 3);
            tg.addChild(fog);

            Box box = new Box(0.125f, 0.125f, 0.125f, app);
            tg.addChild(box);
            break;

        case 1:
            Cone cone = new Cone(0.25f, 0.125f, app);
            tg.addChild(cone);
            break;

        case 2:
            Cylinder cyl = new Cylinder(0.25f, 0.125f, app);
            tg.addChild(cyl);
            break;

        case 3:
            fog = new Fog(FOG_COLOUR3, false);
            fog.setLinearDistance(2, 3);
            tg.addChild(fog);

            Sphere sphere = new Sphere(0.125f, app);
            tg.addChild(sphere);

    }

As you can see, the box and sphere will be effected by local fog, but the cylinder and cone will not be. The visual effect is that you'll see the box and sphere change to their fog colours as they rotate away from the camera, yet the other two will not change colour even though they follow the same path. A snapshot of this effect below shows heavy green tinge of the sphere and blue tinge to the box, while the cone and cylinder remain bright red.

Combined Local and Global Fog

For the third demo, we combine the code from the first two to illustrate how the fog scoping rules work with global and local versions. There's only a small change to the code from the previous examples. To make it clearer where the global effects are and where local ones take place, we've juggled the fog colour constants. The global fog is now grey and the two local versions remain blue and green.

The combined code is placed in the CombinedFogDemo class and when run produces the image below.

Note: One bug that I encountered during this development was issues with the ATI drivers. Prior to the 7 July 2004 version of the drivers, the fog colour changes for each primitive would not take effect. If all you see are grey primitives with no green or blue versions, update your drivers and it should be fixed. It appears there were some bugs with the glFogfv() calls when they were being changed between primitives without drawing a non-fogged primitive in-between.

Volumetric Fog

Volumetric fog makes use of another capability of OpenGL - the ability to give every vertex a specific "depth" in the fog. In previous examples, the amount of fog to apply to a primitive was decided by the depth of that point in space from the camera location. This makes it impossible to do visual effects like a foggy valley in a mountain range. OpenGL introduced a concept called Fog Coordinates that allow you to override that default behaviour and tell the application exactly what numbers to use as the "depth" when interpolating the fog colour over the primitive.

Fog coordinates, because they are associated with individual vertices, are specified as part of the VertexGeometry class. There you will find the method setFogCoordinates() method. As with the other setter methods on this class, it takes an array of values that must correspond with the number of vertices that the geometry contains.

Fog coordinates describe an artifical depth to the geometry to replace the automatically calculated one. This depth is any number that can be greater than or equal to zero (though negative values are allowed, just highly discouraged). As a depth, there is only a single value, so for each coordinate, you only need a single float which is the visual depth to use for applying the fog. These coordinates will then be passed to OpenGL when the geometry is rendered and you will see the fog appear according to the coordinate values.

Illustrating fog coordinates with the previous fog demo applications won't be particularly interesting as you won't be able to tell much visually. However, I did find a cool OpenGL demo application on the internet and decided to make use of it for demonstrating fog coordinates and translated it into java plus Aviatrix3D code.

The rest TBD.