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
|
|
@ -799,6 +799,14 @@ public sealed class GameWindow : IDisposable
|
|||
// from the animation pipeline flip the matching LightSource.IsLit.
|
||||
private AcDream.Core.Lighting.LightingHookSink? _lightingSink;
|
||||
|
||||
// #188 — TransparentPartHook fires from the animation pipeline drive
|
||||
// a per-(entity,part) translucency ramp; WbDrawDispatcher reads it
|
||||
// per frame. Wired into the hook router in OnLoad, advanced once per
|
||||
// frame in the main loop regardless of _animatedEntities membership
|
||||
// (a fade must keep running even if its entity's one-shot cycle ends).
|
||||
private readonly AcDream.Core.Rendering.TranslucencyFadeManager _translucencyFades = new();
|
||||
private AcDream.Core.Rendering.TranslucencyHookSink? _translucencySink;
|
||||
|
||||
// Phase G.1 sky renderer + shared UBO. Created once the GL context
|
||||
// exists in OnLoad; shared across every other renderer via
|
||||
// binding = 1 so terrain/mesh/instanced/sky all read the same
|
||||
|
|
@ -1425,6 +1433,11 @@ public sealed class GameWindow : IDisposable
|
|||
_lightingSink = new AcDream.Core.Lighting.LightingHookSink(Lighting);
|
||||
_hookRouter.Register(_lightingSink);
|
||||
|
||||
// #188 — TransparentPartHook (per-part translucency fade, e.g.
|
||||
// the "fading wall" secret-passage doors) routes here.
|
||||
_translucencySink = new AcDream.Core.Rendering.TranslucencyHookSink(_translucencyFades);
|
||||
_hookRouter.Register(_translucencySink);
|
||||
|
||||
// Phase E.2 audio: init OpenAL + hook sink. Suppressible via
|
||||
// ACDREAM_NO_AUDIO=1 for headless tests / broken audio drivers.
|
||||
if (!_options.NoAudio)
|
||||
|
|
@ -2541,7 +2554,7 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
|
||||
_gl, _meshShader!, _textureCache!, _wbMeshAdapter!, _wbEntitySpawnAdapter, _bindlessSupport!,
|
||||
_classificationCache);
|
||||
_classificationCache, _translucencyFades);
|
||||
// A.5 T22.5: apply A2C gate from quality preset.
|
||||
_wbDrawDispatcher.AlphaToCoverage = _resolvedQuality.AlphaToCoverage;
|
||||
|
||||
|
|
@ -2648,7 +2661,10 @@ public sealed class GameWindow : IDisposable
|
|||
_worldState.TryGetLandblock(id, out var lb))
|
||||
{
|
||||
foreach (var ent in lb!.Entities)
|
||||
{
|
||||
_lightingSink.UnregisterOwner(ent.Id);
|
||||
_translucencyFades.ClearEntity(ent.Id); // #188
|
||||
}
|
||||
}
|
||||
_terrain?.RemoveLandblock(id);
|
||||
_physicsEngine.RemoveLandblock(id);
|
||||
|
|
@ -4708,6 +4724,7 @@ public sealed class GameWindow : IDisposable
|
|||
// double-register the new one. (Was gated on logDelete — harmless only
|
||||
// while live weenies registered no lights, which is no longer true.)
|
||||
_lightingSink?.UnregisterOwner(existingEntity.Id);
|
||||
_translucencyFades.ClearEntity(existingEntity.Id); // #188
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -7882,6 +7899,7 @@ public sealed class GameWindow : IDisposable
|
|||
// sort). Clear the owner's previous registration first: the
|
||||
// re-apply becomes idempotent, and a first apply is a no-op.
|
||||
_lightingSink.UnregisterOwner(entity.Id);
|
||||
_translucencyFades.ClearEntity(entity.Id); // #188
|
||||
var loaded = AcDream.Core.Lighting.LightInfoLoader.Load(
|
||||
datSetup,
|
||||
ownerId: entity.Id,
|
||||
|
|
@ -9045,6 +9063,12 @@ public sealed class GameWindow : IDisposable
|
|||
if (_animatedEntities.Count > 0)
|
||||
TickAnimations((float)deltaSeconds);
|
||||
|
||||
// #188 — advance translucency fades UNCONDITIONALLY (not gated on
|
||||
// _animatedEntities.Count): a one-shot open-cycle animation can
|
||||
// finish and drop its entity from _animatedEntities before the
|
||||
// fade's own Time has elapsed, but the ramp must keep running.
|
||||
_translucencyFades.AdvanceAll((float)deltaSeconds);
|
||||
|
||||
// Phase G.1: weather state machine — deterministic per-day roll
|
||||
// + transitions + lightning flash.
|
||||
var cal = WorldTime.CurrentCalendar;
|
||||
|
|
@ -9303,6 +9327,29 @@ public sealed class GameWindow : IDisposable
|
|||
// WalkEntitiesInto) treat null and an empty set identically, so an
|
||||
// always-non-null (possibly empty) set is behaviorally the same as
|
||||
// the old null-when-nothing-animated local.
|
||||
//
|
||||
// Every entity in _animatedEntities (i.e. every entity with a
|
||||
// Sequencer) is added UNCONDITIONALLY: membership here is the
|
||||
// "re-classify me every frame, don't use the Tier-1 static cache"
|
||||
// flag, and the Tier-1 cache captures an entity's REST pose +
|
||||
// opacity-1.0 exactly once. Any entity whose rendered state can
|
||||
// depart that rest — a door held at its final OPEN frame, a wall
|
||||
// held FADED-OUT by a TransparentPartHook — MUST stay on the
|
||||
// per-frame path so the live sequencer pose + TranslucencyFadeManager
|
||||
// opacity are read; otherwise it flips back to the stale cached
|
||||
// closed/opaque state the instant its animation settles.
|
||||
//
|
||||
// DO NOT re-narrow this to "only if the current cycle is multi-frame"
|
||||
// (the reverted IsEntityCurrentlyMoving gate, 2026-07-09): a settled
|
||||
// one-shot hold IS pose-stable frame-to-frame, but stable at the
|
||||
// HELD pose the cache does NOT contain — that is the door/fade
|
||||
// flip-back. The per-frame re-classification cost that narrowing
|
||||
// saved was a DEBUG-build artifact; Release is GPU-bound (the CPU
|
||||
// work hides under GPU time), so the unconditional add is free where
|
||||
// it matters. If a real Release CPU cost from static-prop
|
||||
// re-classification is ever measured, gate on "entity is at its
|
||||
// captured rest state" (default motion AND no active fade), never on
|
||||
// "is mid-cycle".
|
||||
_animatedIdsScratch.Clear();
|
||||
foreach (var k in _animatedEntities.Keys)
|
||||
_animatedIdsScratch.Add(k);
|
||||
|
|
@ -10440,6 +10487,12 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// IsEntityCurrentlyMoving REMOVED (2026-07-09): it powered a cache-bypass
|
||||
// narrowing that dropped settled-open doors / faded-out walls back onto the
|
||||
// stale Tier-1 rest-pose cache entry (the door/fade "flip-back"). See the
|
||||
// animatedIds build site — every Sequencer entity is now added
|
||||
// unconditionally, which is the known-good pre-optimization behavior.
|
||||
|
||||
// R3-W6: UpdatePlayerAnimation DELETED — the player's sequencer is
|
||||
// driven through the SAME MotionTableDispatchSink/DefaultSink funnel
|
||||
// remotes use (edge-driven DoMotion/StopMotion/set_hold_run in
|
||||
|
|
@ -11650,7 +11703,10 @@ public sealed class GameWindow : IDisposable
|
|||
_worldState.TryGetLandblock(id, out var lb))
|
||||
{
|
||||
foreach (var ent in lb!.Entities)
|
||||
{
|
||||
_lightingSink.UnregisterOwner(ent.Id);
|
||||
_translucencyFades.ClearEntity(ent.Id); // #188
|
||||
}
|
||||
}
|
||||
_terrain?.RemoveLandblock(id);
|
||||
_physicsEngine.RemoveLandblock(id);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue