diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 3f851f0..c2aae70 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -19,17 +19,8 @@ public sealed class GameWindow : IDisposable private GL? _gl; private IInputContext? _input; 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; /// Phase N.5b: terrain_modern.vert/.frag program. Owned by - /// at draw time but allocated + disposed here. Lives - /// in parallel with (legacy terrain.vert/.frag) until - /// Task 9 deletes the legacy renderer. + /// at draw time but allocated + disposed here. private Shader? _terrainModernShader; private CameraController? _cameraController; private IMouse? _capturedMouse; @@ -985,13 +976,10 @@ public sealed class GameWindow : IDisposable _gl.Enable(EnableCap.DepthTest); 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 + - // glMultiDrawElementsIndirect dispatch path. Loaded in parallel with - // the legacy `_shader`; Task 9 will retire the legacy program. + // glMultiDrawElementsIndirect dispatch path. The only terrain shader + // since Task 9 retired the legacy terrain.vert/.frag program. _terrainModernShader = new Shader(_gl, Path.Combine(shadersDir, "terrain_modern.vert"), Path.Combine(shadersDir, "terrain_modern.frag")); @@ -1451,10 +1439,6 @@ public sealed class GameWindow : IDisposable _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 centerY = (int)((centerLandblockId >> 16) & 0xFFu); @@ -1612,7 +1596,6 @@ public sealed class GameWindow : IDisposable _lightingSink.UnregisterOwner(ent.Id); } _terrain?.RemoveLandblock(id); - _terrainLegacy?.RemoveLandblock(id); // Phase N.5b benchmark toggle (TEMPORARY). _physicsEngine.RemoveLandblock(id); _cellVisibility.RemoveLandblock((id >> 16) & 0xFFFFu); }); @@ -4762,7 +4745,7 @@ public sealed class GameWindow : IDisposable float localY = spawn.LocalPosition.Y; // Prefer the physics engine's terrain sampler (TerrainSurface.SampleZ) // — 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 // walks on. If physics hasn't registered this landblock yet, // fall back to the local bilinear sample. @@ -5133,7 +5116,6 @@ public sealed class GameWindow : IDisposable var meshData = AcDream.Core.Terrain.LandblockMesh.Build( lb.Heightmap, lbXu, lbYu, _heightTable, _blendCtx, _surfaceCache); _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. 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 // is cheap; only the periodic Console.WriteLine is gated. _terrainCpuStopwatch.Restart(); - // Phase N.5b benchmark toggle (TEMPORARY): pick renderer per ACDREAM_LEGACY_TERRAIN. - if (_useLegacyTerrain) - _terrainLegacy?.Draw(camera, frustum, neverCullLandblockId: playerLb); - else - _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); + _terrain?.Draw(camera, frustum, neverCullLandblockId: playerLb); _terrainCpuStopwatch.Stop(); // 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 @@ -8788,7 +8766,7 @@ public sealed class GameWindow : IDisposable double cpuMedUs = cpuMedHundredthsUs / 100.0; double cpuP95Us = cpuP95HundredthsUs / 100.0; 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 " + $"visible={_terrain?.VisibleSlots ?? 0} " + $"loaded={_terrain?.LoadedSlots ?? 0} " + @@ -8843,8 +8821,6 @@ public sealed class GameWindow : IDisposable _meshShader?.Dispose(); _terrain?.Dispose(); - _terrainLegacy?.Dispose(); // Phase N.5b benchmark toggle (TEMPORARY). - _shader?.Dispose(); _terrainModernShader?.Dispose(); _sceneLightingUbo?.Dispose(); _particleRenderer?.Dispose(); diff --git a/src/AcDream.App/Rendering/Shaders/terrain.frag b/src/AcDream.App/Rendering/Shaders/terrain.frag deleted file mode 100644 index 479939d..0000000 --- a/src/AcDream.App/Rendering/Shaders/terrain.frag +++ /dev/null @@ -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); -} diff --git a/src/AcDream.App/Rendering/Shaders/terrain.vert b/src/AcDream.App/Rendering/Shaders/terrain.vert deleted file mode 100644 index 11e691d..0000000 --- a/src/AcDream.App/Rendering/Shaders/terrain.vert +++ /dev/null @@ -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); -} diff --git a/src/AcDream.App/Rendering/Shaders/terrain_modern.vert b/src/AcDream.App/Rendering/Shaders/terrain_modern.vert index 2f2f822..473cba5 100644 --- a/src/AcDream.App/Rendering/Shaders/terrain_modern.vert +++ b/src/AcDream.App/Rendering/Shaders/terrain_modern.vert @@ -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) { diff --git a/src/AcDream.App/Rendering/TerrainChunkRenderer.cs b/src/AcDream.App/Rendering/TerrainChunkRenderer.cs deleted file mode 100644 index cd2df6a..0000000 --- a/src/AcDream.App/Rendering/TerrainChunkRenderer.cs +++ /dev/null @@ -1,454 +0,0 @@ -using System.Numerics; -using AcDream.Core.Terrain; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -/// -/// 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) -/// -public sealed unsafe class TerrainChunkRenderer : IDisposable -{ - // ------------------------------------------------------------------------- - // Constants - // ------------------------------------------------------------------------- - - /// Number of landblocks per chunk dimension (matching ACME). - public const int ChunkSizeInLandblocks = 16; - - /// Max landblock slots per chunk (16x16 = 256). - public const int SlotsPerChunk = ChunkSizeInLandblocks * ChunkSizeInLandblocks; - - /// Vertices per landblock: 64 cells x 6 verts = 384. - public const int VerticesPerLandblock = LandblockMesh.VerticesPerLandblock; - - /// Indices per landblock (trivial 0..383, same count as vertices). - public const int IndicesPerLandblock = VerticesPerLandblock; - - /// Byte size of one TerrainVertex (40 bytes). - private static readonly int VertexSize = sizeof(TerrainVertex); - - /// Max VBO size per chunk: 256 slots x 384 verts x 40 bytes = ~3.75 MB. - private static readonly nuint MaxVboBytes = - (nuint)(SlotsPerChunk * VerticesPerLandblock * VertexSize); - - /// Max EBO size per chunk: 256 slots x 384 indices x 4 bytes = ~393 KB. - private static readonly nuint MaxEboBytes = - (nuint)(SlotsPerChunk * IndicesPerLandblock * sizeof(uint)); - - // ------------------------------------------------------------------------- - // Fields - // ------------------------------------------------------------------------- - - private readonly GL _gl; - private readonly Shader _shader; - private readonly TerrainAtlas _atlas; - - /// Active chunks keyed by (chunkX, chunkY) packed into a ulong. - private readonly Dictionary _chunks = new(); - - /// Reverse map: landblockId -> chunkId, for fast RemoveLandblock. - private readonly Dictionary _landblockToChunk = new(); - - // ------------------------------------------------------------------------- - // Construction - // ------------------------------------------------------------------------- - - public TerrainChunkRenderer(GL gl, Shader shader, TerrainAtlas atlas) - { - _gl = gl; - _shader = shader; - _atlas = atlas; - } - - // ------------------------------------------------------------------------- - // Public API - // ------------------------------------------------------------------------- - - /// - /// Add (or replace) a landblock's terrain mesh. Vertices are baked to world - /// space using , then uploaded to the correct - /// chunk buffer slot via glBufferSubData. - /// - 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); - } - - /// - /// Remove a landblock from its chunk. If the chunk becomes empty, dispose it. - /// - 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); - } - } - - /// - /// Draw all visible terrain chunks. One glDrawElements per non-empty chunk. - /// Frustum culling is performed at the chunk AABB level. - /// - 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; - - /// - /// Allocate a new chunk with max-size VBO and empty EBO, plus a configured VAO. - /// - 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; - } - - /// - /// Set up vertex attribute pointers on the chunk's VAO. Identical layout - /// to the old TerrainRenderer. - /// - 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); - } - - /// - /// 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. - /// - 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; - } - - /// - /// Recompute the chunk's world-space AABB from all occupied landblock slots. - /// - 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 - // ------------------------------------------------------------------------- - - /// - /// Per-landblock slot tracking within a chunk's VBO. - /// - private struct LandblockSlot - { - public uint LandblockId; - public Vector3 WorldOrigin; - public float MinZ; - public float MaxZ; - } - - /// - /// GPU resources and metadata for a single 16x16 terrain chunk. - /// - private sealed class ChunkData - { - public int ChunkX; - public int ChunkY; - - // GPU handles. - public uint Vao; - public uint Vbo; - public uint Ebo; - - /// Per-slot landblock data. Indexed by (localX * 16 + localY). - public readonly LandblockSlot[] Slots = new LandblockSlot[SlotsPerChunk]; - - /// Set of occupied slot indices within this chunk. - public readonly HashSet Occupied = new(); - - /// Current number of valid indices in the EBO (set by RebuildChunkEbo). - public int IndexCount; - - /// World-space AABB for chunk-level frustum culling. - public Vector3 AabbMin; - public Vector3 AabbMax; - - public void Dispose(GL gl) - { - gl.DeleteVertexArray(Vao); - gl.DeleteBuffer(Vbo); - gl.DeleteBuffer(Ebo); - } - } -} diff --git a/src/AcDream.App/Rendering/TerrainRenderer.cs b/src/AcDream.App/Rendering/TerrainRenderer.cs deleted file mode 100644 index 15bee67..0000000 --- a/src/AcDream.App/Rendering/TerrainRenderer.cs +++ /dev/null @@ -1,247 +0,0 @@ -using System.Numerics; -using AcDream.Core.Terrain; -using Silk.NET.OpenGL; - -namespace AcDream.App.Rendering; - -/// -/// 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) -/// -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 _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); - } - - /// - /// Concatenate all loaded landblocks into a single VBO + EBO and upload. - /// Called on the cold path (landblock load / unload), not per frame. - /// - 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(); - public uint[] Indices = Array.Empty(); - public float MinZ; - public float MaxZ; - // Set by RebuildGpuBuffers: - public nint EboByteOffset; - public int IndexCount; - } -}