/ Work / procedural-terrain

Procedural terrain generation in Unity

Editor tool that generates terrains with Midpoint Displacement, Diamond-Square and fractal noise, erodes and populates them, and renders them with triplanar, reactive snow and water shaders.

Node
procedural-terrain
Runs on
GPU
Stack
  • Unity
  • C#
  • HLSL
  • Shader Graph
Media pending

Generating a terrain from scratch with the editor tool: hybrid multifractal FBM, thermal erosion and object spawning, ending on the shaded scene.

Context

Procedural terrain generation builds a landscape from an algorithm instead of sculpting it by hand. A heightmap is filled by a subdivision or noise function, reshaped by simulated erosion, and then dressed with objects and materials. Changing a seed or a parameter produces a new terrain in seconds, which is why these techniques sit under most open-world and simulation pipelines.

This project covers that whole chain inside Unity: a custom editor tool that generates, erodes and populates terrains, and a set of shaders that render them with triplanar texturing, snow that reacts to the scene, and animated water. Everything runs in a playable scene where the terrain can be explored and hit.

Overview

The project is organised as a pipeline of five stages:

  1. Generate a heightmap with Midpoint Displacement, Diamond-Square or fractal Brownian motion.
  2. Erode it with thermal erosion.
  3. Populate it with object instances.
  4. Shade it with triplanar mapping, orientation-based snow and a water surface.
  5. Interact with it from a first-person controller that changes the amount of snow and fires projectiles at it.

Generation, erosion and spawning live in one editor tool in the Unity inspector. The terrain shaders are hand-written HLSL inside Custom Function nodes (TriplanarMapping, TriplanarSnowBlend, SnowVertexDisplacement) wired together in Shader Graph, while the water is built entirely from Shader Graph nodes.

The terrain editor tool

All generation happens from a single panel in the Unity inspector, split into six blocks from top to bottom:

  1. Terrain Configuration (Global): the number of divisions N, the noise amplitude A, the amplitude falloff H and the seed shared by the subdivision algorithms.
  2. Midpoint Terrain: a target terrain and a button that generates it with Midpoint Displacement.
  3. Diamond-Square Terrain: the same for Diamond-Square.
  4. Fractal Brownian Motion: amplitude, frequency, gain, frequency delta, octaves, its own seed and the Hybrid Multifractal toggle.
  5. Thermal Erosion: a target terrain, the erosion factor c, the threshold T and the resulting height range.
  6. Add 3D Elements: a target terrain, the prefab, the number of instances and a scale range.

Each technique points at its own Terrain, so the algorithms can be compared side by side on separate terrains.

The editor tool in the Unity inspector: global configuration, Midpoint, Diamond-Square, fractal Brownian motion, thermal erosion and object spawning.

Midpoint Displacement and Diamond-Square

Both algorithms build the heightmap by recursive subdivision. They start from the corners of the grid, fill in new points from the average of their neighbours plus a random offset, and repeat on ever smaller cells. N sets how many subdivision levels run, A sets the size of the first offsets, and H reduces that amplitude at every level, which decides how rough or smooth the result looks.

Midpoint Displacement fills the midpoints along the edges of each cell. Diamond-Square alternates two steps: the diamond step sets the centre of each square from its four corners, and the square step sets the edge midpoints from the surrounding diamond. That alternation spreads the random offsets more evenly and reduces the axis-aligned creases that plain midpoint subdivision tends to leave.

Fractal Brownian motion and hybrid multifractal

Fractal Brownian motion sums several layers, or octaves, of noise. Each octave multiplies the frequency by the frequency delta and the amplitude by the amplitude delta, so every layer adds finer and fainter detail. The capture uses 8 octaves, a base amplitude of 0.5 and frequency of 1, an amplitude delta (gain) of 0.5 and a frequency delta of 2.

With Hybrid Multifractal enabled, each octave is weighted by the value of the ones before it. Low, smooth areas stay smooth and detail concentrates on higher ground, which gives valleys and ridges a different character instead of a uniformly noisy surface.

Three generated terrains in the same scene, left to right: Midpoint Displacement, Diamond-Square and hybrid multifractal FBM.
Media pending

Changing octaves, gain and frequency in the inspector and regenerating the FBM terrain.

Thermal erosion

Thermal erosion simulates material sliding down slopes that are too steep to hold it. For each cell, the height difference with its lower neighbours is compared to a threshold T; where it is exceeded, a fraction c of the excess moves downhill. Repeating the pass softens sharp peaks and piles material at the foot of slopes. The capture uses c = 0.5 and T = 0.01, and the tool reports the minimum and maximum height of the terrain after erosion (0.0108 and 1.0).

Media pending

The same FBM terrain before and after applying thermal erosion.

Populating the terrain

The last section of the tool scatters instances of a prefab over a target terrain. It takes the prefab, the number of instances and a minimum and maximum scale, so each object gets its own size within that range (stone_4, 100 instances and a 0.8 to 1.5 scale range in the capture). [PENDING: how positions and rotations are chosen, for example random over the surface or aligned to the terrain normal]

