ludic/packages/ludic.render3d/shaders/model.frag
Orkuncakilkaya d6ffdfa638 perf(render3d): a depth prepass for the near tree foliage
Needle-card crowns are many cut-out quads deep and a discarding shader turns early depth
rejection off, so every card behind the front one ran the full lighting shader. The near
tree LODs now write depth first (depth.frag, the same alpha coverage test), terrain under
them is rejected before shading, and the lit pass draws them with no discard and an equal
depth test (invariant gl_Position). R3D_NOPREPASS=1 restores the old path.

Dense forest on a Windows PC: 61 -> 78 fps at 3840x2160, 116 -> 164 fps at 1920x1080.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 04:18:38 +03:00

190 lines
7.9 KiB
GLSL

in vec3 v_wpos;
in vec3 v_nrm;
in vec2 v_uv;
in float v_seed;
in vec2 v_rot;
in float v_hull;
uniform float u_model_h;
out vec4 o_color;
uniform sampler2D u_diff;
uniform sampler2D u_nrm;
uniform sampler2D u_arm;
uniform mat4 u_view;
uniform vec3 u_tint;
uniform float u_rough_scale;
uniform float u_emissive; // self-lit (a flame): albedo added back after shading
#ifdef BLADE
uniform vec3 u_blade_base;
uniform vec3 u_blade_tip;
#endif
#ifdef CARD
uniform float u_cull; // the layer's cull distance (m); 0 = none
#endif
mat3 cotangentFrame(vec3 N, vec3 p, vec2 uv) {
vec3 dp1 = dFdx(p), dp2 = dFdy(p);
vec2 duv1 = dFdx(uv), duv2 = dFdy(uv);
vec3 dp2perp = cross(dp2, N), dp1perp = cross(N, dp1);
vec3 T = dp2perp * duv1.x + dp1perp * duv2.x;
vec3 B = dp2perp * duv1.y + dp1perp * duv2.y;
float invmax = inversesqrt(max(dot(T, T), dot(B, B)) + 1e-12);
return mat3(T * invmax, B * invmax, N);
}
void main() {
#ifndef EQ_PASS
// (the EQ_PASS variant draws over the foliage prepass: any discard at all would switch
// early depth rejection off and bring the overdraw back, and the prepass has already
// cut both the clip plane and the alpha)
if (v_wpos.y < u_clip_y) discard;
#endif
vec3 N = normalize(v_nrm);
if (!gl_FrontFacing && v_hull >= 0.0) N = -N;
#ifdef CARD
// a baked card: albedo with coverage (premultiplied mips), normal in the card's own frame
vec4 ca = texture(u_diff, v_uv);
float rawA = ca.a;
ca.rgb /= max(ca.a, 1e-3);
#ifdef SHADOW_PASS
if (ca.a < 0.3) discard;
float cardAlpha = 1.0;
return;
#else
float cov = (ca.a - 0.4) / max(fwidth(ca.a), 1e-4) + 0.5;
if (cov < 0.02) discard;
float cardAlpha = clamp(cov, 0.0, 1.0);
#endif
#endif
float dist = length(v_wpos - u_cam_pos);
float viewDepth = -(u_view * vec4(v_wpos, 1.0)).z;
#if defined(CARD) && !defined(SHADOW_PASS)
// A clump card at 400 m is a two-pixel disc of pure leaf colour on whatever the ground
// is doing — on dark scree it glows. Dissolve toward the cull distance: the carpet
// drape on the ground carries the meadow's colour from there on.
if (u_cull > 1.0) cardAlpha *= 1.0 - smoothstep(u_cull * 0.55, u_cull, dist);
if (cardAlpha < 0.02) discard;
#endif
vec3 alb;
vec3 n;
vec3 arm;
float meshAlpha = 1.0;
#ifdef BLADE
// a procedural blade: dark at the root, lighter at the tip; some blades gone to seed
float t = clamp(v_uv.y, 0.0, 1.0);
alb = mix(u_blade_base, u_blade_tip, t * t) * regionTint(v_wpos, 0.35);
float dry = smoothstep(0.7, 0.8, fract(v_seed * 3.17));
alb = mix(alb, vec3(0.40, 0.36, 0.13) * (0.45 + 0.55 * t), dry * 0.75);
float patchy = fbm(v_wpos.xz * 0.045, 3) * 0.5 + 0.5;
alb *= mix(vec3(0.7, 0.8, 0.55), vec3(1.1, 1.05, 0.85), patchy);
// a far tuft is a patch of the meadow, darker than a lit blade tip and never straw
if (v_hull < 0.0) alb = mix(u_blade_base, u_blade_tip, 0.45) * regionTint(v_wpos, 0.35) * mix(vec3(0.7, 0.8, 0.55), vec3(1.1, 1.05, 0.85), patchy) * 0.72;
// a rounded cross-section reads softer than a flat card
vec3 side = normalize(cross(N, vec3(0.0, 1.0, 0.0)) + vec3(1e-4));
n = (v_hull < 0.0) ? N : normalize(N + side * (v_uv.x * 2.0 - 1.0) * 0.6);
arm = vec3(mix(0.2, 1.0, t * t), 0.85, 0.0);
#elif defined(CARD)
// thin grass is lit from either side: face the card toward the sun before shading
if (dot(N, u_sun_dir) < 0.0) N = -N;
// the normal atlas is premultiplied by coverage like the albedo (see impostor.frag)
vec4 cn = texture(u_nrm, v_uv) / max(rawA, 1e-3);
cn = clamp(cn, 0.0, 1.0);
vec3 bn = cn.rgb * 2.0 - 1.0;
// the card frame: right along the quad, up, and out of the quad
vec3 T = normalize(cross(vec3(0.0, 1.0, 0.0), N) + vec3(1e-5));
n = normalize(T * -bn.x + vec3(0.0, 1.0, 0.0) * bn.y + N * max(abs(bn.z), 0.25));
n = normalize(mix(n, normalize(N + vec3(0.0, 0.8, 0.0)), 0.35));
// A clump is a few pixels at 100 m: what the eye reads there is the hillside's shading,
// and a card facing the sun on a slope facing away from it glows against the ground
// like a sticker. Light it with the ground's own normal as it recedes (as the blades
// already do), so cover and terrain darken together.
#ifdef CHEAP
n = terrainNormalAt(v_wpos);
#else
n = normalize(mix(n, terrainNormalAt(v_wpos), smoothstep(25.0, 90.0, dist)));
#endif
alb = ca.rgb * 1.05 * regionTint(v_wpos, 0.8);
arm = vec3(mix(0.5, 1.0, clamp(v_uv.y, 0.0, 1.0)) * (0.6 + 0.4 * cn.a), 0.85, 0.0);
#elif defined(FLOWER)
// a lupine: green stem (uv.x < 1), violet florets above (uv.x in [1,2]), tinted per plant
float hue = fract(v_seed * 5.71);
float vy = clamp(v_uv.y, 0.0, 1.0);
vec3 violet = mix(vec3(0.07, 0.03, 0.32), vec3(0.28, 0.06, 0.36), hue);
vec3 tipc = mix(violet, vec3(0.5, 0.3, 0.7), 0.3);
if (v_uv.x >= 2.0) {
alb = vec3(0.05, 0.13, 0.025) * (0.7 + 0.6 * vy);
} else if (v_uv.x >= 1.0) {
float f = fract(v_uv.x);
alb = mix(violet, tipc, vy) * (0.65 + 0.35 * abs(f * 2.0 - 1.0));
// florets as little lobes: darker between them, a paler lip on each
float lobe = 0.55 + 0.45 * abs(sin(vy * 9.0 + f * 6.0));
alb = mix(alb * lobe, vec3(0.55, 0.45, 0.75), 0.18 * smoothstep(0.6, 1.0, lobe));
} else {
alb = vec3(0.07, 0.16, 0.03);
}
vec3 side = normalize(cross(N, vec3(0.0, 1.0, 0.0)) + vec3(1e-4));
n = normalize(N + side * (fract(v_uv.x) * 2.0 - 1.0) * 0.7);
arm = vec3(0.9, 0.7, 0.0);
#else
vec4 d = texture(u_diff, v_uv);
#ifdef ALPHA_TEST
// A needle sprig's alpha averages away in the mips, so a plain 0.5 test strips the
// crown bare past 50 m. Scale the alpha back up by the mip level (Castano's alpha
// mipmaps, done at sample time) and sharpen the edge with its screen derivative, then
// let alpha-to-coverage resolve it.
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.02) discard;
meshAlpha = clamp(cov, 0.0, 1.0);
#endif
vec3 tn = texture(u_nrm, v_uv).rgb * 2.0 - 1.0;
mat3 tbn = cotangentFrame(N, v_wpos, v_uv);
n = normalize(tbn * tn);
arm = texture(u_arm, v_uv).rgb;
n = normalize(mix(n, N, smoothstep(30.0, 120.0, dist)));
alb = d.rgb;
// a crown's interior is occluded by its own cards
if (u_model_h > 2.0) arm.r *= mix(0.5, 1.0, v_hull);
#endif
alb *= u_tint * (0.85 + 0.3 * fract(v_seed * 7.13));
#ifdef CARD
// a card is its own caster: look up the shadow a little above and in front of it, and let
// light bleed through the thin clump as real grass does
#ifdef CHEAP
float shadow = cloudShadow(v_wpos) * terrainShadow(v_wpos);
#else
float shadow = mix(1.0, sunShadow(v_wpos + N * 0.1 + vec3(0.0, 0.2, 0.0), N, viewDepth), 0.55);
#endif
#else
float shadow = sunShadow(v_wpos, N, viewDepth);
#endif
#ifdef FOLIAGE
// leaves and needles are matte at every angle: no grazing Fresnel on a two-sided card
float roughF = 1.0;
#else
float roughF = clamp(arm.g * u_rough_scale, 0.35, 1.0);
#endif
vec3 col = shade(v_wpos, n, alb, roughF, 0.0, arm.r, shadow, viewDepth);
#ifdef FOLIAGE
// thin-leaf translucency: light leaking through toward the viewer, and a wrapped diffuse
vec3 v = normalize(u_cam_pos - v_wpos);
float back = pow(max(dot(-v, u_sun_dir), 0.0), 3.0);
float wrap = max(dot(N, u_sun_dir) * 0.5 + 0.5, 0.0);
// Only a thin leaf a few metres away is translucent. A clump card at 200 m is a whole
// bush in two pixels, and giving it the leaf's glow toward the sun painted the
// backlit hillsides with lime discs. The term fades out with distance.
float thin = 1.0 - smoothstep(30.0, 140.0, dist);
// a dense crown of cards is not a thin leaf: much less light comes through it
if (u_model_h > 2.0) thin *= 0.3;
col += alb * u_sun_color * (0.14 * back + 0.05 * wrap) * thin * shadow * cloudShadow(v_wpos);
col += alb * skyIrradiance(vec3(0, 1, 0)) * 0.12 * arm.r;
#endif
col += alb * u_emissive;
if (any(isnan(col))) col = vec3(0.0);
col = applyFog(col, v_wpos, dist);
#ifdef CARD
o_color = vec4(sane(col), cardAlpha);
#else
o_color = vec4(sane(col), meshAlpha);
#endif
}