diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index ba1c021..976be25 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -396,7 +396,10 @@ public sealed class GameWindow : IDisposable // objects live here rather than in LandBlockInfo.Objects. EnvCell ids for a // landblock are packed at 0xAAAABBBB where AAAA is the landblock id high word // and BBBB starts at 0x0100 — documented on LandBlockInfo.NumCells. + // Phase 7.1: also build each EnvCell's room geometry (walls/floors/ceilings) + // from CellStruct.Polygons + EnvCell.Surfaces. int interiorSpawned = 0; + int cellMeshSpawned = 0; uint interiorIdCounter = 0x40000000u; // distinct from scenery (0x80000000+) and stabs foreach (var lb in worldView.Landblocks) { @@ -416,9 +419,64 @@ public sealed class GameWindow : IDisposable uint firstCellId = (lb.LandblockId & 0xFFFF0000u) | 0x0100u; for (uint offset = 0; offset < lbInfo.NumCells; offset++) { - var envCell = _dats.Get(firstCellId + offset); + uint envCellId = firstCellId + offset; + var envCell = _dats.Get(envCellId); if (envCell is null) continue; + // Phase 7.1: build and register room geometry for this EnvCell. + // Each EnvCell has an EnvironmentId that points to an Environment dat + // containing CellStruct geometry (vertex arrays + polygons). The surfaces + // list on the EnvCell (not the CellStruct) maps polygon PosSurface indices + // to unqualified surface ids (OR with 0x08000000 for the full dat id). + if (envCell.EnvironmentId != 0) + { + var environment = _dats.Get(0x0D000000u | envCell.EnvironmentId); + if (environment is not null + && environment.Cells.TryGetValue(envCell.CellStructure, out var cellStruct)) + { + var cellSubMeshes = AcDream.Core.Meshing.CellMesh.Build(envCell, cellStruct); + if (cellSubMeshes.Count > 0) + { + // Use the EnvCell dat id as the GPU upload key. EnvCell ids + // live in 0xAAAA01xx space which is disjoint from GfxObj + // (0x01xxxxxx) and Setup (0x02xxxxxx) ids — no collision risk. + _staticMesh.EnsureUploaded(envCellId, cellSubMeshes); + + // Cell vertices are in env-local space. The per-cell world + // transform is: rotate(envCell.Position.Orientation) then + // translate(envCell.Position.Origin + landblock offset). + // We bake the full transform into PartTransform and leave + // the WorldEntity at identity so the renderer's + // model = PartTransform * entityRoot = cellTransform * I + // gives the correctly positioned cell mesh. + var cellTransform = + System.Numerics.Matrix4x4.CreateFromQuaternion(envCell.Position.Orientation) * + System.Numerics.Matrix4x4.CreateTranslation(envCell.Position.Origin + lbOffset); + + var cellMeshRef = new AcDream.Core.World.MeshRef(envCellId, cellTransform); + + var cellEntity = new AcDream.Core.World.WorldEntity + { + Id = interiorIdCounter++, + SourceGfxObjOrSetupId = envCellId, + Position = System.Numerics.Vector3.Zero, + Rotation = System.Numerics.Quaternion.Identity, + MeshRefs = new[] { cellMeshRef }, + }; + hydratedEntities.Add(cellEntity); + + var cellSnapshot = new AcDream.Plugin.Abstractions.WorldEntitySnapshot( + Id: cellEntity.Id, + SourceId: cellEntity.SourceGfxObjOrSetupId, + Position: cellEntity.Position, + Rotation: cellEntity.Rotation); + _worldGameState.Add(cellSnapshot); + _worldEvents.FireEntitySpawned(cellSnapshot); + cellMeshSpawned++; + } + } + } + foreach (var stab in envCell.StaticObjects) { // Resolve stab id to mesh (same as LandBlockInfo.Objects). @@ -483,6 +541,7 @@ public sealed class GameWindow : IDisposable } } Console.WriteLine($"interior: spawned {interiorSpawned} static objects from EnvCells"); + Console.WriteLine($"interior: built {cellMeshSpawned} cell room meshes"); _entities = hydratedEntities; Console.WriteLine($"hydrated {_entities.Count} entities total (stabs + buildings + scenery + interior)"); diff --git a/src/AcDream.Core/Meshing/CellMesh.cs b/src/AcDream.Core/Meshing/CellMesh.cs new file mode 100644 index 0000000..06075b8 --- /dev/null +++ b/src/AcDream.Core/Meshing/CellMesh.cs @@ -0,0 +1,107 @@ +using System.Numerics; +using AcDream.Core.Terrain; +using DatReaderWriter.DBObjs; +using DatReaderWriter.Types; + +namespace AcDream.Core.Meshing; + +/// +/// Builds renderable sub-meshes from an EnvCell's room geometry (walls, +/// floors, ceilings). The geometry lives in the linked Environment dat: +/// EnvCell.EnvironmentId → Environment → Cells[CellStructure] → CellStruct. +/// This mirrors GfxObjMesh.Build but reads surfaces from EnvCell.Surfaces +/// (not from the CellStruct itself) and uses the same fan-triangulation +/// and per-surface deduplication pattern. +/// +public static class CellMesh +{ + /// + /// Walk a CellStruct's polygons and produce one + /// per referenced Surface. Surfaces are resolved from .Surfaces + /// (OR'd with 0x08000000 to form the full dat id). Polygons are triangulated as fans. + /// + public static IReadOnlyList Build(EnvCell envCell, CellStruct cellStruct) + { + // Group output vertices and indices per surface dat id. + var perSurface = new Dictionary Vertices, List Indices, Dictionary<(int pos, int uv), uint> Dedupe)>(); + + foreach (var kvp in cellStruct.Polygons) + { + var poly = kvp.Value; + + if (poly.VertexIds.Count < 3) + continue; // degenerate polygon + + // Skip if NoPos stippling is set (polygon has no positive surface geometry). + if (poly.Stippling.HasFlag(DatReaderWriter.Enums.StipplingType.NoPos)) + continue; + + int surfaceIdx = poly.PosSurface; + if (surfaceIdx < 0 || surfaceIdx >= envCell.Surfaces.Count) + continue; // out-of-range surface index + + // Surfaces on EnvCell are unqualified ids; OR with 0x08000000 for the full dat id. + uint surfaceId = (uint)envCell.Surfaces[surfaceIdx] | 0x08000000u; + + if (!perSurface.TryGetValue(surfaceId, out var bucket)) + { + bucket = (new List(), new List(), new Dictionary<(int, int), uint>()); + perSurface[surfaceId] = bucket; + } + + // Collect output vertex indices for this polygon. + var polyOut = new List(poly.VertexIds.Count); + bool skipPoly = false; + + for (int i = 0; i < poly.VertexIds.Count; i++) + { + int posIdx = poly.VertexIds[i]; + int uvIdx = i < poly.PosUVIndices.Count ? poly.PosUVIndices[i] : 0; + + if (!cellStruct.VertexArray.Vertices.TryGetValue((ushort)posIdx, out var sw)) + { + skipPoly = true; + break; + } + + var texcoord = uvIdx >= 0 && uvIdx < sw.UVs.Count + ? new Vector2(sw.UVs[uvIdx].U, sw.UVs[uvIdx].V) + : Vector2.Zero; + + // Use normal from vertex data; fall back to up-vector if missing. + var normal = sw.Normal != Vector3.Zero ? sw.Normal : Vector3.UnitZ; + + var key = (posIdx, uvIdx); + if (!bucket.Dedupe.TryGetValue(key, out var outIdx)) + { + outIdx = (uint)bucket.Vertices.Count; + bucket.Vertices.Add(new Vertex(sw.Origin, normal, texcoord, TerrainLayer: 0)); + bucket.Dedupe[key] = outIdx; + } + polyOut.Add(outIdx); + } + + if (skipPoly || polyOut.Count < 3) + continue; + + // Fan triangulation: (v0, v1, v2), (v0, v2, v3), ... + for (int i = 1; i < polyOut.Count - 1; i++) + { + bucket.Indices.Add(polyOut[0]); + bucket.Indices.Add(polyOut[i]); + bucket.Indices.Add(polyOut[i + 1]); + } + } + + // Emit one sub-mesh per surface. + var result = new List(perSurface.Count); + foreach (var kvp in perSurface) + { + result.Add(new GfxObjSubMesh( + SurfaceId: kvp.Key, + Vertices: kvp.Value.Vertices.ToArray(), + Indices: kvp.Value.Indices.ToArray())); + } + return result; + } +}