using System.Numerics; using AcDream.Core.Terrain; using DatReaderWriter; 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. /// /// The EnvCell that owns the surface list. /// The CellStruct containing the polygon + vertex geometry. /// /// Optional dat collection used to read Surface.Type flags and set /// . When null (e.g. offline tests) /// all sub-meshes default to . /// public static IReadOnlyList Build(EnvCell envCell, CellStruct cellStruct, DatCollection? dats = null) { // 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) { // Resolve Surface.Type flags when a DatCollection is available so the // renderer can split the draw into opaque and translucent passes. var translucency = TranslucencyKind.Opaque; if (dats is not null) { var surface = dats.Get(kvp.Key); if (surface is not null) translucency = TranslucencyKindExtensions.FromSurfaceType(surface.Type); } result.Add(new GfxObjSubMesh( SurfaceId: kvp.Key, Vertices: kvp.Value.Vertices.ToArray(), Indices: kvp.Value.Indices.ToArray()) { Translucency = translucency, }); } return result; } }