plain text 87 lines
shader_type spatial;
render_mode specular_disabled, cull_disabled;
// The river surface — a stylised toon water in place of the flat blue strip. A slow broad
// tone varies the deep channel; crisp highlight bands drift along the current (TIME) so it
// reads as flowing water; the banks lighten to a shallow tone and then fray irregularly into
// the grass. Cel-banded like every other surface. Driven per river ribbon by MapView, which
// passes the ribbon's half-width so the bank shaping works in object space.
//
// The flow bands ride a world-space diagonal (x - z) — the axis the river runs along — so the
// pattern is continuous across the separate ribbon segments (no seams at the joins) and scrolls
// roughly along the watercourse.
// Deep channel, mid surface, the bright crest the drifting bands lift to, and the lighter
// shallow tone the water fades to at its banks.
uniform vec3 water_deep : source_color = vec3(0.04, 0.14, 0.32);
uniform vec3 water_mid : source_color = vec3(0.09, 0.27, 0.48);
uniform vec3 water_crest : source_color = vec3(0.34, 0.60, 0.78);
uniform vec3 water_shallow : source_color = vec3(0.28, 0.52, 0.58);
// Broad tone clump size, and the drifting flow bands: their spacing (frequency), scroll speed,
// where along the wave the crest cuts in, and how far the crest lifts toward `water_crest`.
uniform float tone_scale = 0.004;
uniform float wave_freq = 0.013;
uniform float wave_speed = 2.2;
uniform float wave_cut : hint_range(0.0, 1.0) = 0.62;
uniform float wave_strength : hint_range(0.0, 1.0) = 0.5;
// Bank shaping (across the ribbon, 0 centre … 1 edge from UV.x): where the shallow lightening
// begins, where the fray begins, and the noise that breaks the bank up.
uniform float shallow_start : hint_range(0.0, 1.0) = 0.5;
uniform float fray_start : hint_range(0.0, 1.0) = 0.82;
uniform float fray_scale = 0.012;
uniform float fray_jitter : hint_range(0.0, 1.0) = 0.4;
const float MID_TONE = 0.5;
const float LOW_CUT = 0.25;
const float HIGH_CUT = 0.6;
varying vec3 world_pos;
float hash(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float value_noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
float a = hash(i);
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));
vec2 u = f * f * (3.0 - 2.0 * f);
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
void vertex() {
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
void fragment() {
float edge = clamp(abs(UV.x - 0.5) * 2.0, 0.0, 1.0); // 0 centre … 1 bank, from the ribbon UV
// Fray the banks into the grass, the same noise cut the dirt path takes.
float bite = (value_noise(world_pos.xz * fray_scale) - 0.5) * fray_jitter;
if (edge > fray_start && edge - fray_start + bite > 0.0) {
discard;
}
// Broad slow tone across the channel, then crisp highlight bands drifting along the flow.
float tone = value_noise(world_pos.xz * tone_scale);
vec3 surface = mix(water_deep, water_mid, tone);
float wave = sin((world_pos.x - world_pos.z) * wave_freq - TIME * wave_speed) * 0.5 + 0.5;
float crest = smoothstep(wave_cut, wave_cut + 0.08, wave);
surface = mix(surface, water_crest, crest * wave_strength);
float shallow = smoothstep(shallow_start, 1.0, edge);
ALBEDO = mix(surface, water_shallow, shallow);
ROUGHNESS = 1.0;
METALLIC = 0.0;
}
void light() {
float ndl = max(dot(NORMAL, LIGHT), 0.0);
float toon = step(LOW_CUT, ndl) * MID_TONE + step(HIGH_CUT, ndl) * (1.0 - MID_TONE);
DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR * ATTENUATION * toon;
}