Adds the GPU mechanism to clip drawing to a per-cell screen-space convex
region via gl_ClipDistance, consumed by the mesh + terrain vertex shaders.
This is the MECHANISM only — every instance defaults to slot 0 (no-clip /
pass-all) and terrain to count 0, so the running game renders IDENTICALLY to
pre-U.3 (verified: offline launch compiles both shaders and reaches steady
state; no GL errors). U.4 populates real clip data from portal visibility.
Binding contract (define once, both sides obey):
- mesh_modern.vert: SSBO binding=2 CellClip[] (shared per-frame regions, slot 0
reserved no-clip) + SSBO binding=3 uint[] per-instance slot, indexed by the
IDENTICAL gl_BaseInstanceARB+gl_InstanceID used for binding=0. binding=0/1
untouched.
- terrain_modern.vert: UBO binding=2 TerrainClip { int count; vec4 planes[8]; }
for the single OutsideView region (UBO namespace; SceneLighting is UBO
binding=1, so binding=2 is free and does not collide with the mesh SSBO
binding=2). count 0 = ungated.
- Both redeclare out gl_PerVertex { vec4 gl_Position; float gl_ClipDistance[8]; }
and set unused planes (i >= count) to +1.0 so they pass everything.
CellClip std430 layout (144 bytes/slot): count@0, 3 pad uints@4/8/12,
planes[8]@16 (vec4 stride 16). Terrain UBO std140: count@0 (padded to 16),
planes[8]@16 → 144 bytes. Verified by ClipFrameLayoutTests (8 new tests).
Pieces:
- ClipFrame: per-frame container + uploader for the SHARED clip data (binding=2
SSBO + terrain UBO). NoClip() = slot 0 + terrain count 0. AppendSlot /
SetTerrainClip pack std430/std140 bytes for U.4. UploadShared binds both.
- WbDrawDispatcher + EnvCellRenderer: each owns its binding=3 zero buffer
(all-zeros sized to its instance count → slot 0), re-binds binding=2 from the
shared ClipFrame id (or an internal no-clip fallback if unwired) before MDI.
gl_ClipDistance is per-vertex, so the single glMultiDrawElementsIndirect per
group is preserved — no draw splitting.
- TerrainModernRenderer: binds the terrain clip UBO (shared or no-clip fallback)
before its draw.
- GameWindow: glEnable(GL_CLIP_DISTANCE0..7) once at init (unused planes pass-all
so always-on avoids per-draw thrash); per frame builds ClipFrame.NoClip(),
UploadShared, and hands the buffer ids to the three renderers (tiny diff; U.4
swaps NoClip() for the real portal-visibility frame).
Gate: dotnet build green; App suite 134/134; offline launch confirms both
shaders compile + link with no GL errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
177 lines
7.6 KiB
GLSL
177 lines
7.6 KiB
GLSL
#version 460 core
|
|
#extension GL_ARB_bindless_texture : require
|
|
|
|
// Phase N.5b: terrain shader on the modern bindless dispatcher.
|
|
// Math identical to terrain.vert (Phase 3c per-cell mesh + Phase G AdjustPlanes
|
|
// lighting). The only structural change is the version + bindless extension
|
|
// — sampler access in the fragment stage is unchanged at the GLSL level.
|
|
|
|
layout(location = 0) in vec3 aPos;
|
|
layout(location = 1) in vec3 aNormal;
|
|
layout(location = 2) in uvec4 aPacked0;
|
|
layout(location = 3) in uvec4 aPacked1;
|
|
layout(location = 4) in uvec4 aPacked2;
|
|
layout(location = 5) in uvec4 aPacked3;
|
|
|
|
uniform mat4 uView;
|
|
uniform mat4 uProjection;
|
|
|
|
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;
|
|
};
|
|
|
|
// === Phase U.3: terrain screen-space clip gate (OutsideView region) ===========
|
|
// Terrain is a single global region (the OutsideView), so it needs one set of
|
|
// clip planes, not a per-instance slot table like the mesh shader. A std140 UBO
|
|
// at binding=2 carries it. The UBO binding namespace is distinct from the SSBO
|
|
// binding namespace, so this does NOT collide with the mesh shader's SSBO
|
|
// binding=2 — and within THIS shader binding=1 (SceneLighting) is the only other
|
|
// UBO, leaving binding=2 free. uTerrainClipCount == 0 (the U.3 default) ungates
|
|
// terrain entirely (the second loop sets all 8 distances to +1.0). Uploaded by
|
|
// ClipFrame.UploadShared each frame; TerrainModernRenderer binds it before draw.
|
|
layout(std140, binding = 2) uniform TerrainClip {
|
|
int uTerrainClipCount;
|
|
vec4 uTerrainClipPlanes[8];
|
|
};
|
|
|
|
// Core profile: redeclare gl_PerVertex so writing gl_ClipDistance[] is legal.
|
|
// Sized 8 to match GL_MAX_CLIP_DISTANCES >= 8. Host enables GL_CLIP_DISTANCE0..7
|
|
// once at startup; unused planes are set to +1.0 below so they pass everything.
|
|
out gl_PerVertex {
|
|
vec4 gl_Position;
|
|
float gl_ClipDistance[8];
|
|
};
|
|
|
|
out vec2 vBaseUV;
|
|
out vec3 vWorldNormal;
|
|
out vec3 vWorldPos;
|
|
out vec3 vLightingRGB;
|
|
out vec4 vOverlay0;
|
|
out vec4 vOverlay1;
|
|
out vec4 vOverlay2;
|
|
out vec4 vRoad0;
|
|
out vec4 vRoad1;
|
|
flat out float vBaseTexIdx;
|
|
|
|
// Retail's N·L floor from FUN_00532440 lines 2119/2138/2157/2176 at
|
|
// chunk_00530000.c (AdjustPlanes). The decompile reads:
|
|
// if (fVar3 < DAT_00796344) fVar3 = DAT_00796344;
|
|
// applied to the clamped Lambert result BEFORE it's multiplied into
|
|
// dirColor. DAT_00796344's exact literal isn't pinned by the decompile
|
|
// but every other "floor" use in retail clamps negatives to zero (the
|
|
// physically-correct Lambert half-space). Our previous 0.08 was a
|
|
// defensive guess from early acdream days that made back-lit terrain
|
|
// visibly brighter than retail (user-observed 2026-04-24 "acdream
|
|
// warmer / less blue than retail"). Reverting to 0.0 matches retail
|
|
// per the decompile and lets ambient fill in the back side.
|
|
// Cross-ref: docs/research/2026-04-24-lambert-brightness-split.md.
|
|
const float MIN_FACTOR = 0.0;
|
|
|
|
vec4 unpackOverlayLayer(uint texIdxU, uint alphaIdxU, uint rotIdx, vec2 baseUV) {
|
|
float texIdx = float(texIdxU);
|
|
float alphaIdx = float(alphaIdxU);
|
|
if (texIdx >= 254.0) texIdx = -1.0;
|
|
if (alphaIdx >= 254.0) alphaIdx = -1.0;
|
|
|
|
vec2 rotatedUV = baseUV;
|
|
if (rotIdx == 1u) rotatedUV = vec2(1.0 - baseUV.y, baseUV.x);
|
|
else if (rotIdx == 2u) rotatedUV = vec2(1.0 - baseUV.x, 1.0 - baseUV.y);
|
|
else if (rotIdx == 3u) rotatedUV = vec2( baseUV.y, 1.0 - baseUV.x);
|
|
|
|
return vec4(rotatedUV.x, rotatedUV.y, texIdx, alphaIdx);
|
|
}
|
|
|
|
void main() {
|
|
// Unpack rotation fields from aPacked3. Bit layout (data3):
|
|
// .x (byte 0): bits 0-1 rotBase (unused), 2-3 rotOvl0, 4-5 rotOvl1, 6-7 rotOvl2
|
|
// .y (byte 1): bits 0-1 rotRd0 (= data3 bit 8-9),
|
|
// bits 2-3 rotRd1 (= data3 bit 10-11),
|
|
// bit 4 splitDir (= data3 bit 12)
|
|
uint rotOvl0 = (aPacked3.x >> 2u) & 3u;
|
|
uint rotOvl1 = (aPacked3.x >> 4u) & 3u;
|
|
uint rotOvl2 = (aPacked3.x >> 6u) & 3u;
|
|
uint rotRd0 = aPacked3.y & 3u;
|
|
uint rotRd1 = (aPacked3.y >> 2u) & 3u;
|
|
uint splitDir= (aPacked3.y >> 4u) & 1u;
|
|
|
|
// Derive which of the 4 cell corners this vertex represents from
|
|
// gl_VertexID % 6. The CPU-side LandblockMesh emits vertices in a
|
|
// specific order for each split direction; the tables below must stay
|
|
// in lockstep with LandblockMesh.Build's SWtoNE/SEtoNW branches.
|
|
// 2026-04-21 fix: geometry re-derived to match ACE's ConstructPolygons
|
|
// convention. SWtoNE (cut BL→TR, y=x diagonal) now maps to the {BL,BR,TR}
|
|
// + {BL,TR,TL} triangle pair; SEtoNW (cut BR→TL, x+y=1 diagonal) maps to
|
|
// {BL,BR,TL} + {BR,TR,TL}.
|
|
int vIdx = gl_VertexID % 6;
|
|
int corner = 0;
|
|
if (splitDir == 0u) {
|
|
// SWtoNE order: BL, BR, TR, BL, TR, TL → corners 0, 1, 2, 0, 2, 3
|
|
if (vIdx == 0) corner = 0;
|
|
else if (vIdx == 1) corner = 1;
|
|
else if (vIdx == 2) corner = 2;
|
|
else if (vIdx == 3) corner = 0;
|
|
else if (vIdx == 4) corner = 2;
|
|
else corner = 3;
|
|
} else {
|
|
// SEtoNW order: BL, BR, TL, BR, TR, TL → corners 0, 1, 3, 1, 2, 3
|
|
if (vIdx == 0) corner = 0;
|
|
else if (vIdx == 1) corner = 1;
|
|
else if (vIdx == 2) corner = 3;
|
|
else if (vIdx == 3) corner = 1;
|
|
else if (vIdx == 4) corner = 2;
|
|
else corner = 3;
|
|
}
|
|
|
|
vec2 baseUV;
|
|
if (corner == 0) baseUV = vec2(0.0, 1.0);
|
|
else if (corner == 1) baseUV = vec2(1.0, 1.0);
|
|
else if (corner == 2) baseUV = vec2(1.0, 0.0);
|
|
else baseUV = vec2(0.0, 0.0);
|
|
|
|
vBaseUV = baseUV;
|
|
vWorldPos = aPos;
|
|
vWorldNormal = normalize(aNormal);
|
|
|
|
// Retail AdjustPlanes bake (terrain.vert:124-134 — identical math).
|
|
vec3 sunDir = uLights[0].dirAndRange.xyz;
|
|
vec3 sunCol = uLights[0].colorAndIntensity.xyz * uLights[0].colorAndIntensity.w;
|
|
float L = max(dot(vWorldNormal, -sunDir), MIN_FACTOR);
|
|
vLightingRGB = sunCol * L + uCellAmbient.xyz;
|
|
|
|
float baseTex = float(aPacked0.x);
|
|
if (baseTex >= 254.0) baseTex = -1.0;
|
|
vBaseTexIdx = baseTex;
|
|
|
|
vOverlay0 = unpackOverlayLayer(aPacked0.z, aPacked0.w, rotOvl0, baseUV);
|
|
vOverlay1 = unpackOverlayLayer(aPacked1.x, aPacked1.y, rotOvl1, baseUV);
|
|
vOverlay2 = unpackOverlayLayer(aPacked1.z, aPacked1.w, rotOvl2, baseUV);
|
|
vRoad0 = unpackOverlayLayer(aPacked2.x, aPacked2.y, rotRd0, baseUV);
|
|
vRoad1 = unpackOverlayLayer(aPacked2.z, aPacked2.w, rotRd1, baseUV);
|
|
|
|
// Retail zFightTerrainAdjust (acclient_2013_pseudo_c.txt:1120769 = 0.00999999978,
|
|
// applied per terrain vertex inside ACRender::landPolysDraw at line 702254,
|
|
// address 006b6402). Render terrain 1 cm below its physical Z so coplanar
|
|
// building floors win the depth test. Physics path is unaffected — it reads
|
|
// the un-nudged heightmap via TerrainSurface.SampleZ.
|
|
// Closes issue #100; supersedes the hiddenTerrainCells cell-collapse hack.
|
|
vec3 terrainPos = vec3(aPos.xy, aPos.z - 0.01);
|
|
gl_Position = uProjection * uView * vec4(terrainPos, 1.0);
|
|
|
|
// Phase U.3: terrain clip gate against the single OutsideView region. With
|
|
// uTerrainClipCount == 0 (U.3 default) the first loop is skipped and the
|
|
// second sets all 8 distances to +1.0 ⇒ no clipping ⇒ identical terrain.
|
|
for (int i = 0; i < uTerrainClipCount; ++i)
|
|
gl_ClipDistance[i] = dot(uTerrainClipPlanes[i], gl_Position);
|
|
for (int i = uTerrainClipCount; i < 8; ++i)
|
|
gl_ClipDistance[i] = 1.0;
|
|
}
|