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