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:

Field2.get2
method.
lerp
function tells us its value:
$$
\begin{array}{c}
75 + 0.7 \times (97 - 75) \\
\downarrow \\
90.4
\end{array}
$$
lerp
function tells us its value:
$$
\begin{array}{c}
94 + 0.7 \times (62 - 94) \\
\downarrow \\
71.6
\end{array}
$$
lerp
function tells us its value:
$$
\begin{array}{c}
90.4 + 0.2 \times (71.6 - 90.4) \\
\downarrow \\
86.64
\end{array}
$$
Three lerps later, we have the point's interpolated height.
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.