From 1375780e140a78eb0e64b795b889f52b1c63f16c Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Apr 2026 18:32:09 +0200 Subject: [PATCH] feat(app): render static meshes from Holtburg LandBlockInfo --- src/AcDream.App/Rendering/GameWindow.cs | 74 ++++++++++ src/AcDream.App/Rendering/Shaders/mesh.frag | 9 ++ src/AcDream.App/Rendering/Shaders/mesh.vert | 15 ++ .../Rendering/StaticMeshRenderer.cs | 134 ++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 src/AcDream.App/Rendering/Shaders/mesh.frag create mode 100644 src/AcDream.App/Rendering/Shaders/mesh.vert create mode 100644 src/AcDream.App/Rendering/StaticMeshRenderer.cs diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 59a4bb2..a6e041c 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -21,6 +21,10 @@ public sealed class GameWindow : IDisposable private DatCollection? _dats; private float _lastMouseX; private float _lastMouseY; + private StaticMeshRenderer? _staticMesh; + private Shader? _meshShader; + private TextureCache? _textureCache; + private IReadOnlyList _entities = Array.Empty(); public GameWindow(string datDir) => _datDir = datDir; @@ -83,6 +87,10 @@ public sealed class GameWindow : IDisposable Path.Combine(shadersDir, "terrain.vert"), Path.Combine(shadersDir, "terrain.frag")); + _meshShader = new Shader(_gl, + Path.Combine(shadersDir, "mesh.vert"), + Path.Combine(shadersDir, "mesh.frag")); + _camera = new OrbitCamera { Aspect = _window!.Size.X / (float)_window.Size.Y, @@ -117,16 +125,82 @@ public sealed class GameWindow : IDisposable var meshData = LandblockMesh.Build(block); _terrain = new TerrainRenderer(_gl, meshData, _shader); + + _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(); + + // 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 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)); + } + } + 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) + { + var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup); + foreach (var mr in flat) + { + var gfx = _dats.Get(mr.GfxObjId); + if (gfx is null) continue; + var subMeshes = AcDream.Core.Meshing.GfxObjMesh.Build(gfx); + _staticMesh.EnsureUploaded(mr.GfxObjId, subMeshes); + meshRefs.Add(mr); + } + } + } + + if (meshRefs.Count > 0) + { + hydratedEntities.Add(new AcDream.Core.World.WorldEntity + { + Id = e.Id, + SourceGfxObjOrSetupId = e.SourceGfxObjOrSetupId, + Position = e.Position, + Rotation = e.Rotation, + MeshRefs = meshRefs, + }); + } + } + + _entities = hydratedEntities; + Console.WriteLine($"hydrated {_entities.Count} entities on landblock 0x{landblockId:X8}"); } private void OnRender(double deltaSeconds) { _gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); _terrain?.Draw(_camera!); + _staticMesh?.Draw(_camera!, _entities); } private void OnClosing() { + _staticMesh?.Dispose(); + _textureCache?.Dispose(); + _meshShader?.Dispose(); _terrain?.Dispose(); _shader?.Dispose(); _dats?.Dispose(); diff --git a/src/AcDream.App/Rendering/Shaders/mesh.frag b/src/AcDream.App/Rendering/Shaders/mesh.frag new file mode 100644 index 0000000..dcd182c --- /dev/null +++ b/src/AcDream.App/Rendering/Shaders/mesh.frag @@ -0,0 +1,9 @@ +#version 430 core +in vec2 vTex; +out vec4 fragColor; + +uniform sampler2D uDiffuse; + +void main() { + fragColor = texture(uDiffuse, vTex); +} diff --git a/src/AcDream.App/Rendering/Shaders/mesh.vert b/src/AcDream.App/Rendering/Shaders/mesh.vert new file mode 100644 index 0000000..b288c04 --- /dev/null +++ b/src/AcDream.App/Rendering/Shaders/mesh.vert @@ -0,0 +1,15 @@ +#version 430 core +layout(location = 0) in vec3 aPos; +layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aTex; + +uniform mat4 uModel; +uniform mat4 uView; +uniform mat4 uProjection; + +out vec2 vTex; + +void main() { + vTex = aTex; + gl_Position = uProjection * uView * uModel * vec4(aPos, 1.0); +} diff --git a/src/AcDream.App/Rendering/StaticMeshRenderer.cs b/src/AcDream.App/Rendering/StaticMeshRenderer.cs new file mode 100644 index 0000000..621cc31 --- /dev/null +++ b/src/AcDream.App/Rendering/StaticMeshRenderer.cs @@ -0,0 +1,134 @@ +// src/AcDream.App/Rendering/StaticMeshRenderer.cs +using System.Numerics; +using AcDream.Core.Meshing; +using AcDream.Core.Terrain; +using AcDream.Core.World; +using Silk.NET.OpenGL; + +namespace AcDream.App.Rendering; + +public sealed unsafe class StaticMeshRenderer : IDisposable +{ + private readonly GL _gl; + private readonly Shader _shader; + private readonly TextureCache _textures; + + // One GPU bundle per unique GfxObj id. Each GfxObj can have multiple sub-meshes. + private readonly Dictionary> _gpuByGfxObj = new(); + + public StaticMeshRenderer(GL gl, Shader shader, TextureCache textures) + { + _gl = gl; + _shader = shader; + _textures = textures; + } + + public void EnsureUploaded(uint gfxObjId, IReadOnlyList subMeshes) + { + if (_gpuByGfxObj.ContainsKey(gfxObjId)) + return; + + var list = new List(subMeshes.Count); + foreach (var sm in subMeshes) + list.Add(UploadSubMesh(sm)); + _gpuByGfxObj[gfxObjId] = list; + } + + private SubMeshGpu UploadSubMesh(GfxObjSubMesh sm) + { + uint vao = _gl.GenVertexArray(); + _gl.BindVertexArray(vao); + + uint vbo = _gl.GenBuffer(); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, vbo); + fixed (void* p = sm.Vertices) + _gl.BufferData(BufferTargetARB.ArrayBuffer, + (nuint)(sm.Vertices.Length * sizeof(Vertex)), p, BufferUsageARB.StaticDraw); + + uint ebo = _gl.GenBuffer(); + _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, ebo); + fixed (void* p = sm.Indices) + _gl.BufferData(BufferTargetARB.ElementArrayBuffer, + (nuint)(sm.Indices.Length * sizeof(uint)), p, BufferUsageARB.StaticDraw); + + uint stride = (uint)sizeof(Vertex); + _gl.EnableVertexAttribArray(0); + _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0); + _gl.EnableVertexAttribArray(1); + _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float))); + _gl.EnableVertexAttribArray(2); + _gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float))); + + _gl.BindVertexArray(0); + + return new SubMeshGpu + { + Vao = vao, + Vbo = vbo, + Ebo = ebo, + IndexCount = sm.Indices.Length, + SurfaceId = sm.SurfaceId, + }; + } + + public void Draw(OrbitCamera camera, IEnumerable entities) + { + _shader.Use(); + _shader.SetMatrix4("uView", camera.View); + _shader.SetMatrix4("uProjection", camera.Projection); + + foreach (var entity in entities) + { + if (entity.MeshRefs.Count == 0) + continue; + + foreach (var meshRef in entity.MeshRefs) + { + if (!_gpuByGfxObj.TryGetValue(meshRef.GfxObjId, out var subMeshes)) + continue; + + // model = entity root transform * per-part transform + var entityRoot = + Matrix4x4.CreateFromQuaternion(entity.Rotation) * + Matrix4x4.CreateTranslation(entity.Position); + var model = meshRef.PartTransform * entityRoot; + + _shader.SetMatrix4("uModel", model); + + foreach (var sub in subMeshes) + { + uint tex = _textures.GetOrUpload(sub.SurfaceId); + _gl.ActiveTexture(TextureUnit.Texture0); + _gl.BindTexture(TextureTarget.Texture2D, tex); + + _gl.BindVertexArray(sub.Vao); + _gl.DrawElements(PrimitiveType.Triangles, (uint)sub.IndexCount, DrawElementsType.UnsignedInt, (void*)0); + } + } + } + _gl.BindVertexArray(0); + } + + public void Dispose() + { + foreach (var subs in _gpuByGfxObj.Values) + { + foreach (var sub in subs) + { + _gl.DeleteBuffer(sub.Vbo); + _gl.DeleteBuffer(sub.Ebo); + _gl.DeleteVertexArray(sub.Vao); + } + } + _gpuByGfxObj.Clear(); + } + + private sealed class SubMeshGpu + { + public uint Vao; + public uint Vbo; + public uint Ebo; + public int IndexCount; + public uint SurfaceId; + } +}