ludic/packages/ludic.render3d/shaders/depth.frag
Orkuncakilkaya 18d23ca20d feat(render3d): a bole that does not quake, leaves out of the lens, a light with a reach
layer_flutter's mask was the vertex's height alone, so on a ten metre aspen every part of
the trunk above 1.2 m trembled with the leaves and the bole read as cloth. What separates
a leaf from a bole is distance from the model's centre line, not height; the mask is
radial now, and small plants keep the old one because a flower is all leaf.

NEAR_FADE dithers tree foliage out inside arm's length of the camera, in the depth
prepass, so the lit pass never sees those pixels and the equal-depth optimisation is
untouched. gl_Position.w is the view depth, so it costs one varying and no uniform.
Blades, cards and flowers are excluded - they live at the player's feet - and the shadow
pass keeps every leaf.

daylight_hand takes a reach in metres. The falloff was an inverse square windowed between
26 and 6 with both numbers hard-coded: two per cent of its own near field at ten metres,
so a torch lit your boots. It is a gentle power out to the reach now.

Measured in Maroon Lake, twice per side: the crown mask moves 1.7% of an aspen frame and
nothing at all in the meadow under it; the near fade moves 7.1% of a first-person frame
at a conifer and 0% where there is no foliage in the lens; the torch's reach lifts the lit
ground 20 to 45% and leaves the sky bit-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 00:06:50 +03:00

31 lines
1.6 KiB
GLSL

// depth.frag — the foliage depth prepass (scatter_draw_depth, render.ludic).
//
// A crown of needle cards is many cut-out quads deep from any angle. Lit directly, every
// card behind the front one runs the whole lighting shader and is thrown away afterwards,
// because a shader that can discard switches early depth rejection off. So the near tree
// LODs first write depth with this: the same alpha coverage test the lit shader applies,
// and nothing else. The lit pass then draws them with no discard and an equal depth test
// (model.frag's EQ_PASS), and each pixel of foliage is lit once.
//
// The test must match model.frag's ALPHA_TEST exactly - a texel this pass keeps and that
// one would have cut is drawn solid - so it is the same formula, taken at coverage 0.5
// because without MSAA a fragment is either there or not.
in vec3 v_wpos;
in vec2 v_uv;
in float v_near;
uniform sampler2D u_diff;
void main() {
if (v_wpos.y < u_clip_y) discard;
#ifdef NEAR_FADE
// Leaves in the camera's face (model.vert's v_near). Dithered out rather than blended:
// no sorting, no second pass and no halo, and because it happens in the PREPASS the lit
// pass never sees those pixels either - its equal-depth test simply finds nothing to
// match. A branch a metre from the lens stops being an opaque wall in front of the body.
if (v_near < 0.999 && v_near < fract(sin(dot(gl_FragCoord.xy, vec2(12.9898, 78.233))) * 43758.5453)) discard;
#endif
vec4 d = texture(u_diff, v_uv);
float lod = textureQueryLod(u_diff, v_uv).x;
float a = min(d.a * (1.0 + 0.45 * max(lod, 0.0)), 1.0);
float cov = (a - 0.4) / max(fwidth(a), 1e-4) + 0.5;
if (cov < 0.5) discard;
}