OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / shaders / erosion_terrain.gdshader

220 lines · gdshader ref:
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;
}