using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Gpu; namespace AcDream.App.Tests.Rendering; public sealed class RetailDetailTextureContractTests { private static readonly TerrainAtlas.RetailDetailTextureBinding Available = new( new GpuTextureSlot(7), Tiling: 4f, SurfaceTextureId: 0x05001787, RenderSurfaceId: 0x06006D58, Width: 256, Height: 256); [Fact] public void ExistingBuildingDetailSettingIsTheLivePassGate() { Assert.False(RetailDetailTextureContract.ShouldRender(false, Available)); Assert.True(RetailDetailTextureContract.ShouldRender(true, Available)); Assert.False(RetailDetailTextureContract.ShouldRender(true, default)); } [Fact] public void OpaqueDetailUsesDepthEqualityWhileTransparentDetailDoesNot() { Assert.Equal( GpuCompareOp.Equal, RetailDetailTextureContract.DetailDepthCompare(transparent: false)); Assert.Equal( GpuCompareOp.LessOrEqual, RetailDetailTextureContract.DetailDepthCompare(transparent: true)); } [Theory] [InlineData(0.1f, 0.2f, 0.3f)] [InlineData(0.0f, 0.0f, 0.0f)] [InlineData(1.0f, 1.0f, 1.0f)] [InlineData(0.5f, 0.9f, 0.05f)] public void ZeroDetailAlphaIsExactNoOp(float r, float g, float b) { var baseColour = new Vector3(r, g, b); var transparentDetail = new Vector4(0.9f, 0.1f, 0.7f, 0f); Assert.Equal( baseColour, RetailDetailTextureContract.Expected(baseColour, transparentDetail, opacity: 1f)); Assert.True(RetailDetailTextureContract.IsNeutral(transparentDetail, opacity: 1f)); } [Fact] public void ZeroOpacityIsExactNoOp() { var baseColour = new Vector3(0.5f, 0.5f, 0.5f); var derethCategory = new Vector4(0.165f, 0.165f, 0.165f, 0.132f); // opacity 0 models a translucency fade that has finished disappearing: // even a fully-opaque detail texel contributes nothing. Assert.Equal( baseColour, RetailDetailTextureContract.Expected(baseColour, derethCategory, opacity: 0f)); Assert.True(RetailDetailTextureContract.IsNeutral(derethCategory, opacity: 0f)); } [Fact] public void LiveDerethCategoryTextureDarkensMidtones() { // Live category texture 0x06006D58 (VM2), opaque diffuse (opacity 1). var detail = new Vector4(0.165f, 0.165f, 0.161f, 0.132f); var baseColour = new Vector3(0.5f, 0.5f, 0.5f); Vector3 result = RetailDetailTextureContract.Expected(baseColour, detail, opacity: 1f); Assert.True(result.X < baseColour.X); Assert.True(result.Y < baseColour.Y); Assert.True(result.Z < baseColour.Z); // lerp(base, detail.rgb, 0.132) ~= 0.868 * base + 0.0218 per channel — // the mild darkening VM2 measured, not the two-pass fallback's // brightening. Compared as an absolute difference (not xunit's // decimal-rounding `precision` parameter, which would flip a channel // sitting on a rounding boundary) against the stated 1e-3 tolerance. float approx = 0.868f * baseColour.X + 0.0218f; // same for every channel: baseColour is uniform Assert.True(MathF.Abs(result.X - approx) < 1e-3f, $"R off by {result.X - approx}"); Assert.True(MathF.Abs(result.Y - approx) < 1e-3f, $"G off by {result.Y - approx}"); Assert.True(MathF.Abs(result.Z - approx) < 1e-3f, $"B off by {result.Z - approx}"); Assert.False(RetailDetailTextureContract.IsNeutral(detail, opacity: 1f)); } [Fact] public void FullAlphaReplacesBaseWithDetailColour() { var baseColour = new Vector3(0.9f, 0.1f, 0.4f); var detail = new Vector4(0.2f, 0.3f, 0.4f, 1f); Vector3 result = RetailDetailTextureContract.Expected(baseColour, detail, opacity: 1f); // Tolerance rather than bit-exact equality: Lerp's `a + (b - a) * t` // form is not guaranteed to cancel `a` perfectly at t=1 in floating // point for arbitrary inputs. Assert.True(MathF.Abs(result.X - detail.X) < 1e-6f); Assert.True(MathF.Abs(result.Y - detail.Y) < 1e-6f); Assert.True(MathF.Abs(result.Z - detail.Z) < 1e-6f); } [Fact] public void FogAppliedPerDrawMatchesRetailsFogAfterCombineOrder() { // Review fix (post-05970306): retail fogs the pixel AFTER the // texture-stage combine (mix(lerp(base,detail,a), fog, f)), but // acdream draws the combine as two separate passes, each fogging its // own colour before the fixed-function blend recombines them // (lerp(mix(base,fog,f), mix(detail,fog,f), a)). This pins that the // two orders are the same pixel for arbitrary inputs — see // ExpectedFogged's doc comment and mesh_detail.frag's header comment // for the algebra. var random = new Random(0x226226); for (int i = 0; i < 200; i++) { var baseColour = NextColour(random); var detail = new Vector4(NextColour(random), random.NextSingle()); float opacity = random.NextSingle(); var fog = NextColour(random); float fogFactor = random.NextSingle(); Vector3 twoDrawOrder = RetailDetailTextureContract.ExpectedFogged( baseColour, detail, opacity, fog, fogFactor); Vector3 retailOrder = Vector3.Lerp( RetailDetailTextureContract.Expected(baseColour, detail, opacity), fog, fogFactor); Assert.True( MathF.Abs(twoDrawOrder.X - retailOrder.X) < 1e-6f, $"R: two-draw {twoDrawOrder.X} vs retail {retailOrder.X} (sample {i})"); Assert.True( MathF.Abs(twoDrawOrder.Y - retailOrder.Y) < 1e-6f, $"G: two-draw {twoDrawOrder.Y} vs retail {retailOrder.Y} (sample {i})"); Assert.True( MathF.Abs(twoDrawOrder.Z - retailOrder.Z) < 1e-6f, $"B: two-draw {twoDrawOrder.Z} vs retail {retailOrder.Z} (sample {i})"); } } private static Vector3 NextColour(Random random) => new(random.NextSingle(), random.NextSingle(), random.NextSingle()); }