feat(world): port retail portal-space viewport
Replace the opaque teleport cover with the DAT-authored CreatureMode tunnel, exact retail camera/light/animation timing and easing, and animation-hook audio routing. Compose portal and fade passes below retained UI so gameplay windows and input remain active through F751 travel. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
0eab7497c1
commit
eab23cbdd1
14 changed files with 983 additions and 45 deletions
|
|
@ -4,11 +4,10 @@ using Silk.NET.OpenGL;
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Fullscreen black quad at a given alpha — the teleport fade cover (retail-teleport
|
||||
/// spec §C, 2026-06-22). Drawn LAST in the frame (over the world + UI) so it covers
|
||||
/// everything; the <see cref="AcDream.Core.World.TeleportAnimSequencer"/> drives the alpha
|
||||
/// (opaque-black through the transit, ramping the world back in on WorldFadeIn). The
|
||||
/// authentic 3D portal-swirl is deferred — this black cover replaces it for now.
|
||||
/// Fullscreen black quad at a given alpha for retail's world/portal viewport
|
||||
/// transitions. It is drawn after the active 3-D viewport but before retained
|
||||
/// UI, matching <c>gmSmartBoxUI::UseTime</c>: the world or portal scene fades
|
||||
/// while chat, toolbar, windows, and cursor remain fully visible.
|
||||
///
|
||||
/// <para>Draws in NDC, so it needs no view/projection. Self-contained GL state
|
||||
/// (feedback_render_self_contained_gl_state): sets blend + disables depth, restores the
|
||||
|
|
@ -85,8 +84,7 @@ void main() { FragColor = vec4(0.0, 0.0, 0.0, uAlpha); }";
|
|||
|
||||
/// <summary>
|
||||
/// Draw the fullscreen black cover at <paramref name="alpha"/> (0 = clear → no-op,
|
||||
/// 1 = opaque). Must be called LAST in the frame, after the world and UI, so it covers
|
||||
/// everything on screen.
|
||||
/// 1 = opaque). Call after the active 3-D viewport and before retained UI.
|
||||
/// </summary>
|
||||
public void Draw(float alpha)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -221,7 +221,8 @@ public sealed class GameWindow : IDisposable
|
|||
// each frame on an indoor root (null on the outdoor root).
|
||||
private AcDream.App.Rendering.RetailPViewRenderer? _retailPViewRenderer;
|
||||
private AcDream.App.Rendering.PortalDepthMaskRenderer? _portalDepthMask;
|
||||
private AcDream.App.Rendering.FadeOverlay? _fadeOverlay; // teleport fade cover (spec C)
|
||||
private AcDream.App.Rendering.PortalTunnelPresentation? _portalTunnel;
|
||||
private AcDream.App.Rendering.FadeOverlay? _fadeOverlay;
|
||||
private AcDream.App.Rendering.InteriorEntityPartition.Result? _interiorPartition;
|
||||
|
||||
// Phase U.3: the shared per-frame clip data (binding=2 mesh SSBO + terrain
|
||||
|
|
@ -2588,6 +2589,14 @@ public sealed class GameWindow : IDisposable
|
|||
// DrawPortalPolyInternal (Ghidra 0x0059bc90).
|
||||
_portalDepthMask = new AcDream.App.Rendering.PortalDepthMaskRenderer(_gl);
|
||||
_fadeOverlay = new AcDream.App.Rendering.FadeOverlay(_gl);
|
||||
_portalTunnel = AcDream.App.Rendering.PortalTunnelPresentation.TryCreate(
|
||||
_gl,
|
||||
_dats!,
|
||||
_animLoader!,
|
||||
_hookRouter,
|
||||
_wbDrawDispatcher,
|
||||
_sceneLightingUbo!,
|
||||
_wbMeshAdapter!);
|
||||
}
|
||||
|
||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||
|
|
@ -8872,9 +8881,9 @@ public sealed class GameWindow : IDisposable
|
|||
// destination landblock this frame) and the live-session drain, so a destination
|
||||
// that became resident this frame materializes the same frame. The TAS holds at
|
||||
// Tunnel until worldReady, fires Place (materialize, hidden), then after the min-
|
||||
// continue + fades fires FireLoginComplete (regain control + ack). The fade overlay
|
||||
// is opaque black during the tunnel states (we render a fade, not the 3D swirl) and
|
||||
// ramps the world back in on WorldFadeIn.
|
||||
// continue + fades fires FireLoginComplete (regain control + ack). PortalTunnelPresentation
|
||||
// replaces the world viewport through the tunnel-family states; FadeOverlay transitions
|
||||
// between the two viewports below the retained UI.
|
||||
if (_teleportInProgress)
|
||||
{
|
||||
bool haveDest = _pendingTeleportCell != 0u;
|
||||
|
|
@ -8889,8 +8898,14 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
var (snap, evts) = _teleportAnim.Tick((float)dt, ready);
|
||||
_teleportFadeAlpha = snap.ShowTunnel ? 1f : snap.FadeAlpha;
|
||||
int tunnelFrame = _portalTunnel?.CurrentAnimationFrame ?? 0;
|
||||
var (snap, evts) = _teleportAnim.Tick((float)dt, ready, tunnelFrame);
|
||||
// The fade is a viewport transition below retained UI. If the
|
||||
// installed DATs are corrupt and the retail portal scene could
|
||||
// not be built, retain an opaque world cover during transit.
|
||||
_teleportFadeAlpha = _portalTunnel is null && snap.ShowTunnel
|
||||
? 1f
|
||||
: snap.FadeAlpha;
|
||||
|
||||
foreach (var e in evts)
|
||||
{
|
||||
|
|
@ -8904,6 +8919,12 @@ public sealed class GameWindow : IDisposable
|
|||
_streamingController.PriorityRadius = 0;
|
||||
}
|
||||
break;
|
||||
case AcDream.Core.World.TeleportAnimEvent.EnterTunnel:
|
||||
_portalTunnel?.Enter();
|
||||
break;
|
||||
case AcDream.Core.World.TeleportAnimEvent.PlayExitSound:
|
||||
_portalTunnel?.Exit();
|
||||
break;
|
||||
case AcDream.Core.World.TeleportAnimEvent.FireLoginComplete:
|
||||
if (_playerController is not null)
|
||||
_playerController.State = AcDream.App.Input.PlayerState.InWorld;
|
||||
|
|
@ -8916,10 +8937,14 @@ public sealed class GameWindow : IDisposable
|
|||
_teleportFadeAlpha = 0f;
|
||||
break;
|
||||
default:
|
||||
// PlayEnterSound / EnterTunnel / PlayExitSound — audio polish deferred.
|
||||
// The retained audio engine does not yet own the
|
||||
// ClientUISystem sound-table enum path. Enter/exit
|
||||
// visual edges are handled above.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
_portalTunnel?.Tick((float)dt);
|
||||
}
|
||||
|
||||
// Phase K.1a — tick the input dispatcher so Hold-type bindings
|
||||
|
|
@ -10455,6 +10480,16 @@ public sealed class GameWindow : IDisposable
|
|||
SkipWorldGeometry: ;
|
||||
}
|
||||
|
||||
// Retail gmSmartBoxUI swaps the world SmartBox viewport for a
|
||||
// CreatureMode portal-space viewport. This replacement scene and
|
||||
// its black transition draw BEFORE retained UI, so chat, windows,
|
||||
// cursor, and toolbar remain visible and interactive in transit.
|
||||
_portalTunnel?.Draw(
|
||||
_window!.Size.X,
|
||||
_window.Size.Y,
|
||||
_cameraController!.Active.Projection);
|
||||
_fadeOverlay?.Draw(_teleportFadeAlpha);
|
||||
|
||||
// Phase D.2b Sub-phase C Slice 2 — paperdoll 3-D doll: render the re-dressed player clone into the
|
||||
// viewport's off-screen texture BEFORE the 2-D UI pass blits it, but only when the inventory window
|
||||
// is open AND the paperdoll is in doll-view (the widget's Visible is driven by the Slots toggle).
|
||||
|
|
@ -10481,11 +10516,6 @@ public sealed class GameWindow : IDisposable
|
|||
_retailUiRuntime.Draw(new System.Numerics.Vector2(_window!.Size.X, _window.Size.Y));
|
||||
}
|
||||
|
||||
// Teleport fade cover (retail-teleport spec C). Drawn AFTER the world + retail UI so
|
||||
// it covers them during a transit; the ImGui devtools below composite on top so they
|
||||
// stay visible for debugging. Alpha is 0 outside a teleport → Draw is a no-op.
|
||||
_fadeOverlay?.Draw(_teleportFadeAlpha);
|
||||
|
||||
// Phase D.2a — end ImGui frame. Runs AFTER all scene + debug draws
|
||||
// so ImGui composites on top. ImGuiController save/restores the
|
||||
// GL state it touches (blend, scissor, VAO, shader, texture); any
|
||||
|
|
@ -14649,6 +14679,8 @@ public sealed class GameWindow : IDisposable
|
|||
_effectPoses.Clear();
|
||||
}
|
||||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_portalTunnel?.Dispose();
|
||||
_portalTunnel = null;
|
||||
_wbDrawDispatcher?.Dispose();
|
||||
_envCellRenderer?.Dispose(); // Phase A8
|
||||
_portalDepthMask?.Dispose(); // T1
|
||||
|
|
|
|||
53
src/AcDream.App/Rendering/PortalTunnelCamera.cs
Normal file
53
src/AcDream.App/Rendering/PortalTunnelCamera.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Retail portal-space camera from <c>gmSmartBoxUI::PostInit</c>
|
||||
/// (<c>0x004D6D8A</c>) and <c>CreatureMode::SetCameraDirection_Degrees</c>
|
||||
/// (<c>0x00453760</c>). Identity looks along AC +Y with +Z up; the animated
|
||||
/// angle rotates that view around +Z.
|
||||
/// </summary>
|
||||
public sealed class PortalTunnelCamera : ICamera
|
||||
{
|
||||
public static readonly Vector3 RetailEye = new(0.24f, -2.7f, 0.88f);
|
||||
|
||||
public float DirectionDegrees { get; set; }
|
||||
public float FovRadians { get; set; } = MathF.PI / 4f;
|
||||
public float Near { get; set; } = 0.1f;
|
||||
public float Far { get; set; } = 50f;
|
||||
public float Aspect { get; set; } = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CreatureMode::UseSmartboxFOV</c> reads the active SmartBox
|
||||
/// projection each draw. Recover its vertical field of view from that
|
||||
/// perspective matrix while retaining this private scene's near/far planes.
|
||||
/// </summary>
|
||||
public void UseSmartBoxFov(Matrix4x4 smartBoxProjection)
|
||||
{
|
||||
float verticalScale = smartBoxProjection.M22;
|
||||
if (!float.IsFinite(verticalScale) || verticalScale <= 0f)
|
||||
return;
|
||||
|
||||
float fov = 2f * MathF.Atan(1f / verticalScale);
|
||||
if (float.IsFinite(fov) && fov > 0f && fov < MathF.PI)
|
||||
FovRadians = fov;
|
||||
}
|
||||
|
||||
public Matrix4x4 View
|
||||
{
|
||||
get
|
||||
{
|
||||
float radians = DirectionDegrees * (MathF.PI / 180f);
|
||||
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, radians);
|
||||
Vector3 forward = Vector3.Transform(Vector3.UnitY, rotation);
|
||||
return Matrix4x4.CreateLookAt(RetailEye, RetailEye + forward, Vector3.UnitZ);
|
||||
}
|
||||
}
|
||||
|
||||
public Matrix4x4 Projection => Matrix4x4.CreatePerspectiveFieldOfView(
|
||||
FovRadians,
|
||||
Aspect <= 0f ? 1f : Aspect,
|
||||
Near,
|
||||
Far);
|
||||
}
|
||||
363
src/AcDream.App/Rendering/PortalTunnelPresentation.cs
Normal file
363
src/AcDream.App/Rendering/PortalTunnelPresentation.cs
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Content.Vfx;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics.Motion;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Types;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// The retail portal-space CreatureMode scene owned by
|
||||
/// <c>gmSmartBoxUI</c> (<c>PostInit 0x004D6B60</c>,
|
||||
/// <c>UseTime 0x004D6E30</c>). It is a synthetic DAT Setup animated at
|
||||
/// 40 frames/second and drawn as a replacement 3-D viewport beneath the
|
||||
/// retained gameplay UI.
|
||||
/// </summary>
|
||||
public sealed class PortalTunnelPresentation : IDisposable
|
||||
{
|
||||
public const uint SetupClientEnum = 0x10000001u;
|
||||
public const uint AnimationClientEnum = 0x10000002u;
|
||||
public const uint ClientEnumCategory = 7u;
|
||||
|
||||
private const uint SyntheticEntityId = 0xFFFF_FF01u;
|
||||
private const uint SyntheticLandblockId = 0u;
|
||||
private const float RotationDurationMin = 0.6f;
|
||||
private const float RotationDurationMax = 1.8f;
|
||||
|
||||
private static readonly HashSet<uint> AnimatedIds = new() { SyntheticEntityId };
|
||||
|
||||
private readonly GL _gl;
|
||||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly SceneLightingUboBinding _lightUbo;
|
||||
private readonly IWbMeshAdapter _meshAdapter;
|
||||
private readonly Setup _setup;
|
||||
private readonly uint _setupDid;
|
||||
private readonly uint _animationDid;
|
||||
private readonly CSequence _sequence;
|
||||
private readonly PortalAnimationHookQueue _animationHooks;
|
||||
private readonly WorldEntity _entity;
|
||||
private readonly PortalTunnelCamera _camera = new();
|
||||
private readonly Random _random;
|
||||
private readonly Action<string>? _displayNotice;
|
||||
private readonly uint[] _registeredGfxObjects;
|
||||
|
||||
private bool _visible;
|
||||
private double _rotationElapsed;
|
||||
private double _rotationDuration;
|
||||
private float _rotationStartAngle;
|
||||
private float _rotationEndAngle;
|
||||
private float _rotationCurrentAngle;
|
||||
private bool _disposed;
|
||||
|
||||
private PortalTunnelPresentation(
|
||||
GL gl,
|
||||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IWbMeshAdapter meshAdapter,
|
||||
Setup setup,
|
||||
uint setupDid,
|
||||
uint animationDid,
|
||||
IAnimationLoader animationLoader,
|
||||
IAnimationHookSink hookSink,
|
||||
Random random,
|
||||
Action<string>? displayNotice)
|
||||
{
|
||||
_gl = gl;
|
||||
_dispatcher = dispatcher;
|
||||
_lightUbo = lightUbo;
|
||||
_meshAdapter = meshAdapter;
|
||||
_setup = setup;
|
||||
_setupDid = setupDid;
|
||||
_animationDid = animationDid;
|
||||
_sequence = new CSequence(animationLoader);
|
||||
_animationHooks = new PortalAnimationHookQueue(hookSink, SyntheticEntityId);
|
||||
_sequence.HookObj = _animationHooks;
|
||||
_random = random;
|
||||
_displayNotice = displayNotice;
|
||||
|
||||
_entity = new WorldEntity
|
||||
{
|
||||
Id = SyntheticEntityId,
|
||||
SourceGfxObjOrSetupId = setupDid,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = SetupMesh.Flatten(setup),
|
||||
};
|
||||
|
||||
_registeredGfxObjects = setup.Parts
|
||||
.Select(part => (uint)part)
|
||||
.Where(id => id != 0u)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
foreach (uint gfxObjId in _registeredGfxObjects)
|
||||
_meshAdapter.IncrementRefCount(gfxObjId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolve retail's enum-mapped portal Setup and animation through the
|
||||
/// installed DATs. Missing assets fail closed with an actionable message;
|
||||
/// no substitute tunnel is fabricated.
|
||||
/// </summary>
|
||||
public static PortalTunnelPresentation? TryCreate(
|
||||
GL gl,
|
||||
DatCollection dats,
|
||||
IAnimationLoader animationLoader,
|
||||
IAnimationHookSink hookSink,
|
||||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IWbMeshAdapter meshAdapter,
|
||||
Action<string>? displayNotice = null,
|
||||
Random? random = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(dats);
|
||||
ArgumentNullException.ThrowIfNull(animationLoader);
|
||||
ArgumentNullException.ThrowIfNull(hookSink);
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
ArgumentNullException.ThrowIfNull(lightUbo);
|
||||
ArgumentNullException.ThrowIfNull(meshAdapter);
|
||||
|
||||
uint setupDid = RetailDataIdResolver.Resolve(dats, SetupClientEnum, ClientEnumCategory);
|
||||
uint animationDid = RetailDataIdResolver.Resolve(dats, AnimationClientEnum, ClientEnumCategory);
|
||||
Setup? setup = setupDid == 0u ? null : dats.Get<Setup>(setupDid);
|
||||
Animation? animation = animationDid == 0u
|
||||
? null
|
||||
: animationLoader.LoadAnimation(animationDid);
|
||||
|
||||
if (setup is null || animation is null)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[portal-space] retail DAT assets unavailable: "
|
||||
+ $"setup=0x{setupDid:X8} ({(setup is null ? "missing" : "ok")}), "
|
||||
+ $"animation=0x{animationDid:X8} ({(animation is null ? "missing" : "ok")})");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PortalTunnelPresentation(
|
||||
gl,
|
||||
dispatcher,
|
||||
lightUbo,
|
||||
meshAdapter,
|
||||
setup,
|
||||
setupDid,
|
||||
animationDid,
|
||||
animationLoader,
|
||||
hookSink,
|
||||
random ?? Random.Shared,
|
||||
displayNotice);
|
||||
}
|
||||
|
||||
public bool IsVisible => _visible;
|
||||
public int CurrentAnimationFrame => _sequence.GetCurrFrameNumber();
|
||||
public uint SetupDid => _setupDid;
|
||||
public uint AnimationDid => _animationDid;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>set_sequence_animation(anim, clear=1, low=1, fps=40)</c>
|
||||
/// on the edge where portal space becomes visible.
|
||||
/// </summary>
|
||||
public void Enter()
|
||||
{
|
||||
ThrowIfDisposed();
|
||||
_animationHooks.Clear();
|
||||
_sequence.ClearAnimations();
|
||||
_sequence.AppendAnimation(new AnimData
|
||||
{
|
||||
AnimId = (QualifiedDataId<Animation>)_animationDid,
|
||||
LowFrame = 1,
|
||||
HighFrame = -1,
|
||||
Framerate = TeleportAnimSequencer.TunnelFramesPerSecond,
|
||||
});
|
||||
|
||||
_rotationElapsed = 0.0;
|
||||
_rotationDuration = 0.0;
|
||||
_rotationStartAngle = 0f;
|
||||
_rotationEndAngle = 0f;
|
||||
_rotationCurrentAngle = 0f;
|
||||
_camera.DirectionDegrees = 0f;
|
||||
_visible = true;
|
||||
RebuildPose();
|
||||
}
|
||||
|
||||
/// <summary>Hide the CreatureMode viewport and clear its sequence.</summary>
|
||||
public void Exit()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
_visible = false;
|
||||
_animationHooks.Clear();
|
||||
_sequence.ClearAnimations();
|
||||
}
|
||||
|
||||
public void Tick(float dt)
|
||||
{
|
||||
if (!_visible || dt < 0f)
|
||||
return;
|
||||
|
||||
_sequence.Update(dt, frame: null);
|
||||
RebuildPose();
|
||||
_animationHooks.Drain(Vector3.Zero);
|
||||
TickRotation(dt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replace the already-rendered world viewport with retail portal space.
|
||||
/// The caller then draws the fade and retained UI above this pass.
|
||||
/// </summary>
|
||||
public void Draw(int width, int height, Matrix4x4 smartBoxProjection)
|
||||
{
|
||||
if (!_visible || width <= 0 || height <= 0 || _entity.MeshRefs.Count == 0)
|
||||
return;
|
||||
|
||||
_camera.Aspect = width / (float)height;
|
||||
_camera.UseSmartBoxFov(smartBoxProjection);
|
||||
using var scope = new GLStateScope(_gl);
|
||||
|
||||
_gl.Viewport(0, 0, (uint)width, (uint)height);
|
||||
_gl.Disable(EnableCap.ScissorTest);
|
||||
_gl.ClearColor(0f, 0f, 0f, 1f);
|
||||
_gl.ClearDepth(1.0);
|
||||
_gl.DepthMask(true);
|
||||
_gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
|
||||
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
_gl.DepthFunc(DepthFunction.Less);
|
||||
_gl.Enable(EnableCap.CullFace);
|
||||
_gl.CullFace(TriangleFace.Back);
|
||||
_gl.FrontFace(FrontFaceDirection.Ccw);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
|
||||
UploadRetailLight();
|
||||
// The dispatcher is shared with the world pass. Portal space is its
|
||||
// own CreatureMode scene: it has no world-cell clip routing and no
|
||||
// world point lights, only the distant light installed above.
|
||||
_dispatcher.ClearClipRouting();
|
||||
_dispatcher.SetSceneLights(null);
|
||||
|
||||
var entries = new (uint, Vector3, Vector3, IReadOnlyList<WorldEntity>, IReadOnlyDictionary<uint, WorldEntity>?)[]
|
||||
{
|
||||
(SyntheticLandblockId,
|
||||
new Vector3(-16f, -16f, -16f),
|
||||
new Vector3(16f, 16f, 16f),
|
||||
new WorldEntity[] { _entity },
|
||||
null),
|
||||
};
|
||||
|
||||
_dispatcher.Draw(
|
||||
_camera,
|
||||
entries,
|
||||
frustum: null,
|
||||
neverCullLandblockId: SyntheticLandblockId,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: AnimatedIds);
|
||||
}
|
||||
|
||||
private void TickRotation(float dt)
|
||||
{
|
||||
_rotationElapsed += dt;
|
||||
if (_rotationElapsed >= _rotationDuration)
|
||||
{
|
||||
_rotationCurrentAngle = _rotationEndAngle;
|
||||
_rotationElapsed = 0.0;
|
||||
_rotationDuration = NextDouble(RotationDurationMin, RotationDurationMax);
|
||||
_rotationStartAngle = _rotationCurrentAngle;
|
||||
_rotationEndAngle = (float)NextDouble(0.0, 360.0);
|
||||
_displayNotice?.Invoke("In Portal Space - Please Wait...");
|
||||
}
|
||||
else
|
||||
{
|
||||
float t = _rotationDuration <= 0.0
|
||||
? 1f
|
||||
: (float)(_rotationElapsed / _rotationDuration);
|
||||
float level = TeleportAnimSequencer.GetRetailAnimationLevel(t) / 1024f;
|
||||
_rotationCurrentAngle = _rotationStartAngle
|
||||
+ ((_rotationEndAngle - _rotationStartAngle) * level);
|
||||
}
|
||||
|
||||
_camera.DirectionDegrees = _rotationCurrentAngle;
|
||||
}
|
||||
|
||||
private double NextDouble(double min, double max) => min + (_random.NextDouble() * (max - min));
|
||||
|
||||
private void RebuildPose()
|
||||
{
|
||||
AnimationFrame? frame = _sequence.GetCurrAnimframe();
|
||||
_entity.MeshRefs = SetupMesh.Flatten(_setup, frame);
|
||||
}
|
||||
|
||||
private void UploadRetailLight()
|
||||
{
|
||||
Vector3 direction = Vector3.Normalize(new Vector3(0.3f, -1.9f, 0.65f));
|
||||
_lightUbo.Upload(new SceneLightingUbo
|
||||
{
|
||||
Light0 = new UboLight
|
||||
{
|
||||
PosAndKind = Vector4.Zero,
|
||||
DirAndRange = new Vector4(direction, 1e9f),
|
||||
ColorAndIntensity = new Vector4(1f, 1f, 1f, 2f),
|
||||
ConeAngleEtc = Vector4.Zero,
|
||||
},
|
||||
CellAmbient = new Vector4(0.3f, 0.3f, 0.3f, 1f),
|
||||
FogParams = new Vector4(1e9f, 1e9f, 0f, 0f),
|
||||
FogColor = Vector4.Zero,
|
||||
CameraAndTime = new Vector4(PortalTunnelCamera.RetailEye, 0f),
|
||||
});
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
|
||||
/// <summary>
|
||||
/// Retail stages animation hooks while advancing the sequence, then drains
|
||||
/// them after the object's current pose has been committed. Portal space
|
||||
/// has no live-world record, so it owns this small equivalent queue and
|
||||
/// forwards its DAT-authored hooks through the shared hook router.
|
||||
/// </summary>
|
||||
private sealed class PortalAnimationHookQueue : IAnimHookQueue
|
||||
{
|
||||
private readonly IAnimationHookSink _sink;
|
||||
private readonly uint _ownerId;
|
||||
private readonly List<AnimationHook> _pending = new();
|
||||
|
||||
public PortalAnimationHookQueue(IAnimationHookSink sink, uint ownerId)
|
||||
{
|
||||
_sink = sink;
|
||||
_ownerId = ownerId;
|
||||
}
|
||||
|
||||
public void AddAnimHook(AnimationHook hook) => _pending.Add(hook);
|
||||
|
||||
public void AddAnimDoneHook()
|
||||
{
|
||||
// The tunnel animation is retail's cyclic tail, so AnimDone is
|
||||
// not observable during the portal-space presentation.
|
||||
}
|
||||
|
||||
public void Drain(Vector3 worldPosition)
|
||||
{
|
||||
for (int i = 0; i < _pending.Count; i++)
|
||||
_sink.OnHook(_ownerId, worldPosition, _pending[i]);
|
||||
_pending.Clear();
|
||||
}
|
||||
|
||||
public void Clear() => _pending.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
Exit();
|
||||
_animationHooks.Clear();
|
||||
foreach (uint gfxObjId in _registeredGfxObjects)
|
||||
_meshAdapter.DecrementRefCount(gfxObjId);
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue