acdream/src/AcDream.App/Rendering/TeleportViewPlaneController.cs

155 lines
5.7 KiB
C#

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. At the exit edge retail swaps directly
/// from portal space to the destination world at the transition projection;
/// there is no black-alpha compositor between them.
/// </summary>
public sealed class TeleportViewPlaneController
{
public const float TransitionViewPlaneDistance = 0.001f;
private float _gameViewPlaneDistance = 1f;
private readonly ProjectionOverrideCamera _projectionCamera = new();
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 retail's view-plane state machine. Here "fade" is retail's state
/// name for a projection transition: normal to 0.001 on fade-out, and
/// 0.001 back to normal on fade-in. A logout transition retains the
/// captured override during stable Tunnel; <c>EndTeleportAnimation</c>
/// (<c>0x004D65D5</c>) releases it when entering TunnelContinue.
/// </summary>
public void Update(TeleportAnimSnapshot snapshot)
{
switch (snapshot.State)
{
case TeleportAnimState.WorldFadeOut:
case TeleportAnimState.TunnelFadeIn:
case TeleportAnimState.TunnelFadeOut:
case TeleportAnimState.WorldFadeIn:
Enabled = true;
CurrentViewPlaneDistance = Lerp(
_gameViewPlaneDistance,
TransitionViewPlaneDistance,
snapshot.ViewPlaneBlend);
break;
case TeleportAnimState.Tunnel:
// Retail preserves the captured FOV override through the
// logout path's stable tunnel. A normal portal begins in
// Tunnel with no override, so its disabled state is retained.
if (Enabled)
CurrentViewPlaneDistance = _gameViewPlaneDistance;
break;
case TeleportAnimState.TunnelContinue:
case TeleportAnimState.Off:
default:
Enabled = false;
CurrentViewPlaneDistance = _gameViewPlaneDistance;
break;
}
}
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);
}
/// <summary>
/// Decorate the active camera with the same projection returned by
/// <see cref="Apply(Matrix4x4)"/>. Retail's <c>Render::set_vdst</c> is
/// global to every 3-D draw; callers must pass this returned camera to
/// terrain, meshes, particles, sky, weather, and portal-cell rendering so
/// rasterization and frustum culling cannot use different projections.
/// </summary>
public ICamera ApplyTo(ICamera baseCamera)
{
ArgumentNullException.ThrowIfNull(baseCamera);
_projectionCamera.Update(baseCamera, Apply(baseCamera.Projection));
return _projectionCamera;
}
private static float Lerp(float from, float to, float amount) =>
from + (to - from) * Math.Clamp(amount, 0f, 1f);
private sealed class ProjectionOverrideCamera : ICamera
{
private ICamera _source = null!;
public Matrix4x4 View => _source.View;
public Matrix4x4 Projection { get; private set; } = Matrix4x4.Identity;
public float Aspect
{
get => _source.Aspect;
set => _source.Aspect = value;
}
public void Update(ICamera source, Matrix4x4 projection)
{
_source = source;
Projection = projection;
}
}
}