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.
Because the two cameras are so similar, we might be tempted to have TerrainCamera
inherit from FirstPersonCamera
. That's a nice idea, but there's a problem. The FirstPersonCamera
constructor calls reorient
, which considers the camera's position. A TerrainCamera
should therefore adjust the camera's y-position before reorient
gets called. But a subclass can't do anything before its superclass's constructor has been called.
When inheritance doesn't work, we have other options. The most expedient option 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. We implement it by unscaling the camera's xz-position, looking up the height, and raising or lower 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, while the camera is skating 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.get
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.
The adjustY
must itself be adjusted. As before, it unscales the camera's xz-coordinates, looks up the height, and sets the camera's y-position. But this time it doesn't truncate the xz-coordinates to integers. Instead it queries the intermediate height using the blerp
method.
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. These 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.