fix(world): match retail portal passage and exit warp

This commit is contained in:
Erik 2026-07-15 22:14:21 +02:00
parent eab23cbdd1
commit 842bd89c16
11 changed files with 355 additions and 28 deletions

View file

@ -0,0 +1,98 @@
using System.Numerics;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
/// <summary>
/// Ports retail's teleport projection transition. <c>gmSmartBoxUI::UseTime</c>
/// (<c>0x004D6E30</c>) eases SmartBox's view-plane distance between the
/// active game's value and <c>TRANSITION_VIEW_PLANE_DISTANCE = 0.001</c>.
/// <c>Render::set_vdst</c> (<c>0x0054B240</c>) converts that distance back to
/// FOV and adjusts the near plane. This is the wide projection warp visible
/// while the destination world emerges from black.
/// </summary>
public sealed class TeleportViewPlaneController
{
public const float TransitionViewPlaneDistance = 0.001f;
private float _gameViewPlaneDistance = 1f;
public bool Enabled { get; private set; }
public float CurrentViewPlaneDistance { get; private set; } = 1f;
/// <summary>
/// Retail <c>BeginTeleportAnimation</c> captures
/// <c>SmartBox::GetOverrideFovDistance</c> once when starting from Off.
/// For a standard perspective matrix, M22 is exactly
/// <c>cot(verticalFov / 2)</c>, retail's view-plane-distance value.
/// </summary>
public void Begin(Matrix4x4 gameProjection)
{
float distance = gameProjection.M22;
if (!float.IsFinite(distance) || distance <= 0f)
distance = 1f;
_gameViewPlaneDistance = distance;
CurrentViewPlaneDistance = distance;
Enabled = false;
}
/// <summary>
/// Apply the four retail fade-state branches. Their projection blend is
/// numerically the same table-driven level as the black fade: normal to
/// 0.001 on fade-out, and 0.001 back to normal on fade-in.
/// </summary>
public void Update(TeleportAnimSnapshot snapshot)
{
Enabled = snapshot.State is TeleportAnimState.WorldFadeOut
or TeleportAnimState.TunnelFadeIn
or TeleportAnimState.TunnelFadeOut
or TeleportAnimState.WorldFadeIn;
CurrentViewPlaneDistance = Enabled
? Lerp(_gameViewPlaneDistance, TransitionViewPlaneDistance, snapshot.FadeAlpha)
: _gameViewPlaneDistance;
}
public void Reset()
{
Enabled = false;
CurrentViewPlaneDistance = _gameViewPlaneDistance;
}
/// <summary>
/// Rebuild a perspective projection with retail's active view-plane
/// distance while preserving the source projection's aspect and far
/// plane. <c>Render::set_vdst</c> uses
/// <c>FOV = 2 * atan(1 / distance)</c> and
/// <c>znear = max(0.1, distance * 0.25)</c>.
/// </summary>
public Matrix4x4 Apply(Matrix4x4 baseProjection)
{
if (!Enabled)
return baseProjection;
float aspect = baseProjection.M11 != 0f
? baseProjection.M22 / baseProjection.M11
: 1f;
float far = baseProjection.M33 != -1f
? baseProjection.M43 / (baseProjection.M33 + 1f)
: 5000f;
if (!float.IsFinite(aspect) || aspect <= 0f)
aspect = 1f;
if (!float.IsFinite(far) || far <= 0.1f)
far = 5000f;
float distance = MathF.Max(CurrentViewPlaneDistance, TransitionViewPlaneDistance);
float fov = 2f * MathF.Atan(1f / distance);
float near = MathF.Max(0.1f, distance * 0.25f);
if (near >= far)
near = MathF.Min(0.1f, far * 0.5f);
return Matrix4x4.CreatePerspectiveFieldOfView(fov, aspect, near, far);
}
private static float Lerp(float from, float to, float amount) =>
from + (to - from) * Math.Clamp(amount, 0f, 1f);
}