feat(A.5 T20): MSAA 4x + alpha-to-coverage on foliage

Per Phase A.5 spec §4.9.2: ClipMap foliage uses binary alpha-cutoff.
At N₂=12 horizon distance the pixel-stepped silhouettes are visible.
A2C with MSAA 4x produces smooth retail-faithful tree edges.

GL context now requests Samples=4. WbDrawDispatcher's opaque pass
toggles GL_SAMPLE_ALPHA_TO_COVERAGE on/off around the multi-draw
indirect call. mesh_modern.frag's opaque pass now discards only
truly-empty (α<0.05) so the GPU derives sample mask from coverage;
transparent pass boundary logic is unchanged.

MSAA audit: no custom FBOs found — all rendering uses default
framebuffer. Sky/particles/ImGui are all MSAA-compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-10 08:25:59 +02:00
parent 4b84e5650b
commit 26b2871b10
3 changed files with 10 additions and 1 deletions

View file

@ -830,6 +830,7 @@ public sealed class GameWindow : IDisposable
ContextFlags.ForwardCompatible, ContextFlags.ForwardCompatible,
new APIVersion(4, 3)), new APIVersion(4, 3)),
VSync = false, // off during development so the perf overlay shows true framerate VSync = false, // off during development so the perf overlay shows true framerate
Samples = 4, // A.5 T20: MSAA 4x for A2C foliage smoothing
}; };
_window = Window.Create(options); _window = Window.Create(options);

View file

@ -80,8 +80,11 @@ void main() {
vec4 color = texture(tex, vec3(vTexCoord, float(vTextureLayer))); vec4 color = texture(tex, vec3(vTexCoord, float(vTextureLayer)));
// Two-pass alpha-test (N.5 Decision 2). // Two-pass alpha-test (N.5 Decision 2).
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
// derives the MSAA sample mask from it — ClipMap foliage edges become smooth.
// Discard only fully-transparent (α < 0.05); the GPU handles coverage masking.
if (uRenderPass == 0) { if (uRenderPass == 0) {
if (color.a < 0.95) discard; // opaque pass if (color.a < 0.05) discard; // opaque pass — kill truly empty only (A2C)
} else { } else {
if (color.a >= 0.95) discard; // transparent pass if (color.a >= 0.95) discard; // transparent pass
if (color.a < 0.05) discard; // skip totally-empty if (color.a < 0.05) discard; // skip totally-empty

View file

@ -488,6 +488,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
{ {
_gl.Disable(EnableCap.Blend); _gl.Disable(EnableCap.Blend);
_gl.DepthMask(true); _gl.DepthMask(true);
// A.5 T20: enable A2C for ClipMap foliage — GPU derives sample mask
// from the alpha written by mesh_modern.frag so foliage edges are
// smooth under MSAA 4x. A no-op for fully-opaque (α=1) batches.
_gl.Enable(EnableCap.SampleAlphaToCoverage);
_shader.SetInt("uRenderPass", 0); _shader.SetInt("uRenderPass", 0);
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer); _gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
if (diag && _gpuQueriesInitialized) _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryOpaque); if (diag && _gpuQueriesInitialized) _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryOpaque);
@ -498,6 +502,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
(uint)_opaqueDrawCount, (uint)_opaqueDrawCount,
(uint)DrawCommandStride); (uint)DrawCommandStride);
if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed); if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed);
_gl.Disable(EnableCap.SampleAlphaToCoverage);
} }
// ── Phase 8: transparent pass ──────────────────────────────────────── // ── Phase 8: transparent pass ────────────────────────────────────────