Terrain Camera

How to 3D

Chapter 7: Camera and Terrain

Terrain Camera

The TerrainCamera behaves almost exactly like FirstPersonCamera. The only difference is that whenever the from position changes, we raise or lower the camera so it floats at some fixed offset above the terrain surface.

We might be tempted to have TerrainCamera inherit from FirstPersonCamera. That's a nice idea, but there's a problem. The FirstPersonCamera constructor depends on the camera already having its position set. The TerrainCamera derives the position from the terrain, but it can't do this before its superclass constructor is called. Inheritance won't work.

Inheritance isn't our only option. The most expedient alternative is to copy and paste the FirstPersonCamera class, name the clone TerrainCamera, and make the following edits to the class:

class TerrainCamera {
  // ...
  field: Field2;
  offset: number;
  factors: Vector3;

  constructor(from: Vector3, to: Vector3, field: Field2, offset: number, factors: Vector3) {
    // ...
    this.worldUp = new Vector3(0, 1, 0);
    this.field = field;
    this.offset = offset;
    this.factors = factors;
    this.adjustY();
    this.reorient();
  }

  // ...

  advance(distance: number) {
    // ...
    this.adjustY();
    this.reorient();
  }

  strafe(distance: number) {
    // ...
    this.adjustY();
    this.reorient();
  }
}
class TerrainCamera {
  // ...
  field: Field2;
  offset: number;
  factors: Vector3;

  constructor(from: Vector3, to: Vector3, field: Field2, offset: number, factors: Vector3) {
    // ...
    this.worldUp = new Vector3(0, 1, 0);
    this.field = field;
    this.offset = offset;
    this.factors = factors;
    this.adjustY();
    this.reorient();
  }

  // ...

  advance(distance: number) {
    // ...
    this.adjustY();
    this.reorient();
  }

  strafe(distance: number) {
    // ...
    this.adjustY();
    this.reorient();
  }
}

Whenever from changes—as it does in the constructor, advance, and strafe—we immediately call adjustY, which we haven't written yet.

Our first attempt at implementing adjustY unscales the camera's xz-position, looks up the height, and raises or lowers the camera so it floats above the terrain.

Let's test this. Move around in this renderer, which uses the code above:

The movement is very choppy. Why? Because we're only using the known heights at the vertices, but the camera passes through the spaces between the vertices. We need a way to guess the heights at arbitrary locations on the terrain grid. That sounds like a job for lerp.

There's a catch. Our lerp function only handles interpolation along one dimension and between two known data points. A terrain is indexed in two dimensions. How do we extend linear interpolation to a higher-dimensional space? Step through this breakdown of the algorithm to see how it's done:

Linearly interpolating between four points in two dimensions is called bilinear interpolation or blerp. Bilinear interpolation reduces down to three linear interpolations, two along one of the dimensions and a third along the other dimension.

We'll eventually need blerping for more than just terrain, so let's add a reusable blerp method to the Field2 class.

We adjust the adjustY method to query the intermediate height using the blerp method instead truncating the coordinates and using get2:

See the effect of this interpolation by enabling the use of blerp in the renderer above.

Summary

WebGL has no notion of a viewer. It just renders whatever's in the unit box. To render a different chunk of the world, we compose translation and rotation matrices that transform the chunk into the unit box. This task is complex enough to warrant camera abstractions, which include several behaviors that change the camera interactively: advance, strafe, yaw, and pitch the viewer. A first-person camera makes the viewer feel like a character in the scene. A third-person camera puts the viewer over the shoulder of an avatar. These cameras fly through a scene without constraint. To offer a more grounded interaction, we load in a heightmap of elevation data and expand it into a trimesh. A terrain camera stays pinned to the terrain as the viewer moves about. Heights are bilinearly interpolated from the heightmap.

← HeightmapsLab: Stroll →