155 lines
5.7 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|