metaverse-terrain

image

metaverse-terrain

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.

Install

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).


Using with jsDelivr

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.

Textures

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`,
};

Minimal example

<!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>

Painting

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;
});

Configuration

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();

Optional DOM helpers

import { bindTerrainPainting, bindTextureDrop } from 'metaverse-terrain';

bindTerrainPainting(region, { domElement: canvas, camera, raycaster, pointer });
bindTextureDrop(region); // needs [data-texture-drop="grass"] elements in HTML

Using with npm

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);

API

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.

Demo apps

git clone https://github.com/richardanaya/metaverse-terrain.git
cd metaverse-terrain
python3 -m http.server 8080

License

MIT — Richard Anaya. See LICENSE.

Reference textures: MIT — Richard Anaya. See ASSET_LICENSES.md.

References

Hex-tiling based on Morten S. Mikkelsen, Practical Real-Time Hex-Tiling, JCGT Vol. 11 No. 2, 2022.