diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs
index df9ddada..f3e18c42 100644
--- a/src/AcDream.App/Rendering/TextureCache.cs
+++ b/src/AcDream.App/Rendering/TextureCache.cs
@@ -495,7 +495,8 @@ public sealed class TextureCache
DecodedTexture decoded = DecodeFromDats(
surfaceId,
origTextureOverride: overrideOrigTextureId,
- paletteOverride: null);
+ paletteOverride: null,
+ bakeAuthoredTranslucency: true);
return composites.TryAddAndAcquire(ownerLocalId, key, decoded, out BindlessTextureLocation added)
? added
: default;
@@ -534,7 +535,8 @@ public sealed class TextureCache
DecodedTexture decoded = DecodeFromDats(
surfaceId,
origTextureOverride: overrideOrigTextureId,
- paletteOverride: paletteOverride);
+ paletteOverride: paletteOverride,
+ bakeAuthoredTranslucency: true);
return composites.TryAddAndAcquire(ownerLocalId, key, decoded, out BindlessTextureLocation added)
? added
: default;
@@ -806,7 +808,21 @@ public sealed class TextureCache
Console.WriteLine($"[N6-DUMP] Surface histogram written to {outPath} ({seen.Count} textures, {totalBytes} bytes)");
}
- private DecodedTexture DecodeFromDats(uint surfaceId, uint? origTextureOverride, PaletteOverride? paletteOverride)
+ ///
+ /// Apply the surface's authored Translucency to the decoded alpha, the same
+ /// bake the shared-atlas extraction performs. TRUE for the world composite paths
+ /// (palette / original-texture overrides) — without it an override-carrying item's
+ /// translucent part paints alpha=1: it still sorts as see-through in the alpha
+ /// queue but erases the particles composited behind it. FALSE for the sky (its
+ /// shader applies the authored opacity separately — baking would double-apply)
+ /// and for particle sheets (emitter-driven alpha, no authored-translucency
+ /// consumer today).
+ ///
+ private DecodedTexture DecodeFromDats(
+ uint surfaceId,
+ uint? origTextureOverride,
+ PaletteOverride? paletteOverride,
+ bool bakeAuthoredTranslucency = false)
{
var surface = _dats.Get(surfaceId);
if (surface is null)
@@ -863,7 +879,20 @@ public sealed class TextureCache
bool isClipMap = surface.Type.HasFlag(SurfaceType.Base1ClipMap);
bool isAdditive = surface.Type.HasFlag(SurfaceType.Additive);
- return SurfaceDecoder.DecodeRenderSurface(rs, effectivePalette, isClipMap, isAdditive);
+ DecodedTexture decoded =
+ SurfaceDecoder.DecodeRenderSurface(rs, effectivePalette, isClipMap, isAdditive);
+
+ // The decoders return the shared Magenta sentinel on failure; it must never
+ // be scaled in place. Fresh decodes are caller-owned, so the in-place bake
+ // is safe.
+ if (bakeAuthoredTranslucency
+ && surface.Translucency > 0.0f
+ && !ReferenceEquals(decoded, DecodedTexture.Magenta))
+ {
+ decoded = SurfaceDecoder.ApplyAuthoredTranslucency(decoded, surface.Translucency);
+ }
+
+ return decoded;
}
///
diff --git a/src/AcDream.Core/Textures/SurfaceDecoder.cs b/src/AcDream.Core/Textures/SurfaceDecoder.cs
index f727a59c..6cbd108f 100644
--- a/src/AcDream.Core/Textures/SurfaceDecoder.cs
+++ b/src/AcDream.Core/Textures/SurfaceDecoder.cs
@@ -93,6 +93,26 @@ public static class SurfaceDecoder
Height: 1);
}
+ ///
+ /// Scale a decoded texture's alpha channel by a surface's authored translucency
+ /// (AC's convention: 0.0 fully opaque, 1.0 fully transparent). This is the same
+ /// bake the shared-atlas extraction applies (MeshExtractor,
+ /// alphaScale = 1 - Surface.Translucency); runtime composite decodes must
+ /// apply it too or an override-carrying surface silently loses its authored
+ /// translucency. Scales IN PLACE and returns the same instance — the caller must
+ /// own the buffer (never pass a shared/cached texture such as
+ /// ).
+ ///
+ public static DecodedTexture ApplyAuthoredTranslucency(DecodedTexture texture, float translucency)
+ {
+ if (translucency <= 0f) return texture;
+ float alphaScale = Math.Clamp(1f - translucency, 0f, 1f);
+ byte[] rgba = texture.Rgba8;
+ for (int i = 3; i < rgba.Length; i += 4)
+ rgba[i] = (byte)(rgba[i] * alphaScale);
+ return texture;
+ }
+
///
/// Decode single-byte-per-pixel alpha (PFID_A8 / PFID_CUSTOM_LSCAPE_ALPHA) into RGBA8.
/// When is true: R=G=B=A=val (terrain alpha masks and
diff --git a/tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs b/tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs
index 7a440e0a..e50ff372 100644
--- a/tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs
+++ b/tests/AcDream.Core.Tests/Textures/SurfaceDecoderTests.cs
@@ -7,6 +7,47 @@ namespace AcDream.Core.Tests.Textures;
public class SurfaceDecoderTests
{
+ [Fact]
+ public void ApplyAuthoredTranslucency_ScalesAlphaOnly_InPlace()
+ {
+ // Runtime analogue of the shared-atlas bake (MeshExtractor's
+ // alphaScale = 1 - Surface.Translucency): the composite decode paths
+ // apply this so an override-carrying item keeps its authored
+ // translucency instead of painting alpha=1 and erasing the particles
+ // composited behind it.
+ var texture = new DecodedTexture(
+ Rgba8: [10, 20, 30, 200, 40, 50, 60, 100],
+ Width: 2,
+ Height: 1);
+
+ var result = SurfaceDecoder.ApplyAuthoredTranslucency(texture, 0.5f);
+
+ Assert.Same(texture, result);
+ Assert.Equal(new byte[] { 10, 20, 30, 100, 40, 50, 60, 50 }, result.Rgba8);
+ }
+
+ [Fact]
+ public void ApplyAuthoredTranslucency_FullTranslucency_ZeroesAlpha()
+ {
+ var texture = new DecodedTexture(Rgba8: [255, 255, 255, 255], Width: 1, Height: 1);
+
+ var result = SurfaceDecoder.ApplyAuthoredTranslucency(texture, 1f);
+
+ Assert.Equal(0, result.Rgba8[3]);
+ Assert.Equal(255, result.Rgba8[0]);
+ }
+
+ [Fact]
+ public void ApplyAuthoredTranslucency_ZeroOrNegative_IsANoOp()
+ {
+ var texture = new DecodedTexture(Rgba8: [1, 2, 3, 4], Width: 1, Height: 1);
+
+ Assert.Same(texture, SurfaceDecoder.ApplyAuthoredTranslucency(texture, 0f));
+ Assert.Equal(4, texture.Rgba8[3]);
+ Assert.Same(texture, SurfaceDecoder.ApplyAuthoredTranslucency(texture, -0.25f));
+ Assert.Equal(4, texture.Rgba8[3]);
+ }
+
[Fact]
public void Decode_A8R8G8B8_ConvertsToRgba8()
{