metaverse-terrain

image

metaverse-terrain

Three.js terrain library with PBR texture blending, animated water, and heightmap brush editing.

TerrainRegion owns the terrain mesh, TSL node materials, 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. The runtime uses Three.js Shading Language (TSL), so host apps must use WebGPURenderer from three/webgpu (with Three’s WebGL fallback where available). 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",
          "three/webgpu": "https://cdn.jsdelivr.net/npm/three/build/three.webgpu.js",
          "three/tsl": "https://cdn.jsdelivr.net/npm/three/build/three.tsl.js",
          "metaverse-terrain": "https://cdn.jsdelivr.net/npm/metaverse-terrain/index.js"
        }
      }
    </script>
  </head>
  <body>
    <script type="module">
      import * as THREE from 'three/webgpu';
      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`,
};

PBR textures (optional)

PBR is optional — albedo-only terrain still works. To enable physically based shading, provide per-layer normal, metallic, roughness, and AO maps (or a pre-packed MRAO texture). Reference maps ship with the package:

const pbrTextures = {
  sand: {
    metal: `${TEXTURE_BASE}/terrain-sand_metal.png`,
    roughness: `${TEXTURE_BASE}/terrain-sand_roughness.png`,
    normal: `${TEXTURE_BASE}/terrain-sand_normal.png`,
    ao: `${TEXTURE_BASE}/terrain-sand_ao.png`,
  },
  grass: {
    metal: `${TEXTURE_BASE}/terrain-grass_metal.png`,
    roughness: `${TEXTURE_BASE}/terrain-grass_roughness.png`,
    normal: `${TEXTURE_BASE}/terrain-grass_normal.png`,
    ao: `${TEXTURE_BASE}/terrain-grass_ao.png`,
  },
  rock: {
    metal: `${TEXTURE_BASE}/terrain-rock_metal.png`,
    roughness: `${TEXTURE_BASE}/terrain-rock_roughness.png`,
    normal: `${TEXTURE_BASE}/terrain-rock_normal.png`,
    ao: `${TEXTURE_BASE}/terrain-rock_ao.png`,
  },
  snow: {
    metal: `${TEXTURE_BASE}/terrain-snow_metal.png`,
    roughness: `${TEXTURE_BASE}/terrain-snow_roughness.png`,
    normal: `${TEXTURE_BASE}/terrain-snow_normal.png`,
    ao: `${TEXTURE_BASE}/terrain-snow_ao.png`,
  },
  water: {
    metal: `${TEXTURE_BASE}/terrain-water_metal.png`,
    roughness: `${TEXTURE_BASE}/terrain-water_roughness.png`,
    normal: `${TEXTURE_BASE}/terrain-water_normal.png`,
    ao: `${TEXTURE_BASE}/terrain-water_ao.png`,
  },
};

loadPBRTextureSet() loads the maps and packs metallic, roughness, and AO into a single MRAO texture per layer:

import { TerrainRegion, loadPBRTextureSet } from 'metaverse-terrain';

const packedPBR = await loadPBRTextureSet(pbrTextures);

const region = new TerrainRegion({
  textures,
  pbrTextures: packedPBR,
  normalStrength: 1.0,
  terrainAOIntensity: 1.0,
});

For best results with WebGPURenderer, set scene.environment from an equirectangular HDRI (texture.mapping = THREE.EquirectangularReflectionMapping). The bundled examples load a Venice sunset HDR via Three.js RGBELoader. PBR terrain uses MeshStandardNodeMaterial; water uses a TSL MeshBasicNodeMaterial with fresnel, specular/glint, wave, foam, and normal detail. Disable PBR water at runtime with region.setPBREnabled(false) on low-end devices.

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",
          "three/webgpu": "https://cdn.jsdelivr.net/npm/three/build/three.webgpu.js",
          "three/tsl": "https://cdn.jsdelivr.net/npm/three/build/three.tsl.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/webgpu';
      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.WebGPURenderer({ antialias: true });
      await renderer.init();
      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,
});

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/webgpu';
import { TerrainRegion, loadPBRTextureSet } 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';
import sandMetal from 'metaverse-terrain/texture/terrain-sand_metal.png';
import sandRoughness from 'metaverse-terrain/texture/terrain-sand_roughness.png';
import sandNormal from 'metaverse-terrain/texture/terrain-sand_normal.png';
import sandAO from 'metaverse-terrain/texture/terrain-sand_ao.png';
// ... same pattern for grass, rock, snow, water

const pbrTextures = await loadPBRTextureSet({
  sand: { metal: sandMetal, roughness: sandRoughness, normal: sandNormal, ao: sandAO },
  // grass, rock, snow, water ...
});

const region = new TerrainRegion({
  textures: { sand, grass, rock, snow, water },
  pbrTextures,
  seed: 42,
});

scene.add(region.group);

Omit pbrTextures for albedo-only rendering.


API

Export Purpose
TerrainRegion Terrain mesh, water, heightmap, brush state
loadPBRTextureSet Load and pack PBR maps (metal, roughness, normal, AO → MRAO)
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']
PBR_CHANNELS ['metal', 'roughness', 'normal', 'ao']
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 Texture tiling density
setTextureHeights Sand/grass/rock/snow height bands
setNormalStrength / setTerrainAOIntensity PBR normal and AO intensity
setPBREnabled / setWaterIOR PBR water toggle and index of refraction
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.