 The One Man MMO Project
The story of a lone developer's quest to build an online world :: MMO programming, design, and industry commentary
Terrain Following in OpenGL
By Robert Basler on 2013-01-28 23:27:24
Homepage: www.onemanmmo.com email:one at onemanmmo dot com

I'm writing this up because of the unbelievable pain I went through to get it working. It seems like something that shouldn't be that difficult: getting a model to follow terrain by adjusting its up vector to match the up vector of the terrain mesh under it and to face in a given direction, but try and try again, I could not get it to work. Rotation matrices are my bane. Nothing else I do is so unbelievably frustrating. If they worked or didn't work that would be fine, but as I went along I found a lot of variations that would almost work and it wasn't always easy to tell what was wrong. My favourite variation worked perfect when going north or south, but squashed the model into a diagonally tilted pancake as it rotated through east and west.

I found a lot of information on the topic on the internet, but all the detailed examples were for DirectX, the OpenGL ones were all brief, too brief evidently.

Since this matters, I'll give you the specifics of my render setup: the +X axis is to the right, the +Y axis is up, and the +Z axis comes out of the screen. My terrain is all in the +X, +Z plane with the top left corner of the terrain at the origin. My camera looks down the -Z axis.

The first thing is to figure out what direction the model is going. I use the atan2 function with the distance from where the model is (posX/Z) and where it is going to (x/zDestination). The rotation value I calculate results in a radians rotation value where zero is along the +Z axis and rotation is clockwise looking down towards -Y and ranges from 0 to 2PI.

`    float64 xDelta = xDestination - posX;    float64 zDelta = zDestination - posZ;    // +Z = 0, -X = 90, -Z = 180, +X 270.    float64 newDirection = Math::ATan2( zDelta, xDelta ) - Math::HalfPI;    if ( newDirection < 0.0 )    {        newDirection = newDirection + 2.0 * Math::PI;    }`

Once I have newDirection I need to figure out the up, left and forward vectors for the model being positioned. I originally had an axis and angle system, but I couldn't ever get that to work. I might have to take another quick look at that now and see if I can get it to work.

To orient the model I need the terrainUpVector from my terrain. To get nice smooth transitions as I move the model over the terrain, I use a linear interpolation of the normals of the four vertices at the corners of the current terrain square.

First there's my lerp template function:

`    //! brief Linear interpolation function template.    template  T lerp( const D& distance, const T& lo, const T& hi )     {        return lo + ( hi - lo ) * distance;    }`

And here's the code that uses it to calculate the interpolated normal.

`    const Vector3& topLeft = block->GetNormal( tileRow, tileCol );    const Vector3& topRight = block->GetNormal( tileRow, tileCol + 1 );    const Vector3& bottomLeft = block->GetNormal( tileRow + 1, tileCol );    const Vector3& bottomRight = block->GetNormal( tileRow + 1, tileCol + 1 );    // these distances are the distance from the origin of this terrain square (topLeft vertex)     // to the x/z coordinate within the square that we want the normal of.    float32 distanceZ = CoordinatesToMetresZ( z % tileZDimension ) / CoordinatesToMetresZ( tileZDimension );    float32 distanceX = CoordinatesToMetresX( x % tileXDimension ) / CoordinatesToMetresX( tileXDimension );    Vector3 top = lerp( distanceX, topLeft, topRight );    Vector3 bottom = lerp( distanceX, bottomLeft, bottomRight );    N = lerp( distanceZ, top, bottom );    N.Normalize();`

To build a rotation matrix to orient the model, I need to build up, left and forward vectors which are orthagonal to each other. I already have a good up vector from the terrain, but I need another vector to generate the third. I use sin and cos of the model heading to generate a forward vector which points in the direction that the model is heading. (This will not work if your model is headed up or down the Y axis.) Note that the forward and up vectors aren't orthagonal, but that doesn't matter, because we'll fix that in a sec. Taking the cross product of the up and forward vector gives us the left vector. Then I take the cross product of the left and up vector to generate a forward vector which is orthagonal. Neat trick. I normalize the resulting vectors. I don't think that is strictly necessary, but better safe than sorry.

`    Vector3 forwardVector;    Vector3 leftVector;    Vector3 terrainUpVector = GetTerrainNormal( positionComponent>GetX(), positionComponent>GetZ() );    // Sin has the wrong sign to get a vector pointing in the correct direction.    forwardVector[ 0 ] = -1 * sin( newDirection );    forwardVector[ 1 ] = 0.0;    forwardVector[ 2 ] = cos( newDirection );    leftVector = CrossProduct( terrainUpVector, forwardVector );    forwardVector = CrossProduct( leftVector, terrainUpVector );    terrainUpVector.Normalize();    forwardVector.Normalize();    leftVector.Normalize();    modelTransformNode->SetVectors( forwardVector, leftVector, terrainUpVector );`

At this point if you're following along, you'll want to try heading your model along one or more of the axes and take a close look at the three vectors' values to make sure they are pointing up, left and forward as they should be.

The last step is to turn these three vectors into a rotation matrix. This was the root of my problems. It is super important to note that my matrix class's interface is row-major so if you are putting these values into a 2D array of floats that you're going to pass to OpenGL (which is column major), you'll need to swap the row and column parameters. For the longest time I had these values transposed and it almost worked (it appeared to work with models moving left to right, but not with models moving right to left!)

`    rotation.Identity();    // First parameter is row, second is column.    rotation.Set( 0, 0, mLeft[ 0 ] );    rotation.Set( 1, 0, mLeft[ 1 ] );    rotation.Set( 2, 0, mLeft[ 2 ] );    rotation.Set( 0, 1, mUp[ 0 ] );    rotation.Set( 1, 1, mUp[ 1 ] );    rotation.Set( 2, 1, mUp[ 2 ] );    rotation.Set( 0, 2, mForward[ 0 ] );    rotation.Set( 1, 2, mForward[ 1 ] );    rotation.Set( 2, 2, mForward[ 2 ] );`

So there you are, I hope it helps somebody. I have seen more mangled models the last few days than I have the entire length of this project. I can't express how glad I am to have this working at long last.