Media pending

Spawning object instances over the eroded terrain from the editor tool.

Triplanar mapping

Triplanar mapping is a texturing technique that solves the classic problem of applying textures to terrains with steep slopes. With normal UVs, the texture stretches on vertical areas because the coordinates are meant for horizontal surfaces. Triplanar projects the texture from three orthogonal directions (X, Y, Z) and blends the results based on the surface normal, eliminating stretching regardless of orientation. It’s the foundation of almost any modern game terrain with cliffs or non-flat geometry.

The triplanar shader samples the same texture three times, once per each orthogonal plane (XY, XZ, YZ), using world coordinates as UVs. It then blends the three samples using the surface normal as weight: the axis whose normal component is largest dominates the result.

On a horizontal surface (normal pointing up), the XZ projection dominates and you see the texture from the Y axis. On a vertical cliff (normal pointing sideways), the lateral projection wins and the texture is projected correctly, without stretching. The transition between projections is continuous because the blending uses the squared components of the normal, which gives a smooth weight with no discontinuities.

The terrain material in Shader Graph: TriplanarMapping, TriplanarSnowBlend and SnowVertexDisplacement as Custom Function nodes feeding the vertex and fragment stages.
Media pending

Terrain slope with standard UV texturing, showing the stretched texture.

The same slope with triplanar mapping, clean across all geometry.

Snow accumulation by surface orientation

Snow doesn’t cover the entire terrain uniformly. In reality it accumulates on horizontal surfaces and barely appears on vertical ones. The shader replicates this by using the Y component of the normal as a mask: surfaces with a normal pointing upward receive snow, sideways surfaces don’t.

The result is that snow appears on peaks and flatlands, respects cliffs by leaving them with their original rock texture, and creates natural transitions on intermediate slopes. No extra geometry, no manually painted masks.

Media pending

Capture of the snow-covered terrain showing how vertical walls stay rocky and horizontal surfaces stay snowy.

Real-time control from the player

The shader exposes a _SnowStrenght parameter that controls how much snow there is in the scene. The FPSController in C# adjusts it with the keys X (more snow) and Z (less), and the change is reflected immediately on the terrain material. _MaxThickness is also adjusted, controlling the physical vertex displacement, so raising snow amount doesn’t just change color — the actual geometry grows upward.

This is implemented in SnowVertexDisplacement, a shader that displaces the terrain mesh vertices upward according to the _MaxThickness value, attenuated by the same surface orientation mask. Snow gains physical volume, not just a texture change.

Media pending

The terrain with snow varying in real time as X/Z are pressed, showing how the geometry grows and shrinks.

Reaction to physical impacts

The most specific part of the system is local deformation on impact. When the player fires a ball with left click (FPSController.Shoot()), the ball carries a BulletBounce component that detects collision against the terrain. On impact:

  1. The exact collision point is obtained with collision.contacts[0].point.
  2. The point height is adjusted by adding the current _MaxThickness, because snow has volume and the physical collision point sits at the base ground, not at the visible snow surface.
  3. Four parameters are passed to the shader: _HitPos (impact position), _HitTime (impact time), _BounceRadius (affected area radius), and _BounceStrength (bounce intensity).

SnowVertexDisplacement uses these parameters to modify the local displacement with an elastic bounce: vertices within the impact radius sink and rise back in a damped oscillation, leaving no permanent crater. It’s the same effect you see in Fortnite when you shoot at a structure: a brief, visible deformation that recovers. Simplified but effective, without real physics simulation.

The detail of adding _MaxThickness to the impact point Y is what makes the deformation feel visually correct. Without that adjustment, with thick snow enabled, bounces would happen below the visible surface.

Media pending

The impact system: multiple balls fired in succession, showing how the snow bounces locally where they hit.

Water shader

The water surface is built in Shader Graph, with its motion in the vertex stage and its pattern in the fragment stage.

In the vertex stage, the object-space position is split and its X and Z components are offset by time multiplied by WaveSpeed. That scrolling coordinate feeds a Simple Noise node scaled by NoiseScale; the noise is multiplied by WaveStrength, offset by Height, and written back as the vertex Y. The surface rises and falls in slowly travelling waves.

In the fragment stage, a Voronoi node tiled 10 by 10, with a cell density of 3 and its angle offset driven by time, produces cells that shift continuously. Raising the result to the power of 5 keeps only the brightest cell borders, and adding them to a blue base colour draws bright, moving lines across the water. Smoothness is set to 0.5 and transparency comes from an Opacity property.

The water shader in Shader Graph: noise-driven vertex waves and a Voronoi pattern sharpened with Power over a blue base colour.
Media pending

The water surface in the scene: travelling waves and moving Voronoi lines.

Rest of the scene

  • Tree with wind: shader (this one in Shader Graph) that simulates leaf deformation from wind, using temporal noise.

Everything integrates into a playable scene with a custom FPS controller (FPSController, MouseLookFPS), where you can move, shoot, and modify the terrain in real time.