Tutorials

Sections

j3d.org

Implementing Terrain Following in Java 3D

Terrain following is a particularly important feature when you are building terrestrial style environments. In these cases, you want to implement something that looks reasonably realistic. That is, as the avatar moves around, you want it to look like they are walking: come to a set of stairs - climb it, come to a slippery slope and go down it. If the stairs are too step, then don't climb it. This is what terrain following is all about.

Depending on your view of the world, terrain following is sort of like collision detection. However, this time, instead of looking where we are going, we'll be looking down at our feet to see if we trip up on something.

Just like the collision detection system, what we present here will be a generalised algorithm that is applicable for most applications. There are also a lot of ways to optimise this process and we'll point those out as we go and also don't forget all of the tips from the previous section.

     Note
    If you want to use the basic algorithms appearing here, there is a complete implementation available in the j3d.org Code Repository. This code is the generalised solution that you see here and so can be plugged into any application immediately.

 

Basic Process

As we have already mentioned, terrain following is a bit like collision detection. The fundamental process is about casting a pick ray into the scene and seeing what gets returned and then adjusting our avatar position with the results.

Navel Gazing

Well, actually, it's more like toenail gazing - in order to follow terrain you really need to know what is under your feet. Just like in the real world climbing up a dirt track, you want to see what you are about to step on and work out exactly where to place your feet. Even then, we come to some cliff that is too high for us to step over so we have to find some alternate path.

Just like collision detection, actions for terrain following are based on dealing with knowing where we are about to go, rather than where we are now. The whole point of terrain following is to make sure that we are the correct height above the ground now, rather than correcting for mistakes of the past. To do this, we must look during this frame where we are going to be next frame and adjust the positions accordingly.

Where collision avoidance and terrain following is the direction they look and how far they look. Collision detection was really only interested in knowing about the next frame so it purposefully limited its search to a very small distance. In terrain following, we really don't know where we are located above the underlying terrain (if at all!). To know this, we have to cast an endless ray into the scenegraph rather than just a line segment.

The second difference is where we are looking. Collision detection looked out along the -Z axis to see what was in front of us. Terrain following requires looking at our feet so this time the direction is along the -Y axis.

Jumping to Conclusions

Once we have found something under our feet, we have to decide just what to do about it. We already know our height above the ground, but what should we do with next time. As we aluded to in the setup section, there are certain considerations that we want to take into account. Most of the details are shown in Figure 3.

The concept that we introduced was that of a step height - the maximum positive change in the terrain delta between our current position and the next position before we decide to disallow it and call it a collision. For example, climbing a set of stairs requires the use of sharp discontinuities in height, but if the stair becomes too big, it becomes a wall. Each time we deal with terrain changes, we have to decide whether the delta is too big. Also, one assumption here is that we don't care about descending terrain - the user can fall as far as they like.

Information used to calculate terrain following
Figure 3: Data and terms used when dealing with terrains

Another interesting consideration in this work is - what if the viewpoint that the user first gives us is not at the exact height above the terrain? Well, this might be deliberate. Think of a viewpoint where the world author is attempting to make a series of snapshot views but offer no navigation at that point. They wouldn't take to kindly to us "correcting" their obviously mistaken judgement, so we limit the algorithm to apply only when the user starts to move around. Only after they have made that first step can you be assured that it is OK to move the viewpoint around to be at the right height above the terrain.

Gravity does the work

An aspect of how closely we model the real world is the effect of gravity. Should the user step over a really large cliff, do they instantly drop to the bottom or do we apply some more gradual descent. This is an important decision to make as it effects how we run our algorithms - does the detection continue to run even after we let the mouse button up or not?

All terrain following has a form of gravity, it is just a case of deciding how quickly we want it to take effect. Also, getting gravity right is a very tricky problem to solve. In many cases real world accelaration style gravity just feels wrong. A constant value for gravity feels more gentle on the user. Step over a cliff and you get this nice steady descent and soft landing rather than as a bug splat on the ground.

 

