ajhahn.de
← Theria
plain text 69 lines
shader_type spatial;
render_mode specular_disabled;

// The jungle short-grass ground. A flat plane on its own reads as a dead slab, so this
// breaks the surface into flat patches of two grass greens with a toon-quantised value
// noise — reading as clumped short grass from the close follow-camera — and bands the key
// light into the same flat tones the units wear (cel.gdshader), so a unit and the ground
// it stands on share one lighting treatment. World-space, so the pattern tiles evenly
// across the whole arena regardless of where the plane sits.

// The two grass tones the patches blend between — a deep shade green and a brighter one.
uniform vec3 grass_low : source_color = vec3(0.13, 0.30, 0.12);
uniform vec3 grass_high : source_color = vec3(0.22, 0.45, 0.18);

// How tight the grass clumps are: the broad-patch frequency and the finer speckle on top,
// per world unit, and how many flat steps the blend is quantised into for the toon look.
uniform float patch_scale = 0.004;
uniform float speckle_scale = 0.02;
uniform float patch_steps = 4.0;

// The two light levels the key light steps through — the same three-tone ramp the units
// wear, so the ground tone sits in the same family as the bodies on it.
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);
}

// Smoothed value noise in [0, 1] — bilinear blend of four lattice hashes, the building
// block the grass dapple is layered from.
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 broad = value_noise(world_pos.xz * patch_scale);
	float fine = value_noise(world_pos.xz * speckle_scale);
	float t = clamp(broad * 0.7 + fine * 0.3, 0.0, 1.0);
	t = floor(t * patch_steps) / (patch_steps - 1.0);
	ALBEDO = mix(grass_low, grass_high, t);
	ROUGHNESS = 1.0;
	METALLIC = 0.0;
}

// Bands the key light into three flat tones, the shadow side left to the ambient fill —
// the same step the units take, so the field lights as one surface.
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;
}