diff --git a/src/AcDream.App/Rendering/TerrainAtlas.cs b/src/AcDream.App/Rendering/TerrainAtlas.cs new file mode 100644 index 0000000..470231b --- /dev/null +++ b/src/AcDream.App/Rendering/TerrainAtlas.cs @@ -0,0 +1,164 @@ +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; + +/// +/// Builds a GL_TEXTURE_2D_ARRAY from the set of terrain types seen in the loaded +/// landblocks, one layer per unique terrain type. LandblockMesh writes per-vertex +/// layer indices into Vertex.TerrainLayer; the terrain fragment shader samples +/// texture(uAtlas, vec3(uv, float(vLayer))). +/// +public sealed unsafe class TerrainAtlas : IDisposable +{ + private readonly GL _gl; + public uint GlTexture { get; } + public IReadOnlyDictionary TerrainTypeToLayer { get; } + public int LayerCount { get; } + + private TerrainAtlas(GL gl, uint glTexture, IReadOnlyDictionary map, int layerCount) + { + _gl = gl; + GlTexture = glTexture; + TerrainTypeToLayer = map; + LayerCount = layerCount; + } + + /// + /// 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 terrainDesc = region.TerrainInfo?.LandSurfaces?.TexMerge?.TerrainDesc; + if (terrainDesc is null || terrainDesc.Count == 0) + { + // Fallback: upload a single 1x1 white layer as layer 0. + Console.WriteLine("WARN: TerrainDesc missing, using single white fallback layer"); + return BuildFallback(gl); + } + + // Walk TerrainDesc. Each TMTerrainDesc has a TerrainType (enum cast to uint) + // and a TerrainTex with a QualifiedDataId TextureId. Decode + // each referenced SurfaceTexture → RenderSurface → RGBA8 via SurfaceDecoder. + 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; + } + + // Allocate the GL_TEXTURE_2D_ARRAY with the max dimensions seen. Textures + // smaller than (maxW, maxH) are scaled up naively by nearest-neighbor + // replication into a resized RGBA8 buffer. Phase 2b doesn't need mip chains. + 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} layers at {maxW}x{maxH}"); + return new TerrainAtlas(gl, tex, map, layerCount); + } + + 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); + return new TerrainAtlas(gl, tex, new Dictionary { [0] = 0u }, 1); + } + + public void Dispose() => _gl.DeleteTexture(GlTexture); +}