Implementation

Many of the principles of the implementation you have already seen in the previous section. However, the details are quite a bit different.

Design Assumptions

For this implementation we are going to build a system that is generic, just like collision detection. This means that we have to provide a list of assumed things about the world that we are operating in.
  1. do we always have triangle sets or do we have to deal with arbitrary sized data.
  2. Can we guarantee that there is only one GeometryArray per Shape3D?
  3. Has the user structured the scene graph properly so that we never have intersecting geometry bounding boxes for the type of operation that we want to do (collisions or terrain following)
  4. Do either the viewpoint or the data exist under a large set of transforms or are they all at the world root?
  5. Is gravity at work or not?
  6. How do we react when there is no ground?
All of these points should look remarkably familiar. All except the last two are copied from the collision avoidance assumptions. That is, we have the same set of problems to cope with.

In this first implementation, we are not going to worry about gravity handling. If we encounter a large drop, we will jump that distance immediately.

Dealing with no ground below us can be an interesting problem. For safety, if we discover that there is no ground then we just don't play with anything. This allows us to deal with problems where we might get a very slight mismatch in the boundaries between two sets of polygons that define terrain trials. Another mistake it catches is having a world that has no ground but the user accidently selected terrain following anyway.

Stock standard bits

For terrain following, we need the same set of global variables and standard pieces as collision detection. Therefore, we'll just list them again and won't bother to explain.
viewTg The TransformGroup that we are using to define our avatar's location in the virtual world.
viewTx The Transform3D that belongs to the view transform group. Used to get and set the local transformation for each frame.
worldEyeTransform The Transform3D that contains the complete transformation information about where our avatar is in world coordinates.
oneFrameTranslation The pre-calculated amount that we going to be moving this frame assuming everything else turns out right. We can adjust values in here as required because it has not yet been used to modify the view transformation.
lastTerrainHeight A double value that holds the working height that we are currently at (not the one we are about to be at) so that we can determine what the step height is between now and future.

Looking down upon something

Starting with setting up the picking again, we need to create the same sort of setup as before. This time, instead of the fixed length PickSegment we use the infinite length PickRay. We still have to prepare the start point as before:
    viewTg.getLocalToVworld(worldEyeTransform);
    worldEyeTransform.mul(viewTx);

    worldEyeTransform.get(locationVector);
    locationPoint.add(locationVector, oneFrameTranslation);

     Note
    If you are using both collision avoidance and terrain following together, you don't need to recalculate the new location. This will save you a few more CPU cycles per frame.

The next piece you need to deal with is projecting the down vector. Because this time we are concerned about everything that might be below us, there is actually less work to do. All we have to do is swing the local "down" vector into the world coordinate space.

    worldEyeTransform.transform(Y_DOWN, downVector);
The final step is to set the values in the pick shape:
    terrainPicker.set(locationPoint, downVector);

Finding the terrain

Pick shape charged and ready to go, we fire it off into the scene to find out what is below us. Like collisions, if we don't find anything at all, we just exit now - there is no point doing any further processing.
    SceneGraphPath[] ground = terrain.pickAllSorted(terrainPicker);

    if(ground == null)
    return;
Unlike collision avoidance, one particular concern to us this time is knowing exactly which object is the closest to us. Although the picking returns us a list of sorted shapes immediately, these are actually sorted by bounding box centre distance, not actual closest object first. Another problem is that within each reported Shape3D instance, the actual intersecting geometry is going to vary and so we have to test all of them all the time just to make sure that we really do have the closest object.

