From dc60405ebc239636b20bc6048100707b47d41ef3 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Apr 2026 19:12:05 +0200 Subject: [PATCH] fix(textures): palette-indexed surfaces + alpha cutout shader Addresses the 'doors, windows, and alpha-keyed parts render bright pink' issue the user observed after the Phase 2a visual checkpoint. SurfaceDecoder gains a second overload taking an optional Palette parameter. When the render surface format is PFID_INDEX16 and a palette is supplied, each 16-bit value in SourceData is treated as an index into Palette.Colors (a List) and the corresponding ARGB color's channels are written to the output buffer. The original no-palette overload is preserved so the Task 3 unit tests that confirm INDEX16 -> magenta fallback still describe their behavior correctly (INDEX16 without a palette still returns magenta). TextureCache now resolves the RenderSurface's DefaultPaletteId via the dats and passes the resulting Palette (or null) to the decoder. mesh.frag adds an alpha cutout: fragments with sampled alpha < 0.5 are discarded. Without this, transparent regions of alpha-keyed textures (doors, windows, foliage cutouts) would render as opaque rectangles using the texture's background color. This is the standard alpha-tested approach, simpler than full alpha blending and matches how AC's original client rendered these surfaces. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.App/Rendering/Shaders/mesh.frag | 7 +++- src/AcDream.App/Rendering/TextureCache.cs | 9 ++++- src/AcDream.Core/Textures/SurfaceDecoder.cs | 38 ++++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/AcDream.App/Rendering/Shaders/mesh.frag b/src/AcDream.App/Rendering/Shaders/mesh.frag index dcd182c..382895e 100644 --- a/src/AcDream.App/Rendering/Shaders/mesh.frag +++ b/src/AcDream.App/Rendering/Shaders/mesh.frag @@ -5,5 +5,10 @@ out vec4 fragColor; uniform sampler2D uDiffuse; void main() { - fragColor = texture(uDiffuse, vTex); + vec4 sampled = texture(uDiffuse, vTex); + // Alpha cutout for doors, windows, vegetation, and other alpha-keyed textures. + // Without this, zero-alpha pixels in palette-indexed textures render as opaque + // rectangles where the transparent parts should be. + if (sampled.a < 0.5) discard; + fragColor = sampled; } diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs index 2794038..94666df 100644 --- a/src/AcDream.App/Rendering/TextureCache.cs +++ b/src/AcDream.App/Rendering/TextureCache.cs @@ -49,7 +49,14 @@ public sealed unsafe class TextureCache : IDisposable if (rs is null) return DecodedTexture.Magenta; - return SurfaceDecoder.DecodeRenderSurface(rs); + // Palette lookup for indexed formats (doors, windows, alpha-keyed foliage). + // If DefaultPaletteId is 0 or unresolvable, SurfaceDecoder falls back to magenta + // for PFID_INDEX16 surfaces. + Palette? palette = rs.DefaultPaletteId != 0 + ? _dats.Get(rs.DefaultPaletteId) + : null; + + return SurfaceDecoder.DecodeRenderSurface(rs, palette); } private uint UploadRgba8(DecodedTexture decoded) diff --git a/src/AcDream.Core/Textures/SurfaceDecoder.cs b/src/AcDream.Core/Textures/SurfaceDecoder.cs index 481b947..191f7b0 100644 --- a/src/AcDream.Core/Textures/SurfaceDecoder.cs +++ b/src/AcDream.Core/Textures/SurfaceDecoder.cs @@ -11,9 +11,19 @@ public static class SurfaceDecoder /// /// Decode a RenderSurface's pixel bytes into RGBA8. Returns - /// for unsupported formats, null data, or corrupt sizing. + /// for unsupported formats, null data, or corrupt sizing. This overload does NOT + /// support PFID_INDEX16 — use + /// when a palette is available. /// public static DecodedTexture DecodeRenderSurface(RenderSurface rs) + => DecodeRenderSurface(rs, palette: null); + + /// + /// Decode a RenderSurface's pixel bytes into RGBA8 with optional palette support. + /// When is non-null and the format is PFID_INDEX16, each + /// 16-bit value in SourceData is treated as an index into . + /// + public static DecodedTexture DecodeRenderSurface(RenderSurface rs, Palette? palette) { if (rs.SourceData is null || rs.Width <= 0 || rs.Height <= 0) return DecodedTexture.Magenta; @@ -26,6 +36,7 @@ public static class SurfaceDecoder PixelFormat.PFID_DXT1 => DecodeBc(rs, CompressionFormat.Bc1), PixelFormat.PFID_DXT3 => DecodeBc(rs, CompressionFormat.Bc2), PixelFormat.PFID_DXT5 => DecodeBc(rs, CompressionFormat.Bc3), + PixelFormat.PFID_INDEX16 when palette is not null => DecodeIndex16(rs, palette), _ => DecodedTexture.Magenta, }; } @@ -35,6 +46,31 @@ public static class SurfaceDecoder } } + private static DecodedTexture DecodeIndex16(RenderSurface rs, Palette palette) + { + 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]; + int paletteMax = palette.Colors.Count - 1; + for (int i = 0; i < rs.Width * rs.Height; i++) + { + // Read each 16-bit value little-endian as a palette index + int src = i * 2; + ushort idx = (ushort)(rs.SourceData[src] | (rs.SourceData[src + 1] << 8)); + if (idx > paletteMax) idx = 0; + var c = palette.Colors[idx]; + + int dst = i * 4; + rgba[dst + 0] = c.Red; + rgba[dst + 1] = c.Green; + rgba[dst + 2] = c.Blue; + rgba[dst + 3] = c.Alpha; + } + return new DecodedTexture(rgba, rs.Width, rs.Height); + } + private static DecodedTexture DecodeA8R8G8B8(RenderSurface rs) { int expected = rs.Width * rs.Height * 4;