130 lines
5.6 KiB
GLSL
130 lines
5.6 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;
|
||
|
||
#include "directional_shadow_receiver.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
|
||
|
||
// 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;
|
||
|
||
void main() {
|
||
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
||
|
||
// 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 (color.a < 0.05) discard; // opaque pass — kill truly empty only (A2C)
|
||
} else {
|
||
// Transparent pass.
|
||
//
|
||
// Phase Post-A.5 (ISSUE #52, 2026-05-10): do NOT discard α≥0.95 here.
|
||
// Native AC transparent-flagged surfaces routinely include
|
||
// effectively-opaque pixels — e.g. the Holtburg lifestone crystal core
|
||
// (surface 0x080011DE) which the spawn manifest classifies as
|
||
// transparent (batch.IsTransparent=True) but whose decoded texture
|
||
// alpha lands ≥0.95 across the visible surface. Those pixels still
|
||
// compose correctly under (SrcAlpha, 1-SrcAlpha) alpha-blending, so
|
||
// discarding them here threw away the whole crystal. The original
|
||
// N.5 §2 rationale (high-α fragments belong in the opaque pass) does
|
||
// not apply when the SURFACE is dat-flagged transparent — those
|
||
// pixels can't reach the opaque pass at all.
|
||
//
|
||
// Keep the α<0.05 short-circuit as a fragment-cost optimization
|
||
// (skip fully-empty pixels — saves blend bandwidth on alpha-keyed
|
||
// sprites with large transparent margins).
|
||
if (color.a < 0.05) discard;
|
||
}
|
||
|
||
// 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) {
|
||
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));
|
||
|
||
vec3 rgb = color.rgb * lit;
|
||
rgb = applyFog(rgb, vWorldPos);
|
||
// #188: multiply the FINAL alpha only — the discard thresholds above stay
|
||
// keyed on the raw sampled color.a, so the last few frames of a fade
|
||
// (multiplier crossing under 0.05) still ramp smoothly toward zero rather
|
||
// than popping invisible early against the discard cutoff.
|
||
FragColor = vec4(rgb, color.a * vOpacityMultiplier);
|
||
}
|