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

// The toon treatment every procedural map object wears — the mountain ring, the scattered
// jungle plants, the hills, and the camp props. It is the unit cel shader (src/client/cel.gdshader)
// stripped to the one channel these meshes need: their colour is baked into the mesh as
// per-vertex COLOR by JungleDecor (rock grey rising to a lighter cap, trunk wood, leaf green
// fading to a sun-tipped tip), and the same three-tone light banding folds it into the low-poly
// look so a fern and a hero read as one art family. No texture, no team tint — the colour is
// the geometry's. `cull_disabled` so a thin frond, blade, or banner is lit from both faces and
// never disappears edge-on, which also makes the faceted solids forgiving of winding.

// The two light levels the matte (shadowed) tone steps up through, giving three flat bands in
// all — identical cuts to the unit shader so the toon step matches across the field.
const float MID_TONE = 0.5;
const float LOW_CUT = 0.25;
const float HIGH_CUT = 0.6;

// Canopy fade: when the player's hero stands under tall growth, the parts of it over the hero
// dissolve so the character is never hidden — but the silhouette edge is kept, so a faded tree
// still reads as "a tree you are under" rather than vanishing outright. Driven by main.gd, which
// feeds the hero's world position each frame; the default sits the hero far away so nothing fades
// until a match sets it. The dissolve is a screen-door stipple (discarded fragments on a stable
// per-pixel pattern) — no transparency, so no blend sorting and the toon shading is untouched.
uniform vec3 hero_pos = vec3(1.0e9);
uniform float fade_radius = 680.0;  // horizontal reach of the fade around the hero
uniform float fade_height = 190.0;  // only growth above this (the canopy) fades; trunks/ground stay

varying vec3 v_color;
varying vec3 v_world;

void vertex() {
	v_color = COLOR.rgb;
	v_world = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}

void fragment() {
	float dxz = distance(v_world.xz, hero_pos.xz);
	float prox = 1.0 - smoothstep(fade_radius * 0.62, fade_radius, dxz);  // 1 over the hero, 0 away
	float tall = smoothstep(fade_height, fade_height + 70.0, v_world.y);  // 0 low, 1 up in the canopy
	// abs() so a flat frond fades whichever way its baked normal points — only a true silhouette
	// edge (normal across the view) is kept opaque, so the hero shows through the canopy over it.
	float facing = abs(dot(normalize(NORMAL), normalize(VIEW)));
	float edge = 1.0 - smoothstep(0.04, 0.30, facing);  // 1 only at the grazing silhouette
	float hide = prox * tall * (1.0 - edge);
	// Stable per-pixel screen-door threshold, so the dissolve stipples instead of blending.
	float dither = fract(sin(dot(floor(FRAGCOORD.xy), vec2(12.9898, 78.233))) * 43758.5453);
	if (hide > dither) {
		discard;
	}
	ALBEDO = v_color;
	ROUGHNESS = 1.0;
	METALLIC = 0.0;
}

// Bands the key light into three flat tones with hard edges — the toon step. The shadow side
// is left to the environment's ambient fill (added by the engine), so it reads as a deliberate
// matte tone rather than black, the same as the units standing on top of it.
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;
}