THREE.Terrain: procedural heightmaps and scattered foliage for Three.js
A procedural terrain generation engine for use with the Three.js 3D graphics library for the web.
At a glance
- What is it?
- THREE.Terrain is a JavaScript library that generates procedural terrain meshes for Three.js, with a menu of noise and fractal generators, elevation based texture blending, grass and mesh scattering, and a seeded random function so a landscape can be reproduced exactly. It is MIT licensed and published as three.terrain.js on npm.
- Who is it for?
- THREE.Terrain suits a web developer who needs a landscape in a Three.js scene quickly, with a generator choice broad enough to pick a shape rather than settle for noise, and who wants the same seed to reproduce it later. It does not suit a project that needs terrain streamed in at scale, since the documented approach builds the mesh in one pass, and it is not a physics or navigation framework, because it produces geometry and decoration rather than collision data.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 8 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A heightmap generator plus a decoration layer for Three.js
THREE.Terrain is described as a procedural terrain generation engine for use with the Three.js 3D graphics library for the web. The package description is more specific: it extends the Three.js web based 3D graphics framework to support generating random terrains and rendering terrain from predetermined heightmaps.
So there are two directions of travel, and both are supported. You can generate a landscape from a procedural function, or you can hand it an image of a heightmap and it builds the mesh from that. The README says the library can also export a generated terrain back to a heightmap image, which closes the loop and lets a generated landscape be edited in an image editor and re-imported.
That export path matters more than it first appears. The documentation says the easiest way to generate a static heightmap is to use the demo and save the heightmap shown in the upper-left corner, and that the demo even has a sculpt tool for manual modification. If you want custom manipulations before export, you export the heightmap yourself through TerrainNS.toHeightmap, which returns a canvas you can append to the document body to view or right-click to save as a PNG.
The audience is web game and visualisation developers who want terrain in a scene without writing the generation maths. The topic tags give the vocabulary the library works in: diamond-square-algorithm, generative, heightmap, noise, procedural, materials and scene.
The immediate alternative is writing your own heightmap function and feeding it into a PlaneGeometry with displaced vertices, which is a known exercise and about twenty lines. What the library adds is the menu of generators, the texture blending, the scattering and the seeded reproducibility, and the convenience of a single Terrain() call producing a ready scene graph node.
The generator menu, from Diamond-Square to Worley noise
The default set of procedural generation methods is broad, and the README lists them: Cosine, Diamond-Square, which it describes as a better version of Midpoint Displacement, fault lines, feature picking, particle deposition, Perlin and Simplex noise, value noise, Weierstrass functions, Worley noise, also known as Cell or Voronoi noise, Brownian motion, arbitrary curves, and various combinations of those.
That list covers both the fractal families people expect from terrain work and the structural ones people forget. Perlin, Simplex and value noise give the smooth rolling hills. Diamond-Square and Weierstrass give the self-similar detail that reads as mountains. Worley noise gives the cellular pattern used for cracks and plateaus. Fault lines and feature picking are the ones that produce deliberate shapes rather than texture, which is what you want when the terrain needs to read as a place rather than as noise.
The functions live on the TerrainNS namespace, which the README describes as holding generators, filters and other helpers, with DiamondSquare, Smooth and ScatterMeshes given as examples. The default export, Terrain, is what actually builds the mesh.
Parameters are entirely optional. The README states that all parameters are optional and thoroughly documented in the source code, and points to the src/ directory on the gh-pages branch for that documentation. That is a real limitation of the README itself, which shows no parameter reference at all, but the source is described as thorough and there is a demo to play with.
A typical call shows what the options look like in practice.
var xS = 63, yS = 63;
var terrainScene = Terrain({
easing: TerrainNS.Linear,
frequency: 2.5,
heightmap: TerrainNS.DiamondSquare,
material: new THREE.MeshBasicMaterial({color: 0x5566aa}),
maxHeight: 100,
minHeight: -100,
steps: 1,
xSegments: xS,
xSize: 1024,
ySegments: yS,
ySize: 1024,
});
scene.add(terrainScene);The geometry parameters are xSegments and ySegments for the resolution, xSize and ySize for the extent, minHeight and maxHeight for the elevation range, and steps with an easing function that controls how the interpolation between steps is shaped. The README notes that terrainScene.children[0] is the most detailed version of the terrain mesh, which is the handle you need for anything that operates on the geometry.
Seeded randomness is what makes a landscape reproducible
createSeededRandom takes a seed and returns a function similar to Math.random() which returns a deterministic sequence of random values based on the seed, and it can be passed as an option to other methods.
var terrainScene = Terrain({
heightmap: TerrainNS.PerlinDiamond,
random: createSeededRandom(12345),
// ...other terrain options
});
var decoScene = TerrainNS.ScatterMeshes(terrainScene.children[0].geometry, {
mesh: new THREE.Mesh(new THREE.CylinderGeometry(2, 2, 12, 6)),
random: createSeededRandom(67890),
});Two separate seeds in that example, and the reason is worth noting. The terrain shape and the decoration placement are independent streams, so you can change the scatter density or the foliage type without regenerating a different landscape. In practice that is the difference between a scene you can iterate on and one where every change reshuffles everything.
The README connects this to the export workflow. Since the library supports seeded randomness, you may not need to export and import the whole terrain, because you might just need the seed you used to generate the terrain of interest. That is a much better workflow than shipping a PNG: a seed is a few bytes, a heightmap is a megabyte, and the seed regenerates exactly.
The same option appears on the scatter calls, which use it in place of Math.random(). The default in the earlier example is randomness: Math.random, so an unseeded scene is different every time it loads, which is what you want for a demo and what you do not want for a screenshot or a test.
Elevation based texture blending without writing a shader
The generateBlendedMaterial utility produces a shading texture that applies to terrain automatically based on elevation or biome, slope and location. The README is careful to note that other than blending textures together it is the same as a MeshLambertMaterial, so lighting behaves the way you expect from a standard Three.js shading model rather than something bespoke.
The mechanism is a list of textures with levels describing where each blends in and out.
var material = generateBlendedMaterial([
// The first texture is the base; other textures are blended in on top.
{ texture: t1 },
// Start blending in at height -80; opaque between -35 and 20; blend out by 50
{ texture: t2, levels: [-80, -35, 20, 50] },
]);Four levels per texture means a fade-in height, a fully opaque band, and a fade-out height. The comment in the example spells out the semantics, with a texture starting to blend in at height -80, opaque between -35 and 20, and blended out by 50.
There is also a glsl property that allows specifying a GLSL expression for texture blending, which is the escape hatch for cases the level system cannot express. That matters because height based blending alone cannot distinguish a snow line on a flat plateau from a snow line on a steep face, and slope aware blending is exactly the case where you need to write the expression yourself.
The function is available both as a named export and as TerrainNS.generateBlendedMaterial, and the same generateBlendedMaterial layer is what the grass scattering uses, since the scatter spread function in the grass example tests whether a vertex falls in the fully grass-covered part of that layer.
Scattering foliage and instanced grass
Decoration is where the library goes beyond a bare mesh, and it works in two stages.
The first is TerrainNS.ScatterMeshes, which distributes copies of a mesh across the terrain geometry. The example uses a cylinder as foliage, with a spread value of 0.02 and randomness set to Math.random.
var geo = terrainScene.children[0].geometry;
var decoScene = TerrainNS.ScatterMeshes(geo, {
mesh: new THREE.Mesh(new THREE.CylinderGeometry(2, 2, 12, 6)),
w: xS,
h: yS,
spread: 0.02,
randomness: Math.random,
});
terrainScene.add(decoScene);The second is grass, which is instanced rather than scattered as individual meshes. createGrass creates grass blades for terrain cover, with options and helpers supporting tints, clustering, density and swaying in the wind.
var grass = createGrass({
bladeCount: 20,
height: 9,
minimumLight: 0.58,
width: 7,
});
var grassScene = TerrainNS.ScatterGrass(geo, {
instanced: true,
mesh: grass,
spread: function(vertex, faceIndex, faceNormal) {
return grassMeshWeight(vertex.z, faceNormal.angleTo(new THREE.Vector3(0, 0, 1))) > 0;
},
maxTilt: 0,
randomRotationAxis: 'y',
positionJitter: 2.5,
tintRange: { min: 0x496b34, max: 0x78934a },
});
terrainScene.add(grassScene);The spread function is the interesting parameter. Instead of a number, it can be a callback receiving the vertex, face index and face normal, so you decide per position whether to place a blade. Here it checks grassMeshWeight against the angle between the face normal and straight up, so grass only appears on upward facing surfaces covered by the blended texture layer.
Two update calls have to happen in the render loop, and the comment in the README makes that explicit: update the wind and compact distant instances before drawing.
updateGrass(grass, elapsedSeconds);
updateGrassLOD(grassScene, camera, 1200);The second call takes the camera and a distance, and compacts instances beyond that range. That is the library handling level of detail for the grass, and it is a thing you would otherwise have to build yourself if you scatter a few hundred thousand blades.
Packaging, licensing and the odd shape of the repository
The package is published on npm as three.terrain.js, installed with npm install three.terrain.js and imported as an ES module. Three.js r160 or later is required, and the manifest states this as a peer dependency of three >=0.160.0, which is the correct way to declare it: the library uses your Three.js instance rather than bundling its own.
The manifest also ships both module formats. The type is module, main points at a UMD CommonJS build, and the exports map resolves import to the ESM bundle and require to the CommonJS one, so the library works in a bundler and in a Node script that uses require.
The licence field says MIT and the repository root has LICENSE.txt, which is a permissive licence with attribution requirements and no copyleft obligations. This is not legal advice, but for a library you embed in a web application the terms are conventional.
The repository layout has some quirks worth knowing. The default branch is gh-pages, which is unusual, and the same branch is where the hosted demo and the documentation source live. There is a dist/ directory checked in alongside src/, a vendor/ directory, an index.html at the root, a vite-client.js, separate vite configs for the demo and the client, a test/ directory, a statistics/ directory, and a scripts/ directory holding the release and screenshot tooling. The npm scripts are npm start for the Vite dev server, npm run build, npm test which uses the node built-in test runner, npm run release, and npm run vendor which runs two scripts to vendor dependencies and build something called eztree.
Two things stand out for an adopter. There is an AGENTS.md file at the root and a SKILL.md file, plus an llms.txt, and the README points at SKILL.md explicitly as a way to help AI agents use the library. That is a deliberate move: the library ships machine readable guidance for coding assistants, which is worth knowing about if you work this way. And the demo directory is substantial, with separate files for camera, world, landscape, flowers, sculpt, settings and UI, so the demo is a real application rather than a minimal page, and reading those files is the fastest way to understand what the library can do.
Editorial conclusion
THREE.Terrain suits a web developer who needs a landscape in a Three.js scene quickly, with a generator choice broad enough to pick a shape rather than settle for noise, and who wants the same seed to reproduce it later. It does not suit a project that needs terrain streamed in at scale, since the documented approach builds the mesh in one pass, and it is not a physics or navigation framework, because it produces geometry and decoration rather than collision data. Before adopting it, check your Three.js version against the r160 or later requirement, read the parameter documentation in the src/ directory since every option is optional and undocumented in the README, and look at the demo's sculpt tool if you need hand edited terrain rather than purely generated landscape.
Frequently asked questions
How do I install THREE.Terrain?
Install it from npm with npm install three.terrain.js and import it as an ES module. You also need a compatible version of three.js, r160 or later, which the package declares as a peer dependency of three >=0.160.0.
Which terrain generation algorithms does THREE.Terrain provide?
The documentation lists Cosine, Diamond-Square, fault lines, feature picking, particle deposition, Perlin and Simplex noise, value noise, Weierstrass functions, Worley noise, Brownian motion, arbitrary curves, and combinations of those. The generators are exposed on the TerrainNS namespace alongside filters and helpers.
Can I reproduce the same terrain on every page load?
Yes. createSeededRandom takes a seed and returns a Math.random-like function that produces a deterministic sequence, and it can be passed as the random option to Terrain and to the scatter functions. The documentation also notes you may only need to keep the seed rather than exporting the whole heightmap.
How do I texture a terrain by elevation in THREE.Terrain?
generateBlendedMaterial takes an array of textures with levels that describe where each blends in and out, so a texture can start fading in at a height, be fully opaque over a band, and fade out higher up. A glsl property allows specifying a GLSL expression for blending when the level system is not enough.
Does THREE.Terrain work with a heightmap image instead of procedural generation?
Yes. You create a terrain as usual and pass the loaded heightmap image, or a canvas containing one, to the heightmap option instead of a procedural generation function. The library also exports a terrain back to a heightmap canvas through TerrainNS.toHeightmap.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/icecreamyou-three-terrain)Was this useful?