#version 460 core #extension GL_ARB_bindless_texture : require // Phase N.5b: terrain fragment shader on the modern bindless dispatcher. // Math identical to terrain.frag (Phase 3c per-cell maskBlend3 + // Phase G fog + lightning flash). // // Texture reads go through ACDREAM_SAMPLE_ARRAY, which // tools/ShaderCompiler/VulkanGlslPreamble.cs injects to index the set-2 // descriptor array. GL is deleted (Campaign V slice V11); before that this // macro expanded (via the now-deleted common.glsl) to the uvec2-handle + // sampler2DArray-constructor pattern this shader used on that arm — the // documented "always works" form per the ARB_bindless_texture spec, and the // one that avoided the GL_INVALID_OPERATION the alternative (`uniform // sampler2DArray` set via glProgramUniformHandleARB) produced on at least one // driver in practice. The extension requirement above is dropped for Vulkan // by the compiler's preamble, where it would be an error rather than a no-op. in vec2 vBaseUV; in vec3 vWorldNormal; in vec3 vWorldPos; in vec3 vLightingRGB; in vec4 vOverlay0; in vec4 vOverlay1; in vec4 vOverlay2; in vec4 vRoad0; in vec4 vRoad1; flat in float vBaseTexIdx; out vec4 fragColor; // Campaign V slice V2b (2026-07-27): uTerrainHandle/uAlphaHandle (uvec2, raw // ARB_bindless_texture handles) became uTextureIndexA/uTextureIndexB (slots // into the global texture table, ACDREAM_TEXTURE_HANDLE injected by // tools/ShaderCompiler/VulkanGlslPreamble.cs). Named to match the pinned // GpuPushConstants.TextureIndexA/B fields so V4d's // move to push constants is a rename, not a redesign — there is no // push-constant plumbing yet, so these stay plain uniforms for now. uniform uint uTextureIndexA; uniform uint uTextureIndexB; // Campaign V slice V6f-3: the two atlases are sampled through the // dialect-neutral table read instead of a GL sampler-from-handle constructor. // `sampler2DArray(handle)` is a GL_ARB_bindless_texture form with no Vulkan // equivalent — Vulkan's table is an opaque descriptor array in set 2, and there // is no handle to construct a sampler from. ACDREAM_SAMPLE_ARRAY asks the // question both dialects can answer ("sample table slot N at these // coordinates") and expands to the right thing on each. // // A SAMPLING macro, not a sampler-returning one, for the reason // VulkanGlslPreamble.cs records: under Vulkan the expansion carries `nonuniformEXT` on the indexing // expression, and binding the result to a local sampler2DArray first is exactly // where an implementation may drop that qualifier. The old `#define uTerrain // sampler2DArray(...)` was that shape textually, so keeping it would have // reintroduced the hazard at every use site. #define sampleTerrain(uvw) ACDREAM_SAMPLE_ARRAY(uTextureIndexA, uvw) #define sampleAlpha(uvw) ACDREAM_SAMPLE_ARRAY(uTextureIndexB, uvw) // Campaign V slice V6f-2: the 36 per-layer tiling factors moved out of a loose // `uniform float uTexTiling[36]` and into the uniform buffer GpuBindingModel // reserved binding 3 for. Vulkan GLSL has no default uniform block, so the loose // array was unspellable there, and at 144 bytes of payload it cannot ride the // 96-byte push-constant block either. A uniform buffer is the only legal home, // and the same declaration is legal in both dialects. // // The ELEMENT TYPE is deliberately unchanged. std140 pads every array element to // 16 bytes, so the block is 576 bytes rather than 144, and packing four floats // per vec4 would be tighter — but it would also change every use site below. // Keeping `float uTexTiling[36]` means `uTexTiling[int(layer)]` reads exactly as // it did before, so this commit's pixel gate is measuring the move to a uniform // buffer and nothing else. TerrainTextureTilingTable.UniformBufferBytes and the // stride beside it are the CPU half of this layout. layout(std140, ACDREAM_UBO_SET binding = 3) uniform TerrainTiling { float uTexTiling[36]; }; 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; }; // Retail TexMerge::CopyAndTile (0x00503580) and TexMerge::Merge // (0x005038C0) pass TerrainTex::tex_tiling to every terrain source before // the cell-scale alpha mask is applied. The atlas stores that value by layer. float terrainTiling(float layer) { return uTexTiling[int(layer)]; } vec4 maskBlend3(vec4 t0, vec4 t1, vec4 t2, float h0, float h1, float h2) { float a0 = h0 == 0.0 ? 1.0 : t0.a; float a1 = h1 == 0.0 ? 1.0 : t1.a; float a2 = h2 == 0.0 ? 1.0 : t2.a; float aR = 1.0 - (a0 * a1 * a2); float aRsafe = max(aR, 1e-6); a0 = 1.0 - a0; a1 = 1.0 - a1; a2 = 1.0 - a2; vec3 r0 = (a0 * t0.rgb + (1.0 - a0) * a1 * t1.rgb + (1.0 - a1) * a2 * t2.rgb); return vec4(r0 / aRsafe, aR); } vec4 combineOverlays(vec2 baseUV, vec4 pOverlay0, vec4 pOverlay1, vec4 pOverlay2) { float h0 = pOverlay0.z < 0.0 ? 0.0 : 1.0; float h1 = pOverlay1.z < 0.0 ? 0.0 : 1.0; float h2 = pOverlay2.z < 0.0 ? 0.0 : 1.0; vec4 t0 = vec4(0.0), t1 = vec4(0.0), t2 = vec4(0.0); if (h0 > 0.0) { t0 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay0.z), pOverlay0.z)); if (pOverlay0.w >= 0.0) { vec4 a = sampleAlpha(vec3(pOverlay0.xy, pOverlay0.w)); t0.a = a.a; } } if (h1 > 0.0) { t1 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay1.z), pOverlay1.z)); if (pOverlay1.w >= 0.0) { vec4 a = sampleAlpha(vec3(pOverlay1.xy, pOverlay1.w)); t1.a = a.a; } } if (h2 > 0.0) { t2 = sampleTerrain(vec3(baseUV * terrainTiling(pOverlay2.z), pOverlay2.z)); if (pOverlay2.w >= 0.0) { vec4 a = sampleAlpha(vec3(pOverlay2.xy, pOverlay2.w)); t2.a = a.a; } } return maskBlend3(t0, t1, t2, h0, h1, h2); } vec4 combineRoad(vec2 baseUV, vec4 pRoad0, vec4 pRoad1) { float h0 = pRoad0.z < 0.0 ? 0.0 : 1.0; float h1 = pRoad1.z < 0.0 ? 0.0 : 1.0; vec4 result = vec4(0.0); if (h0 > 0.0) { result = sampleTerrain(vec3(baseUV * terrainTiling(pRoad0.z), pRoad0.z)); if (pRoad0.w >= 0.0) { vec4 a0 = sampleAlpha(vec3(pRoad0.xy, pRoad0.w)); result.a = 1.0 - a0.a; if (h1 > 0.0 && pRoad1.w >= 0.0) { vec4 a1 = sampleAlpha(vec3(pRoad1.xy, pRoad1.w)); result.a = 1.0 - (a0.a * a1.a); } } } return result; } 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); } void main() { vec4 baseColor = vec4(0.0); if (vBaseTexIdx >= 0.0) { baseColor = sampleTerrain(vec3(vBaseUV * terrainTiling(vBaseTexIdx), vBaseTexIdx)); } vec4 overlays = vec4(0.0); if (vOverlay0.z >= 0.0) overlays = combineOverlays(vBaseUV, vOverlay0, vOverlay1, vOverlay2); vec4 roads = vec4(0.0); if (vRoad0.z >= 0.0) roads = combineRoad(vBaseUV, vRoad0, vRoad1); vec3 baseMasked = baseColor.rgb * ((1.0 - overlays.a) * (1.0 - roads.a)); vec3 ovlMasked = overlays.rgb * (overlays.a * (1.0 - roads.a)); vec3 roadMasked = roads.rgb * roads.a; vec3 rgb = clamp(baseMasked + ovlMasked + roadMasked, 0.0, 1.0); vec3 lit = rgb * min(vLightingRGB, vec3(1.0)); float flash = uFogParams.z; lit += flash * vec3(0.6, 0.6, 0.75); lit = applyFog(lit, vWorldPos); fragColor = vec4(lit, 1.0); }