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
|
|
@ -19,17 +19,8 @@ public sealed class GameWindow : IDisposable
|
||||||
private GL? _gl;
|
private GL? _gl;
|
||||||
private IInputContext? _input;
|
private IInputContext? _input;
|
||||||
private TerrainModernRenderer? _terrain;
|
private TerrainModernRenderer? _terrain;
|
||||||
// Phase N.5b benchmark toggle (TEMPORARY — removed in Task 9 along with TerrainChunkRenderer):
|
|
||||||
// when ACDREAM_LEGACY_TERRAIN=1, route Draw through the legacy renderer
|
|
||||||
// for direct perf comparison. Both renderers are constructed and fed
|
|
||||||
// AddLandblock/RemoveLandblock; only one is drawn per frame.
|
|
||||||
private TerrainChunkRenderer? _terrainLegacy;
|
|
||||||
private bool _useLegacyTerrain;
|
|
||||||
private Shader? _shader;
|
|
||||||
/// <summary>Phase N.5b: terrain_modern.vert/.frag program. Owned by
|
/// <summary>Phase N.5b: terrain_modern.vert/.frag program. Owned by
|
||||||
/// <see cref="_terrain"/> at draw time but allocated + disposed here. Lives
|
/// <see cref="_terrain"/> at draw time but allocated + disposed here.</summary>
|
||||||
/// in parallel with <see cref="_shader"/> (legacy terrain.vert/.frag) until
|
|
||||||
/// Task 9 deletes the legacy renderer.</summary>
|
|
||||||
private Shader? _terrainModernShader;
|
private Shader? _terrainModernShader;
|
||||||
private CameraController? _cameraController;
|
private CameraController? _cameraController;
|
||||||
private IMouse? _capturedMouse;
|
private IMouse? _capturedMouse;
|
||||||
|
|
@ -985,13 +976,10 @@ public sealed class GameWindow : IDisposable
|
||||||
_gl.Enable(EnableCap.DepthTest);
|
_gl.Enable(EnableCap.DepthTest);
|
||||||
|
|
||||||
string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
|
string shadersDir = Path.Combine(AppContext.BaseDirectory, "Rendering", "Shaders");
|
||||||
_shader = new Shader(_gl,
|
|
||||||
Path.Combine(shadersDir, "terrain.vert"),
|
|
||||||
Path.Combine(shadersDir, "terrain.frag"));
|
|
||||||
|
|
||||||
// Phase N.5b: terrain_modern shader pair — bindless texture handles +
|
// Phase N.5b: terrain_modern shader pair — bindless texture handles +
|
||||||
// glMultiDrawElementsIndirect dispatch path. Loaded in parallel with
|
// glMultiDrawElementsIndirect dispatch path. The only terrain shader
|
||||||
// the legacy `_shader`; Task 9 will retire the legacy program.
|
// since Task 9 retired the legacy terrain.vert/.frag program.
|
||||||
_terrainModernShader = new Shader(_gl,
|
_terrainModernShader = new Shader(_gl,
|
||||||
Path.Combine(shadersDir, "terrain_modern.vert"),
|
Path.Combine(shadersDir, "terrain_modern.vert"),
|
||||||
Path.Combine(shadersDir, "terrain_modern.frag"));
|
Path.Combine(shadersDir, "terrain_modern.frag"));
|
||||||
|
|
@ -1451,10 +1439,6 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
_terrain = new TerrainModernRenderer(_gl, _bindlessSupport, _terrainModernShader!, terrainAtlas);
|
_terrain = new TerrainModernRenderer(_gl, _bindlessSupport, _terrainModernShader!, terrainAtlas);
|
||||||
|
|
||||||
// Phase N.5b benchmark toggle (TEMPORARY — see field declaration).
|
|
||||||
_useLegacyTerrain = Environment.GetEnvironmentVariable("ACDREAM_LEGACY_TERRAIN") == "1";
|
|
||||||
_terrainLegacy = new TerrainChunkRenderer(_gl, _shader!, terrainAtlas);
|
|
||||||
|
|
||||||
int centerX = (int)((centerLandblockId >> 24) & 0xFFu);
|
int centerX = (int)((centerLandblockId >> 24) & 0xFFu);
|
||||||
int centerY = (int)((centerLandblockId >> 16) & 0xFFu);
|
int centerY = (int)((centerLandblockId >> 16) & 0xFFu);
|
||||||
|
|
||||||
|
|
@ -1612,7 +1596,6 @@ public sealed class GameWindow : IDisposable
|
||||||
_lightingSink.UnregisterOwner(ent.Id);
|
_lightingSink.UnregisterOwner(ent.Id);
|
||||||
}
|
}
|
||||||
_terrain?.RemoveLandblock(id);
|
_terrain?.RemoveLandblock(id);
|
||||||
_terrainLegacy?.RemoveLandblock(id); // Phase N.5b benchmark toggle (TEMPORARY).
|
|
||||||
_physicsEngine.RemoveLandblock(id);
|
_physicsEngine.RemoveLandblock(id);
|
||||||
_cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu);
|
_cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu);
|
||||||
});
|
});
|
||||||
|
|
@ -4762,7 +4745,7 @@ public sealed class GameWindow : IDisposable
|
||||||
float localY = spawn.LocalPosition.Y;
|
float localY = spawn.LocalPosition.Y;
|
||||||
// Prefer the physics engine's terrain sampler (TerrainSurface.SampleZ)
|
// Prefer the physics engine's terrain sampler (TerrainSurface.SampleZ)
|
||||||
// — it uses the same AC2D render split-direction formula the
|
// — it uses the same AC2D render split-direction formula the
|
||||||
// TerrainChunkRenderer uses for the visible terrain mesh. This
|
// TerrainModernRenderer uses for the visible terrain mesh. This
|
||||||
// guarantees trees are placed on the SAME Z height the player
|
// guarantees trees are placed on the SAME Z height the player
|
||||||
// walks on. If physics hasn't registered this landblock yet,
|
// walks on. If physics hasn't registered this landblock yet,
|
||||||
// fall back to the local bilinear sample.
|
// fall back to the local bilinear sample.
|
||||||
|
|
@ -5133,7 +5116,6 @@ public sealed class GameWindow : IDisposable
|
||||||
var meshData = AcDream.Core.Terrain.LandblockMesh.Build(
|
var meshData = AcDream.Core.Terrain.LandblockMesh.Build(
|
||||||
lb.Heightmap, lbXu, lbYu, _heightTable, _blendCtx, _surfaceCache);
|
lb.Heightmap, lbXu, lbYu, _heightTable, _blendCtx, _surfaceCache);
|
||||||
_terrain.AddLandblock(lb.LandblockId, meshData, origin);
|
_terrain.AddLandblock(lb.LandblockId, meshData, origin);
|
||||||
_terrainLegacy?.AddLandblock(lb.LandblockId, meshData, origin); // Phase N.5b benchmark toggle (TEMPORARY).
|
|
||||||
|
|
||||||
// Step 4: drain pending LoadedCells from the worker thread.
|
// Step 4: drain pending LoadedCells from the worker thread.
|
||||||
while (_pendingCells.TryTake(out var cell))
|
while (_pendingCells.TryTake(out var cell))
|
||||||
|
|
@ -6358,11 +6340,7 @@ public sealed class GameWindow : IDisposable
|
||||||
// (gated on ACDREAM_WB_DIAG=1, same env var as [WB-DIAG]). Stopwatch
|
// (gated on ACDREAM_WB_DIAG=1, same env var as [WB-DIAG]). Stopwatch
|
||||||
// is cheap; only the periodic Console.WriteLine is gated.
|
// is cheap; only the periodic Console.WriteLine is gated.
|
||||||
_terrainCpuStopwatch.Restart();
|
_terrainCpuStopwatch.Restart();
|
||||||
// Phase N.5b benchmark toggle (TEMPORARY): pick renderer per ACDREAM_LEGACY_TERRAIN.
|
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
||||||
if (_useLegacyTerrain)
|
|
||||||
_terrainLegacy?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
|
||||||
else
|
|
||||||
_terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb);
|
|
||||||
_terrainCpuStopwatch.Stop();
|
_terrainCpuStopwatch.Stop();
|
||||||
// Multiply by 100 then divide by 100 in the diag print to keep
|
// Multiply by 100 then divide by 100 in the diag print to keep
|
||||||
// 0.01 µs precision in the long-typed sample buffer. Terrain Draw
|
// 0.01 µs precision in the long-typed sample buffer. Terrain Draw
|
||||||
|
|
@ -8788,7 +8766,7 @@ public sealed class GameWindow : IDisposable
|
||||||
double cpuMedUs = cpuMedHundredthsUs / 100.0;
|
double cpuMedUs = cpuMedHundredthsUs / 100.0;
|
||||||
double cpuP95Us = cpuP95HundredthsUs / 100.0;
|
double cpuP95Us = cpuP95HundredthsUs / 100.0;
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[TERRAIN-DIAG{(_useLegacyTerrain ? "/legacy" : "/modern")}] cpu_us={cpuMedUs:F2}m/{cpuP95Us:F2}p95 " +
|
$"[TERRAIN-DIAG] cpu_us={cpuMedUs:F2}m/{cpuP95Us:F2}p95 " +
|
||||||
$"draws={_terrain?.VisibleSlots ?? 0}/frame " +
|
$"draws={_terrain?.VisibleSlots ?? 0}/frame " +
|
||||||
$"visible={_terrain?.VisibleSlots ?? 0} " +
|
$"visible={_terrain?.VisibleSlots ?? 0} " +
|
||||||
$"loaded={_terrain?.LoadedSlots ?? 0} " +
|
$"loaded={_terrain?.LoadedSlots ?? 0} " +
|
||||||
|
|
@ -8843,8 +8821,6 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
_meshShader?.Dispose();
|
_meshShader?.Dispose();
|
||||||
_terrain?.Dispose();
|
_terrain?.Dispose();
|
||||||
_terrainLegacy?.Dispose(); // Phase N.5b benchmark toggle (TEMPORARY).
|
|
||||||
_shader?.Dispose();
|
|
||||||
_terrainModernShader?.Dispose();
|
_terrainModernShader?.Dispose();
|
||||||
_sceneLightingUbo?.Dispose();
|
_sceneLightingUbo?.Dispose();
|
||||||
_particleRenderer?.Dispose();
|
_particleRenderer?.Dispose();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
out vec4 vRoad1;
|
||||||
flat out float vBaseTexIdx;
|
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;
|
const float MIN_FACTOR = 0.0;
|
||||||
|
|
||||||
vec4 unpackOverlayLayer(uint texIdxU, uint alphaIdxU, uint rotIdx, vec2 baseUV) {
|
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() {
|
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 rotOvl0 = (aPacked3.x >> 2u) & 3u;
|
||||||
uint rotOvl1 = (aPacked3.x >> 4u) & 3u;
|
uint rotOvl1 = (aPacked3.x >> 4u) & 3u;
|
||||||
uint rotOvl2 = (aPacked3.x >> 6u) & 3u;
|
uint rotOvl2 = (aPacked3.x >> 6u) & 3u;
|
||||||
|
|
@ -65,6 +82,14 @@ void main() {
|
||||||
uint rotRd1 = (aPacked3.y >> 2u) & 3u;
|
uint rotRd1 = (aPacked3.y >> 2u) & 3u;
|
||||||
uint splitDir= (aPacked3.y >> 4u) & 1u;
|
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 vIdx = gl_VertexID % 6;
|
||||||
int corner = 0;
|
int corner = 0;
|
||||||
if (splitDir == 0u) {
|
if (splitDir == 0u) {
|
||||||
|
|
|
||||||
|
|
@ -1,454 +0,0 @@
|
||||||
using System.Numerics;
|
|
||||||
using AcDream.Core.Terrain;
|
|
||||||
using Silk.NET.OpenGL;
|
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Chunk-based terrain renderer matching ACME's architecture. Each 16x16
|
|
||||||
/// landblock region gets its own VAO/VBO/EBO with pre-allocated max-size
|
|
||||||
/// buffers. Landblocks are added/removed incrementally via glBufferSubData
|
|
||||||
/// instead of rebuilding the entire buffer.
|
|
||||||
///
|
|
||||||
/// Attribute layout (same as TerrainRenderer, see TerrainVertex):
|
|
||||||
/// location 0: vec3 aPos (3 floats, world space)
|
|
||||||
/// location 1: vec3 aNormal (3 floats)
|
|
||||||
/// location 2: uvec4 aPacked0 (4 bytes, Data0)
|
|
||||||
/// location 3: uvec4 aPacked1 (4 bytes, Data1)
|
|
||||||
/// location 4: uvec4 aPacked2 (4 bytes, Data2)
|
|
||||||
/// location 5: uvec4 aPacked3 (4 bytes, Data3)
|
|
||||||
/// </summary>
|
|
||||||
public sealed unsafe class TerrainChunkRenderer : IDisposable
|
|
||||||
{
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Constants
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>Number of landblocks per chunk dimension (matching ACME).</summary>
|
|
||||||
public const int ChunkSizeInLandblocks = 16;
|
|
||||||
|
|
||||||
/// <summary>Max landblock slots per chunk (16x16 = 256).</summary>
|
|
||||||
public const int SlotsPerChunk = ChunkSizeInLandblocks * ChunkSizeInLandblocks;
|
|
||||||
|
|
||||||
/// <summary>Vertices per landblock: 64 cells x 6 verts = 384.</summary>
|
|
||||||
public const int VerticesPerLandblock = LandblockMesh.VerticesPerLandblock;
|
|
||||||
|
|
||||||
/// <summary>Indices per landblock (trivial 0..383, same count as vertices).</summary>
|
|
||||||
public const int IndicesPerLandblock = VerticesPerLandblock;
|
|
||||||
|
|
||||||
/// <summary>Byte size of one TerrainVertex (40 bytes).</summary>
|
|
||||||
private static readonly int VertexSize = sizeof(TerrainVertex);
|
|
||||||
|
|
||||||
/// <summary>Max VBO size per chunk: 256 slots x 384 verts x 40 bytes = ~3.75 MB.</summary>
|
|
||||||
private static readonly nuint MaxVboBytes =
|
|
||||||
(nuint)(SlotsPerChunk * VerticesPerLandblock * VertexSize);
|
|
||||||
|
|
||||||
/// <summary>Max EBO size per chunk: 256 slots x 384 indices x 4 bytes = ~393 KB.</summary>
|
|
||||||
private static readonly nuint MaxEboBytes =
|
|
||||||
(nuint)(SlotsPerChunk * IndicesPerLandblock * sizeof(uint));
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Fields
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private readonly GL _gl;
|
|
||||||
private readonly Shader _shader;
|
|
||||||
private readonly TerrainAtlas _atlas;
|
|
||||||
|
|
||||||
/// <summary>Active chunks keyed by (chunkX, chunkY) packed into a ulong.</summary>
|
|
||||||
private readonly Dictionary<ulong, ChunkData> _chunks = new();
|
|
||||||
|
|
||||||
/// <summary>Reverse map: landblockId -> chunkId, for fast RemoveLandblock.</summary>
|
|
||||||
private readonly Dictionary<uint, ulong> _landblockToChunk = new();
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Construction
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
public TerrainChunkRenderer(GL gl, Shader shader, TerrainAtlas atlas)
|
|
||||||
{
|
|
||||||
_gl = gl;
|
|
||||||
_shader = shader;
|
|
||||||
_atlas = atlas;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Public API
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add (or replace) a landblock's terrain mesh. Vertices are baked to world
|
|
||||||
/// space using <paramref name="worldOrigin"/>, then uploaded to the correct
|
|
||||||
/// chunk buffer slot via glBufferSubData.
|
|
||||||
/// </summary>
|
|
||||||
public void AddLandblock(uint landblockId, LandblockMeshData meshData, Vector3 worldOrigin)
|
|
||||||
{
|
|
||||||
// If this landblock already exists, remove it first.
|
|
||||||
if (_landblockToChunk.ContainsKey(landblockId))
|
|
||||||
RemoveLandblock(landblockId);
|
|
||||||
|
|
||||||
// Determine chunk coordinates and slot index.
|
|
||||||
// Landblock ID format: 0xXXYYnnnn (X at bits 24-31, Y at bits 16-23).
|
|
||||||
int lbX = (int)(landblockId >> 24) & 0xFF;
|
|
||||||
int lbY = (int)(landblockId >> 16) & 0xFF;
|
|
||||||
int chunkX = lbX / ChunkSizeInLandblocks;
|
|
||||||
int chunkY = lbY / ChunkSizeInLandblocks;
|
|
||||||
ulong chunkId = PackChunkId(chunkX, chunkY);
|
|
||||||
|
|
||||||
int localX = lbX % ChunkSizeInLandblocks;
|
|
||||||
int localY = lbY % ChunkSizeInLandblocks;
|
|
||||||
int slotIndex = localX * ChunkSizeInLandblocks + localY;
|
|
||||||
|
|
||||||
// Create chunk on demand.
|
|
||||||
if (!_chunks.TryGetValue(chunkId, out var chunk))
|
|
||||||
{
|
|
||||||
chunk = CreateChunk(chunkX, chunkY);
|
|
||||||
_chunks[chunkId] = chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bake world-space vertices.
|
|
||||||
var worldVerts = new TerrainVertex[meshData.Vertices.Length];
|
|
||||||
float zMin = float.MaxValue, zMax = float.MinValue;
|
|
||||||
for (int i = 0; i < meshData.Vertices.Length; i++)
|
|
||||||
{
|
|
||||||
var v = meshData.Vertices[i];
|
|
||||||
var worldPos = v.Position + worldOrigin;
|
|
||||||
worldVerts[i] = new TerrainVertex(worldPos, v.Normal, v.Data0, v.Data1, v.Data2, v.Data3);
|
|
||||||
if (worldPos.Z < zMin) zMin = worldPos.Z;
|
|
||||||
if (worldPos.Z > zMax) zMax = worldPos.Z;
|
|
||||||
}
|
|
||||||
if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; }
|
|
||||||
|
|
||||||
// Upload vertices into the slot's region of the VBO.
|
|
||||||
nint vboOffset = (nint)(slotIndex * VerticesPerLandblock * VertexSize);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, chunk.Vbo);
|
|
||||||
fixed (void* p = worldVerts)
|
|
||||||
{
|
|
||||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, vboOffset,
|
|
||||||
(nuint)(worldVerts.Length * VertexSize), p);
|
|
||||||
}
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
||||||
|
|
||||||
// Track the slot.
|
|
||||||
chunk.Slots[slotIndex] = new LandblockSlot
|
|
||||||
{
|
|
||||||
LandblockId = landblockId,
|
|
||||||
WorldOrigin = worldOrigin,
|
|
||||||
MinZ = zMin,
|
|
||||||
MaxZ = zMax,
|
|
||||||
};
|
|
||||||
chunk.Occupied.Add(slotIndex);
|
|
||||||
_landblockToChunk[landblockId] = chunkId;
|
|
||||||
|
|
||||||
// Rebuild the EBO for this chunk (only includes occupied slots).
|
|
||||||
RebuildChunkEbo(chunk);
|
|
||||||
|
|
||||||
// Update chunk AABB.
|
|
||||||
UpdateChunkBounds(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove a landblock from its chunk. If the chunk becomes empty, dispose it.
|
|
||||||
/// </summary>
|
|
||||||
public void RemoveLandblock(uint landblockId)
|
|
||||||
{
|
|
||||||
if (!_landblockToChunk.TryGetValue(landblockId, out var chunkId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_landblockToChunk.Remove(landblockId);
|
|
||||||
|
|
||||||
if (!_chunks.TryGetValue(chunkId, out var chunk))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Find which slot this landblock occupies.
|
|
||||||
int slotIndex = -1;
|
|
||||||
foreach (var s in chunk.Occupied)
|
|
||||||
{
|
|
||||||
if (chunk.Slots[s].LandblockId == landblockId)
|
|
||||||
{
|
|
||||||
slotIndex = s;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (slotIndex < 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Zero out the VBO region for this slot (optional but clean).
|
|
||||||
nint vboOffset = (nint)(slotIndex * VerticesPerLandblock * VertexSize);
|
|
||||||
nuint vboSize = (nuint)(VerticesPerLandblock * VertexSize);
|
|
||||||
var zeros = new byte[VerticesPerLandblock * VertexSize];
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, chunk.Vbo);
|
|
||||||
fixed (void* p = zeros)
|
|
||||||
{
|
|
||||||
_gl.BufferSubData(BufferTargetARB.ArrayBuffer, vboOffset, vboSize, p);
|
|
||||||
}
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
||||||
|
|
||||||
chunk.Slots[slotIndex] = default;
|
|
||||||
chunk.Occupied.Remove(slotIndex);
|
|
||||||
|
|
||||||
if (chunk.Occupied.Count == 0)
|
|
||||||
{
|
|
||||||
// Chunk is empty -- dispose GPU resources.
|
|
||||||
chunk.Dispose(_gl);
|
|
||||||
_chunks.Remove(chunkId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RebuildChunkEbo(chunk);
|
|
||||||
UpdateChunkBounds(chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Draw all visible terrain chunks. One glDrawElements per non-empty chunk.
|
|
||||||
/// Frustum culling is performed at the chunk AABB level.
|
|
||||||
/// </summary>
|
|
||||||
public void Draw(ICamera camera, FrustumPlanes? frustum = null, uint? neverCullLandblockId = null)
|
|
||||||
{
|
|
||||||
if (_chunks.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Determine which chunk the never-cull landblock lives in.
|
|
||||||
ulong? neverCullChunkId = null;
|
|
||||||
if (neverCullLandblockId is not null && _landblockToChunk.TryGetValue(neverCullLandblockId.Value, out var ncId))
|
|
||||||
neverCullChunkId = ncId;
|
|
||||||
|
|
||||||
_shader.Use();
|
|
||||||
_shader.SetMatrix4("uView", camera.View);
|
|
||||||
_shader.SetMatrix4("uProjection", camera.Projection);
|
|
||||||
|
|
||||||
// Phase G: light direction + ambient + fog come from the shared
|
|
||||||
// SceneLighting UBO (binding=1) uploaded by GameWindow once per
|
|
||||||
// frame. Terrain bakes per-vertex AdjustPlanes lighting (r13 §7)
|
|
||||||
// from the UBO's slot-0 sun + uCellAmbient, then the fragment
|
|
||||||
// stage adds fog + lightning flash. No per-program uniforms here.
|
|
||||||
|
|
||||||
// Terrain atlas on unit 0, alpha atlas on unit 1.
|
|
||||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2DArray, _atlas.GlTexture);
|
|
||||||
_gl.ActiveTexture(TextureUnit.Texture1);
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2DArray, _atlas.GlAlphaTexture);
|
|
||||||
|
|
||||||
int terrainLoc = _gl.GetUniformLocation(_shader.Program, "uTerrain");
|
|
||||||
if (terrainLoc >= 0) _gl.Uniform1(terrainLoc, 0);
|
|
||||||
int alphaLoc = _gl.GetUniformLocation(_shader.Program, "uAlpha");
|
|
||||||
if (alphaLoc >= 0) _gl.Uniform1(alphaLoc, 1);
|
|
||||||
|
|
||||||
foreach (var (chunkId, chunk) in _chunks)
|
|
||||||
{
|
|
||||||
if (chunk.IndexCount == 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// Chunk-level frustum cull.
|
|
||||||
if (frustum is not null && chunkId != neverCullChunkId)
|
|
||||||
{
|
|
||||||
if (!FrustumCuller.IsAabbVisible(frustum.Value, chunk.AabbMin, chunk.AabbMax))
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gl.BindVertexArray(chunk.Vao);
|
|
||||||
_gl.DrawElements(
|
|
||||||
PrimitiveType.Triangles,
|
|
||||||
(uint)chunk.IndexCount,
|
|
||||||
DrawElementsType.UnsignedInt,
|
|
||||||
(void*)0);
|
|
||||||
}
|
|
||||||
|
|
||||||
_gl.BindVertexArray(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
foreach (var chunk in _chunks.Values)
|
|
||||||
chunk.Dispose(_gl);
|
|
||||||
|
|
||||||
_chunks.Clear();
|
|
||||||
_landblockToChunk.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Private helpers
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private static ulong PackChunkId(int chunkX, int chunkY)
|
|
||||||
=> ((ulong)(uint)chunkX << 32) | (uint)chunkY;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Allocate a new chunk with max-size VBO and empty EBO, plus a configured VAO.
|
|
||||||
/// </summary>
|
|
||||||
private ChunkData CreateChunk(int chunkX, int chunkY)
|
|
||||||
{
|
|
||||||
var chunk = new ChunkData
|
|
||||||
{
|
|
||||||
ChunkX = chunkX,
|
|
||||||
ChunkY = chunkY,
|
|
||||||
Vao = _gl.GenVertexArray(),
|
|
||||||
Vbo = _gl.GenBuffer(),
|
|
||||||
Ebo = _gl.GenBuffer(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pre-allocate VBO to max size with DynamicDraw.
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, chunk.Vbo);
|
|
||||||
_gl.BufferData(BufferTargetARB.ArrayBuffer, MaxVboBytes, null, BufferUsageARB.DynamicDraw);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
|
|
||||||
|
|
||||||
// Pre-allocate EBO (empty initially, will be rebuilt on first AddLandblock).
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, chunk.Ebo);
|
|
||||||
_gl.BufferData(BufferTargetARB.ElementArrayBuffer, MaxEboBytes, null, BufferUsageARB.DynamicDraw);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
|
|
||||||
|
|
||||||
// Configure VAO with the same attribute layout as the old TerrainRenderer.
|
|
||||||
ConfigureVao(chunk);
|
|
||||||
|
|
||||||
return chunk;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set up vertex attribute pointers on the chunk's VAO. Identical layout
|
|
||||||
/// to the old TerrainRenderer.
|
|
||||||
/// </summary>
|
|
||||||
private void ConfigureVao(ChunkData chunk)
|
|
||||||
{
|
|
||||||
_gl.BindVertexArray(chunk.Vao);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, chunk.Vbo);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, chunk.Ebo);
|
|
||||||
|
|
||||||
uint stride = (uint)VertexSize;
|
|
||||||
|
|
||||||
// location 0: Position (12 bytes)
|
|
||||||
_gl.EnableVertexAttribArray(0);
|
|
||||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
|
||||||
// location 1: Normal (12 bytes, offset 12)
|
|
||||||
_gl.EnableVertexAttribArray(1);
|
|
||||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
|
||||||
|
|
||||||
// location 2..5: Data0..Data3 as uvec4 byte attributes (4 bytes each, offsets 24, 28, 32, 36).
|
|
||||||
nint dataOffset = 6 * sizeof(float); // 24 bytes
|
|
||||||
_gl.EnableVertexAttribArray(2);
|
|
||||||
_gl.VertexAttribIPointer(2, 4, VertexAttribIType.UnsignedByte, stride, (void*)dataOffset);
|
|
||||||
_gl.EnableVertexAttribArray(3);
|
|
||||||
_gl.VertexAttribIPointer(3, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 4));
|
|
||||||
_gl.EnableVertexAttribArray(4);
|
|
||||||
_gl.VertexAttribIPointer(4, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 8));
|
|
||||||
_gl.EnableVertexAttribArray(5);
|
|
||||||
_gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12));
|
|
||||||
|
|
||||||
_gl.BindVertexArray(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rebuild the EBO for a chunk, emitting rebased indices only for occupied
|
|
||||||
/// slots. Each slot's indices are offset by (slotIndex * VerticesPerLandblock)
|
|
||||||
/// so they point to the correct region of the VBO.
|
|
||||||
/// </summary>
|
|
||||||
private void RebuildChunkEbo(ChunkData chunk)
|
|
||||||
{
|
|
||||||
int totalIndices = chunk.Occupied.Count * IndicesPerLandblock;
|
|
||||||
var indices = new uint[totalIndices];
|
|
||||||
|
|
||||||
int writePos = 0;
|
|
||||||
foreach (var slotIndex in chunk.Occupied)
|
|
||||||
{
|
|
||||||
uint vertexBase = (uint)(slotIndex * VerticesPerLandblock);
|
|
||||||
for (uint i = 0; i < IndicesPerLandblock; i++)
|
|
||||||
indices[writePos++] = vertexBase + i;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, chunk.Ebo);
|
|
||||||
fixed (void* p = indices)
|
|
||||||
{
|
|
||||||
_gl.BufferSubData(BufferTargetARB.ElementArrayBuffer, 0,
|
|
||||||
(nuint)(totalIndices * sizeof(uint)), p);
|
|
||||||
}
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0);
|
|
||||||
|
|
||||||
chunk.IndexCount = totalIndices;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Recompute the chunk's world-space AABB from all occupied landblock slots.
|
|
||||||
/// </summary>
|
|
||||||
private static void UpdateChunkBounds(ChunkData chunk)
|
|
||||||
{
|
|
||||||
float minX = float.MaxValue, minY = float.MaxValue, minZ = float.MaxValue;
|
|
||||||
float maxX = float.MinValue, maxY = float.MinValue, maxZ = float.MinValue;
|
|
||||||
|
|
||||||
foreach (var slotIndex in chunk.Occupied)
|
|
||||||
{
|
|
||||||
var slot = chunk.Slots[slotIndex];
|
|
||||||
float ox = slot.WorldOrigin.X;
|
|
||||||
float oy = slot.WorldOrigin.Y;
|
|
||||||
|
|
||||||
if (ox < minX) minX = ox;
|
|
||||||
if (oy < minY) minY = oy;
|
|
||||||
if (slot.MinZ < minZ) minZ = slot.MinZ;
|
|
||||||
|
|
||||||
float ex = ox + LandblockMesh.LandblockSize;
|
|
||||||
float ey = oy + LandblockMesh.LandblockSize;
|
|
||||||
if (ex > maxX) maxX = ex;
|
|
||||||
if (ey > maxY) maxY = ey;
|
|
||||||
if (slot.MaxZ > maxZ) maxZ = slot.MaxZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minX == float.MaxValue)
|
|
||||||
{
|
|
||||||
chunk.AabbMin = Vector3.Zero;
|
|
||||||
chunk.AabbMax = Vector3.Zero;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
chunk.AabbMin = new Vector3(minX, minY, minZ);
|
|
||||||
chunk.AabbMax = new Vector3(maxX, maxY, maxZ);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Inner types
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Per-landblock slot tracking within a chunk's VBO.
|
|
||||||
/// </summary>
|
|
||||||
private struct LandblockSlot
|
|
||||||
{
|
|
||||||
public uint LandblockId;
|
|
||||||
public Vector3 WorldOrigin;
|
|
||||||
public float MinZ;
|
|
||||||
public float MaxZ;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// GPU resources and metadata for a single 16x16 terrain chunk.
|
|
||||||
/// </summary>
|
|
||||||
private sealed class ChunkData
|
|
||||||
{
|
|
||||||
public int ChunkX;
|
|
||||||
public int ChunkY;
|
|
||||||
|
|
||||||
// GPU handles.
|
|
||||||
public uint Vao;
|
|
||||||
public uint Vbo;
|
|
||||||
public uint Ebo;
|
|
||||||
|
|
||||||
/// <summary>Per-slot landblock data. Indexed by (localX * 16 + localY).</summary>
|
|
||||||
public readonly LandblockSlot[] Slots = new LandblockSlot[SlotsPerChunk];
|
|
||||||
|
|
||||||
/// <summary>Set of occupied slot indices within this chunk.</summary>
|
|
||||||
public readonly HashSet<int> Occupied = new();
|
|
||||||
|
|
||||||
/// <summary>Current number of valid indices in the EBO (set by RebuildChunkEbo).</summary>
|
|
||||||
public int IndexCount;
|
|
||||||
|
|
||||||
/// <summary>World-space AABB for chunk-level frustum culling.</summary>
|
|
||||||
public Vector3 AabbMin;
|
|
||||||
public Vector3 AabbMax;
|
|
||||||
|
|
||||||
public void Dispose(GL gl)
|
|
||||||
{
|
|
||||||
gl.DeleteVertexArray(Vao);
|
|
||||||
gl.DeleteBuffer(Vbo);
|
|
||||||
gl.DeleteBuffer(Ebo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,247 +0,0 @@
|
||||||
using System.Numerics;
|
|
||||||
using AcDream.Core.Terrain;
|
|
||||||
using Silk.NET.OpenGL;
|
|
||||||
|
|
||||||
namespace AcDream.App.Rendering;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Draws the Phase 3c per-cell terrain mesh. All loaded landblocks share a
|
|
||||||
/// single VBO + EBO + VAO. Vertex positions are baked in world space so no
|
|
||||||
/// uModel uniform is needed. The VAO is bound once per frame; each visible
|
|
||||||
/// landblock gets one glDrawElements call into its sub-range of the shared EBO.
|
|
||||||
///
|
|
||||||
/// Attribute layout (see TerrainVertex for the byte layout):
|
|
||||||
/// location 0: vec3 aPos (3 floats, world space)
|
|
||||||
/// location 1: vec3 aNormal (3 floats)
|
|
||||||
/// location 2: uvec4 aPacked0 (4 bytes, Data0)
|
|
||||||
/// location 3: uvec4 aPacked1 (4 bytes, Data1)
|
|
||||||
/// location 4: uvec4 aPacked2 (4 bytes, Data2)
|
|
||||||
/// location 5: uvec4 aPacked3 (4 bytes, Data3)
|
|
||||||
/// </summary>
|
|
||||||
public sealed unsafe class TerrainRenderer : IDisposable
|
|
||||||
{
|
|
||||||
private readonly GL _gl;
|
|
||||||
private readonly Shader _shader;
|
|
||||||
private readonly TerrainAtlas _atlas;
|
|
||||||
|
|
||||||
// Logical per-landblock data (CPU side).
|
|
||||||
private readonly Dictionary<uint, LandblockEntry> _entries = new();
|
|
||||||
|
|
||||||
// Shared GPU buffers — rebuilt whenever a landblock is added or removed.
|
|
||||||
private uint _vao;
|
|
||||||
private uint _vbo;
|
|
||||||
private uint _ebo;
|
|
||||||
private bool _gpuDirty = true; // true = buffers need rebuilding before next Draw
|
|
||||||
|
|
||||||
public TerrainRenderer(GL gl, Shader shader, TerrainAtlas atlas)
|
|
||||||
{
|
|
||||||
_gl = gl;
|
|
||||||
_shader = shader;
|
|
||||||
_atlas = atlas;
|
|
||||||
|
|
||||||
_vao = _gl.GenVertexArray();
|
|
||||||
_vbo = _gl.GenBuffer();
|
|
||||||
_ebo = _gl.GenBuffer();
|
|
||||||
ConfigureVao();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddLandblock(uint landblockId, LandblockMeshData meshData, Vector3 worldOrigin)
|
|
||||||
{
|
|
||||||
if (_entries.ContainsKey(landblockId))
|
|
||||||
_entries.Remove(landblockId);
|
|
||||||
|
|
||||||
// Bake world-space positions: offset every vertex by worldOrigin.
|
|
||||||
var worldVerts = new TerrainVertex[meshData.Vertices.Length];
|
|
||||||
float zMin = float.MaxValue, zMax = float.MinValue;
|
|
||||||
for (int i = 0; i < meshData.Vertices.Length; i++)
|
|
||||||
{
|
|
||||||
var v = meshData.Vertices[i];
|
|
||||||
var worldPos = v.Position + worldOrigin;
|
|
||||||
worldVerts[i] = new TerrainVertex(worldPos, v.Normal, v.Data0, v.Data1, v.Data2, v.Data3);
|
|
||||||
if (worldPos.Z < zMin) zMin = worldPos.Z;
|
|
||||||
if (worldPos.Z > zMax) zMax = worldPos.Z;
|
|
||||||
}
|
|
||||||
if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; }
|
|
||||||
|
|
||||||
_entries[landblockId] = new LandblockEntry
|
|
||||||
{
|
|
||||||
LandblockId = landblockId,
|
|
||||||
WorldOrigin = worldOrigin,
|
|
||||||
Vertices = worldVerts,
|
|
||||||
Indices = meshData.Indices, // local 0..N-1; will be rebased on rebuild
|
|
||||||
MinZ = zMin,
|
|
||||||
MaxZ = zMax,
|
|
||||||
};
|
|
||||||
|
|
||||||
_gpuDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveLandblock(uint landblockId)
|
|
||||||
{
|
|
||||||
if (_entries.Remove(landblockId))
|
|
||||||
_gpuDirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Draw(ICamera camera, FrustumPlanes? frustum = null, uint? neverCullLandblockId = null)
|
|
||||||
{
|
|
||||||
if (_entries.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_gpuDirty)
|
|
||||||
RebuildGpuBuffers();
|
|
||||||
|
|
||||||
_shader.Use();
|
|
||||||
_shader.SetMatrix4("uView", camera.View);
|
|
||||||
_shader.SetMatrix4("uProjection", camera.Projection);
|
|
||||||
|
|
||||||
// Terrain atlas on unit 0, alpha atlas on unit 1.
|
|
||||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2DArray, _atlas.GlTexture);
|
|
||||||
_gl.ActiveTexture(TextureUnit.Texture1);
|
|
||||||
_gl.BindTexture(TextureTarget.Texture2DArray, _atlas.GlAlphaTexture);
|
|
||||||
|
|
||||||
int terrainLoc = _gl.GetUniformLocation(_shader.Program, "uTerrain");
|
|
||||||
if (terrainLoc >= 0) _gl.Uniform1(terrainLoc, 0);
|
|
||||||
int alphaLoc = _gl.GetUniformLocation(_shader.Program, "uAlpha");
|
|
||||||
if (alphaLoc >= 0) _gl.Uniform1(alphaLoc, 1);
|
|
||||||
|
|
||||||
// Bind the shared VAO once for the entire frame.
|
|
||||||
_gl.BindVertexArray(_vao);
|
|
||||||
|
|
||||||
foreach (var entry in _entries.Values)
|
|
||||||
{
|
|
||||||
// Per-landblock frustum cull using world-space AABB.
|
|
||||||
if (frustum is not null && entry.LandblockId != neverCullLandblockId)
|
|
||||||
{
|
|
||||||
var aabbMin = new Vector3(entry.WorldOrigin.X, entry.WorldOrigin.Y, entry.MinZ);
|
|
||||||
var aabbMax = new Vector3(entry.WorldOrigin.X + 192f, entry.WorldOrigin.Y + 192f, entry.MaxZ);
|
|
||||||
if (!FrustumCuller.IsAabbVisible(frustum.Value, aabbMin, aabbMax))
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw only this landblock's sub-range in the shared EBO.
|
|
||||||
// EboOffset is in bytes (uint = 4 bytes).
|
|
||||||
_gl.DrawElements(
|
|
||||||
PrimitiveType.Triangles,
|
|
||||||
(uint)entry.IndexCount,
|
|
||||||
DrawElementsType.UnsignedInt,
|
|
||||||
(void*)(entry.EboByteOffset));
|
|
||||||
}
|
|
||||||
|
|
||||||
_gl.BindVertexArray(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_gl.DeleteVertexArray(_vao);
|
|
||||||
_gl.DeleteBuffer(_vbo);
|
|
||||||
_gl.DeleteBuffer(_ebo);
|
|
||||||
_entries.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Private helpers
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private void ConfigureVao()
|
|
||||||
{
|
|
||||||
_gl.BindVertexArray(_vao);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ebo);
|
|
||||||
|
|
||||||
uint stride = (uint)sizeof(TerrainVertex);
|
|
||||||
|
|
||||||
// location 0: Position (12 bytes)
|
|
||||||
_gl.EnableVertexAttribArray(0);
|
|
||||||
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
|
|
||||||
// location 1: Normal (12 bytes, offset 12)
|
|
||||||
_gl.EnableVertexAttribArray(1);
|
|
||||||
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
|
|
||||||
|
|
||||||
// location 2..5: Data0..Data3 as uvec4 byte attributes (4 bytes each,
|
|
||||||
// offsets 24, 28, 32, 36).
|
|
||||||
nint dataOffset = 6 * sizeof(float); // 24 bytes
|
|
||||||
_gl.EnableVertexAttribArray(2);
|
|
||||||
_gl.VertexAttribIPointer(2, 4, VertexAttribIType.UnsignedByte, stride, (void*)dataOffset);
|
|
||||||
_gl.EnableVertexAttribArray(3);
|
|
||||||
_gl.VertexAttribIPointer(3, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 4));
|
|
||||||
_gl.EnableVertexAttribArray(4);
|
|
||||||
_gl.VertexAttribIPointer(4, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 8));
|
|
||||||
_gl.EnableVertexAttribArray(5);
|
|
||||||
_gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12));
|
|
||||||
|
|
||||||
_gl.BindVertexArray(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Concatenate all loaded landblocks into a single VBO + EBO and upload.
|
|
||||||
/// Called on the cold path (landblock load / unload), not per frame.
|
|
||||||
/// </summary>
|
|
||||||
private void RebuildGpuBuffers()
|
|
||||||
{
|
|
||||||
// Measure totals.
|
|
||||||
int totalVerts = 0;
|
|
||||||
int totalIndices = 0;
|
|
||||||
foreach (var e in _entries.Values)
|
|
||||||
{
|
|
||||||
totalVerts += e.Vertices.Length;
|
|
||||||
totalIndices += e.Indices.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
var allVerts = new TerrainVertex[totalVerts];
|
|
||||||
var allIndices = new uint[totalIndices];
|
|
||||||
|
|
||||||
int vertBase = 0;
|
|
||||||
int indexBase = 0;
|
|
||||||
|
|
||||||
foreach (var entry in _entries.Values)
|
|
||||||
{
|
|
||||||
// Copy world-space vertices.
|
|
||||||
entry.Vertices.CopyTo(allVerts, vertBase);
|
|
||||||
|
|
||||||
// Rebase local indices (0..N-1) → absolute (vertBase..vertBase+N-1).
|
|
||||||
for (int i = 0; i < entry.Indices.Length; i++)
|
|
||||||
allIndices[indexBase + i] = (uint)(vertBase + entry.Indices[i]);
|
|
||||||
|
|
||||||
// Record where this landblock's indices live in the EBO (byte offset).
|
|
||||||
entry.EboByteOffset = (nint)(indexBase * sizeof(uint));
|
|
||||||
entry.IndexCount = entry.Indices.Length;
|
|
||||||
|
|
||||||
vertBase += entry.Vertices.Length;
|
|
||||||
indexBase += entry.Indices.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload to GPU.
|
|
||||||
_gl.BindVertexArray(_vao);
|
|
||||||
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
|
|
||||||
fixed (void* p = allVerts)
|
|
||||||
_gl.BufferData(BufferTargetARB.ArrayBuffer,
|
|
||||||
(nuint)(totalVerts * sizeof(TerrainVertex)), p, BufferUsageARB.DynamicDraw);
|
|
||||||
|
|
||||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ebo);
|
|
||||||
fixed (void* p = allIndices)
|
|
||||||
_gl.BufferData(BufferTargetARB.ElementArrayBuffer,
|
|
||||||
(nuint)(totalIndices * sizeof(uint)), p, BufferUsageARB.DynamicDraw);
|
|
||||||
|
|
||||||
_gl.BindVertexArray(0);
|
|
||||||
_gpuDirty = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Data types
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
private sealed class LandblockEntry
|
|
||||||
{
|
|
||||||
public uint LandblockId;
|
|
||||||
public Vector3 WorldOrigin;
|
|
||||||
public TerrainVertex[] Vertices = Array.Empty<TerrainVertex>();
|
|
||||||
public uint[] Indices = Array.Empty<uint>();
|
|
||||||
public float MinZ;
|
|
||||||
public float MaxZ;
|
|
||||||
// Set by RebuildGpuBuffers:
|
|
||||||
public nint EboByteOffset;
|
|
||||||
public int IndexCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue