using AcDream.Core.Textures; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Enums; using Silk.NET.OpenGL; using DatPixelFormat = DatReaderWriter.Enums.PixelFormat; using GLPixelFormat = Silk.NET.OpenGL.PixelFormat; namespace AcDream.App.Rendering; /// /// Holds both texture arrays the terrain renderer samples from: /// /// /// Terrain atlas — one GL_TEXTURE_2D_ARRAY layer per terrain type /// (grass, dirt, sand, forest...), sourced from /// Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc. /// /// /// Alpha atlas — one GL_TEXTURE_2D_ARRAY layer per blend mask, /// sourced from CornerTerrainMaps / SideTerrainMaps / RoadMaps in the /// same TexMerge. Used by the fragment shader to blend up to three /// terrain overlays and two roads on top of a base cell texture. /// /// /// The alpha atlas is built but not yet sampled by any shader — that wiring /// lands in Phase 3c.4 along with the shader rewrite. /// public sealed unsafe class TerrainAtlas : IDisposable { private readonly GL _gl; // --- Terrain atlas (unchanged public API from Phase 2b) --- public uint GlTexture { get; } // terrain atlas, kept as GlTexture for back-compat with TerrainRenderer public IReadOnlyDictionary TerrainTypeToLayer { get; } public int LayerCount { get; } // --- Alpha atlas (new in Phase 3c.2) --- public uint GlAlphaTexture { get; } public int AlphaLayerCount { get; } /// Layer indices in the alpha atlas for CornerTerrainMaps (typically 4 entries). public IReadOnlyList CornerAlphaLayers { get; } /// Layer indices in the alpha atlas for SideTerrainMaps (typically 4 entries). public IReadOnlyList SideAlphaLayers { get; } /// Layer indices in the alpha atlas for RoadMaps (variable count). public IReadOnlyList RoadAlphaLayers { get; } // --- Parallel TCode/RCode arrays (added in Phase 3c.4 for BuildSurface) --- /// TCode for each CornerTerrainMap, parallel to . public IReadOnlyList CornerAlphaTCodes { get; } /// TCode for each SideTerrainMap, parallel to . public IReadOnlyList SideAlphaTCodes { get; } /// RCode for each RoadMap, parallel to . public IReadOnlyList RoadAlphaRCodes { get; } private TerrainAtlas( GL gl, uint glTexture, IReadOnlyDictionary map, int layerCount, uint glAlphaTexture, int alphaLayerCount, IReadOnlyList cornerLayers, IReadOnlyList sideLayers, IReadOnlyList roadLayers, IReadOnlyList cornerTCodes, IReadOnlyList sideTCodes, IReadOnlyList roadRCodes) { _gl = gl; GlTexture = glTexture; TerrainTypeToLayer = map; LayerCount = layerCount; GlAlphaTexture = glAlphaTexture; AlphaLayerCount = alphaLayerCount; CornerAlphaLayers = cornerLayers; SideAlphaLayers = sideLayers; RoadAlphaLayers = roadLayers; CornerAlphaTCodes = cornerTCodes; SideAlphaTCodes = sideTCodes; RoadAlphaRCodes = roadRCodes; } /// /// Build the atlas by walking Region.TerrainInfo.LandSurfaces.TexMerge.TerrainDesc /// for the mapping from TerrainTextureType to SurfaceTexture id, decoding each /// to RGBA8, and uploading as layers in a single GL_TEXTURE_2D_ARRAY. /// public static TerrainAtlas Build(GL gl, DatCollection dats) { var region = dats.Get(0x13000000u) ?? throw new InvalidOperationException("Region dat id 0x13000000 missing"); var texMerge = region.TerrainInfo?.LandSurfaces?.TexMerge; var terrainDesc = texMerge?.TerrainDesc; if (terrainDesc is null || terrainDesc.Count == 0) { Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer"); return BuildFallback(gl); } // ---- Terrain atlas (unchanged Phase 2b logic) ---- var decodedByType = new Dictionary(); int maxW = 1, maxH = 1; foreach (var tmtd in terrainDesc) { uint typeKey = (uint)tmtd.TerrainType; if (decodedByType.ContainsKey(typeKey)) continue; var surfaceTextureId = (uint)tmtd.TerrainTex.TextureId; var st = dats.Get(surfaceTextureId); if (st is null || st.Textures.Count == 0) { Console.WriteLine($"WARN: TerrainType {tmtd.TerrainType} SurfaceTexture 0x{surfaceTextureId:X8} missing"); decodedByType[typeKey] = DecodedTexture.Magenta; continue; } var rs = dats.Get((uint)st.Textures[0]); if (rs is null) { decodedByType[typeKey] = DecodedTexture.Magenta; continue; } Palette? palette = rs.DefaultPaletteId != 0 ? dats.Get(rs.DefaultPaletteId) : null; var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette); decodedByType[typeKey] = decoded; if (decoded.Width > maxW) maxW = decoded.Width; if (decoded.Height > maxH) maxH = decoded.Height; } int layerCount = decodedByType.Count; uint tex = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, tex); gl.TexImage3D( TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, (uint)maxW, (uint)maxH, (uint)layerCount, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); var map = new Dictionary(); int layerIdx = 0; foreach (var kvp in decodedByType) { byte[] buffer = ResizeRgba8Nearest(kvp.Value, maxW, maxH); fixed (byte* p = buffer) { gl.TexSubImage3D( TextureTarget.Texture2DArray, 0, 0, 0, layerIdx, (uint)maxW, (uint)maxH, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); } map[kvp.Key] = (uint)layerIdx; layerIdx++; } gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); gl.BindTexture(TextureTarget.Texture2DArray, 0); Console.WriteLine($"TerrainAtlas: {layerCount} terrain layers at {maxW}x{maxH}"); // ---- Alpha atlas (new in Phase 3c.2) ---- // texMerge is guaranteed non-null here: the early return above exited // if texMerge?.TerrainDesc was null. var alphaBuild = BuildAlphaAtlas(gl, dats, texMerge!); return new TerrainAtlas( gl, tex, map, layerCount, alphaBuild.gl, alphaBuild.layerCount, alphaBuild.corner, alphaBuild.side, alphaBuild.road, alphaBuild.cornerTCodes, alphaBuild.sideTCodes, alphaBuild.roadRCodes); } /// /// Load corner, side, and road alpha maps from the TexMerge into a second /// GL_TEXTURE_2D_ARRAY. AC ships these as 512×512 PFID_A8 textures; /// expands each alpha byte /// into all four RGBA channels so the shader can sample from any channel. /// /// Layers are appended in TexMerge insertion order: corners first, then /// sides, then roads. The returned index lists tell /// TerrainBlending.BuildSurface which layer to cite for each /// corner/side/road alpha source. /// private readonly record struct AlphaAtlasBuildResult( uint gl, int layerCount, IReadOnlyList corner, IReadOnlyList side, IReadOnlyList road, IReadOnlyList cornerTCodes, IReadOnlyList sideTCodes, IReadOnlyList roadRCodes); private static AlphaAtlasBuildResult BuildAlphaAtlas( GL gl, DatCollection dats, DatReaderWriter.Types.TexMerge texMerge) { var decoded = new List(); var cornerLayers = new List(); var sideLayers = new List(); var roadLayers = new List(); var cornerTCodes = new List(); var sideTCodes = new List(); var roadRCodes = new List(); foreach (var entry in texMerge.CornerTerrainMaps) { if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex)) { cornerLayers.Add((byte)decoded.Count); cornerTCodes.Add(entry.TCode); decoded.Add(dtex); } else { Console.WriteLine($"WARN: CornerTerrainMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode"); } } foreach (var entry in texMerge.SideTerrainMaps) { if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex)) { sideLayers.Add((byte)decoded.Count); sideTCodes.Add(entry.TCode); decoded.Add(dtex); } else { Console.WriteLine($"WARN: SideTerrainMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode"); } } foreach (var entry in texMerge.RoadMaps) { if (TryDecodeAlphaMap(dats, (uint)entry.TextureId, out var dtex)) { roadLayers.Add((byte)decoded.Count); roadRCodes.Add(entry.RCode); decoded.Add(dtex); } else { Console.WriteLine($"WARN: RoadMap TextureId 0x{(uint)entry.TextureId:X8} failed to decode"); } } if (decoded.Count == 0) { Console.WriteLine("WARN: no alpha maps loaded; alpha atlas will be a 1x1 white fallback"); uint fallbackAlpha = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, fallbackAlpha); gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; fixed (byte* p = white) gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); gl.BindTexture(TextureTarget.Texture2DArray, 0); return new AlphaAtlasBuildResult( fallbackAlpha, 1, cornerLayers, sideLayers, roadLayers, cornerTCodes, sideTCodes, roadRCodes); } // Alpha maps should all be uniform size (WorldBuilder asserts 512×512). // Fall back to the max observed so a stray mismatch doesn't crash us. int aMaxW = 1, aMaxH = 1; foreach (var d in decoded) { if (d.Width > aMaxW) aMaxW = d.Width; if (d.Height > aMaxH) aMaxH = d.Height; } uint glAlpha = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, glAlpha); gl.TexImage3D( TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, (uint)aMaxW, (uint)aMaxH, (uint)decoded.Count, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); for (int i = 0; i < decoded.Count; i++) { var buffer = ResizeRgba8Nearest(decoded[i], aMaxW, aMaxH); fixed (byte* p = buffer) { gl.TexSubImage3D( TextureTarget.Texture2DArray, 0, 0, 0, i, (uint)aMaxW, (uint)aMaxH, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); } } gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); gl.BindTexture(TextureTarget.Texture2DArray, 0); Console.WriteLine( $"AlphaAtlas: {decoded.Count} layers at {aMaxW}x{aMaxH} " + $"(corners={cornerLayers.Count}, sides={sideLayers.Count}, roads={roadLayers.Count})"); return new AlphaAtlasBuildResult( glAlpha, decoded.Count, cornerLayers, sideLayers, roadLayers, cornerTCodes, sideTCodes, roadRCodes); } private static bool TryDecodeAlphaMap(DatCollection dats, uint surfaceTextureId, out DecodedTexture decoded) { decoded = DecodedTexture.Magenta; var st = dats.Get(surfaceTextureId); if (st is null || st.Textures.Count == 0) return false; var rs = dats.Get((uint)st.Textures[0]); if (rs is null) return false; // Alpha maps ship as PFID_CUSTOM_LSCAPE_ALPHA (AC's landscape-alpha // format) or the more generic PFID_A8; SurfaceDecoder routes both // through the same "replicate single byte to RGBA" path. Palette is // not used. var d = SurfaceDecoder.DecodeRenderSurface(rs, palette: null); if (ReferenceEquals(d, DecodedTexture.Magenta)) return false; decoded = d; return true; } private static byte[] ResizeRgba8Nearest(DecodedTexture src, int dstW, int dstH) { if (src.Width == dstW && src.Height == dstH) return src.Rgba8; var dst = new byte[dstW * dstH * 4]; for (int y = 0; y < dstH; y++) { int srcY = y * src.Height / dstH; for (int x = 0; x < dstW; x++) { int srcX = x * src.Width / dstW; int si = (srcY * src.Width + srcX) * 4; int di = (y * dstW + x) * 4; dst[di + 0] = src.Rgba8[si + 0]; dst[di + 1] = src.Rgba8[si + 1]; dst[di + 2] = src.Rgba8[si + 2]; dst[di + 3] = src.Rgba8[si + 3]; } } return dst; } private static TerrainAtlas BuildFallback(GL gl) { uint tex = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, tex); var white = new byte[] { 0xFF, 0xFF, 0xFF, 0xFF }; gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); fixed (byte* p = white) gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); gl.BindTexture(TextureTarget.Texture2DArray, 0); // Fallback alpha atlas: 1x1 white, no layers tracked uint alphaTex = gl.GenTexture(); gl.BindTexture(TextureTarget.Texture2DArray, alphaTex); gl.TexImage3D(TextureTarget.Texture2DArray, 0, InternalFormat.Rgba8, 1, 1, 1, 0, GLPixelFormat.Rgba, PixelType.UnsignedByte, null); fixed (byte* p = white) gl.TexSubImage3D(TextureTarget.Texture2DArray, 0, 0, 0, 0, 1, 1, 1, GLPixelFormat.Rgba, PixelType.UnsignedByte, p); gl.BindTexture(TextureTarget.Texture2DArray, 0); return new TerrainAtlas( gl, tex, new Dictionary { [0] = 0u }, 1, alphaTex, 1, Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty(), Array.Empty()); } public void Dispose() { _gl.DeleteTexture(GlTexture); _gl.DeleteTexture(GlAlphaTexture); } }