Three.js Maps: 3D Terrain and Map Visualization

A 3D map or terrain in Three.js is typically built from a heightmap (a grayscale image where pixel brightness encodes elevation) used to displace the vertices of a plane geometry, either on the CPU (reading pixel data once and setting vertex positions directly) or on the GPU (sampling the heightmap texture in a vertex shader), with the GPU approach scaling to much larger terrains.

Last updated . Verified against three.js r181.

Read the FAQJump to code

Heightmap-driven terrain

The standard technique: start with a PlaneGeometry with enough subdivisions to have meaningful vertex density, then displace each vertex's Y (or Z, depending on orientation) by sampling a heightmap image at that vertex's UV coordinate. Done on the CPU, you read the heightmap's pixel data once (via a canvas getImageData call) and directly set each vertex position. Simple, but limited by how many vertices you can afford to have in the geometry.

Done on the GPU, sampling the heightmap texture directly in a vertex shader (positionNode in TSL, displacing along the normal by a sampled height value), the displacement happens per-vertex on the GPU at render time. This scales to much higher subdivision counts than a CPU-computed mesh, and enables live-adjustable terrain (change a uniform, the whole terrain updates) without regenerating geometry.

Tiling for large terrain

A single large heightmap and mesh works fine for a bounded scene, but genuinely large terrain (an open-world-scale map) is usually split into tiles: separate mesh chunks, each with its own heightmap region, loaded and unloaded based on camera distance. This is the same level-of-detail thinking as instanced grass: don't pay the cost of detail the camera can't currently see or resolve. Combined with LOD per tile (fewer subdivisions for distant tiles), this is how large real-time terrain systems stay performant.

Layering real geographic/map data

For visualizing actual geographic data rather than a fictional landscape, real elevation datasets (SRTM, USGS) can be converted to heightmap images with GIS tools, and real map imagery (satellite photos, street map tiles) can be applied as the terrain's texture, using the same texture loading pipeline as any other material. For a whole-Earth scale visualization rather than a local terrain patch, see the globe guide instead, which uses spherical rather than planar geometry.

See it in production

Browse the full showcase →

Code

CPU heightmap displacement (r181)
1const geometry = new THREE.PlaneGeometry(50, 50, 128, 128)
2geometry.rotateX(-Math.PI / 2)
3
4const canvas = document.createElement('canvas')
5const ctx = canvas.getContext('2d')
6const img = await loadImage('/heightmaps/terrain.png') // your own image-load helper
7canvas.width = img.width
8canvas.height = img.height
9ctx.drawImage(img, 0, 0)
10const data = ctx.getImageData(0, 0, img.width, img.height).data
11
12const position = geometry.attributes.position
13for (let i = 0; i < position.count; i++) {
14 const u = (position.getX(i) + 25) / 50
15 const v = (position.getZ(i) + 25) / 50
16 const px = Math.floor(u * (img.width - 1))
17 const py = Math.floor((1 - v) * (img.height - 1))
18 const height = data[(py * img.width + px) * 4] / 255 // red channel as elevation
19 position.setY(i, height * 8)
20}
21position.needsUpdate = true
22geometry.computeVertexNormals()
GPU heightmap displacement (TSL, r181)
1import { texture, uv, positionLocal, normalLocal } from 'three/tsl'
2
3const heightmap = texture(heightmapTexture)
4const height = heightmap.sample(uv()).r // red channel as elevation, sampled per-vertex
5
6const material = new THREE.MeshStandardNodeMaterial()
7material.positionNode = positionLocal.add(normalLocal.mul(height.mul(8)))
8// Scales to far higher subdivision counts than the CPU version. The
9// displacement runs on the GPU at render time, not once in JavaScript.

Learn this properly

TSL for Beginners: Build a Living Planet

Noise: Continents Appear

This lesson builds procedural terrain with TSL noise, directly relevant to heightmap-driven terrain.

Start the lesson (8 minutes)

Frequently asked questions

How do I create terrain in Three.js?

Displace a PlaneGeometry's vertices using a heightmap image: either read once on the CPU and applied directly to vertex positions, or sampled per-vertex in a shader on the GPU for higher detail and better performance at scale.

How do I make large terrain performant in Three.js?

Split it into tiles loaded/unloaded based on camera distance, combined with per-tile level of detail (fewer subdivisions for distant tiles), the same principle as any large-scene culling strategy, applied to terrain chunks.

Can I use real elevation data for Three.js terrain?

Yes. Real elevation datasets (SRTM, USGS) can be converted to heightmap images using GIS tools, then used the same way as any other heightmap to displace terrain geometry.

Keep reading