Tectonic Plates — Spherical Geometry & Real Displacement

Mapping Earth's crustal plates onto a sphere with displacement maps and smooth boundary animations. Three.js, spherical geometry and more trigonometry than I anticipated.

The Challenge: A Globe That Moves

The tectonic plates simulation had a clear visual target: a globe of Earth with visible plate boundaries that slowly drift, collide and subduct in real time. Simple to describe. Surprisingly tricky to implement.

The main difficulty isn't the plate mechanics — it's the geometry of a sphere. Everything that's easy on a flat plane becomes moderately awkward on a sphere: distances, interpolation, UV mapping, normal computation after displacement.

Building the Sphere Correctly

Three.js ships SphereGeometry which is fine, but it has a pole singularity problem: the UV coordinates pinch at the north and south poles, causing texture seams and distortion. For a globe, that's visible as ugly artefacts around Antarctica and the Arctic.

The standard fix is to use a cube-sphere instead: start with a cube whose faces are subdivided, then project all vertices onto the unit sphere. This distributes vertices much more uniformly.

// Project cube vertex onto sphere
function cubeToSphere(x, y, z) {
  const x2 = x * x, y2 = y * y, z2 = z * z;
  return {
    x: x * Math.sqrt(1 - y2/2 - z2/2 + y2*z2/3),
    y: y * Math.sqrt(1 - z2/2 - x2/2 + z2*x2/3),
    z: z * Math.sqrt(1 - x2/2 - y2/2 + x2*y2/3)
  };
}

Displacement Maps for Topography

The terrain height is driven by a displacement map — a greyscale texture where white = high elevation, black = ocean floor. In the vertex shader, each vertex is pushed outward along its normal by an amount proportional to the texture value:

// Vertex shader snippet
float height = texture2D(displacementMap, vUv).r;
vec3 displaced = position + normal * height * displacementScale;
gl_Position = projectionMatrix * modelViewMatrix * vec4(displaced, 1.0);

After displacement, normals need to be recomputed — displaced normals point in the wrong direction and produce incorrect lighting. Three.js can compute tangent-space normals from a normalMap, but for procedural displacement I did it analytically in the shader using partial derivatives.

Plate Boundaries and Drift

Each of Earth's ~15 major plates is assigned an angular velocity vector (the real geological values, scaled down by 10⁸ for useful visual speed). The plate membership of each texel is stored in a separate mask texture.

At each frame, I rotate each plate's texture region by its angular velocity using a rotation matrix in the fragment shader. Plate boundaries are highlighted by computing the gradient of the plate-ID texture — a sharp value gradient means a boundary.

The Trigonometry Trap

Spherical geometry has a reputation for subtlety, and it deserves it. I spent most of one afternoon debugging why my distance calculations were slightly wrong. The issue: I was computing Euclidean distances between longitude/latitude pairs instead of great-circle distances.

On a sphere, the shortest path between two points is a great circle arc. The haversine formula computes this correctly:

function haversineDistance(lat1, lon1, lat2, lon2, R = 6371) {
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLon = (lon2 - lon1) * Math.PI / 180;
  const a = Math.sin(dLat/2)**2 +
            Math.cos(lat1 * Math.PI/180) *
            Math.cos(lat2 * Math.PI/180) *
            Math.sin(dLon/2)**2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}

Try the simulation at /tectonic-plates/. You can toggle between realistic and accelerated drift speed.