perf(terrain): single shared VAO/VBO/EBO for all landblocks
Replace 25 per-landblock VAOs with one shared buffer set. Vertex positions are now baked in world space during AddLandblock (worldOrigin added to each vertex), so uModel is eliminated from terrain.vert entirely. Buffer rebuild happens on the cold path (landblock load/unload) via RebuildGpuBuffers. Draw loop: bind VAO once, then one glDrawElements per visible landblock into its sub-range of the shared EBO — same frustum-cull logic, no VAO/VBO rebind overhead per landblock. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
787e0f0aff
commit
d35e4b6de7
2 changed files with 171 additions and 100 deletions
|
|
@ -6,7 +6,6 @@ layout(location = 3) in uvec4 aPacked1; // bytes: ovl1Tex, ovl1Alpha, ovl2Tex,
|
|||
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 uModel;
|
||||
uniform mat4 uView;
|
||||
uniform mat4 uProjection;
|
||||
|
||||
|
|
@ -89,7 +88,8 @@ void main() {
|
|||
else baseUV = vec2(0.0, 0.0);
|
||||
|
||||
vBaseUV = baseUV;
|
||||
vWorldNormal = normalize(mat3(uModel) * aNormal);
|
||||
// Vertices are baked in world space; normals need no model transform.
|
||||
vWorldNormal = normalize(aNormal);
|
||||
|
||||
float baseTex = float(aPacked0.x);
|
||||
if (baseTex >= 254.0) baseTex = -1.0;
|
||||
|
|
@ -101,5 +101,5 @@ void main() {
|
|||
vRoad0 = unpackOverlayLayer(aPacked2.x, aPacked2.y, rotRd0, baseUV);
|
||||
vRoad1 = unpackOverlayLayer(aPacked2.z, aPacked2.w, rotRd1, baseUV);
|
||||
|
||||
gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0);
|
||||
gl_Position = uProjection * uView * vec4(aPos, 1.0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ using Silk.NET.OpenGL;
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Draws the Phase 3c per-cell terrain mesh. Each landblock owns its own
|
||||
/// VBO+EBO+VAO (no chunking yet — deferred to a hypothetical Phase 3d) and
|
||||
/// gets drawn with a single DrawElements call per landblock.
|
||||
/// 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)
|
||||
/// 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)
|
||||
|
|
@ -22,58 +23,131 @@ public sealed unsafe class TerrainRenderer : IDisposable
|
|||
private readonly GL _gl;
|
||||
private readonly Shader _shader;
|
||||
private readonly TerrainAtlas _atlas;
|
||||
private readonly Dictionary<uint, LandblockGpu> _landblocks = new();
|
||||
|
||||
// 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 (_landblocks.TryGetValue(landblockId, out var existing))
|
||||
{
|
||||
_gl.DeleteBuffer(existing.Vbo);
|
||||
_gl.DeleteBuffer(existing.Ebo);
|
||||
_gl.DeleteVertexArray(existing.Vao);
|
||||
_landblocks.Remove(landblockId);
|
||||
}
|
||||
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;
|
||||
foreach (var v in meshData.Vertices)
|
||||
for (int i = 0; i < meshData.Vertices.Length; i++)
|
||||
{
|
||||
float z = v.Position.Z;
|
||||
if (z < zMin) zMin = z;
|
||||
if (z > zMax) zMax = z;
|
||||
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;
|
||||
}
|
||||
// Fall back to zero if no vertices (shouldn't happen in practice).
|
||||
if (zMin == float.MaxValue) { zMin = 0f; zMax = 0f; }
|
||||
|
||||
var gpu = new LandblockGpu
|
||||
_entries[landblockId] = new LandblockEntry
|
||||
{
|
||||
LandblockId = landblockId,
|
||||
Vao = _gl.GenVertexArray(),
|
||||
WorldOrigin = worldOrigin,
|
||||
IndexCount = meshData.Indices.Length,
|
||||
MinZ = zMin,
|
||||
MaxZ = zMax,
|
||||
Vertices = worldVerts,
|
||||
Indices = meshData.Indices, // local 0..N-1; will be rebased on rebuild
|
||||
MinZ = zMin,
|
||||
MaxZ = zMax,
|
||||
};
|
||||
|
||||
_gl.BindVertexArray(gpu.Vao);
|
||||
_gpuDirty = true;
|
||||
}
|
||||
|
||||
gpu.Vbo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, gpu.Vbo);
|
||||
fixed (void* p = meshData.Vertices)
|
||||
_gl.BufferData(BufferTargetARB.ArrayBuffer,
|
||||
(nuint)(meshData.Vertices.Length * sizeof(TerrainVertex)), p, BufferUsageARB.StaticDraw);
|
||||
public void RemoveLandblock(uint landblockId)
|
||||
{
|
||||
if (_entries.Remove(landblockId))
|
||||
_gpuDirty = true;
|
||||
}
|
||||
|
||||
gpu.Ebo = _gl.GenBuffer();
|
||||
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, gpu.Ebo);
|
||||
fixed (void* p = meshData.Indices)
|
||||
_gl.BufferData(BufferTargetARB.ElementArrayBuffer,
|
||||
(nuint)(meshData.Indices.Length * sizeof(uint)), p, BufferUsageARB.StaticDraw);
|
||||
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);
|
||||
|
||||
|
|
@ -85,7 +159,7 @@ public sealed unsafe class TerrainRenderer : IDisposable
|
|||
_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). The shader reads .x/.y/.z/.w as 8-bit fields.
|
||||
// offsets 24, 28, 32, 36).
|
||||
nint dataOffset = 6 * sizeof(float); // 24 bytes
|
||||
_gl.EnableVertexAttribArray(2);
|
||||
_gl.VertexAttribIPointer(2, 4, VertexAttribIType.UnsignedByte, stride, (void*)dataOffset);
|
||||
|
|
@ -97,80 +171,77 @@ public sealed unsafe class TerrainRenderer : IDisposable
|
|||
_gl.VertexAttribIPointer(5, 4, VertexAttribIType.UnsignedByte, stride, (void*)(dataOffset + 12));
|
||||
|
||||
_gl.BindVertexArray(0);
|
||||
_landblocks[landblockId] = gpu;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Release GPU buffers for a previously-added landblock. No-op if the
|
||||
/// landblock wasn't added. Called by the streaming system when a
|
||||
/// landblock falls outside the visible window.
|
||||
/// Concatenate all loaded landblocks into a single VBO + EBO and upload.
|
||||
/// Called on the cold path (landblock load / unload), not per frame.
|
||||
/// </summary>
|
||||
public void RemoveLandblock(uint landblockId)
|
||||
private void RebuildGpuBuffers()
|
||||
{
|
||||
if (!_landblocks.TryGetValue(landblockId, out var gpu))
|
||||
return;
|
||||
|
||||
_gl.DeleteBuffer(gpu.Vbo);
|
||||
_gl.DeleteBuffer(gpu.Ebo);
|
||||
_gl.DeleteVertexArray(gpu.Vao);
|
||||
_landblocks.Remove(landblockId);
|
||||
}
|
||||
|
||||
public void Draw(ICamera camera, FrustumPlanes? frustum = null, uint? neverCullLandblockId = null)
|
||||
{
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uView", camera.View);
|
||||
_shader.SetMatrix4("uProjection", camera.Projection);
|
||||
|
||||
// Bind terrain atlas on unit 0 and 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 lb in _landblocks.Values)
|
||||
// Measure totals.
|
||||
int totalVerts = 0;
|
||||
int totalIndices = 0;
|
||||
foreach (var e in _entries.Values)
|
||||
{
|
||||
if (frustum is not null && lb.LandblockId != neverCullLandblockId)
|
||||
{
|
||||
var aabbMin = new Vector3(lb.WorldOrigin.X, lb.WorldOrigin.Y, lb.MinZ);
|
||||
var aabbMax = new Vector3(lb.WorldOrigin.X + 192f, lb.WorldOrigin.Y + 192f, lb.MaxZ);
|
||||
if (!FrustumCuller.IsAabbVisible(frustum.Value, aabbMin, aabbMax))
|
||||
continue;
|
||||
}
|
||||
|
||||
var model = Matrix4x4.CreateTranslation(lb.WorldOrigin);
|
||||
_shader.SetMatrix4("uModel", model);
|
||||
_gl.BindVertexArray(lb.Vao);
|
||||
_gl.DrawElements(PrimitiveType.Triangles, (uint)lb.IndexCount, DrawElementsType.UnsignedInt, (void*)0);
|
||||
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;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var lb in _landblocks.Values)
|
||||
{
|
||||
_gl.DeleteBuffer(lb.Vbo);
|
||||
_gl.DeleteBuffer(lb.Ebo);
|
||||
_gl.DeleteVertexArray(lb.Vao);
|
||||
}
|
||||
_landblocks.Clear();
|
||||
}
|
||||
// -------------------------------------------------------------------------
|
||||
// Data types
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private sealed class LandblockGpu
|
||||
private sealed class LandblockEntry
|
||||
{
|
||||
public uint LandblockId;
|
||||
public uint Vao;
|
||||
public uint Vbo;
|
||||
public uint Ebo;
|
||||
public int IndexCount;
|
||||
public Vector3 WorldOrigin;
|
||||
public float MinZ;
|
||||
public float MaxZ;
|
||||
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