Our LandblockMesh, terrain.vert corner tables, and TerrainSurface.SampleZ
used the OPPOSITE diagonal for each CellSplitDirection enum value from
what ACE (and the decompiled retail client at FUN_00532a50) picks for the
same sign bit. Same formula, same sign-bit mapping, inverted geometry.
Symptom: remote players rendered at server-broadcast Z hovered or clipped
by up to ~1m on sloped cells. Flat cells masked the bug because all four
corner heights were equal so any triangle pair returned the same Z. Live
diagnostic confirmed +0.79m hover on cell (7,5) at lb(AA,B4) — a ~20°
slope — while flat neighbors agreed to floating-point noise.
Three coordinated edits so CPU mesh + GPU corner lookup + CPU sampler all
agree on the retail geometry:
- LandblockMesh: SWtoNE branch now emits {BL,BR,TR}+{BL,TR,TL} (y=x cut),
SEtoNW emits {BL,BR,TL}+{BR,TR,TL} (x+y=1 cut).
- terrain.vert: corner-index tables updated to match.
- TerrainSurface.SampleZ: swapped the two branches' interpolation.
After the fix, 19 live DIAG samples across flat + two slope transitions
all land within 0.01m of server Z. Staircase pattern during remote motion
on slopes is a separate bug (no per-frame collision resolution) and will
be addressed via the transition/FindValidPosition port.
Cross-verified against: ACE LandblockStruct.ConstructPolygons lines 221-
244, decompiled retail FUN_00532a50 (chunk_00530000.c:2235), ClientReference
IsSWtoNECut (tests/AcDream.Core.Tests/Terrain/ClientReference.cs).
Updated test SplitDirection_TerrainSurface_AgreesWith_TerrainBlending
with corrected expectations (Z values swap between the two branches).
All 717 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
138 lines
5.6 KiB
GLSL
138 lines
5.6 KiB
GLSL
#version 430 core
|
|
layout(location = 0) in vec3 aPos;
|
|
layout(location = 1) in vec3 aNormal;
|
|
layout(location = 2) in uvec4 aPacked0; // bytes: baseTex, baseAlpha(255), ovl0Tex, ovl0Alpha
|
|
layout(location = 3) in uvec4 aPacked1; // bytes: ovl1Tex, ovl1Alpha, ovl2Tex, ovl2Alpha
|
|
layout(location = 4) in uvec4 aPacked2; // bytes: road0Tex, road0Alpha, road1Tex, road1Alpha
|
|
layout(location = 5) in uvec4 aPacked3; // bits: rot fields + splitDir (see below)
|
|
|
|
uniform mat4 uView;
|
|
uniform mat4 uProjection;
|
|
|
|
// Phase G.1+G.2: sky/scene UBO. Terrain reads uLights[0] for the sun
|
|
// (slot 0 is reserved) plus uCellAmbient for outdoor ambient; the fog
|
|
// fields are consumed by the fragment stage.
|
|
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;
|
|
};
|
|
|
|
out vec2 vBaseUV;
|
|
out vec3 vWorldNormal;
|
|
out vec3 vWorldPos;
|
|
out vec3 vLightingRGB; // pre-computed sun+ambient contribution for retail-style AdjustPlanes bake
|
|
// Per-layer "UV.xy in cell-local 0..1 space, tex index .z, alpha index .w".
|
|
// Negative .z means "layer not present, skip it in the fragment shader."
|
|
out vec4 vOverlay0;
|
|
out vec4 vOverlay1;
|
|
out vec4 vOverlay2;
|
|
out vec4 vRoad0;
|
|
out vec4 vRoad1;
|
|
flat out float vBaseTexIdx;
|
|
|
|
// Retail's "ambient floor" constant from the decompiled AdjustPlanes
|
|
// path (r13 §7, DAT_00796344). Even a back-lit vertex sees at least
|
|
// this fraction of the sun color — NOT additive with ambient.
|
|
const float MIN_FACTOR = 0.08;
|
|
|
|
// Port of WorldBuilder's Landscape.vert unpackOverlayLayer: sentinel-check
|
|
// 255 → -1 (shader skips), then rotate the cell-local UV by the overlay's
|
|
// 90° rotation count.
|
|
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 (r13 §7):
|
|
// L = max(N · -sunDir, MIN_FACTOR)
|
|
// vertex.color = sun_color * L + ambient_color
|
|
//
|
|
// Slot 0 of the UBO is the sun (directional). We read its forward
|
|
// vector and pre-multiplied color, apply the ambient floor, layer
|
|
// in the scene ambient separately.
|
|
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);
|
|
|
|
gl_Position = uProjection * uView * vec4(aPos, 1.0);
|
|
}
|