Internally, we have a similar loop to the collision avoidance:

    double shortest_length = -1;

    for(int i = 0; i < ground.length; i++)
    {
        Transform3D local_tx = ground[i].getTransform();
        local_tx.get(locationVector);

        Shape3D i_shape = (Shape3D)ground[i].getObject();

        Enumeration geom_list = i_shape.getAllGeometries();

        while(geom_list.hasMoreElements())
        {
            GeometryArray geom = (GeometryArray)geom_list.nextElement();

            if(geom == null)
                continue;

            if(insection detection)
            {
                diffVec.sub(locationPoint, this_intersection_point);

                if((shortest_length == -1) ||
                   (diffVec.lengthSquared() < shortest_length))
                {
                    shortest_length = diffVec.lengthSquared();
                    intersectionPoint.set(wkPoint);
                }
            }
        }
    }
What we have to deal with now working out what the closest object is. To do this we keep a rough running tally of what we have found so far as the closest object. We start with a floating point value that has a negative value. This is because our ray has a fixed point, there can never be a a picked object that has a negative distance from you, this is our first test to see if there is something close and we haven't set anything to compare against. After that we set a distance value. We really don't care how this is calculated, just so long as we have some relative measure that one object is closer than any previous selected ones. For this reason, we use the lengthSquared() method to determine the distance away. While we could find the real distance, that involves computing a square root value. Square roots are very expensive mathematical operations. We actually gain nothing by taking the square root and so we opt for the much simpler and cheaper square value.

Now, at the end of this loop we have the closest intersection point of our terrain stored in intersectionPoint and we also have an indicator as to whether we found anything. Remember - if the loop never found a real intersection, the value of shortest_length will still be negative. If we didn't find anything exit now.

What's up shorty?

Terrain below us, closest point selected, let's work out what to do. Our first point is to work out exactly how high above the terrain we are going to be and therefore how much of a delta we've made from our current position.
    double terrain_step = intersectionPoint.y - lastTerrainHeight;
    double height_above_terrain = locationPoint.y - intersectionPoint.y;
If our height above the terrain is exactly what our avatar height is, we don't need to do anything at all. We can exit now.

When the heights are not the same. we check to see which set of conditions hold and act accordingly:

  • If the change in height is zero then we need to adjust the height of the avatar to be the correct height above the ground.
  • If the change in height of the terrain is less than the step height, we adjust the height to be the new height plus the avatar's height. (This includes dealing with negative changes - falling terrain).
  • Otherwise the change in height greater than the step height and therefore we treat it as a collision and prevent any movement this turn.
All of this is summed up in the following code snippet:
    if(height_above_terrain != avatarHeight)
    {
        if(terrain_step == 0)
        {
            oneFrameTranslation.y = avatarHeight - height_above_terrain;
        }
        else if(terrain_step < avatarStep)
        {
            oneFrameTranslation.y += terrain_step;
            ret_val = true;
        }
        else
        {
            ret_val = false;
        }
    }

    lastTerrainHeight = (float)intersectionPoint.y;
The last step here is to make sure that we set the terrain height to be the right value for the next time we do the calculations.

Getting Started

Hang on, we've almost finished haven't we?

Nope. In the above pieces of code we've assumed that the value of lastTerrainHeight is set from the last time around so that we can make some valid judgements. Problem is, what happens the first frame where we don't have any pre-established data?

To get around this problem, what we do is make sure that when the viewpoint information is first set, to do a terrain intersection run then. This will establish what the fundamental height is of the terrain. Note that at this point we don't even adjust the viewpoint, we just make note of the current terrain height for future reference.

 

Summary

That's all there is to know. These pieces of code will implement your complete terrain following and collision avoidance systems.

As we've hinted throughout this tutorial, what we have covered is a generalised system that should work for any environment. However, if you know that you can apply certain restrictions to your scenegraph structure and you don't have to deal with arbitrary data, then quite a number of performance optimisations can be made to the code.

We hope that this has been a useful tutorial. Please feel free to send the usual feedback on areas that you think could do with more work or explanation. Lastly, if you don't feel like implementing all of this code yourself, please drop by the J3d.org Code Repository for the implementation there.