feat(app): render static meshes from Holtburg LandBlockInfo

This commit is contained in:
Erik 2026-04-10 18:32:09 +02:00
parent cefc689ba8
commit 1375780e14
4 changed files with 232 additions and 0 deletions

View file

@ -21,6 +21,10 @@ public sealed class GameWindow : IDisposable
private DatCollection? _dats; private DatCollection? _dats;
private float _lastMouseX; private float _lastMouseX;
private float _lastMouseY; private float _lastMouseY;
private StaticMeshRenderer? _staticMesh;
private Shader? _meshShader;
private TextureCache? _textureCache;
private IReadOnlyList<AcDream.Core.World.WorldEntity> _entities = Array.Empty<AcDream.Core.World.WorldEntity>();
public GameWindow(string datDir) => _datDir = datDir; public GameWindow(string datDir) => _datDir = datDir;
@ -83,6 +87,10 @@ public sealed class GameWindow : IDisposable
Path.Combine(shadersDir, "terrain.vert"), Path.Combine(shadersDir, "terrain.vert"),
Path.Combine(shadersDir, "terrain.frag")); Path.Combine(shadersDir, "terrain.frag"));
_meshShader = new Shader(_gl,
Path.Combine(shadersDir, "mesh.vert"),
Path.Combine(shadersDir, "mesh.frag"));
_camera = new OrbitCamera _camera = new OrbitCamera
{ {
Aspect = _window!.Size.X / (float)_window.Size.Y, Aspect = _window!.Size.X / (float)_window.Size.Y,
@ -117,16 +125,82 @@ public sealed class GameWindow : IDisposable
var meshData = LandblockMesh.Build(block); var meshData = LandblockMesh.Build(block);
_terrain = new TerrainRenderer(_gl, meshData, _shader); _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<DatReaderWriter.DBObjs.LandBlockInfo>((landblockId & 0xFFFF0000u) | 0xFFFEu);
var entities = info is not null
? AcDream.Core.World.LandblockLoader.BuildEntitiesFromInfo(info)
: Array.Empty<AcDream.Core.World.WorldEntity>();
// 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<AcDream.Core.World.WorldEntity>(entities.Count);
foreach (var e in entities)
{
var meshRefs = new List<AcDream.Core.World.MeshRef>();
if ((e.SourceGfxObjOrSetupId & 0xFF000000u) == 0x01000000u)
{
// GfxObj: one mesh ref with identity transform.
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(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<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
if (setup is not null)
{
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
foreach (var mr in flat)
{
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(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) private void OnRender(double deltaSeconds)
{ {
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); _gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
_terrain?.Draw(_camera!); _terrain?.Draw(_camera!);
_staticMesh?.Draw(_camera!, _entities);
} }
private void OnClosing() private void OnClosing()
{ {
_staticMesh?.Dispose();
_textureCache?.Dispose();
_meshShader?.Dispose();
_terrain?.Dispose(); _terrain?.Dispose();
_shader?.Dispose(); _shader?.Dispose();
_dats?.Dispose(); _dats?.Dispose();

View file

@ -0,0 +1,9 @@
#version 430 core
in vec2 vTex;
out vec4 fragColor;
uniform sampler2D uDiffuse;
void main() {
fragColor = texture(uDiffuse, vTex);
}

View file

@ -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);
}

View file

@ -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<uint, List<SubMeshGpu>> _gpuByGfxObj = new();
public StaticMeshRenderer(GL gl, Shader shader, TextureCache textures)
{
_gl = gl;
_shader = shader;
_textures = textures;
}
public void EnsureUploaded(uint gfxObjId, IReadOnlyList<GfxObjSubMesh> subMeshes)
{
if (_gpuByGfxObj.ContainsKey(gfxObjId))
return;
var list = new List<SubMeshGpu>(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<WorldEntity> 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;
}
}