shader_type spatial;opusFFw
render_mode unshaded, skip_vertex_transform;
// Direct translation of runevision's eroded terrain Shadertoy
// https://www.shadertoy.com/view/4VByzW
// clayjohn (2018) → Fewes (2023) → runevision (2025). MIT licensed.
uniform float time_offset : hint_range(-10.0, 10.0) = 0.0;
uniform float height_scale : hint_range(0.0, 20.0) = 5.0;
// Erosion parameters (runevision's defaults)
const float EROSION_SCALE = 0.08333;
const float EROSION_STRENGTH = 0.16;
const float EROSION_SLOPE_POWER = 0.6;
const float EROSION_CELL_SCALE = 1.0;
const int EROSION_OCTAVES = 5;
const float EROSION_GAIN = 0.5;
const float EROSION_LACUNARITY = 2.0;
const float EROSION_HEIGHT_OFFSET = -0.5;
// Base height noise
const float HEIGHT_TILES = 3.0;
const int HEIGHT_OCTAVES = 3;
const float HEIGHT_AMP = 0.25;
const float HEIGHT_GAIN = 0.1;
const float HEIGHT_LACUNARITY = 2.0;
// Water
const float WATER_HEIGHT = 0.465;
// Colors
const vec3 CLIFF_COLOR = vec3(0.22, 0.2, 0.2);
const vec3 DIRT_COLOR = vec3(0.6, 0.5, 0.4);
const vec3 GRASS_COLOR1 = vec3(0.15, 0.3, 0.1);
const vec3 GRASS_COLOR2 = vec3(0.4, 0.5, 0.2);
const vec3 SAND_COLOR = vec3(0.8, 0.7, 0.6);
const vec3 WATER_COLOR = vec3(0.0, 0.15, 0.3);
const vec3 SNOW_COLOR = vec3(1.0, 1.0, 1.0);
// PI is already defined by Godot's shader language
vec2 hash(vec2 x) {
const vec2 k = vec2(0.3183099, 0.3678794);
x = x * k + k.yx;
return -1.0 + 2.0 * fract(16.0 * k * fract(x.x * x.y * (x.x + x.y)));
}
// iq's gradient noise with analytical derivatives
vec3 noised(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
vec2 du = 30.0 * f * f * (f * (f - 2.0) + 1.0);
vec2 ga = hash(i + vec2(0.0, 0.0));
vec2 gb = hash(i + vec2(1.0, 0.0));
vec2 gc = hash(i + vec2(0.0, 1.0));
vec2 gd = hash(i + vec2(1.0, 1.0));
float va = dot(ga, f - vec2(0.0, 0.0));
float vb = dot(gb, f - vec2(1.0, 0.0));
float vc = dot(gc, f - vec2(0.0, 1.0));
float vd = dot(gd, f - vec2(1.0, 1.0));
return vec3(
va + u.x * (vb - va) + u.y * (vc - va) + u.x * u.y * (va - vb - vc + vd),
ga + u.x * (gb - ga) + u.y * (gc - ga) + u.x * u.y * (ga - gb - gc + gd) +
du * (u.yx * (va - vb - vc + vd) + vec2(vb, vc) - va));
}
vec3 Gullies(vec2 p, vec2 slope) {
vec2 sideDir = slope.yx * vec2(-1.0, 1.0) * 2.0 * PI;
vec2 pInt = floor(p);
vec2 pFrac = fract(p);
vec3 heightAndSlope = vec3(0.0);
float weightSum = 0.0;
for (int i = -1; i <= 2; i++) {
for (int j = -1; j <= 2; j++) {
vec2 gridOffset = vec2(float(i), float(j));
vec2 gridPoint = pInt + gridOffset;
vec2 randomOffset = hash(gridPoint) * 0.5;
vec2 vectorFromCellPoint = pFrac - gridOffset - randomOffset;
float sqrDist = dot(vectorFromCellPoint, vectorFromCellPoint);
float weight = max(0.0, exp(-sqrDist * 2.0) - 0.01111);
weightSum += weight;
float waveInput = dot(vectorFromCellPoint, sideDir);
heightAndSlope += vec3(
cos(waveInput),
-sin(waveInput) * sideDir
) * weight;
}
}
return heightAndSlope / weightSum;
}
vec3 Erosion(vec2 p, vec3 heightAndSlope, float scale, float strength,
float slopePower, float cellScale, int octaves, float gain, float lacunarity) {
vec3 inputHAS = heightAndSlope;
float freq = 1.0 / (scale * cellScale);
strength *= scale;
for (int i = 0; i < octaves; i++) {
float sqrLen = dot(heightAndSlope.yz, heightAndSlope.yz);
vec2 inputSlope = heightAndSlope.yz * pow(sqrLen, 0.5 * (slopePower - 1.0));
heightAndSlope += Gullies(p * freq, inputSlope * cellScale)
* strength * vec3(1.0, freq, freq);
strength *= gain;
freq *= lacunarity;
}
return heightAndSlope - inputHAS;
}
vec3 FractalNoise(vec2 p, float freq, int octaves, float lacunarity, float gain) {
vec3 n = vec3(0.0);
float nf = freq;
float na = 1.0;
for (int i = 0; i < octaves; i++) {
n += noised(p * nf) * na * vec3(1.0, nf, nf);
na *= gain;
nf *= lacunarity;
}
return n;
}
float MagnitudeSum(int octaves, float gain) {
return (1.0 - pow(gain, float(octaves))) / (1.0 - gain);
}
vec2 Heightmap(vec2 p) {
vec3 n = FractalNoise(p, HEIGHT_TILES, HEIGHT_OCTAVES, HEIGHT_LACUNARITY, HEIGHT_GAIN)
* HEIGHT_AMP;
n = n * 0.5 + vec3(0.5, 0.0, 0.0);
float strength = EROSION_STRENGTH;
strength *= smoothstep(WATER_HEIGHT - 0.1, WATER_HEIGHT + 0.1, n.x);
vec3 h = Erosion(p, n, EROSION_SCALE, strength,
EROSION_SLOPE_POWER, EROSION_CELL_SCALE,
EROSION_OCTAVES, EROSION_GAIN, EROSION_LACUNARITY);
float erosionMagnitude = EROSION_SCALE * strength
* MagnitudeSum(EROSION_OCTAVES, EROSION_GAIN);
float offset = erosionMagnitude * EROSION_HEIGHT_OFFSET;
return vec2(n.x + h.x + offset, h.x / max(erosionMagnitude, 0.001));
}
varying float v_height;
varying float v_erosion;
varying vec3 v_world_normal;
void vertex() {
vec2 uv = UV;
uv.x += time_offset * 0.1;
vec2 h = Heightmap(uv);
float height = h.x;
v_height = height;
v_erosion = h.y;
// Displace vertex upward by height
VERTEX.y += height * height_scale;
// Compute normal from finite differences
float eps = 1.0 / 256.0;
float h1 = Heightmap(uv + vec2(eps, 0.0)).x;
float h2 = Heightmap(uv + vec2(0.0, eps)).x;
vec3 n = normalize(vec3((height - h1) * height_scale, eps * 10.0, (height - h2) * height_scale));
v_world_normal = n;
NORMAL = n;
// Apply model-view-projection
VERTEX = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
void fragment() {
float height = v_height;
float erosion = v_erosion;
vec3 normal = normalize(v_world_normal);
// Simple directional light
vec3 sun = normalize(vec3(-1.0, 0.4, 0.05));
float NdotL = max(0.0, dot(normal, sun));
float ambient = 0.3;
// Material by altitude + slope + erosion
float occlusion = clamp(erosion + 0.5, 0.0, 1.0);
vec3 color = CLIFF_COLOR * smoothstep(0.4, 0.52, height);
// Dirt in eroded areas
color = mix(color, DIRT_COLOR, smoothstep(0.3, 0.0, occlusion - 0.2));
// Grass near water where flat
vec3 grassMix = mix(GRASS_COLOR1, GRASS_COLOR2, smoothstep(0.4, 0.6, height));
color = mix(color, grassMix,
smoothstep(WATER_HEIGHT + 0.05, WATER_HEIGHT + 0.02, height) *
smoothstep(0.8, 1.0, normal.y));
// Snow at high altitude
color = mix(color, SNOW_COLOR, smoothstep(0.53, 0.6, height));
// Sand at water line
color = mix(color, SAND_COLOR, smoothstep(WATER_HEIGHT + 0.005, WATER_HEIGHT, height));
// Water
if (height < WATER_HEIGHT) {
color = mix(WATER_COLOR, vec3(0.0, 0.25, 0.25), exp(-(WATER_HEIGHT - height) * 60.0));
}
// Lighting
color *= (NdotL * 0.7 + ambient);
ALBEDO = color;
}