feat(#188): fading-wall + sliding-door translucency; hold open past animation settle
Lands the fading-secret-door feature and fixes the door "flip-back" that surfaced while testing it. #188 — fading-wall doors (e.g. "Pedestal Weak Spot") fade their wall part out via TransparentPartHook instead of swinging: - TranslucencyHookSink consumes TransparentPartHook -> TranslucencyFadeManager (per-(entity,part) linear translucency ramp; holds at End frame). - WbDrawDispatcher: new per-instance alpha SSBO (binding 7); ClassifyBatches takes opacityMultiplier (1 - translucency, per CMaterial::SetTranslucencySimple 0x005396f0) forcing AlphaBlend; fully-invisible parts skipped. - mesh_modern.vert/.frag: binding-7 InstanceAlphaBuf -> vOpacityMultiplier -> FragColor.a *= vOpacityMultiplier. - Register AP-89: the fade multiplies sampled texture alpha, not a separate D3D9 material alpha channel (observably identical for texture-alpha==1 surfaces). Door flip-back fix (affected BOTH #188 fading walls AND #187 sliding doors): a door/wall that finished opening holds a single unchanging frame, so the uncommitted IsEntityCurrentlyMoving cache-bypass narrowing dropped it onto the Tier-1 static cache -- which only remembers the REST pose + opacity 1.0 -- snapping it visually shut/opaque while physics stayed open. Reverted that narrowing: every Sequencer entity stays on the per-frame path (live pose + live fade opacity), the known-good pre-optimization behavior. The per-frame CPU cost that narrowing chased was a Debug-build artifact -- Release is GPU-bound (~200 fps in Sawato, measured), so the unconditional add is free where it matters. Left a code comment barring re-introduction. Tests: full Core suite green (2649 passed, 2 skipped). Live visual gate PASSED -- both fading-wall and sliding doors hold open. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e2e285b855
commit
3284dd0aed
12 changed files with 625 additions and 14 deletions
|
|
@ -107,6 +107,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// miss-populate / hit-fast-path through the loop.
|
||||
private readonly EntityClassificationCache _cache;
|
||||
|
||||
// #188 — per-(entity, Setup-part) translucency ramp state (fading doors /
|
||||
// secret-passage walls). ClassifyBatches reads this per part to compute
|
||||
// the instance's opacity multiplier; never mutated here.
|
||||
private readonly AcDream.Core.Rendering.TranslucencyFadeManager _translucencyFades;
|
||||
|
||||
// ACDREAM_DISABLE_TIER1_CACHE=1 A/B diagnostic — forces every static
|
||||
// entity through the slow path. Read once in ctor.
|
||||
private readonly bool _tier1CacheDisabled =
|
||||
|
|
@ -150,6 +155,14 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// Mechanically a clone of _clipSlotData / _clipSlotSsbo.
|
||||
private uint _instIndoorSsbo;
|
||||
private uint[] _indoorData = new uint[256];
|
||||
|
||||
// #188: per-instance opacity multiplier (binding=7), one float per
|
||||
// instance, parallel to _instanceSsbo. 1.0 = unmodified (the dat's own
|
||||
// material/texture alpha, untouched); < 1.0 multiplies the shader's
|
||||
// sampled alpha for an entity mid-TransparentPartHook fade. Mechanically
|
||||
// a clone of _indoorData / _instIndoorSsbo, one binding higher.
|
||||
private uint _instAlphaSsbo;
|
||||
private float[] _alphaData = new float[256];
|
||||
// This frame's point-light snapshot, handed in by GameWindow before Draw via
|
||||
// SetSceneLights. Null/empty ⇒ only ambient + sun render (all instance sets -1).
|
||||
private IReadOnlyList<LightSource>? _pointSnapshot;
|
||||
|
|
@ -340,7 +353,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
WbMeshAdapter meshAdapter,
|
||||
EntitySpawnAdapter entitySpawnAdapter,
|
||||
BindlessSupport bindless,
|
||||
EntityClassificationCache classificationCache)
|
||||
EntityClassificationCache classificationCache,
|
||||
AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(shader);
|
||||
|
|
@ -348,6 +362,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
ArgumentNullException.ThrowIfNull(meshAdapter);
|
||||
ArgumentNullException.ThrowIfNull(entitySpawnAdapter);
|
||||
ArgumentNullException.ThrowIfNull(classificationCache);
|
||||
ArgumentNullException.ThrowIfNull(translucencyFades);
|
||||
|
||||
_gl = gl;
|
||||
_shader = shader;
|
||||
|
|
@ -355,6 +370,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_meshAdapter = meshAdapter;
|
||||
_entitySpawnAdapter = entitySpawnAdapter;
|
||||
_cache = classificationCache;
|
||||
_translucencyFades = translucencyFades;
|
||||
|
||||
_bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
|
||||
_instanceSsbo = _gl.GenBuffer();
|
||||
|
|
@ -364,6 +380,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_globalLightsSsbo = _gl.GenBuffer(); // Fix B binding=4
|
||||
_instLightSetSsbo = _gl.GenBuffer(); // Fix B binding=5
|
||||
_instIndoorSsbo = _gl.GenBuffer(); // #142 binding=6
|
||||
_instAlphaSsbo = _gl.GenBuffer(); // #188 binding=7
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1290,8 +1307,17 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
bool drewAny = false;
|
||||
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
|
||||
{
|
||||
foreach (var (partGfxObjId, partTransform) in renderData.SetupParts)
|
||||
// #188: setupPartIndex is the SAME index space
|
||||
// TransparentPartHook.PartIndex addresses — retail's CPartArray
|
||||
// numbers parts by their ordinal position in the Setup's own
|
||||
// part list (SetupPartTransforms.Compute is the other verified
|
||||
// consumer of this exact indexing: one Matrix4x4 per
|
||||
// Setup.Parts[i]). NOT the outer per-MeshRef loop index — a
|
||||
// MeshRef is acdream's own decomposition of top-level
|
||||
// attachments (weapon/shield/etc), a different concept.
|
||||
for (int setupPartIndex = 0; setupPartIndex < renderData.SetupParts.Count; setupPartIndex++)
|
||||
{
|
||||
var (partGfxObjId, partTransform) = renderData.SetupParts[setupPartIndex];
|
||||
var partData = _meshAdapter.TryGetRenderData(partGfxObjId);
|
||||
if (partData is null)
|
||||
{
|
||||
|
|
@ -1332,15 +1358,42 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
|
||||
var restPose = partTransform * meshRef.PartTransform;
|
||||
ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, palHash, metaTable, restPose, collector);
|
||||
|
||||
// #188 retail CPhysicsPart::Draw (0x0050d7a0) early-out: once a
|
||||
// part's translucency hits EXACTLY 1.0 (fully invisible), retail
|
||||
// sets draw_state|=1 and skips the whole part outright — not a
|
||||
// blend to nothing. TranslucencyFadeManager.AdvanceAll guarantees
|
||||
// t=1 commits the bitwise-exact value so this check is safe.
|
||||
float opacityMultiplier = 1.0f;
|
||||
if (_translucencyFades.TryGetCurrentValue(entity.Id, (uint)setupPartIndex, out float translucencyValue))
|
||||
{
|
||||
if (translucencyValue >= 1.0f) continue; // skip this part's draw entirely
|
||||
opacityMultiplier = 1f - translucencyValue; // CMaterial::SetTranslucencySimple 0x005396f0
|
||||
}
|
||||
|
||||
ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, palHash, metaTable, restPose, opacityMultiplier, collector);
|
||||
drewAny = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var model = meshRef.PartTransform * entityWorld;
|
||||
ClassifyBatches(renderData, gfxObjId, model, entity, meshRef, palHash, metaTable, restPose: meshRef.PartTransform, collector: collector);
|
||||
drewAny = true;
|
||||
// #188: a bare (non-Setup) GfxObj entity has exactly one part —
|
||||
// retail's CPartArray for such an object is a single-entry array,
|
||||
// so TransparentPartHook.PartIndex for it is always 0.
|
||||
float opacityMultiplier = 1.0f;
|
||||
bool fullyInvisible = false;
|
||||
if (_translucencyFades.TryGetCurrentValue(entity.Id, 0u, out float translucencyValue))
|
||||
{
|
||||
if (translucencyValue >= 1.0f) fullyInvisible = true;
|
||||
else opacityMultiplier = 1f - translucencyValue;
|
||||
}
|
||||
|
||||
if (!fullyInvisible)
|
||||
{
|
||||
var model = meshRef.PartTransform * entityWorld;
|
||||
ClassifyBatches(renderData, gfxObjId, model, entity, meshRef, palHash, metaTable, restPose: meshRef.PartTransform, opacityMultiplier: opacityMultiplier, collector: collector);
|
||||
drewAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Track THIS entity for the next iteration's flush check. Only
|
||||
|
|
@ -1427,6 +1480,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (_indoorData.Length < totalInstances)
|
||||
_indoorData = new uint[totalInstances + 256];
|
||||
|
||||
// #188: per-instance opacity buffer, one float per instance, parallel to
|
||||
// _clipSlotData / _instanceData. Grown on demand like the others.
|
||||
if (_alphaData.Length < totalInstances)
|
||||
_alphaData = new float[totalInstances + 256];
|
||||
|
||||
_opaqueDraws.Clear();
|
||||
_translucentDraws.Clear();
|
||||
|
||||
|
|
@ -1462,6 +1520,9 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// #142: IndoorFlags[] is parallel to Matrices[]; write at the same
|
||||
// cursor so binding=6 instanceIndoor[] tracks binding=0 instances.
|
||||
_indoorData[cursor] = grp.IndoorFlags[i];
|
||||
// #188: Opacities[] is parallel to Matrices[]; write at the same
|
||||
// cursor so binding=7 instanceAlpha[] tracks binding=0 instances.
|
||||
_alphaData[cursor] = grp.Opacities[i];
|
||||
cursor++;
|
||||
}
|
||||
|
||||
|
|
@ -1553,6 +1614,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
fixed (uint* dp = _indoorData)
|
||||
UploadSsbo(_instIndoorSsbo, 6, dp, totalInstances * sizeof(uint));
|
||||
|
||||
// #188: per-instance opacity buffer (binding=7), one float per instance,
|
||||
// laid out parallel to _instanceData in Phase 3. Only [0..totalInstances)
|
||||
// is uploaded — stale tail never read (same guarantee as clip-slot above).
|
||||
fixed (float* ap = _alphaData)
|
||||
UploadSsbo(_instAlphaSsbo, 7, ap, totalInstances * sizeof(float));
|
||||
|
||||
// Fix B: global point-light buffer (binding=4) + per-instance light-set
|
||||
// buffer (binding=5). The global buffer is this frame's PointSnapshot; the
|
||||
// per-instance buffer holds 8 int indices into it per instance, laid out
|
||||
|
|
@ -2056,6 +2123,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
grp.Matrices.Add(model);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
// #188: cache-hit entities are always non-animated (the Tier-1 cache
|
||||
// gates on !isAnimated), and TranslucencyFadeManager only ever holds
|
||||
// state for entities whose animation hooks fired — so a cached
|
||||
// instance can never be mid-fade. Always unmodified opacity.
|
||||
grp.Opacities.Add(1.0f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -2181,6 +2253,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
ulong palHash,
|
||||
AcSurfaceMetadataTable metaTable,
|
||||
Matrix4x4 restPose,
|
||||
float opacityMultiplier = 1.0f,
|
||||
List<CachedBatch>? collector = null)
|
||||
{
|
||||
for (int batchIdx = 0; batchIdx < renderData.Batches.Count; batchIdx++)
|
||||
|
|
@ -2199,6 +2272,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
: TranslucencyKind.Opaque;
|
||||
}
|
||||
|
||||
// #188: a mid-fade instance whose surface is otherwise Opaque/ClipMap
|
||||
// must route through the alpha-blend pass so mesh_modern.frag's
|
||||
// (blend-enabled) shader actually composites the reduced alpha —
|
||||
// the no-blend opaque pass would ignore it.
|
||||
if (opacityMultiplier < 1.0f && IsOpaque(translucency))
|
||||
translucency = TranslucencyKind.AlphaBlend;
|
||||
|
||||
ulong texHandle = ResolveTexture(entity, meshRef, batch, palHash);
|
||||
if (texHandle == 0) continue;
|
||||
|
||||
|
|
@ -2228,6 +2308,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
grp.Matrices.Add(model);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices
|
||||
collector?.Add(new CachedBatch(key, texHandle, restPose));
|
||||
}
|
||||
}
|
||||
|
|
@ -2310,6 +2391,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
if (_globalLightsSsbo != 0) _gl.DeleteBuffer(_globalLightsSsbo); // Fix B binding=4
|
||||
if (_instLightSetSsbo != 0) _gl.DeleteBuffer(_instLightSetSsbo); // Fix B binding=5
|
||||
if (_instIndoorSsbo != 0) _gl.DeleteBuffer(_instIndoorSsbo); // #142 binding=6
|
||||
if (_instAlphaSsbo != 0) _gl.DeleteBuffer(_instAlphaSsbo); // #188 binding=7
|
||||
if (_gpuQueriesInitialized)
|
||||
{
|
||||
for (int i = 0; i < GpuQueryRingDepth; i++)
|
||||
|
|
@ -2509,5 +2591,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// for outdoor objects (gets the sun). Written into _indoorData at the same
|
||||
// cursor as Matrices, so binding=6 instanceIndoor[] tracks binding=0.
|
||||
public readonly List<uint> IndoorFlags = new();
|
||||
|
||||
// #188: per-instance opacity multiplier, parallel to Matrices.
|
||||
// Opacities[i] is 1.0=unmodified, or <1.0 while a TransparentPartHook
|
||||
// fade is in flight for the instance whose matrix is Matrices[i]. At
|
||||
// layout time the dispatcher writes Opacities[i] into _alphaData at
|
||||
// the same cursor, so the binding=7 instanceAlpha[] tracks binding=0.
|
||||
public readonly List<float> Opacities = new();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue