feat(app): render 3x3 neighbor landblocks with texture atlas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-10 20:23:21 +02:00
parent 347a7e92ff
commit 560100e5b6
4 changed files with 109 additions and 79 deletions

View file

@ -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<T>
// (returns null on miss) rather than TryGet to sidestep
// [MaybeNullWhen(false)] nullable-generic analysis under
// TreatWarningsAsErrors.
uint landblockId = 0xA9B4FFFFu;
var block = _dats.Get<LandBlock>(landblockId);
if (block is null)
{
foreach (var file in _dats.Cell.Tree)
{
if ((file.Id & 0xFFFFu) == 0xFFFFu)
{
landblockId = file.Id;
block = _dats.Get<LandBlock>(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<DatReaderWriter.DBObjs.Region>(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<uint, uint>());
_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<DatReaderWriter.DBObjs.LandBlockInfo>((landblockId & 0xFFFF0000u) | 0xFFFEu);
var entities = info is not null
? AcDream.Core.World.LandblockLoader.BuildEntitiesFromInfo(info)
: Array.Empty<AcDream.Core.World.WorldEntity>();
// 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<AcDream.Core.World.WorldEntity>(entities.Count);
foreach (var e in entities)
var hydratedEntities = new List<AcDream.Core.World.WorldEntity>(allEntities.Count);
foreach (var e in allEntities)
{
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));
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)
{
@ -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)