phase(N.5b): retire legacy terrain renderers
Deletes:
- TerrainChunkRenderer.cs (454 lines, replaced by TerrainModernRenderer)
- TerrainRenderer.cs (247 lines, older sibling, no production users)
- terrain.vert / terrain.frag (replaced by terrain_modern.{vert,frag})
Removes the temporary Task 8 perf-benchmark toggle (ACDREAM_LEGACY_TERRAIN
env var, _useLegacyTerrain field, parallel _terrainLegacy renderer
instance, [TERRAIN-DIAG/modern|legacy] label suffix). The modern path
is now the only path. Mirror N.5's mandatory-modern amendment: missing
GL_ARB_bindless_texture throws NotSupportedException at startup
(already in place via the BindlessSupport.TryCreate gate).
Three load-bearing research comments preserved verbatim from terrain.vert
into terrain_modern.vert before deletion: the MIN_FACTOR = 0.0 N-dot-L
floor block (cross-ref Lambert brightness split), the aPacked3 bit
layout, the gl_VertexID corner-table 2026-04-21 ConstructPolygons fix.
Also retires the now-orphaned _shader field (legacy terrain pipeline
was its only user).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da56063be5
commit
7dfa2af6c0
6 changed files with 31 additions and 1027 deletions
|
|
@ -1,149 +0,0 @@
|
|||
#version 430 core
|
||||
// Per-cell terrain blending (Phase 3c.4) — ported from WorldBuilder's
|
||||
// Landscape.frag, trimmed of editor-specific features (grid, brush,
|
||||
// walkable-slope highlighting). Phase G extends this with the shared
|
||||
// SceneLighting UBO driving per-vertex sun bake + fragment-stage fog
|
||||
// + lightning flash.
|
||||
|
||||
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;
|
||||
|
||||
uniform sampler2DArray uTerrain; // 33+ layers — TerrainAtlas.GlTexture
|
||||
uniform sampler2DArray uAlpha; // 8+ layers — TerrainAtlas.GlAlphaTexture
|
||||
|
||||
// Shared scene-lighting UBO — fog + flash are consumed here; the per-vertex
|
||||
// AdjustPlanes bake already incorporated sun + ambient.
|
||||
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;
|
||||
};
|
||||
|
||||
// Per-texture tiling repeat count across a cell. WorldBuilder uses
|
||||
// uTexTiling[36] uploaded from the dats; we default to 1.0 (one tile per
|
||||
// cell, 8 tiles across a landblock).
|
||||
const float TILE = 1.0;
|
||||
|
||||
// Three-layer alpha-weighted composite.
|
||||
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 = texture(uTerrain, vec3(baseUV * TILE, pOverlay0.z));
|
||||
if (pOverlay0.w >= 0.0) {
|
||||
vec4 a = texture(uAlpha, vec3(pOverlay0.xy, pOverlay0.w));
|
||||
t0.a = a.a;
|
||||
}
|
||||
}
|
||||
if (h1 > 0.0) {
|
||||
t1 = texture(uTerrain, vec3(baseUV * TILE, pOverlay1.z));
|
||||
if (pOverlay1.w >= 0.0) {
|
||||
vec4 a = texture(uAlpha, vec3(pOverlay1.xy, pOverlay1.w));
|
||||
t1.a = a.a;
|
||||
}
|
||||
}
|
||||
if (h2 > 0.0) {
|
||||
t2 = texture(uTerrain, vec3(baseUV * TILE, pOverlay2.z));
|
||||
if (pOverlay2.w >= 0.0) {
|
||||
vec4 a = texture(uAlpha, 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 = texture(uTerrain, vec3(baseUV * TILE, pRoad0.z));
|
||||
if (pRoad0.w >= 0.0) {
|
||||
vec4 a0 = texture(uAlpha, vec3(pRoad0.xy, pRoad0.w));
|
||||
result.a = 1.0 - a0.a;
|
||||
if (h1 > 0.0 && pRoad1.w >= 0.0) {
|
||||
vec4 a1 = texture(uAlpha, 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 = texture(uTerrain, vec3(vBaseUV * TILE, 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);
|
||||
|
||||
// Composite: base × (1 - ovlA) × (1 - rdA) + ovl × ovlA × (1 - rdA) + road × rdA
|
||||
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);
|
||||
|
||||
// Apply the per-vertex baked sun+ambient.
|
||||
vec3 lit = rgb * min(vLightingRGB, vec3(1.0));
|
||||
|
||||
// Lightning flash — additive.
|
||||
float flash = uFogParams.z;
|
||||
lit += flash * vec3(0.6, 0.6, 0.75);
|
||||
|
||||
// Atmospheric fog.
|
||||
lit = applyFog(lit, vWorldPos);
|
||||
|
||||
fragColor = vec4(lit, 1.0);
|
||||
}
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
#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 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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
|
@ -41,6 +41,18 @@ 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) {
|
||||
|
|
@ -58,6 +70,11 @@ vec4 unpackOverlayLayer(uint texIdxU, uint alphaIdxU, uint rotIdx, vec2 baseUV)
|
|||
}
|
||||
|
||||
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;
|
||||
|
|
@ -65,6 +82,14 @@ void main() {
|
|||
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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue