# Heightfield Ray Marching Terrain Rendering ## Use Cases - Procedural generation of natural landscapes (mountains, canyons, dunes, etc.) in ShaderToy / Fragment Shaders - Complete 3D terrain flythrough scenes in a single pixel shader, without geometry - Cinematic aerial perspective, soft shadows, and layered material effects ## Core Principles Rendering pipeline: height field definition → ray marching intersection → normals & materials → lighting → atmospheric effects - **FBM**: `f(p) = Σ (aⁿ × noise(2ⁿ × R × p))`, a=0.5, R=rotation matrix, 2ⁿ=frequency doubling - **Derivative erosion**: `f(p) = Σ (aⁿ × noise(p) / (1 + dot(d,d)))`, d=accumulated gradient, suppresses detail on steep slopes - **Adaptive step size**: `step = factor × (ray.y - terrain_height)` ## Implementation Steps 1. **Noise & hash** — sin-free hash + Value Noise with analytic derivatives (`noised` returns value + partial derivatives) 2. **FBM terrain** — derivative erosion FBM, `mat2(0.8,-0.6,0.6,0.8)` per-layer rotation to eliminate banding; LOD tiers (L=3/M=9/H=16 octaves) 3. **Ray marching** — upper bound clipping + adaptive step `STEP_FACTOR * h` + distance-adaptive precision `abs(h) < 0.0015*t` 4. **Normals** — finite differences, epsilon increases with distance to avoid distant aliasing, using high-precision `terrainH` 5. **Soft shadows** — march toward sun, track `min(k*h/t)` to estimate penumbra 6. **Materials** — blend rock/grass/snow/sand by height + slope + noise 7. **Lighting** — Lambert diffuse + hemisphere ambient + backlight + Fresnel rim light + Blinn-Phong specular 8. **Atmospheric fog** — wavelength-dependent attenuation `exp(-t*k*vec3(1,1.5,4))` + sun scatter fog color 9. **Sky** — zenith-to-horizon gradient + sun disk/halo 10. **Camera** — Look-At matrix + path-following flight, height tracks terrain ## Complete Code Template ```glsl // ===================================================== // Heightfield Terrain Rendering - Complete Template // ===================================================== #define TERRAIN_OCTAVES 9 // FBM octave count (3~16) #define TERRAIN_SCALE 0.003 // Terrain spatial frequency #define TERRAIN_HEIGHT 120.0 // Terrain elevation scale #define MAX_STEPS 300 // Ray march step count (80~400) #define MAX_DIST 5000.0 // Maximum render distance #define STEP_FACTOR 0.4 // March conservative factor (0.3~0.8) #define SHADOW_STEPS 80 // Shadow step count (32~128) #define SHADOW_K 16.0 // Penumbra softness (8~64) #define FOG_DENSITY 0.00025 // Fog density #define SNOW_HEIGHT 80.0 // Snow line height #define CAM_ALTITUDE 20.0 // Camera height above ground #define SUN_DIR normalize(vec3(0.8, 0.4, -0.6)) #define SUN_COL vec3(8.0, 5.0, 3.0) #define SKY_COL vec3(0.5, 0.7, 1.0) // ---- Hash & Noise ---- float hash(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 19.19); return fract((p3.x + p3.y) * p3.z); } vec3 noised(in vec2 p) { vec2 i = floor(p); vec2 f = fract(p); vec2 u = f * f * (3.0 - 2.0 * f); vec2 du = 6.0 * f * (1.0 - f); float a = hash(i + vec2(0.0, 0.0)); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0)); float v = a + (b - a) * u.x + (c - a) * u.y + (a - b - c + d) * u.x * u.y; vec2 g = du * (vec2(b - a, c - a) + (a - b - c + d) * u.yx); return vec3(v, g); } float noise(in vec2 p) { return noised(p).x; } // ---- FBM Terrain (derivative erosion) + LOD ---- const mat2 m2 = mat2(0.8, -0.6, 0.6, 0.8); float terrainFBM(in vec2 p, int octaves) { p *= TERRAIN_SCALE; float a = 0.0, b = 1.0; vec2 d = vec2(0.0); for (int i = 0; i < 16; i++) { if (i >= octaves) break; vec3 n = noised(p); d += n.yz; a += b * n.x / (1.0 + dot(d, d)); b *= 0.5; p = m2 * p * 2.0; } return a * TERRAIN_HEIGHT; } float terrainL(vec2 p) { return terrainFBM(p, 3); } float terrainM(vec2 p) { return terrainFBM(p, TERRAIN_OCTAVES); } float terrainH(vec2 p) { return terrainFBM(p, 16); } // ---- Ray Marching ---- float raymarch(in vec3 ro, in vec3 rd) { float t = 0.0; if (ro.y > TERRAIN_HEIGHT && rd.y >= 0.0) return -1.0; if (ro.y > TERRAIN_HEIGHT) t = (ro.y - TERRAIN_HEIGHT) / (-rd.y); for (int i = 0; i < MAX_STEPS; i++) { vec3 pos = ro + t * rd; float h = pos.y - terrainM(pos.xz); if (abs(h) < 0.0015 * t) break; if (t > MAX_DIST) return -1.0; t += STEP_FACTOR * h; } return t; } // ---- Normals ---- vec3 calcNormal(in vec3 pos, float t) { float eps = 0.02 + 0.00005 * t * t; float hC = terrainH(pos.xz); float hR = terrainH(pos.xz + vec2(eps, 0.0)); float hU = terrainH(pos.xz + vec2(0.0, eps)); return normalize(vec3(hC - hR, eps, hC - hU)); } // ---- Soft Shadows ---- float calcShadow(in vec3 pos, in vec3 sunDir) { float res = 1.0, t = 1.0; for (int i = 0; i < SHADOW_STEPS; i++) { vec3 p = pos + t * sunDir; float h = p.y - terrainM(p.xz); if (h < 0.001) return 0.0; res = min(res, SHADOW_K * h / t); t += clamp(h, 2.0, 100.0); } return clamp(res, 0.0, 1.0); } // ---- Materials ---- vec3 getMaterial(in vec3 pos, in vec3 nor) { float slope = nor.y, h = pos.y; float nz = noise(pos.xz * 0.04) * noise(pos.xz * 0.005); vec3 rock = vec3(0.10, 0.09, 0.08); vec3 grass = mix(vec3(0.10, 0.08, 0.04), vec3(0.05, 0.09, 0.02), nz); vec3 snow = vec3(0.62, 0.65, 0.70); vec3 sand = vec3(0.50, 0.45, 0.35); vec3 col = rock; col = mix(col, grass, smoothstep(0.5, 0.8, slope)); float snowMask = smoothstep(SNOW_HEIGHT - 20.0 * nz, SNOW_HEIGHT + 10.0, h) * smoothstep(0.3, 0.7, slope); col = mix(col, snow, snowMask); float beachMask = smoothstep(2.5, 0.0, h) * smoothstep(0.5, 0.9, slope); col = mix(col, sand, beachMask); return col; } // ---- Lighting ---- vec3 calcLighting(in vec3 pos, in vec3 nor, in vec3 rd, float shadow) { float dif = clamp(dot(nor, SUN_DIR), 0.0, 1.0); float amb = 0.5 + 0.5 * nor.y; vec3 backDir = normalize(vec3(-SUN_DIR.x, 0.0, -SUN_DIR.z)); float bac = clamp(0.2 + 0.8 * dot(nor, backDir), 0.0, 1.0); float fre = pow(clamp(1.0 + dot(rd, nor), 0.0, 1.0), 2.0); vec3 hal = normalize(SUN_DIR - rd); float spe = pow(clamp(dot(nor, hal), 0.0, 1.0), 16.0) * (0.04 + 0.96 * pow(1.0 + dot(hal, rd), 5.0)); vec3 lin = vec3(0.0); lin += dif * shadow * SUN_COL * 0.1; lin += amb * SKY_COL * 0.2; lin += bac * vec3(0.15, 0.05, 0.04); lin += fre * SKY_COL * 0.3; lin += spe * shadow * SUN_COL * 0.05; return lin; } // ---- Atmosphere ---- vec3 applyFog(in vec3 col, float t, in vec3 rd) { vec3 ext = exp(-t * FOG_DENSITY * vec3(1.0, 1.5, 4.0)); float sundot = clamp(dot(rd, SUN_DIR), 0.0, 1.0); vec3 fogCol = mix(vec3(0.55, 0.55, 0.58), vec3(1.0, 0.7, 0.3), 0.3 * pow(sundot, 8.0)); return col * ext + fogCol * (1.0 - ext); } // ---- Sky ---- vec3 getSky(in vec3 rd) { vec3 col = vec3(0.3, 0.5, 0.85) - rd.y * vec3(0.2, 0.15, 0.0); float horizon = pow(1.0 - max(rd.y, 0.0), 4.0); col = mix(col, vec3(0.8, 0.75, 0.7), 0.5 * horizon); float sundot = clamp(dot(rd, SUN_DIR), 0.0, 1.0); col += vec3(1.0, 0.7, 0.3) * 0.3 * pow(sundot, 8.0); col += vec3(1.0, 0.9, 0.7) * 0.5 * pow(sundot, 64.0); col += vec3(1.0, 1.0, 0.9) * min(pow(sundot, 1150.0), 0.3); return col; } // ---- Camera ---- vec3 cameraPath(float t) { return vec3(100.0 * sin(0.2 * t), 0.0, -100.0 * t); } mat3 setCamera(in vec3 ro, in vec3 ta) { vec3 cw = normalize(ta - ro); vec3 cu = normalize(cross(cw, vec3(0.0, 1.0, 0.0))); vec3 cv = cross(cu, cw); return mat3(cu, cv, cw); } // ======== Main Function ======== void mainImage(out vec4 fragColor, in vec2 fragCoord) { vec2 uv = (2.0 * fragCoord - iResolution.xy) / iResolution.y; float time = iTime * 0.5; vec3 ro = cameraPath(time); ro.y = terrainL(ro.xz) + CAM_ALTITUDE; vec3 ta = cameraPath(time + 2.0); ta.y = terrainL(ta.xz) + CAM_ALTITUDE * 0.5; mat3 cam = setCamera(ro, ta); vec3 rd = cam * normalize(vec3(uv, 1.5)); float t = raymarch(ro, rd); vec3 col; if (t > 0.0) { vec3 pos = ro + t * rd; vec3 nor = calcNormal(pos, t); vec3 mate = getMaterial(pos, nor); float sha = calcShadow(pos + nor * 0.5, SUN_DIR); vec3 lin = calcLighting(pos, nor, rd, sha); col = mate * lin; col = applyFog(col, t, rd); } else { col = getSky(rd); } col = 1.0 - exp(-col * 2.0); col = pow(col, vec3(1.0 / 2.2)); fragColor = vec4(col, 1.0); } ``` ### Binary Refinement (optional, called after raymarch) ```glsl float bisect(in vec3 ro, in vec3 rd, float tNear, float tFar) { for (int i = 0; i < 5; i++) { float tMid = 0.5 * (tNear + tFar); vec3 pos = ro + tMid * rd; float h = pos.y - terrainM(pos.xz); if (h > 0.0) tNear = tMid; else tFar = tMid; } return 0.5 * (tNear + tFar); } ``` ## Common Variants ### Relaxation Marching Automatically increases step size at far distances, covering greater range in 90 steps. ```glsl float raymarchRelax(in vec3 ro, in vec3 rd) { float t = 0.0; float d = (ro + rd * t).y - terrainM((ro + rd * t).xz); for (int i = 0; i < 90; i++) { if (abs(d) < t * 0.0001 || t > 400.0) break; float rl = max(t * 0.02, 1.0); t += d * rl; vec3 pos = ro + t * rd; d = (pos.y - terrainM(pos.xz)) * 0.7; } return t; } ``` ### Sign-Alternating FBM Amplitude flips sign each layer, producing rugged alternating ridge/valley patterns. ```glsl float terrainSignFlip(in vec2 p) { p *= TERRAIN_SCALE; float a = 0.0, w = 1.0; for (int i = 0; i < TERRAIN_OCTAVES; i++) { a += w * noise(p); w = -w * 0.4; p = m2 * p * 2.0; } return a * TERRAIN_HEIGHT; } ``` ### Canyon Style (Texture-Driven + 3D Displacement) Texture sampling + 3D FBM displacement, supporting cliffs/caves and other non-heightfield formations. ```glsl float noise3D(in vec3 x) { vec3 p = floor(x); vec3 f = fract(x); f = f * f * (3.0 - 2.0 * f); vec2 uv = (p.xy + vec2(37.0, 17.0) * p.z) + f.xy; vec2 rg = textureLod(iChannel0, (uv + 0.5) / 256.0, 0.0).yx; return mix(rg.x, rg.y, f.z); } const mat3 m3 = mat3(0.00, 0.80, 0.60, -0.80, 0.36,-0.48, -0.60,-0.48, 0.64); float displacement(vec3 p) { float f = 0.5 * noise3D(p); p = m3 * p * 2.02; f += 0.25 * noise3D(p); p = m3 * p * 2.03; f += 0.125 * noise3D(p); p = m3 * p * 2.01; f += 0.0625 * noise3D(p); return f; } float mapCanyon(vec3 p) { float h = terrainM(p.xz); float dis = displacement(0.25 * p * vec3(1.0, 4.0, 1.0)) * 3.0; return (dis + p.y - h) * 0.25; } ``` ### Directional Erosion Noise Slope direction drives Gabor noise projection, producing realistic dendritic drainage patterns. ```glsl #define EROSION_BRANCH 1.5 vec3 erosionNoise(vec2 p, vec2 dir) { vec2 ip = floor(p); vec2 fp = fract(p) - 0.5; float va = 0.0, wt = 0.0; vec2 dva = vec2(0.0); for (int i = -2; i <= 1; i++) for (int j = -2; j <= 1; j++) { vec2 o = vec2(float(i), float(j)); vec2 h = hash2(ip - o) * 0.5; vec2 pp = fp + o + h; float d = dot(pp, pp); float w = exp(-d * 2.0); float mag = dot(pp, dir); va += cos(mag * 6.283) * w; dva += -sin(mag * 6.283) * dir * w; wt += w; } return vec3(va, dva) / wt; } float terrainErosion(vec2 p, vec2 baseSlope) { float e = 0.0, a = 0.5; vec2 dir = normalize(baseSlope + vec2(0.001)); for (int i = 0; i < 5; i++) { vec3 n = erosionNoise(p * 4.0, dir); e += a * n.x; dir = normalize(dir + n.zy * vec2(1.0, -1.0) * EROSION_BRANCH); a *= 0.5; p *= 2.0; } return e; } ``` ### Volumetric Clouds + God Rays Front-to-back alpha compositing of cloud slabs, accumulating god ray factor. ```glsl #define CLOUD_BASE 200.0 #define CLOUD_TOP 300.0 vec4 raymarchClouds(vec3 ro, vec3 rd) { float tmin = (CLOUD_BASE - ro.y) / rd.y; float tmax = (CLOUD_TOP - ro.y) / rd.y; if (tmin > tmax) { float tmp = tmin; tmin = tmp; tmax = tmp; } if (tmin < 0.0) tmin = 0.0; float t = tmin; vec4 sum = vec4(0.0); float rays = 0.0; for (int i = 0; i < 64; i++) { if (sum.a > 0.99 || t > tmax) break; vec3 pos = ro + t * rd; float hFrac = (pos.y - CLOUD_BASE) / (CLOUD_TOP - CLOUD_BASE); float shape = 1.0 - 2.0 * abs(hFrac - 0.5); float den = shape - 1.6 * (1.0 - noise(pos.xz * 0.01)); if (den > 0.0) { float shadowDen = shape - 1.6 * (1.0 - noise((pos.xz + SUN_DIR.xz * 30.0) * 0.01)); float shadow = clamp(1.0 - shadowDen * 2.0, 0.0, 1.0); vec3 cloudCol = mix(vec3(0.4, 0.4, 0.45), vec3(1.0, 0.95, 0.8), shadow); float alpha = clamp(den * 0.4, 0.0, 1.0); rays += 0.02 * shadow * (1.0 - sum.a); cloudCol *= alpha; sum += vec4(cloudCol, alpha) * (1.0 - sum.a); } t += max(0.5, 0.05 * t); } sum.rgb += pow(rays, 3.0) * 0.4 * vec3(1.0, 0.8, 0.7); return sum; } ``` ## Performance & Composition **Performance:** - LOD tiers: low octaves for marching (3-9), high octaves for normals (16), lowest for camera (3) - Upper bound clipping: intersect ray with terrain max height plane before marching - Adaptive precision: hit threshold `abs(h) < k * t`, tolerates larger error at distance - Texture instead of noise: `textureLod` sampling of pre-baked noise, 2-3x speed - Early exit: `t > MAX_DIST`, `alpha > 0.99`, shadow `h < 0` - Dithered start: `t += hash(fragCoord) * step_size` to eliminate banding artifacts **Composition:** - Terrain + water: water at a fixed y-plane, multi-frequency noise perturbing normals, Fresnel controlling reflection/refraction - Terrain + volumetric clouds: render terrain first, then march cloud slab, front-to-back alpha compositing - Terrain + volumetric fog: additionally sample 3D FBM density field along ray, decay with distance - Terrain + SDF objects: `floor(p.xz/gridSize)` grid placement, `hash(cell)` randomization - Terrain + TAA: inter-frame reprojection blending, ~10% new frame + 90% history frame ## Further Reading For full step-by-step tutorials, mathematical derivations, and advanced usage, see [reference](../reference/terrain-rendering.md)