fix: heightmap transpose + solid-color + translucency + clipmap textures
Three root causes found via systematic debugging after the user reported that thedc60405texture fix and4763b97height table fix had no visible effect on Holtburg. ## Heightmap transpose (LandblockMesh.Build) Phase 1's LandblockMesh.Build indexed block.Height as y*9+x but AC packs per-vertex heights in x-major order (x*9+y, matching ACViewer's LandblockStruct: Height[x * VertexDim + y]). The bug was invisible on flat landblocks (Phase 1 smoke test) but left buildings buried by 10-13 world-Z units on Holtburg, because building Frame.Origin positions reference the un-transposed ground truth. Diagnostic evidence (before fix, Holtburg 0xA9B4FFFF): entity 0x020000A5 at ( 84.6,126.0) entityZ= 66.03 terrainZ= 78.15 delta=-12.13 entity 0x02000118 at ( 74.2,139.9) entityZ= 66.03 terrainZ= 78.92 delta=-12.89 After fix: deltas are 0.03 to 2.18 — buildings now sit on the ground with small positive offsets for foundations. Regression test added: Build_HeightmapPackedAsXMajor_NotYMajor asserts asymmetric heights land at the correct world positions. ## Solid-color surfaces with Translucency=1.0 (SurfaceDecoder.DecodeSolidColor) The "bright pink doors and windows" the user saw were 11 Holtburg surfaces with OrigTextureId==0 — these carry a ColorValue instead of a texture chain. Phase 2a's TextureCache dropped them into the magenta fallback. All 11 turned out to be Base1Solid|Translucent with Translucency=1.00, meaning "fully transparent placeholder surface" (debug ColorValue is gray/green/red/blue/black, never displayed). DecodeSolidColor now takes a translucency parameter and multiplies alpha by (1 - translucency), so Translucency=1.0 → alpha=0, and the mesh shader's existing alpha discard (< 0.5) makes the pixel invisible. TextureCache honors Surface.Type.HasFlag(Base1Solid) and passes surface.Translucency through. Regression tests added: DecodeSolidColor_Opaque_PreservesAlpha and DecodeSolidColor_FullyTranslucent_AlphaGoesToZero. ## Clipmap alpha-key (DecodeIndex16) AC convention (per ACViewer TextureCache.IndexToColor): on surfaces marked Base1ClipMap, palette indices 0..7 are treated as fully transparent regardless of their actual palette color. Without this, low-index pixels on clipmap surfaces (typically doorway cutouts and foliage) render as opaque using whatever sentinel color is at those palette slots. DecodeRenderSurface now takes an isClipMap parameter. TextureCache passes Surface.Type.HasFlag(Base1ClipMap). DecodeIndex16 forces rgba=(0,0,0,0) when isClipMap && idx < 8. Regression test added: DecodeIndex16_ClipMap_ZerosAlphaForLowIndices. ## Notes - dc60405's PFID_INDEX16 palette decoder remains correct — no change. - 4763b97's LandHeightTable wiring remains correct — real-table lookup still runs, it just happens to be linear at Holtburg's height range. The fix is forward-compatible with mountains elsewhere. - All three bugs were invisible to the original unit tests. The new regression tests pin them down. ## State - dotnet build: 0 warnings, 0 errors - dotnet test: 42 passing (was 38 + 4 new) - Runtime: 126 entities hydrated on Holtburg, no exceptions, no magenta fallback (counter was 11, now 0 via diagnostic confirmation) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dc60405ebc
commit
cc55c3f812
5 changed files with 154 additions and 14 deletions
|
|
@ -3,6 +3,7 @@ using AcDream.Core.Textures;
|
|||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using Silk.NET.OpenGL;
|
||||
using SurfaceType = DatReaderWriter.Enums.SurfaceType;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
|
|
@ -41,6 +42,14 @@ public sealed unsafe class TextureCache : IDisposable
|
|||
if (surface is null)
|
||||
return DecodedTexture.Magenta;
|
||||
|
||||
// Base1Solid surfaces (and any with OrigTextureId==0) carry a ColorValue
|
||||
// instead of a texture chain. Without this, surfaces with no texture
|
||||
// would fall through to the magenta fallback. Translucency is honored
|
||||
// so Base1Solid|Translucent surfaces with Translucency=1.0 become
|
||||
// alpha=0, which the mesh shader's discard cutout makes invisible.
|
||||
if (surface.Type.HasFlag(SurfaceType.Base1Solid) || (uint)surface.OrigTextureId == 0)
|
||||
return SurfaceDecoder.DecodeSolidColor(surface.ColorValue, surface.Translucency);
|
||||
|
||||
var surfaceTexture = _dats.Get<SurfaceTexture>((uint)surface.OrigTextureId);
|
||||
if (surfaceTexture is null || surfaceTexture.Textures.Count == 0)
|
||||
return DecodedTexture.Magenta;
|
||||
|
|
@ -49,14 +58,14 @@ public sealed unsafe class TextureCache : IDisposable
|
|||
if (rs is null)
|
||||
return DecodedTexture.Magenta;
|
||||
|
||||
// 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<Palette>(rs.DefaultPaletteId)
|
||||
: null;
|
||||
|
||||
return SurfaceDecoder.DecodeRenderSurface(rs, palette);
|
||||
// Clipmap surfaces use palette indices 0..7 as transparent sentinels.
|
||||
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
|
||||
|
||||
return SurfaceDecoder.DecodeRenderSurface(rs, palette, isClipMap);
|
||||
}
|
||||
|
||||
private uint UploadRgba8(DecodedTexture decoded)
|
||||
|
|
|
|||
|
|
@ -28,9 +28,20 @@ public static class LandblockMesh
|
|||
{
|
||||
for (int x = 0; x < VerticesPerSide; x++)
|
||||
{
|
||||
int i = y * VerticesPerSide + x;
|
||||
float height = heightTable[block.Height[i]];
|
||||
vertices[i] = new Vertex(
|
||||
// Vertex buffer index (row-major, y*9+x) is internal to this mesh
|
||||
// and what the index buffer below references.
|
||||
int vi = y * VerticesPerSide + x;
|
||||
|
||||
// Height dat index is PACKED AS x*9+y — AC stores per-vertex
|
||||
// heights in x-major order (see ACViewer's
|
||||
// LandblockStruct: Height[x * VertexDim + y]). Using y*9+x here
|
||||
// (as Phase 1 did) transposes the terrain along its diagonal,
|
||||
// which is invisible for flat landblocks but leaves buildings
|
||||
// buried by ~10+ units on real terrain like Holtburg.
|
||||
int hi = x * VerticesPerSide + y;
|
||||
|
||||
float height = heightTable[block.Height[hi]];
|
||||
vertices[vi] = new Vertex(
|
||||
Position: new Vector3(x * CellSize, y * CellSize, height),
|
||||
Normal: Vector3.UnitZ,
|
||||
TexCoord: new Vector2(x / (float)CellsPerSide, y / (float)CellsPerSide));
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ public static class SurfaceDecoder
|
|||
/// 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).
|
||||
/// </summary>
|
||||
public static DecodedTexture DecodeRenderSurface(RenderSurface rs, Palette? palette)
|
||||
public static DecodedTexture DecodeRenderSurface(RenderSurface rs, Palette? palette, bool isClipMap = false)
|
||||
{
|
||||
if (rs.SourceData is null || rs.Width <= 0 || rs.Height <= 0)
|
||||
return DecodedTexture.Magenta;
|
||||
|
|
@ -36,7 +38,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),
|
||||
PixelFormat.PFID_INDEX16 when palette is not null => DecodeIndex16(rs, palette, isClipMap),
|
||||
_ => DecodedTexture.Magenta,
|
||||
};
|
||||
}
|
||||
|
|
@ -46,7 +48,7 @@ public static class SurfaceDecoder
|
|||
}
|
||||
}
|
||||
|
||||
private static DecodedTexture DecodeIndex16(RenderSurface rs, Palette palette)
|
||||
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)
|
||||
|
|
@ -63,14 +65,45 @@ public static class SurfaceDecoder
|
|||
var c = palette.Colors[idx];
|
||||
|
||||
int dst = i * 4;
|
||||
// Clipmap alpha-key convention (ACViewer: if (isClipMap && color < 8) r=g=b=a=0):
|
||||
// palette indices 0..7 on clipmap surfaces represent transparent pixels.
|
||||
if (isClipMap && idx < 8)
|
||||
{
|
||||
rgba[dst + 0] = 0;
|
||||
rgba[dst + 1] = 0;
|
||||
rgba[dst + 2] = 0;
|
||||
rgba[dst + 3] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
private static DecodedTexture DecodeA8R8G8B8(RenderSurface rs)
|
||||
{
|
||||
int expected = rs.Width * rs.Height * 4;
|
||||
|
|
|
|||
|
|
@ -81,4 +81,30 @@ public class LandblockMeshTests
|
|||
// AC's Land::LandHeightTable scales height byte index by 2.0f for the simple ramp case.
|
||||
Assert.Equal(10.0f, mesh.Vertices[0].Position.Z);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_HeightmapPackedAsXMajor_NotYMajor()
|
||||
{
|
||||
// Regression: Phase 1 used block.Height[y*9+x] which transposes the terrain
|
||||
// along its diagonal relative to AC's native x-major packing. Invisible on
|
||||
// flat landblocks but catastrophically wrong on Holtburg where static-object
|
||||
// positions reference the un-transposed ground truth, leaving buildings
|
||||
// buried by ~10 world-Z units.
|
||||
//
|
||||
// Set up an asymmetric heightmap: value at x-major index (x=2, y=0) = 5
|
||||
// (scaled to Z=10), everything else 0. The vertex at world position
|
||||
// (x=2*24=48, y=0) should have Z=10. The vertex at (x=0, y=2*24=48) should
|
||||
// have Z=0. Y-major indexing would swap these.
|
||||
var block = BuildFlatLandBlock();
|
||||
block.Height[2 * 9 + 0] = 5; // x=2, y=0 in x-major packing
|
||||
|
||||
var mesh = LandblockMesh.Build(block, IdentityHeightTable);
|
||||
|
||||
// Find vertices by position. Vertex buffer uses y*9+x internally.
|
||||
var vAt_x2_y0 = mesh.Vertices[0 * 9 + 2]; // world (48, 0)
|
||||
var vAt_x0_y2 = mesh.Vertices[2 * 9 + 0]; // world (0, 48)
|
||||
|
||||
Assert.Equal(new Vector3(48, 0, 10), vAt_x2_y0.Position);
|
||||
Assert.Equal(new Vector3(0, 48, 0), vAt_x0_y2.Position);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.Core.Textures;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Enums;
|
||||
using DatReaderWriter.Types;
|
||||
|
||||
namespace AcDream.Core.Tests.Textures;
|
||||
|
||||
|
|
@ -86,4 +87,64 @@ public class SurfaceDecoderTests
|
|||
|
||||
Assert.Same(DecodedTexture.Magenta, decoded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodeSolidColor_Opaque_PreservesAlpha()
|
||||
{
|
||||
var color = new ColorARGB { Alpha = 0xFF, Red = 0x11, Green = 0x22, Blue = 0x33 };
|
||||
|
||||
var decoded = SurfaceDecoder.DecodeSolidColor(color, translucency: 0f);
|
||||
|
||||
Assert.Equal(1, decoded.Width);
|
||||
Assert.Equal(1, decoded.Height);
|
||||
Assert.Equal(new byte[] { 0x11, 0x22, 0x33, 0xFF }, decoded.Rgba8);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodeSolidColor_FullyTranslucent_AlphaGoesToZero()
|
||||
{
|
||||
// Surfaces marked Base1Solid + Translucent with Translucency=1.0 are
|
||||
// AC's convention for "invisible placeholder surfaces" — the engine renders
|
||||
// them as nothing. Alpha must go to 0 so the mesh shader's discard rule
|
||||
// makes them invisible.
|
||||
var color = new ColorARGB { Alpha = 0xFF, Red = 0xC8, Green = 0xC8, Blue = 0xC8 };
|
||||
|
||||
var decoded = SurfaceDecoder.DecodeSolidColor(color, translucency: 1f);
|
||||
|
||||
Assert.Equal(0, decoded.Rgba8[3]); // alpha must be zero
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecodeIndex16_ClipMap_ZerosAlphaForLowIndices()
|
||||
{
|
||||
// Build a 4x1 INDEX16 surface with indices 0, 1, 7, 8.
|
||||
// On a clipmap surface, indices 0..7 should be fully transparent and
|
||||
// index 8 should render with its palette color.
|
||||
var rs = new RenderSurface
|
||||
{
|
||||
Width = 4,
|
||||
Height = 1,
|
||||
Format = PixelFormat.PFID_INDEX16,
|
||||
SourceData = new byte[]
|
||||
{
|
||||
0x00, 0x00, // index 0
|
||||
0x01, 0x00, // index 1
|
||||
0x07, 0x00, // index 7
|
||||
0x08, 0x00, // index 8
|
||||
},
|
||||
};
|
||||
var palette = new Palette();
|
||||
for (int i = 0; i < 16; i++)
|
||||
palette.Colors.Add(new ColorARGB { Alpha = 0xFF, Red = 0xAA, Green = 0xBB, Blue = 0xCC });
|
||||
|
||||
var decoded = SurfaceDecoder.DecodeRenderSurface(rs, palette, isClipMap: true);
|
||||
|
||||
// Pixels 0, 1, 2 (indices 0, 1, 7) should be fully transparent.
|
||||
Assert.Equal(0, decoded.Rgba8[3]); // pixel 0 alpha
|
||||
Assert.Equal(0, decoded.Rgba8[7]); // pixel 1 alpha
|
||||
Assert.Equal(0, decoded.Rgba8[11]); // pixel 2 alpha
|
||||
// Pixel 3 (index 8) should have the palette alpha.
|
||||
Assert.Equal(0xFF, decoded.Rgba8[15]);
|
||||
Assert.Equal(0xAA, decoded.Rgba8[12]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue