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>
31 lines
1.6 KiB
GLSL
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;
|
|
}
|