ajhahn.de
← Theria
plain text 73 lines
shader_type spatial;
render_mode specular_disabled, cull_disabled;

// The trampled dirt path a lane wears — worn earth beaten through the jungle grass rather
// than a flat tan strip. Two dirt tones are dappled into toon patches like the grass, and
// the path's long edges are frayed away with a noise cut so the border breaks into the
// grass irregularly instead of as a clean ruled line. Cel-banded to match every other
// surface. Driven per lane ribbon by MapView, which passes the ribbon's half-width.

// The two dirt tones the worn path dapples between — a dark damp earth and a lighter dust.
uniform vec3 dirt_low : source_color = vec3(0.20, 0.15, 0.10);
uniform vec3 dirt_high : source_color = vec3(0.40, 0.32, 0.20);

// Clump tightness of the dirt dapple (per world unit) and how many flat toon steps it bands
// into — the same treatment the grass takes, so path and grass read as one family.
uniform float patch_scale = 0.006;
uniform float patch_steps = 4.0;

// Edge fray: where across the ribbon (0 centre … 1 edge, read from UV.x) the fray starts, the
// noise frequency that breaks the border up, and how hard the noise bites into it.
uniform float fray_start : hint_range(0.0, 1.0) = 0.62;
uniform float fray_scale = 0.012;
uniform float fray_jitter : hint_range(0.0, 1.0) = 0.55;

// The toon light ramp shared with the units and the grass.
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() {
	// Fray the long edges into the grass: past `fray_start` toward the edge, a noise cut eats
	// the border away irregularly, so the path dissolves into grass rather than ruling a clean
	// line. The centre stays solid. UV.x runs 0 (left) … 1 (right) across the ribbon.
	float edge = clamp(abs(UV.x - 0.5) * 2.0, 0.0, 1.0);
	float bite = (value_noise(world_pos.xz * fray_scale) - 0.5) * fray_jitter;
	if (edge - fray_start + bite > 0.0 && edge > fray_start) {
		discard;
	}
	float t = floor(value_noise(world_pos.xz * patch_scale) * patch_steps) / (patch_steps - 1.0);
	ALBEDO = mix(dirt_low, dirt_high, t);
	ROUGHNESS = 1.0;
	METALLIC = 0.0;
}

void light() {
	float ndl = max(dot(NORMAL, LIGHT), 0.0);
	float tone = step(LOW_CUT, ndl) * MID_TONE + step(HIGH_CUT, ndl) * (1.0 - MID_TONE);
	DIFFUSE_LIGHT += ALBEDO * LIGHT_COLOR * ATTENUATION * tone;
}