feat(app): add TerrainAtlas for GL_TEXTURE_2D_ARRAY terrain textures
This commit is contained in:
parent
78ce099440
commit
347a7e92ff
1 changed files with 164 additions and 0 deletions
164
src/AcDream.App/Rendering/TerrainAtlas.cs
Normal file
164
src/AcDream.App/Rendering/TerrainAtlas.cs
Normal file
|
|
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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))).
|
||||||
|
/// </summary>
|
||||||
|
public sealed unsafe class TerrainAtlas : IDisposable
|
||||||
|
{
|
||||||
|
private readonly GL _gl;
|
||||||
|
public uint GlTexture { get; }
|
||||||
|
public IReadOnlyDictionary<uint, uint> TerrainTypeToLayer { get; }
|
||||||
|
public int LayerCount { get; }
|
||||||
|
|
||||||
|
private TerrainAtlas(GL gl, uint glTexture, IReadOnlyDictionary<uint, uint> map, int layerCount)
|
||||||
|
{
|
||||||
|
_gl = gl;
|
||||||
|
GlTexture = glTexture;
|
||||||
|
TerrainTypeToLayer = map;
|
||||||
|
LayerCount = layerCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public static TerrainAtlas Build(GL gl, DatCollection dats)
|
||||||
|
{
|
||||||
|
var region = dats.Get<Region>(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<SurfaceTexture> TextureId. Decode
|
||||||
|
// each referenced SurfaceTexture → RenderSurface → RGBA8 via SurfaceDecoder.
|
||||||
|
var decodedByType = new Dictionary<uint, DecodedTexture>();
|
||||||
|
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<SurfaceTexture>(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<RenderSurface>((uint)st.Textures[0]);
|
||||||
|
if (rs is null)
|
||||||
|
{
|
||||||
|
decodedByType[typeKey] = DecodedTexture.Magenta;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Palette? palette = rs.DefaultPaletteId != 0
|
||||||
|
? dats.Get<Palette>(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<uint, uint>();
|
||||||
|
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<uint, uint> { [0] = 0u }, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => _gl.DeleteTexture(GlTexture);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue