Three.js terrain library with hex-tiled texture blending, animated water, and heightmap brush editing.
TerrainRegion owns the terrain mesh, shaders, and height data. Your app owns the scene graph, camera, input, and raycasting.
npm
npm install metaverse-terrain three
Requires three >= 0.160 as a peer dependency. Use with Vite, Webpack, or any bundler — no import map needed.
jsDelivr
No install step. Load from cdn.jsdelivr.net/npm/metaverse-terrain in the browser using an import map (see below).
Browsers cannot resolve bare imports like 'metaverse-terrain' on their own. Add an import map in your HTML, then load your app in a <script type="module">.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three/build/three.module.js",
"metaverse-terrain": "https://cdn.jsdelivr.net/npm/metaverse-terrain/index.js"
}
}
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { TerrainRegion } from 'metaverse-terrain';
// your app here
</script>
</body>
</html>
Serve over HTTP (not file://). The snippets below go inside that <script type="module"> block.
Provide all five layers: sand, grass, rock, snow, water. Reference PNGs are on jsDelivr:
const TEXTURE_BASE = 'https://cdn.jsdelivr.net/npm/metaverse-terrain/texture';
const textures = {
sand: `${TEXTURE_BASE}/terrain-sand.png`,
grass: `${TEXTURE_BASE}/terrain-grass.png`,
rock: `${TEXTURE_BASE}/terrain-rock.png`,
snow: `${TEXTURE_BASE}/terrain-snow.png`,
water: `${TEXTURE_BASE}/terrain-water.png`,
};
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three/build/three.module.js",
"metaverse-terrain": "https://cdn.jsdelivr.net/npm/metaverse-terrain/index.js"
}
}
</script>
<style>body { margin: 0; } canvas { display: block; }</style>
</head>
<body>
<script type="module">
import * as THREE from 'three';
import { TerrainRegion } from 'metaverse-terrain';
const TEXTURE_BASE = 'https://cdn.jsdelivr.net/npm/metaverse-terrain/texture';
const textures = {
sand: `${TEXTURE_BASE}/terrain-sand.png`,
grass: `${TEXTURE_BASE}/terrain-grass.png`,
rock: `${TEXTURE_BASE}/terrain-rock.png`,
snow: `${TEXTURE_BASE}/terrain-snow.png`,
water: `${TEXTURE_BASE}/terrain-water.png`,
};
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x9fb7d5);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 900);
camera.position.set(132, 108, 168);
camera.lookAt(0, 10, 0);
const region = new TerrainRegion({ textures, seed: 42 });
scene.add(region.group);
function animate(time) {
region.update(time);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate(0);
</script>
</body>
</html>
The library does not handle pointer events. Raycast the terrain, then call paintAt:
import { TerrainRegion, getTerrainHitFromPointer } from 'metaverse-terrain';
const TEXTURE_BASE = 'https://cdn.jsdelivr.net/npm/metaverse-terrain/texture';
const textures = { /* sand, grass, rock, snow, water — see above */ };
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
const canvas = renderer.domElement;
const region = new TerrainRegion({ textures });
let isPainting = false;
canvas.addEventListener('pointerdown', (event) => {
const hit = getTerrainHitFromPointer(
region, canvas, camera, raycaster, pointer, event.clientX, event.clientY,
);
if (!hit) return;
isPainting = true;
region.beginStroke();
region.paintAt(hit.point, { temporaryLower: event.shiftKey });
});
canvas.addEventListener('pointermove', (event) => {
if (!isPainting) return;
const hit = getTerrainHitFromPointer(
region, canvas, camera, raycaster, pointer, event.clientX, event.clientY,
);
if (hit) region.paintAt(hit.point, { temporaryLower: event.shiftKey });
});
canvas.addEventListener('pointerup', () => {
region.endStroke();
isPainting = false;
});
import { TerrainRegion } from 'metaverse-terrain';
const region = new TerrainRegion({
textures,
regionSize: 256,
seed: 29,
waterLevel: 28,
textureDensity: 10,
hexTileRate: 0.5,
});
region.setBrushMode('raise');
region.setBrushRadius(8);
region.setWaterLevel(24);
region.downloadHeightmap();
import { bindTerrainPainting, bindTextureDrop } from 'metaverse-terrain';
bindTerrainPainting(region, { domElement: canvas, camera, raycaster, pointer });
bindTextureDrop(region); // needs [data-texture-drop="grass"] elements in HTML
import * as THREE from 'three';
import { TerrainRegion } from 'metaverse-terrain';
import sand from 'metaverse-terrain/texture/terrain-sand.png';
import grass from 'metaverse-terrain/texture/terrain-grass.png';
import rock from 'metaverse-terrain/texture/terrain-rock.png';
import snow from 'metaverse-terrain/texture/terrain-snow.png';
import water from 'metaverse-terrain/texture/terrain-water.png';
const region = new TerrainRegion({
textures: { sand, grass, rock, snow, water },
seed: 42,
});
scene.add(region.group);
| Export | Purpose |
|---|---|
TerrainRegion |
Terrain mesh, water, heightmap, brush state |
getTerrainHitFromPointer |
Screen coords → terrain intersection |
bindTerrainPainting |
Wire pointer events to brush painting |
bindTextureDrop |
Drag-and-drop images onto texture swatches |
TERRAIN_TEXTURE_LAYERS |
['sand', 'grass', 'rock', 'snow', 'water'] |
DEFAULT_TEXTURE_HEIGHTS |
Default height blend thresholds |
TerrainRegion methods
| Method | Description |
|---|---|
raycast(raycaster) |
Intersect terrain mesh |
paintAt(point, options?) |
Apply brush and emit onHeightmapChange |
setBrushMode / setBrushRadius / setBrushStrength |
Brush settings |
setWaterLevel / setWaterEnabled |
Water plane |
setTextureDensity / setHexTileRate / setHexTileContrast |
Shader tiling |
setTextureHeights |
Sand/grass/rock/snow height bands |
setTerrainTexture(layer, source) |
Replace a texture at runtime |
randomize(seed?) / level(height?) |
Regenerate or flatten heightmap |
drawHeightmapPreview(canvas) / downloadHeightmap() |
Export heightmap |
update(elapsedTime) |
Animate water |
dispose() |
Free GPU resources |
TypeScript types ship in index.d.ts.
git clone https://github.com/richardanaya/metaverse-terrain.git
cd metaverse-terrain
python3 -m http.server 8080
MIT — Richard Anaya. See LICENSE.
Reference textures: MIT — Richard Anaya. See ASSET_LICENSES.md.
Hex-tiling based on Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, JCGT Vol. 11 No. 2, 2022.