Verbatim copy of 5 WorldBuilder files into src/AcDream.Core/Rendering/Wb/: - TextureHelpers.cs (pixel-format decoders, Chorizite Lib) - SceneryHelpers.cs (scenery transforms, Chorizite Lib) - TerrainUtils.cs, TerrainEntry.cs, CellSplitDirection.cs (WB.Shared Landscape) Namespace migrated from WorldBuilder.* / Chorizite.OpenGLSDLBackend.Lib to AcDream.Core.Rendering.Wb per O-D11. [MemoryPackable] stripped from TerrainEntry per O-D10 (we don't serialize the struct). Updated 3 source files + 1 test file to import from the new namespace. Verbatim discipline (O-D1): only namespace + MemoryPack attribute changed. All algorithm bodies byte-identical to upstream. Note: TextureHelpers omits IsAlphaFormat() and GetCompressedLayerSize() because those reference Chorizite.Core.Render.Enums.TextureFormat, a type that has no path into AcDream.Core without adding an unwanted NuGet dep. Neither method is called from Core or the test suite; the omission is safe. Verified on main checkout: dotnet build green (0 errors), dotnet test green — Failed: 8, Passed: 1147, Skipped: 0, Total: 1155 (baseline maintained). TextureDecodeConformanceTests (9/9) pass byte-for-byte after namespace swap. AcDream.Core project alone builds green in this worktree (App-layer failures are pre-existing, blocked by empty WB submodule, addressed in Tasks 3+4). Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
219 lines
9.8 KiB
C#
219 lines
9.8 KiB
C#
using AcDream.Core.Rendering.Wb;
|
|
using BCnEncoder.Decoder;
|
|
using BCnEncoder.Shared;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Enums;
|
|
|
|
namespace AcDream.Core.Textures;
|
|
|
|
public static class SurfaceDecoder
|
|
{
|
|
private static readonly BcDecoder BcDecoder = new();
|
|
|
|
/// <summary>
|
|
/// Decode a RenderSurface's pixel bytes into RGBA8. Returns <see cref="DecodedTexture.Magenta"/>
|
|
/// for unsupported formats, null data, or corrupt sizing. This overload does NOT
|
|
/// support PFID_INDEX16 — use <see cref="DecodeRenderSurface(RenderSurface, Palette?)"/>
|
|
/// when a palette is available.
|
|
/// </summary>
|
|
public static DecodedTexture DecodeRenderSurface(RenderSurface rs)
|
|
=> DecodeRenderSurface(rs, palette: null, isClipMap: false, isAdditive: false);
|
|
|
|
/// <summary>
|
|
/// Decode a RenderSurface's pixel bytes into RGBA8 with optional palette support.
|
|
/// When <paramref name="palette"/> is non-null and the format is PFID_INDEX16, each
|
|
/// 16-bit value in SourceData is treated as an index into <see cref="Palette.Colors"/>.
|
|
/// When <paramref name="isClipMap"/> is true on an indexed surface, palette indices
|
|
/// below 8 are forced to fully-transparent (AC's clipmap alpha-key convention).
|
|
/// When <paramref name="isAdditive"/> is true, A8/CUSTOM_LSCAPE_ALPHA surfaces
|
|
/// replicate the byte into all four channels (R=G=B=A=val, for terrain alpha masks
|
|
/// and additive surfaces). When false, R=G=B=255, A=val (WB FillA8 semantics).
|
|
/// </summary>
|
|
public static DecodedTexture DecodeRenderSurface(RenderSurface rs, Palette? palette, bool isClipMap = false, bool isAdditive = false)
|
|
{
|
|
if (rs.SourceData is null || rs.Width <= 0 || rs.Height <= 0)
|
|
return DecodedTexture.Magenta;
|
|
|
|
try
|
|
{
|
|
return rs.Format switch
|
|
{
|
|
PixelFormat.PFID_R8G8B8 => DecodeR8G8B8(rs),
|
|
PixelFormat.PFID_A8R8G8B8 => DecodeA8R8G8B8(rs),
|
|
PixelFormat.PFID_X8R8G8B8 => DecodeX8R8G8B8(rs),
|
|
PixelFormat.PFID_DXT1 => DecodeBc(rs, CompressionFormat.Bc1, isClipMap),
|
|
PixelFormat.PFID_DXT3 => DecodeBc(rs, CompressionFormat.Bc2, isClipMap),
|
|
PixelFormat.PFID_DXT5 => DecodeBc(rs, CompressionFormat.Bc3, isClipMap),
|
|
PixelFormat.PFID_A8 or PixelFormat.PFID_CUSTOM_LSCAPE_ALPHA => DecodeA8(rs, isAdditive),
|
|
PixelFormat.PFID_P8 when palette is not null => DecodeP8(rs, palette, isClipMap),
|
|
PixelFormat.PFID_INDEX16 when palette is not null => DecodeIndex16(rs, palette, isClipMap),
|
|
PixelFormat.PFID_R5G6B5 => DecodeR5G6B5(rs),
|
|
PixelFormat.PFID_A4R4G4B4 => DecodeA4R4G4B4(rs),
|
|
_ => DecodedTexture.Magenta,
|
|
};
|
|
}
|
|
catch
|
|
{
|
|
return DecodedTexture.Magenta;
|
|
}
|
|
}
|
|
|
|
private static DecodedTexture DecodeIndex16(RenderSurface rs, Palette palette, bool isClipMap)
|
|
{
|
|
int expectedBytes = rs.Width * rs.Height * 2;
|
|
if (rs.SourceData.Length < expectedBytes || palette.Colors.Count == 0)
|
|
return DecodedTexture.Magenta;
|
|
|
|
var rgba = new byte[rs.Width * rs.Height * 4];
|
|
TextureHelpers.FillIndex16(rs.SourceData, palette, rgba.AsSpan(), rs.Width, rs.Height, isClipMap);
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build a 1x1 RGBA8 texture from a single <see cref="ColorARGB"/> modulated
|
|
/// by a surface translucency value. Used for <c>Surface.Type.HasFlag(Base1Solid)</c>
|
|
/// surfaces that carry a color value instead of a texture chain.
|
|
///
|
|
/// AC's convention: <paramref name="translucency"/> 0.0 is fully opaque, 1.0 is
|
|
/// fully transparent. A surface with Translucency=1.0 should render invisibly,
|
|
/// which the mesh shader's alpha discard (alpha < 0.5) will honor.
|
|
/// </summary>
|
|
public static DecodedTexture DecodeSolidColor(DatReaderWriter.Types.ColorARGB color, float translucency)
|
|
{
|
|
float opacity = Math.Clamp(1f - translucency, 0f, 1f);
|
|
byte alpha = (byte)Math.Clamp(color.Alpha * opacity, 0f, 255f);
|
|
return new DecodedTexture(
|
|
Rgba8: [color.Red, color.Green, color.Blue, alpha],
|
|
Width: 1,
|
|
Height: 1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode single-byte-per-pixel alpha (PFID_A8 / PFID_CUSTOM_LSCAPE_ALPHA) into RGBA8.
|
|
/// When <paramref name="isAdditive"/> is true: R=G=B=A=val (terrain alpha masks and
|
|
/// additive entity textures — the shader reads .r for the blend weight). When false:
|
|
/// R=G=B=255, A=val (WB FillA8 semantics for non-additive entity textures).
|
|
/// </summary>
|
|
private static DecodedTexture DecodeA8(RenderSurface rs, bool isAdditive)
|
|
{
|
|
int expected = rs.Width * rs.Height;
|
|
if (rs.SourceData.Length < expected)
|
|
return DecodedTexture.Magenta;
|
|
|
|
var rgba = new byte[expected * 4];
|
|
if (isAdditive)
|
|
TextureHelpers.FillA8Additive(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height);
|
|
else
|
|
TextureHelpers.FillA8(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height);
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
|
|
private static DecodedTexture DecodeA8R8G8B8(RenderSurface rs)
|
|
{
|
|
int expected = rs.Width * rs.Height * 4;
|
|
if (rs.SourceData.Length < expected)
|
|
return DecodedTexture.Magenta;
|
|
|
|
var rgba = new byte[expected];
|
|
TextureHelpers.FillA8R8G8B8(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height);
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode PFID_P8 (8-bit palette index, one byte per pixel) into RGBA8.
|
|
/// This is the 8-bit sibling of PFID_INDEX16: each byte is a palette index.
|
|
/// The <paramref name="isClipMap"/> convention (indices 0..7 → fully transparent)
|
|
/// is identical to the INDEX16 path.
|
|
/// </summary>
|
|
private static DecodedTexture DecodeP8(RenderSurface rs, Palette palette, bool isClipMap)
|
|
{
|
|
int expectedBytes = rs.Width * rs.Height;
|
|
if (rs.SourceData.Length < expectedBytes || palette.Colors.Count == 0)
|
|
return DecodedTexture.Magenta;
|
|
|
|
var rgba = new byte[rs.Width * rs.Height * 4];
|
|
TextureHelpers.FillP8(rs.SourceData, palette, rgba.AsSpan(), rs.Width, rs.Height, isClipMap);
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode PFID_R8G8B8 (24-bit, 3 bytes per pixel) into RGBA8 with alpha=255.
|
|
/// AC stores R8G8B8 on disk in B,G,R byte order (confirmed by ACE's
|
|
/// GetImageColorArray: <c>byte b = reader.ReadByte(); g = ...; r = ...;</c>).
|
|
/// Output is R,G,B,255 in RGBA8 order for OpenGL PixelFormat.Rgba upload.
|
|
/// </summary>
|
|
private static DecodedTexture DecodeR8G8B8(RenderSurface rs)
|
|
{
|
|
int expectedBytes = rs.Width * rs.Height * 3;
|
|
if (rs.SourceData.Length < expectedBytes)
|
|
return DecodedTexture.Magenta;
|
|
|
|
var rgba = new byte[rs.Width * rs.Height * 4];
|
|
TextureHelpers.FillR8G8B8(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height);
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decode PFID_X8R8G8B8 (32-bit, 4 bytes per pixel) into RGBA8 with alpha=255.
|
|
/// AC stores X8R8G8B8 on disk in B,G,R,X byte order (DirectX little-endian
|
|
/// convention: low byte = B). The X (high) byte is unused padding and is
|
|
/// discarded — it is NOT treated as alpha. Output is R,G,B,255 for OpenGL.
|
|
/// </summary>
|
|
private static DecodedTexture DecodeX8R8G8B8(RenderSurface rs)
|
|
{
|
|
int expectedBytes = rs.Width * rs.Height * 4;
|
|
if (rs.SourceData.Length < expectedBytes)
|
|
return DecodedTexture.Magenta;
|
|
|
|
var rgba = new byte[expectedBytes];
|
|
for (int i = 0; i < rs.Width * rs.Height; i++)
|
|
{
|
|
int s = i * 4;
|
|
// On-disk byte order: B, G, R, X (little-endian 32-bit; high byte X is padding)
|
|
rgba[s + 0] = rs.SourceData[s + 2]; // R
|
|
rgba[s + 1] = rs.SourceData[s + 1]; // G
|
|
rgba[s + 2] = rs.SourceData[s + 0]; // B
|
|
rgba[s + 3] = 0xFF; // A = opaque (X byte discarded)
|
|
}
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
|
|
private static DecodedTexture DecodeR5G6B5(RenderSurface rs)
|
|
{
|
|
int expectedBytes = rs.Width * rs.Height * 2;
|
|
if (rs.SourceData.Length < expectedBytes)
|
|
return DecodedTexture.Magenta;
|
|
|
|
var rgba = new byte[rs.Width * rs.Height * 4];
|
|
TextureHelpers.FillR5G6B5(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height);
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
|
|
private static DecodedTexture DecodeA4R4G4B4(RenderSurface rs)
|
|
{
|
|
int expectedBytes = rs.Width * rs.Height * 2;
|
|
if (rs.SourceData.Length < expectedBytes)
|
|
return DecodedTexture.Magenta;
|
|
|
|
var rgba = new byte[rs.Width * rs.Height * 4];
|
|
TextureHelpers.FillA4R4G4B4(rs.SourceData, rgba.AsSpan(), rs.Width, rs.Height);
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
|
|
private static DecodedTexture DecodeBc(RenderSurface rs, CompressionFormat format, bool isClipMap)
|
|
{
|
|
var pixels = BcDecoder.DecodeRaw(rs.SourceData, rs.Width, rs.Height, format);
|
|
var rgba = new byte[rs.Width * rs.Height * 4];
|
|
for (int i = 0; i < pixels.Length; i++)
|
|
{
|
|
int s = i * 4;
|
|
rgba[s + 0] = pixels[i].r;
|
|
rgba[s + 1] = pixels[i].g;
|
|
rgba[s + 2] = pixels[i].b;
|
|
rgba[s + 3] = pixels[i].a;
|
|
if (isClipMap && rgba[s + 0] == 0 && rgba[s + 1] == 0 && rgba[s + 2] == 0)
|
|
rgba[s + 3] = 0;
|
|
}
|
|
return new DecodedTexture(rgba, rs.Width, rs.Height);
|
|
}
|
|
}
|