fix(render): #226 detail overlay uses retail's single-pass combine; drop the dead distance fade (Campaign VM VM1)
VM2's live cdb read against the PDB-paired retail client (GUID 9e847e2f-777c-4bd9-886c-22256bb87f32) proved m_caps.bCanDoSinglePassDetailing = 1 and trysinglepass = 1 on real hardware, so D3DPolyRender::RenderMeshSubset (0x0059ca10) never falls back to the two-pass framebuffer blend the earlier #226 port reproduced. Every loaded CGfxObj sets use_built_mesh = 1 (CGfxObj::InitLoad 0x005346b0), so buildings and EnvCells always take the single-pass texture-stage combine set up in D3DPolyRender::SetSurface (0x0059c4d0): result = lerp(base * diffuse, detail.rgb, detail.a * diffuse.a) RenderMeshSubset lights opaque built-mesh subsets with tmpmaterial.Diffuse.a = 1, so on the live Dereth category texture 0x06006D58 (mean rgb 0.165, mean alpha 0.132) the combine works out to ~0.868 * base + 0.022 — a mild darkening, the opposite sign of the fallback DstColor blend's brightening. Also removes the invented 10 m / 50 m distance fade. Retail's ACRender::get_alpha_for_z (0x006b6230) is only evaluated in D3DPolyRender::DrawPolyInternal (0x0059d7c0, the immediate-polygon path) and only when the static noFadeDetail (0x00820e38, initialised to 1) is 0 — unreachable for built meshes. Attenuation is the sampler's linear mip chain converging to the texture mean, not a scripted ramp. Changes: - mesh_detail.vert/.frag: drop vDetailFade and its distance term; add vDetailOpacity mirroring mesh_modern.vert's InstanceAlphaBuf (binding 7) read, and output detail.rgb with alpha = detail.a * vDetailOpacity under the corrected pipeline blend. - VulkanViewportMapping.BlendFactorsOf / GpuEnums.GpuBlendMode.RetailDetail: SrcAlpha + OneMinusSrcAlpha instead of DstColor + OneMinusSrcAlpha. - RetailDetailTextureContract: replaced the distance-fade constants and FramebufferFactor with Expected(base, detail, opacity) and IsNeutral, matching the lerp; contract tests cover zero-alpha/zero-opacity no-ops, the measured darkening on the live category texture, and full-alpha replacement. - Regenerated mesh_detail's committed SPIR-V and the shader manifest (tools/compile-shaders.ps1); no other shader pair changed. - Docs: #226's pseudocode note, the docs/ISSUES.md #226 entry, and the retired TS-52 divergence-register row corrected from the two-pass DESTCOLOR description to the single-pass path and the darkening expectation, each citing the VM2 cdb note. Verified: dotnet build AcDream.slnx -c Release (0 warnings, 0 errors); dotnet test on AcDream.App.Tests and AcDream.Core.Tests (Release, hermetic lanes) both green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
30e72a2af8
commit
059703066f
13 changed files with 266 additions and 128 deletions
|
|
@ -146,8 +146,11 @@ public sealed class VulkanViewportMappingTests
|
|||
Assert.Equal(
|
||||
(BlendFactor.OneMinusSrcAlpha, BlendFactor.SrcAlpha),
|
||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.InverseAlpha));
|
||||
// VM2 (2026-08-22): real hardware runs the single-pass detail combine,
|
||||
// a lerp expressed as the ordinary straight-alpha-over factor pair —
|
||||
// not the two-pass DstColor fallback.
|
||||
Assert.Equal(
|
||||
(BlendFactor.DstColor, BlendFactor.OneMinusSrcAlpha),
|
||||
(BlendFactor.SrcAlpha, BlendFactor.OneMinusSrcAlpha),
|
||||
VulkanViewportMapping.BlendFactorsOf(GpuBlendMode.RetailDetail));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,57 +34,73 @@ public sealed class RetailDetailTextureContractTests
|
|||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0f, 1f)]
|
||||
[InlineData(10f, 1f)]
|
||||
[InlineData(30f, 0.5f)]
|
||||
[InlineData(50f, 0f)]
|
||||
[InlineData(80f, 0f)]
|
||||
public void DistanceFadeUsesPositiveViewDepthInMetres(
|
||||
float depthMetres,
|
||||
float expected)
|
||||
[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(
|
||||
expected,
|
||||
RetailDetailTextureContract.FadeForPositiveViewDepthMetres(depthMetres),
|
||||
precision: 5);
|
||||
baseColour,
|
||||
RetailDetailTextureContract.Expected(baseColour, transparentDetail, opacity: 1f));
|
||||
Assert.True(RetailDetailTextureContract.IsNeutral(transparentDetail, opacity: 1f));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FadeZeroIsExactNoOpAndNeutralRgbEqualsAlpha()
|
||||
{
|
||||
var brighteningSample = new Vector4(0.459f, 0.459f, 0.459f, 0.282f);
|
||||
Assert.Equal(
|
||||
Vector3.One,
|
||||
RetailDetailTextureContract.FramebufferFactor(brighteningSample, fade: 0f));
|
||||
|
||||
var neutral = new Vector4(0.4f, 0.4f, 0.4f, 0.4f);
|
||||
Vector3 neutralFactor = RetailDetailTextureContract.FramebufferFactor(neutral, fade: 1f);
|
||||
Assert.Equal(1f, neutralFactor.X, precision: 5);
|
||||
Assert.Equal(1f, neutralFactor.Y, precision: 5);
|
||||
Assert.Equal(1f, neutralFactor.Z, precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RetailBlendPreservesMeasuredBrightening()
|
||||
{
|
||||
var measured = new Vector4(0.459f, 0.459f, 0.459f, 0.282f);
|
||||
Vector3 factor = RetailDetailTextureContract.FramebufferFactor(measured, fade: 1f);
|
||||
|
||||
Assert.Equal(1.177f, factor.X, precision: 5);
|
||||
Assert.Equal(1.177f, factor.Y, precision: 5);
|
||||
Assert.Equal(1.177f, factor.Z, precision: 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EnabledDerethCategoryTextureKeepsItsMeasuredBrightening()
|
||||
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);
|
||||
Vector3 factor = RetailDetailTextureContract.FramebufferFactor(
|
||||
derethCategory,
|
||||
fade: 1f);
|
||||
|
||||
Assert.Equal(1.033f, factor.X, precision: 5);
|
||||
Assert.Equal(1.033f, factor.Y, precision: 5);
|
||||
Assert.Equal(1.033f, factor.Z, precision: 5);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue