diff --git a/docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md b/docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md index 7989f1d..471da6b 100644 --- a/docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md +++ b/docs/superpowers/plans/2026-05-08-phase-n5-modern-rendering.md @@ -600,9 +600,13 @@ void main() { } ``` -- [ ] **Step 5.3: Write mesh_modern.frag** +- [ ] **Step 5.3: Write mesh_modern.frag — preserve existing lighting model** -Create `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`: +**AMENDED 2026-05-08:** original plan draft used hardcoded `uAmbient/uSunDir/uSunColor` uniforms. Reading the actual `src/AcDream.App/Rendering/Shaders/mesh_instanced.frag` revealed it uses a `SceneLighting` UBO at `binding=1` with 8 lights, fog params, and lightning flash. The N.5 shader must preserve this lighting machinery to maintain visual identity to N.4. + +The vert outputs need to ADD `vWorldPos` (used by `accumulateLights` and `applyFog`). Update the vert from Step 5.2 to also emit `out vec3 vWorldPos;` and `vWorldPos = worldPos.xyz;` in main. + +Create `src/AcDream.App/Rendering/Shaders/mesh_modern.frag` with the same lighting UBO + functions as `mesh_instanced.frag`, plus the bindless texture + alpha-test discard logic: ```glsl #version 430 core @@ -610,13 +614,69 @@ Create `src/AcDream.App/Rendering/Shaders/mesh_modern.frag`: in vec3 vNormal; in vec2 vTexCoord; +in vec3 vWorldPos; in flat uvec2 vTextureHandle; in flat uint vTextureLayer; -uniform int uRenderPass; // 0 = opaque (discard alpha<0.95), 1 = transparent (discard alpha>=0.95) -uniform vec3 uAmbient; -uniform vec3 uSunDir; -uniform vec3 uSunColor; +// 0 = opaque (discard alpha<0.95), 1 = transparent (discard alpha>=0.95) +uniform int uRenderPass; + +// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1. +struct Light { + vec4 posAndKind; + vec4 dirAndRange; + vec4 colorAndIntensity; + vec4 coneAngleEtc; +}; +layout(std140, binding = 1) uniform SceneLighting { + Light uLights[8]; + vec4 uCellAmbient; + vec4 uFogParams; + vec4 uFogColor; + vec4 uCameraAndTime; +}; + +vec3 accumulateLights(vec3 N, vec3 worldPos) { + vec3 lit = uCellAmbient.xyz; + int activeLights = int(uCellAmbient.w); + for (int i = 0; i < 8; ++i) { + if (i >= activeLights) break; + int kind = int(uLights[i].posAndKind.w); + vec3 Lcol = uLights[i].colorAndIntensity.xyz * uLights[i].colorAndIntensity.w; + if (kind == 0) { + vec3 Ldir = -uLights[i].dirAndRange.xyz; + float ndl = max(0.0, dot(N, Ldir)); + lit += Lcol * ndl; + } else { + vec3 toL = uLights[i].posAndKind.xyz - worldPos; + float d = length(toL); + float range = uLights[i].dirAndRange.w; + if (d < range && range > 1e-3) { + vec3 Ldir = toL / max(d, 1e-4); + float ndl = max(0.0, dot(N, Ldir)); + float atten = 1.0; + if (kind == 2) { + float cos_edge = cos(uLights[i].coneAngleEtc.x * 0.5); + float cos_l = dot(-Ldir, uLights[i].dirAndRange.xyz); + atten *= (cos_l > cos_edge) ? 1.0 : 0.0; + } + lit += Lcol * ndl * atten; + } + } + } + return lit; +} + +vec3 applyFog(vec3 lit, vec3 worldPos) { + int mode = int(uFogParams.w); + if (mode == 0) return lit; + float d = length(worldPos - uCameraAndTime.xyz); + float fogStart = uFogParams.x; + float fogEnd = uFogParams.y; + float span = max(1e-3, fogEnd - fogStart); + float fog = clamp((d - fogStart) / span, 0.0, 1.0); + return mix(lit, uFogColor.xyz, fog); +} out vec4 FragColor; @@ -624,30 +684,92 @@ void main() { sampler2DArray tex = sampler2DArray(vTextureHandle); vec4 color = texture(tex, vec3(vTexCoord, float(vTextureLayer))); + // Two-pass alpha-test (N.5 Decision 2 — replaces mesh_instanced's + // uTranslucencyKind=1 ClipMap-only discard with a more aggressive + // pattern that also handles AlphaBlend correctly via two passes). if (uRenderPass == 0) { - // Opaque pass: discard soft pixels — they belong to the transparent pass. - if (color.a < 0.95) discard; + if (color.a < 0.95) discard; // opaque pass } else { - // Transparent pass: discard hard pixels (already drawn opaque). - if (color.a >= 0.95) discard; - if (color.a < 0.05) discard; // skip totally-empty fragments + if (color.a >= 0.95) discard; // transparent pass + if (color.a < 0.05) discard; // skip totally-empty } vec3 N = normalize(vNormal); - vec3 L = normalize(uSunDir); - float diff = max(dot(N, L), 0.0); - vec3 lit = uAmbient + uSunColor * diff; - color.rgb *= clamp(lit, 0.0, 1.0); + vec3 lit = accumulateLights(N, vWorldPos); - FragColor = color; + // Lightning flash — additive scene bump (matches mesh_instanced.frag). + lit += uFogParams.z * vec3(0.6, 0.6, 0.75); + + // Retail clamp per-channel to 1.0 (r13 §13.1). + lit = min(lit, vec3(1.0)); + + vec3 rgb = color.rgb * lit; + rgb = applyFog(rgb, vWorldPos); + FragColor = vec4(rgb, color.a); } ``` -Note: this initial version uses `uniform vec3` for the lighting params instead of a UBO. This matches the existing `mesh_instanced.frag` pattern (verify by reading it). If `mesh_instanced.frag` actually uses a UBO, change to match. +- [ ] **Step 5.4: Update mesh_modern.vert to emit vWorldPos** -- [ ] **Step 5.4: Read existing mesh_instanced.frag to verify lighting layout** +Add `vWorldPos` output to the vert from Step 5.2. The full vert becomes: -Read `src/AcDream.App/Rendering/Shaders/mesh_instanced.frag`. Compare its lighting uniform shape to the version above. Adjust `mesh_modern.frag` to match (UBO if existing uses UBO, vec3 uniforms if existing uses uniforms). +```glsl +#version 430 core +#extension GL_ARB_bindless_texture : require +#extension GL_ARB_shader_draw_parameters : require + +layout(location = 0) in vec3 aPosition; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aTexCoord; + +struct InstanceData { + mat4 transform; + // Reserved for Phase B.4 follow-up (selection-blink retail-faithful + // highlight): vec4 highlightColor; — extend stride here, increase the + // _instanceSsbo upload size in WbDrawDispatcher, add a flat varying out, + // and consume in mesh_modern.frag. +}; + +struct BatchData { + uvec2 textureHandle; // bindless handle for sampler2DArray + uint textureLayer; // layer index (always 0 for per-instance composites) + uint flags; // reserved +}; + +layout(std430, binding = 0) readonly buffer InstanceBuffer { + InstanceData Instances[]; +}; + +layout(std430, binding = 1) readonly buffer BatchBuffer { + BatchData Batches[]; +}; + +uniform mat4 uViewProjection; + +out vec3 vNormal; +out vec2 vTexCoord; +out vec3 vWorldPos; +out flat uvec2 vTextureHandle; +out flat uint vTextureLayer; + +void main() { + int instanceIndex = gl_BaseInstanceARB + gl_InstanceID; + mat4 model = Instances[instanceIndex].transform; + + vec4 worldPos = model * vec4(aPosition, 1.0); + gl_Position = uViewProjection * worldPos; + + vWorldPos = worldPos.xyz; + vNormal = normalize(mat3(model) * aNormal); + vTexCoord = aTexCoord; + + BatchData b = Batches[gl_DrawIDARB]; + vTextureHandle = b.textureHandle; + vTextureLayer = b.textureLayer; +} +``` + +(The vert from Step 5.2 should be REPLACED with this. The two are the same except for `vWorldPos` and a small comment cleanup.) - [ ] **Step 5.5: Build to verify shaders are copied to output**