acdream/src/AcDream.App/Rendering/Shaders/mesh_atmospheric.vert
Erik 94ddde69af Fix atmospheric receiver light direction
Keep both atmospheric vertex receivers on retail's exact authored, unnormalized uLights direction in every shadow-gate state while preserving the selected celestial direction for fragment shadow projection and volumetrics. Regenerate the affected SPIR-V and amend IA-24.

Proof adds committed-SPIR-V dataflow checks, parent plain/pipeline source pins, and a real Vulkan mesh+terrain pixel witness owned by Lane=Vulkan.

Mutation first failures (all restored):

1. Restoring the mesh shadow/celestial branch failed CommittedProductionAtmosphericReceivers_KeepAuthoredLightAcrossShadowGate at line 27: expected Pixel 128/128/128/255, actual 0/0/0/255.

2. Restoring the terrain shadow/celestial branch failed the same witness at line 28: expected Pixel 128/128/128/255, actual 0/0/0/255.

3. Normalizing the authored mesh direction failed AssertAuthoredHalfIntensity at line 277: expected 126..129, actual 255.

4. Removing atmospheric_volumetric.frag's celestial xyz use failed ProductionShadowAndVolumetricModules_StillNormalizeTheCelestialProjectionDirection at line 61: Assert.Single found no matching member-5 access.

5. Restoring IA-24's old celestial-base-light claim failed BuiltInAndDeclaredShadowGraphsUseTheSameTypedPriorVisibilitySelector at line 1482: the authored unnormalized uLights sole-base-light assertion was absent.
2026-09-05 04:03:17 +02:00

