|
Implementing Terrain Following in Java 3DTerrain 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.
Basic ProcessAs 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 GazingWell, 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 ConclusionsOnce 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.
![]() 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 workAn 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.
ImplementationMany of the principles of the implementation you have already seen in the previous section. However, the details are quite a bit different.Design AssumptionsFor 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.
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 bitsFor 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.
Looking down upon somethingStarting with setting up the picking again, we need to create the same sort of setup as before. This time, instead of the fixed lengthPickSegment 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);
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 terrainPick 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 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(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 StartedHang on, we've almost finished haven't we?
Nope. In the above pieces of code we've assumed that the value of
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.
SummaryThat'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. |
|
[ j3d.org ]
[ Aviatrix3D ]
[ Code Repository ]
[ Java3D ]
[ OpenGL ]
[ Books ]
[ Contact Us ]
Hosted by Yumetech Last Updated: $Date: 2007/03/28 15:42:05 $ |