diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index b40a392..3c63255 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1,6 +1,4 @@ -using AcDream.Core.Terrain; using DatReaderWriter; -using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using Silk.NET.Input; using Silk.NET.Maths; @@ -98,75 +96,67 @@ public sealed class GameWindow : IDisposable _dats = new DatCollection(_datDir, DatAccessType.Read); - // Find ANY landblock ending in 0xFFFF. Holtburg 0xA9B4FFFF is a - // good default; fall back to the first one we find. Using Get - // (returns null on miss) rather than TryGet to sidestep - // [MaybeNullWhen(false)] nullable-generic analysis under - // TreatWarningsAsErrors. - uint landblockId = 0xA9B4FFFFu; - var block = _dats.Get(landblockId); - if (block is null) - { - foreach (var file in _dats.Cell.Tree) - { - if ((file.Id & 0xFFFFu) == 0xFFFFu) - { - landblockId = file.Id; - block = _dats.Get(landblockId); - break; - } - } - } + uint centerLandblockId = 0xA9B4FFFFu; + Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}"); - if (block is null) - throw new InvalidOperationException("no landblock found in cell dat"); - - Console.WriteLine($"loaded landblock 0x{landblockId:X8}"); - - // Load the non-linear LandHeightTable from the Region dat. AC encodes - // per-vertex heights as byte indices into this 256-entry float table, - // not as a simple * 2.0 ramp — building placements depend on the real - // table, so terrain rendered with the simplified scale would leave - // buildings floating or buried. var region = _dats.Get(0x13000000u); var heightTable = region?.LandDefs.LandHeightTable; if (heightTable is null || heightTable.Length < 256) throw new InvalidOperationException("Region.LandDefs.LandHeightTable missing or truncated"); - var meshData = LandblockMesh.Build(block, heightTable, new Dictionary()); - _terrain = new TerrainRenderer(_gl, meshData, _shader); + // Build the terrain atlas once from the Region dat. + var terrainAtlas = AcDream.App.Rendering.TerrainAtlas.Build(_gl, _dats); + + _terrain = new TerrainRenderer(_gl, _shader, terrainAtlas); + + // Load the 3x3 neighbor grid. + var worldView = AcDream.Core.World.WorldView.Load(_dats, centerLandblockId); + Console.WriteLine($"loaded {worldView.Landblocks.Count} landblocks in 3x3 grid"); + + int centerX = (int)((centerLandblockId >> 24) & 0xFFu); + int centerY = (int)((centerLandblockId >> 16) & 0xFFu); + + foreach (var lb in worldView.Landblocks) + { + var meshData = AcDream.Core.Terrain.LandblockMesh.Build( + lb.Heightmap, heightTable, terrainAtlas.TerrainTypeToLayer); + + // Compute world origin for this landblock relative to the center. + int lbX = (int)((lb.LandblockId >> 24) & 0xFFu); + int lbY = (int)((lb.LandblockId >> 16) & 0xFFu); + var origin = new System.Numerics.Vector3( + (lbX - centerX) * 192f, + (lbY - centerY) * 192f, + 0f); + + _terrain.AddLandblock(meshData, origin); + } _textureCache = new TextureCache(_gl, _dats); _staticMesh = new StaticMeshRenderer(_gl, _meshShader, _textureCache); - // Load LandBlockInfo for Holtburg, hydrate entities. - var info = _dats.Get((landblockId & 0xFFFF0000u) | 0xFFFEu); - var entities = info is not null - ? AcDream.Core.World.LandblockLoader.BuildEntitiesFromInfo(info) - : Array.Empty(); + // Hydrate entities from ALL loaded landblocks, not just the center. + var allEntities = worldView.AllEntities.ToList(); + Console.WriteLine($"hydrating {allEntities.Count} entities across {worldView.Landblocks.Count} landblocks"); - // Populate MeshRefs for each entity by resolving its source id to GfxObj or Setup - // and extracting sub-meshes. Store back onto the entity. Since WorldEntity is - // `required init`, we rebuild the entity here. - var hydratedEntities = new List(entities.Count); - foreach (var e in entities) + var hydratedEntities = new List(allEntities.Count); + foreach (var e in allEntities) { var meshRefs = new List(); if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x01000000u) { - // GfxObj: one mesh ref with identity transform. var gfx = _dats.Get(e.SourceGfxObjOrSetupId); if (gfx is not null) { var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx); _staticMesh.EnsureUploaded(e.SourceGfxObjOrSetupId, subMeshes); - meshRefs.Add(new AcDream.Core.World.MeshRef(e.SourceGfxObjOrSetupId, System.Numerics.Matrix4x4.Identity)); + meshRefs.Add(new AcDream.Core.World.MeshRef( + e.SourceGfxObjOrSetupId, System.Numerics.Matrix4x4.Identity)); } } else if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x02000000u) { - // Setup: flatten into parts, upload each part's GfxObj. var setup = _dats.Get(e.SourceGfxObjOrSetupId); if (setup is not null) { @@ -184,11 +174,21 @@ public sealed class GameWindow : IDisposable if (meshRefs.Count > 0) { + // Add the landblock origin to the entity's position so the static + // mesh renderer draws it at the correct world location. + var sourceLandblock = worldView.Landblocks.First(lb => lb.Entities.Contains(e)); + int lbX = (int)((sourceLandblock.LandblockId >> 24) & 0xFFu); + int lbY = (int)((sourceLandblock.LandblockId >> 16) & 0xFFu); + var worldOffset = new System.Numerics.Vector3( + (lbX - centerX) * 192f, + (lbY - centerY) * 192f, + 0f); + hydratedEntities.Add(new AcDream.Core.World.WorldEntity { Id = e.Id, SourceGfxObjOrSetupId = e.SourceGfxObjOrSetupId, - Position = e.Position, + Position = e.Position + worldOffset, Rotation = e.Rotation, MeshRefs = meshRefs, }); @@ -196,7 +196,7 @@ public sealed class GameWindow : IDisposable } _entities = hydratedEntities; - Console.WriteLine($"hydrated {_entities.Count} entities on landblock 0x{landblockId:X8}"); + Console.WriteLine($"hydrated {_entities.Count} entities"); } private void OnRender(double deltaSeconds) diff --git a/src/AcDream.App/Rendering/Shaders/terrain.frag b/src/AcDream.App/Rendering/Shaders/terrain.frag index d6e747b..6e0e917 100644 --- a/src/AcDream.App/Rendering/Shaders/terrain.frag +++ b/src/AcDream.App/Rendering/Shaders/terrain.frag @@ -1,14 +1,10 @@ #version 430 core -in float vHeight; +in vec2 vTex; +in flat uint vLayer; out vec4 fragColor; +uniform sampler2DArray uAtlas; + void main() { - float t = clamp(vHeight / 200.0, 0.0, 1.0); - vec3 low = vec3(0.10, 0.35, 0.15); // green lowland - vec3 mid = vec3(0.55, 0.45, 0.25); // brown mid - vec3 high = vec3(0.90, 0.90, 0.95); // snowy peak - vec3 color = t < 0.5 - ? mix(low, mid, t * 2.0) - : mix(mid, high, (t - 0.5) * 2.0); - fragColor = vec4(color, 1.0); + fragColor = texture(uAtlas, vec3(vTex, float(vLayer))); } diff --git a/src/AcDream.App/Rendering/Shaders/terrain.vert b/src/AcDream.App/Rendering/Shaders/terrain.vert index 1f3b04f..a9443e2 100644 --- a/src/AcDream.App/Rendering/Shaders/terrain.vert +++ b/src/AcDream.App/Rendering/Shaders/terrain.vert @@ -2,13 +2,17 @@ layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aNormal; layout(location = 2) in vec2 aTex; +layout(location = 3) in uint aTerrainLayer; +uniform mat4 uModel; uniform mat4 uView; uniform mat4 uProjection; -out float vHeight; +out vec2 vTex; +out flat uint vLayer; void main() { - vHeight = aPos.z; - gl_Position = uProjection * uView * vec4(aPos, 1.0); + vTex = aTex; + vLayer = aTerrainLayer; + gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0); } diff --git a/src/AcDream.App/Rendering/TerrainRenderer.cs b/src/AcDream.App/Rendering/TerrainRenderer.cs index 40ba538..c82eda0 100644 --- a/src/AcDream.App/Rendering/TerrainRenderer.cs +++ b/src/AcDream.App/Rendering/TerrainRenderer.cs @@ -1,3 +1,4 @@ +using System.Numerics; using AcDream.Core.Terrain; using Silk.NET.OpenGL; @@ -7,33 +8,39 @@ public sealed unsafe class TerrainRenderer : IDisposable { private readonly GL _gl; private readonly Shader _shader; - private readonly uint _vao; - private readonly uint _vbo; - private readonly uint _ebo; - private readonly int _indexCount; + private readonly TerrainAtlas _atlas; + private readonly List _landblocks = new(); - public TerrainRenderer(GL gl, LandblockMeshData meshData, Shader shader) + public TerrainRenderer(GL gl, Shader shader, TerrainAtlas atlas) { _gl = gl; _shader = shader; - _indexCount = meshData.Indices.Length; + _atlas = atlas; + } - _vao = _gl.GenVertexArray(); - _gl.BindVertexArray(_vao); + public void AddLandblock(LandblockMeshData meshData, Vector3 worldOrigin) + { + var gpu = new LandblockGpu + { + Vao = _gl.GenVertexArray(), + WorldOrigin = worldOrigin, + IndexCount = meshData.Indices.Length, + }; - _vbo = _gl.GenBuffer(); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); + _gl.BindVertexArray(gpu.Vao); + + gpu.Vbo = _gl.GenBuffer(); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, gpu.Vbo); fixed (void* p = meshData.Vertices) _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)(meshData.Vertices.Length * sizeof(Vertex)), p, BufferUsageARB.StaticDraw); - _ebo = _gl.GenBuffer(); - _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ebo); + 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); - // vertex layout: position(3f), normal(3f), texcoord(2f) = 8 floats stride uint stride = (uint)sizeof(Vertex); _gl.EnableVertexAttribArray(0); _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0); @@ -45,22 +52,45 @@ public sealed unsafe class TerrainRenderer : IDisposable _gl.VertexAttribIPointer(3, 1, VertexAttribIType.UnsignedInt, stride, (void*)(8 * sizeof(float))); _gl.BindVertexArray(0); + _landblocks.Add(gpu); } - public void Draw(OrbitCamera camera) + public void Draw(OrbitCamera camera) // ICamera in Task 5 { _shader.Use(); _shader.SetMatrix4("uView", camera.View); _shader.SetMatrix4("uProjection", camera.Projection); - _gl.BindVertexArray(_vao); - _gl.DrawElements(PrimitiveType.Triangles, (uint)_indexCount, DrawElementsType.UnsignedInt, (void*)0); + + _gl.ActiveTexture(TextureUnit.Texture0); + _gl.BindTexture(TextureTarget.Texture2DArray, _atlas.GlTexture); + + foreach (var lb in _landblocks) + { + 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); + } _gl.BindVertexArray(0); } public void Dispose() { - _gl.DeleteBuffer(_vbo); - _gl.DeleteBuffer(_ebo); - _gl.DeleteVertexArray(_vao); + foreach (var lb in _landblocks) + { + _gl.DeleteBuffer(lb.Vbo); + _gl.DeleteBuffer(lb.Ebo); + _gl.DeleteVertexArray(lb.Vao); + } + _landblocks.Clear(); + } + + private sealed class LandblockGpu + { + public uint Vao; + public uint Vbo; + public uint Ebo; + public int IndexCount; + public Vector3 WorldOrigin; } }