360 lines
18 KiB
GLSL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#version 430 core
#extension GL_ARB_shader_draw_parameters : require
#include "directional_shadow_common.glsl"
#include "atmospheric_common.glsl"
#include "foliage_wind.glsl"
layout(location = 0) in vec3 aPosition;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
struct InstanceData {
mat4 transform;
};
// Campaign V slice V2 (2026-07-27): textureHandle (uvec2, a 64-bit
// GL_ARB_bindless_texture handle) became textureIndex (uint) plus an explicit
// pad word. textureIndex is a slot into the global texture table (set 2,
// injected by tools/ShaderCompiler/VulkanGlslPreamble.cs — see
// ACDREAM_TEXTURE_HANDLE/ACDREAM_SAMPLE_ARRAY) which main() below forwards to
// the fragment stage. The pad word keeps textureLayer/flags at their original
// std430 offsets (8/12), so the struct is still 16 bytes and every existing
// CPU writer's layout is unchanged (GpuBindingModel.GpuBatchDataStrideBytes).
struct BatchData {
uint textureIndex; // slot into the global texture table
float surfaceOpacity; // authored material alpha (not base texture alpha)
uint textureLayer; // layer in the shared WB or pooled composite array
uint flags; // reserved — N.5 dispatcher owns all blend state
// (glBlendFunc per pass). If a future phase wants
// shader-side per-batch additive flag (Decision 2
// fallback), encode it here as bit 0.
};
layout(std430, binding = 0) readonly buffer InstanceBuffer {
InstanceData Instances[];
};
// binding=1 here is the SSBO namespace — distinct from the UBO namespace.
// SceneLighting UBO also uses binding=1 in the fragment shader; GL keeps
// GL_SHADER_STORAGE_BUFFER and GL_UNIFORM_BUFFER binding tables separate.
// Task 10 dispatcher binds:
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, instanceSsbo)
// glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, batchSsbo)
// Existing SceneLightingUboBinding handles the UBO side.
layout(std430, binding = 1) readonly buffer BatchBuffer {
BatchData Batches[];
};
// S3 review fix round 1 (F5): the per-cell screen-space gl_ClipDistance gate
// (Phase U.3's binding=2 CellClip region SSBO) is deleted — the CPU-side
// routing that could ever select a non-zero slot for an instance
// (WbDrawDispatcher's per-frame clip-routing arming call) had ZERO production callers, so every
// instance has always mapped to slot 0 (no-clip) in every shipped build; a
// shader-side clip test against a table that only ever holds the reserved
// no-clip slot clips nothing. binding=3 — LOCAL per-submission slot index,
// zero-based independently of the shared binding=0 world-transform arena —
// stays declared (the CPU side still writes it, always 0) but is no longer
// read here; the instance-buffer layout it occupies is S5's to revisit.
layout(std430, binding = 3) readonly buffer ClipSlotBuf {
uint instanceClipSlot[];
};
// === Fix B (A7 #3): per-OBJECT light selection — minimize_object_lighting =====
// retail picks up-to-8 point/spot lights PER OBJECT by the object's own position
// (minimize_object_lighting 0x0054d480), so a torch always lights the wall it
// sits on, camera-INDEPENDENTLY. The previous single global nearest-8-to-CAMERA
// UBO set (LightManager.Tick) made a wall brighten as the camera approached
// (its torches swapping into the global top-8). Two SSBOs replace that for
// point/spot lights (the SUN + ambient still come from the SceneLighting UBO):
//
// binding=4 — GLOBAL point/spot light array, uploaded once per frame from
// LightManager.PointSnapshot. The index of a light here is stable for the frame.
// binding=5 — LOCAL per-submission light SET: MaxLightsPerObject(8) int
// indices per instance INTO gLights[] (-1 = unused slot), addressed from
// local instance zero even when binding=0 begins with a shared shadow prefix.
// WbDrawDispatcher fills it once per entity (the set is constant across the
// entity's parts/tuples).
struct GlobalLight {
vec4 posAndKind;
vec4 dirAndRange;
vec4 colorAndIntensity;
vec4 coneAngleEtc;
};
layout(std430, binding = 4) readonly buffer GlobalLightBuf {
GlobalLight gLights[];
};
layout(std430, binding = 5) readonly buffer InstanceLightSetBuf {
int instanceLightIdx[]; // 8 per instance; -1 = unused
};
// #142: LOCAL per-submission "indoor" flag, 1 per instance. 1 = object
// parented to an EnvCell (skip the sun — retail's useSunlight==0 interior
// stage); 0 = outdoor object (gets the sun). It is indexed from local zero,
// independently of binding=0's shared world-transform prefix.
// Read ONLY inside the uniform `uLightingMode == 0` branch below, so the mode-1
// (EnvCell shell) path provably never touches it — EnvCellRenderer need not bind it.
layout(std430, binding = 6) readonly buffer InstanceIndoorBuf {
uint instanceIndoor[];
};
// #188: LOCAL per-submission opacity multiplier, 1 per instance, indexed from
// local zero independently of binding=0's shared transform arena. 1.0 = unmodified; <1.0
// while a TransparentPartHook translucency fade is in flight for the
// entity/part this instance belongs to (e.g. the "fading wall" secret-
// passage doors). Multiplied against the sampled texture alpha in
// mesh_modern.frag.
layout(std430, binding = 7) readonly buffer InstanceAlphaBuf {
float instanceAlpha[];
};
// Retail SmartBox click confirmation. One LOCAL vec2 per OBJECT instance:
// x = CMaterial luminosity, y = CMaterial diffuse. Normal
// rendering is (0,1); SmartBox alternates LOW=(0,.35) and HIGH=(.99,1).
// EnvCellRenderer uses uLightingMode=1 and deliberately never reads this
// object-only binding.
layout(std430, binding = 8) readonly buffer InstanceSelectionLightingBuf {
vec2 instanceSelectionLighting[];
};
layout(std430, binding = 9) readonly buffer InstanceDetailCategoryBuf {
uint instanceDetailCategory[];
};
uniform mat4 uViewProjection;
// Absolute transform prefix in the shared shadow/world pose arena. Every
// parallel per-instance array remains local to this submission, so only the
// transform lookup keeps the absolute index.
uniform uint uTextureIndexB;
// Phase Post-A.5 (ISSUE #52, 2026-05-10): per-pass offset into Batches[].
// gl_DrawIDARB resets to 0 at the start of each glMultiDrawElementsIndirect
// call, so the transparent pass — which begins later in the indirect buffer
// — was fetching Batches[0..transparentCount) instead of its actual section
// at Batches[opaqueCount..end). The lifestone crystal (a transparent draw)
// ended up reading the FIRST OPAQUE batch's TextureHandle every frame. As
// the camera moved and the opaque front-to-back sort reordered which group
// landed at BatchData[0], the lifestone's apparent texture flickered to
// whatever was first — frequently the player character's body parts.
//
// WbDrawDispatcher.Draw sets this to 0 before the opaque MDI call and to
// _opaqueDrawCount before the transparent MDI call, matching WorldBuilder's
// uDrawIDOffset pattern in BaseObjectRenderManager.cs line 845.
uniform int uDrawIDOffset;
uniform int uLightingMode; // A7 Fix D: 0 = OBJECT (plain Lambert + sun), 1 = ENVCELL (half-Lambert wrap, no sun)
// #176 stripe-hunt isolation modes (ACDREAM_LIGHT_DEBUG, throwaway diagnostic):
// 0 = off; 1 = ambient-only vLit (all point/sun contributions killed);
// 2 = DYNAMIC point lights killed (purples + viewer fill off, statics stay);
// 3 = handled in the frag (raw vLit visualization, texture ignored).
uniform int uLightDebug;
// SceneLighting UBO — binding=1 in the UBO namespace (GL keeps the SSBO and UBO
// binding tables separate, so this coexists with the binding=1 BatchBuffer SSBO
// above). IDENTICAL std140 layout to mesh_modern.frag.
//
// A7 (2026-06-15): lighting moved from the FRAGMENT shader to HERE (per-VERTEX) so
// torch/point lights Gouraud-interpolate across each triangle the way retail's
// fixed-function T&L does (D3D DrawEnvCell vertex bake + minimize_object_lighting for
// objects). A per-PIXEL evaluation made a tight bright "spotlight" pool on flat walls;
// per-vertex spreads it into a soft, broad gradient with no hard edge.
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;
};
// Faithful calc_point_light (0x0059c8b0) contribution from ONE point/spot light —
// the wrap + norm shape, factored out so the per-object SSBO loop shares it. D =
// light vertex, used UN-normalised (length = dist); N is the unit vertex normal.
// Returns the RGB to ADD, already per-channel capped to the light's own colour.
vec3 pointContribution(vec3 N, vec3 worldPos, GlobalLight L) {
int kind = int(L.posAndKind.w);
vec3 toL = L.posAndKind.xyz - worldPos; // D (un-normalised)
float distsq = dot(toL, toL);
float d = sqrt(distsq);
float range = L.dirAndRange.w; // falloff_eff = Falloff × 1.3 (static) / × 1.5 (dynamic)
if (d >= range || range <= 1e-4) return vec3(0.0);
float intensity = L.colorAndIntensity.w;
vec3 baseCol = L.colorAndIntensity.xyz;
// #143: DYNAMIC lights (viewer fill, portal, server-object lights — flagged by
// coneAngleEtc.y==1 from GlobalLightPacker) use retail's D3D hardware attenuation
// (config_hardware_light 0x0059ad30): a POINT light is given Attenuation1=1 ⇒
// att = 1/d (inverse-LINEAR), plain Lambert N·L, hard range cutoff. That spreads
// softly across the room (the portal tint, the viewer fill) instead of the static
// bake's 1/d³ distance-cube, which makes a tight concentrated pool. No per-light
// cap — D3D accumulates then saturates, which accumulateLights does via min(pointAcc,1).
if (L.coneAngleEtc.y > 0.5) {
if (uLightDebug == 2) return vec3(0.0); // #176 stripe hunt: dynamics killed
vec3 Ldir = toL / max(d, 1e-4);
float ndl = max(0.0, dot(N, Ldir));
if (ndl <= 0.0) return vec3(0.0);
if (kind == 2) { // dynamic spot: hard cos-cone gate
if (dot(-Ldir, L.dirAndRange.xyz) <= cos(L.coneAngleEtc.x * 0.5)) return vec3(0.0);
}
return (intensity * ndl / max(d, 1e-3)) * baseCol; // att = 1/d
}
// ── STATIC dat-baked lights: retail's per-vertex bake (calc_point_light 0x0059c8b0) ──
// A7 Fix D D-3: angular term by lighting path. ENVCELL bake (mode 1) keeps the
// half-Lambert wrap (lights surfaces angled away, retail calc_point_light); OBJECT
// mode (0) uses plain Lambert max(0,N·L) so a torch BEHIND a character contributes
// nothing (retail's hardware path). toL is un-normalised (length d).
float angular = (uLightingMode == 1)
? (1.0 / 1.5) * (dot(N, toL) + 0.5 * d) // half-Lambert wrap (EnvCell bake)
: max(0.0, dot(N, toL)); // plain Lambert (object/hardware)
if (angular <= 0.0) return vec3(0.0);
// NORM branch (distance-cube): >1 m → distsq·d ≈ inverse-square soft far halo;
// <1 m → just d (dodge the near singularity). "Punchy near, soft far."
float norm = (distsq > 1.0) ? (distsq * d) : d;
float scale = (1.0 - d / range) * intensity * (angular / norm);
if (kind == 2) {
// Spotlight: hard-edged cos-cone gate layered on the point ramp.
vec3 Ldir = toL / max(d, 1e-4);
float cos_edge = cos(L.coneAngleEtc.x * 0.5);
float cos_l = dot(-Ldir, L.dirAndRange.xyz);
if (cos_l <= cos_edge) scale = 0.0;
}
// Per-channel no-blowout cap to the light's OWN colour (un-intensity-scaled):
// a single light can't push a channel past its colour. Summed lit clamped in frag.
return min(scale * baseCol, baseCol);
}
vec3 accumulateAmbientLocalLights(
vec3 N,
vec3 worldPos,
int instanceIndex,
out vec3 directionalLit)
{
vec3 lit = uCellAmbient.xyz;
directionalLit = vec3(0.0);
if (uLightDebug == 1) return lit; // #176 stripe hunt: ambient only
// SUN / directional — OBJECT path only (mode 0). retail's EnvCell path
// (minimize_envcell_lighting) enables only dynamic lights, NEVER the sun, so
// EnvCell walls (mode 1) get no directional sun wash (A7 Fix D D-4).
// #142: within mode 0, also skip the sun for indoor objects (ParentCellId is an
// EnvCell). This mirrors retail's per-draw-stage useSunlight toggle: the interior
// stage runs useSunlightSet(0) (PView::DrawCells 0x005a49f3), so indoor objects
// get no sun even in windowed buildings where the player's frame is not sun-killed.
if (uLightingMode == 0) {
if (instanceIndoor[instanceIndex] == 0u) { // #142: outdoor objects only get the sun
// Campaign OVERHAUL S5 #469: retail owns one authored outdoor
// directional-light channel (SkyDesc::GetLighting ->
// LScape::set_landscape_lighting -> Render::world_lights.sunlight).
// Keep the EXACT plain mesh_modern.vert direction in every shadow
// gate state: negate uLights[].dirAndRange.xyz and preserve its
// authored, unnormalized magnitude. The selected celestial source
// remains pack-only projection input in the fragment receiver and
// volumetric shaders; it never replaces base vertex lighting.
int activeLights = int(uCellAmbient.w);
for (int i = 0; i < 8; ++i) {
if (i >= activeLights) break;
if (int(uLights[i].posAndKind.w) != 0) continue; // directional only
vec3 Ldir = -uLights[i].dirAndRange.xyz;
float ndl = max(0.0, dot(N, Ldir));
directionalLit += uLights[i].colorAndIntensity.xyz
* uLights[i].colorAndIntensity.w * ndl;
}
}
}
// POINT / SPOT torches: their OWN accumulator (A7 Fix D, D-1). Retail's
// SetStaticLightingVertexColors sums the static point lights from BLACK and
// clamps the SUM to [0,1] before anything else (a baked emissive term), so a
// few warm intensity-100 torches can't push the whole pixel to white the way
// folding them into ambient+sun did. Mirrors LightBake.ComputeVertexColor
// (LightBakeConformanceTests). Per-light cap inside pointContribution is unchanged.
vec3 pointAcc = vec3(0.0);
int base = instanceIndex * 8;
for (int k = 0; k < 8; ++k) {
int gi = instanceLightIdx[base + k];
if (gi < 0) continue;
pointAcc += pointContribution(N, worldPos, gLights[gi]);
}
lit += min(pointAcc, vec3(1.0)); // clamp the torch sum on its own (retail baked emissive)
return lit; // frag still does the final min(lit, 1.0)
}
out vec3 vNormal;
out vec2 vTexCoord;
out vec3 vWorldPos;
out vec3 vAmbientLocalLit; // authored ambient + capped local/point lights
out vec3 vDirectionalLit; // authored outdoor directional sun, shadowable
// Campaign V slice V6e: was `flat uvec2 vTextureHandle` — a raw 64-bit
// GL_ARB_bindless_texture handle handed across the stage boundary. A varying
// cannot carry a Vulkan descriptor, so what travels is the table SLOT and the
// fragment stage does the lookup (see mesh_modern.frag). Under GL the value
// sampled is bit-for-bit the one the vertex stage used to forward; the SSBO
// read simply happens one stage later, and `flat` keeps it one scalar load per
// primitive rather than per fragment.
out flat uint vTextureIndex;
out flat uint vTextureLayer;
out flat float vOpacityMultiplier; // #188
out flat vec2 vSelectionLighting;
out flat uint vReceivesDirectionalShadow;
out flat float vSurfaceOpacity;
out flat uint vBatchFlags;
out flat uint vDetailCategory;
void main() {
int transformIndex = gl_BaseInstanceARB + gl_InstanceID;
int instanceIndex = transformIndex - int(uTextureIndexB);
mat4 model = Instances[transformIndex].transform;
vOpacityMultiplier = instanceAlpha[instanceIndex]; // #188
vSelectionLighting = (uLightingMode == 0)
? instanceSelectionLighting[instanceIndex]
: vec2(0.0, 1.0);
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
vec4 worldPos = model * vec4(aPosition, 1.0);
// Campaign VM VM6: weather-driven foliage sway. acdreamFoliageDisplace is
// a no-op unless b.flags carries the cutout (0x2) or trunk (0x4)
// classification bit — see foliage_wind.glsl. Applied before gl_Position
// so lighting and the fragment stage both see the displaced position; the
// four directional-shadow caster vertex shaders call the identical
// include so the shadow moves with the same vertex.
worldPos.xyz = acdreamFoliageDisplace(
worldPos.xyz,
model[3].xyz,
b.flags,
uAtmosphereClockWind,
uAtmosphereWindAmplitude);
gl_Position = uViewProjection * worldPos;
vWorldPos = worldPos.xyz;
vNormal = normalize(mat3(model) * aNormal);
vAmbientLocalLit = accumulateAmbientLocalLights(
vNormal,
vWorldPos,
instanceIndex,
vDirectionalLit);
// EnvCell-parented objects keep the authored indoor result. The separate
// EnvCell shell renderer never selects this receiver variant at all.
vReceivesDirectionalShadow = (uLightingMode == 0
&& instanceIndoor[instanceIndex] == 0u)
? 1u
: 0u;
vTexCoord = aTexCoord;
// Campaign V slice V6e: forward the table SLOT untouched. V2 looked the
// handle up here and passed the handle; the lookup now lives at the sample
// site in mesh_modern.frag, which is the only form Vulkan can express.
// (b was fetched earlier, before worldPos, so acdreamFoliageDisplace
// could read its flags — Campaign VM VM6.)
vTextureIndex = b.textureIndex;
vTextureLayer = b.textureLayer;
vSurfaceOpacity = b.surfaceOpacity;
vBatchFlags = b.flags;
vDetailCategory = instanceDetailCategory[instanceIndex];
}