acdream/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.frag
Erik 75664805f8 feat(rendering): port retail one-pass detail material
Replace the building and EnvCell detail replay with retail's exact single-pass stage result, including authored surface opacity, squared detail alpha, final-alpha clipping, and the original subset pipeline/order. Arm the ordered walk command in place to close #471, delete the replay pipelines/shaders, and advance prepared content to recipe 9.

Mutation witnesses (each restored before commit):
- X=a*qA: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 174, missing materialAlpha * detail.a * detail.a.
- X*=base alpha: same test line 175, forbidden baseTexel.a found.
- CLIP against base alpha: EnvCellAlphaDrawSourceTests.ClipShaders_UseGreaterEqualForThePerRangeReference line 260, final-X conditional missing.
- second detail draw: EnvCellAlphaDrawSourceTests.DetailOn_EveryEnvCellFamilyDrawsOnceInPlaceWithAuthoredOpacity line 105, Assert.Single saw 2 MDI calls.
- straight-alpha substitution: WalkStaticStreamPopulatorTests.ImmediateBuildingDetail_RetainsOriginalFramebufferFamily line 1244, Additive first failed (only wb-mesh-alpha-1x recorded; InvAlpha also failed).
- omit ordered arm: OrderPreservingSubmitterTests.PrepareThenDraw_OrdinaryBuildingClipBuildingOrdinary_ArmsOnePassInPlace line 305, expected (77,3.5), got (0,0).
- omit atmospheric combine: RetailDetailTextureContractTests.BothShaderFamiliesUseTheSharedOnePassSourceAndDebugPrecedesDetailSample line 173, atmospheric shared include missing.
- drop serialized opacity: ObjectMeshDataSerializerTests.SurfaceOpacity_RoundTripsBitExactlyAndDeterministically line 288, first reported 0.5 bits 1056964608 vs 1065353216.
- stale detail arm: ordered adjacency test line 307, expected following ordinary (0,0), got (77,3.5).
- per-frame surface map: EnvCellAlphaDrawSourceTests.ProductionWholeLeaf_WarmedScanSubmitRhiAndFilteredReplayDoNotAllocate line 178, expected 0 B, got 147456 B.

Verification before commit: shader compiler 23/23; focused App 213/213; Content 75/75; Core Wb 10/10; launcher migration 6/6; Release solution build 0 warnings / 0 errors; git diff --check clean.
2026-09-05 02:03:13 +02:00

136 lines
4.9 KiB
GLSL

#version 430 core
#extension GL_ARB_bindless_texture : require
in vec3 vNormal;
in vec2 vTexCoord;
in vec3 vWorldPos;
in vec3 vAmbientLocalLit;
in vec3 vDirectionalLit;
// Campaign V slice V6e: the table slot, not the bindless handle — see
// mesh_modern.vert. The lookup moved here because a Vulkan varying cannot
// carry a descriptor.
in flat uint vTextureIndex;
in flat uint vTextureLayer;
in flat float vOpacityMultiplier; // #188
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
in flat uint vReceivesDirectionalShadow;
in flat float vSurfaceOpacity;
in flat uint vBatchFlags;
in flat uint vDetailCategory;
#include "directional_shadow_receiver.glsl"
#include "retail_detail_material.glsl"
// uRenderPass values (Phase N.5 Decision 2 — two-pass alpha-test):
// 0 = opaque pass — discard fragments with alpha < 0.95
// (lets the depth write succeed for solid pixels)
// 1 = translucent pass — covers AlphaBlend / Additive / InvAlpha;
// discard alpha >= 0.95 (already drawn opaque) and
// alpha < 0.05 (skip empty fragments — large
// transparent overdraw cost otherwise)
uniform int uRenderPass;
uniform int uLightDebug; // #176 stripe hunt (see mesh_modern.vert) — mode 3 handled here
uniform uint uTextureIndexA;
uniform float uParamA;
uniform float uParamB;
// SceneLighting UBO — IDENTICAL layout to mesh_instanced.frag binding=1.
struct Light {
vec4 posAndKind;
vec4 dirAndRange;
vec4 colorAndIntensity;
vec4 coneAngleEtc;
};
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
Light uLights[8];
vec4 uCellAmbient;
vec4 uFogParams;
vec4 uFogColor;
vec4 uCameraAndTime;
};
// A7 (2026-06-15): per-vertex lighting moved to mesh_modern.vert (Gouraud) to match
// retail's fixed-function per-vertex T&L — a per-pixel evaluation made a hard "spotlight"
// pool. The SceneLighting UBO above is still declared here for fog (uFogParams/uFogColor/
// uCameraAndTime) + the lightning-flash bump; its uLights[]/uCellAmbient are now consumed
// in the vertex shader. The std140 layout must stay identical to the vert + the CPU upload.
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;
bool isRetailClipReference(float value) {
return abs(value - (100.0 / 255.0)) < 0.000001
|| abs(value - (200.0 / 255.0)) < 0.000001;
}
void main() {
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
float alphaCutoff = isRetailClipReference(uParamB) ? uParamB : 0.05;
// Only the authored outdoor directional term is shadowed. Ambient, local
// lights and material selection pulses remain exactly on the retail path.
vec3 surfaceToLight = normalize(uShadowLightDirectionAndSource.xyz);
float directionalVisibility = vReceivesDirectionalShadow != 0u
? acdreamDirectionalShadowVisibility(
vWorldPos,
normalize(vNormal),
uCameraAndTime.xyz,
surfaceToLight)
: 1.0;
vec3 sceneLit = vAmbientLocalLit
+ vDirectionalLit * directionalVisibility;
vec3 lit = vec3(vSelectionLighting.x)
+ vSelectionLighting.y * sceneLit;
// #176 stripe-hunt mode 3: show the raw per-vertex light field (texture
// ignored). Stripes visible HERE = a vertex-lighting artifact; absent =
// the pattern comes from texture/per-pixel machinery. Throwaway diagnostic.
if (uLightDebug == 3) {
if (color.a < alphaCutoff) discard;
FragColor = vec4(min(lit, vec3(1.0)), 1.0);
return;
}
// 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));
bool detailActive = uParamA != 0.0
&& vDetailCategory != 0u
&& (vBatchFlags & 1u) != 0u;
vec3 rgb;
float alpha;
if (detailActive) {
vec4 detail = ACDREAM_SAMPLE_ARRAY(
uTextureIndexA,
vec3(vTexCoord * uParamA, 0.0));
RetailDetailMaterialResult material = acdreamRetailDetailMaterial(
color.rgb,
lit,
detail,
vSurfaceOpacity * vOpacityMultiplier);
rgb = material.rgb;
alpha = material.alpha;
} else {
rgb = color.rgb * lit;
alpha = color.a * vOpacityMultiplier;
}
if (detailActive ? alpha < alphaCutoff : color.a < alphaCutoff)
discard;
rgb = applyFog(rgb, vWorldPos);
FragColor = vec4(rgb, alpha);
}