using System.Numerics; using AcDream.Core.World; namespace AcDream.App.Rendering; /// /// Ports retail's teleport projection transition. gmSmartBoxUI::UseTime /// (0x004D6E30) eases SmartBox's view-plane distance between the /// active game's value and TRANSITION_VIEW_PLANE_DISTANCE = 0.001. /// Render::set_vdst (0x0054B240) 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. /// 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; /// /// Retail BeginTeleportAnimation captures /// SmartBox::GetOverrideFovDistance once when starting from Off. /// For a standard perspective matrix, M22 is exactly /// cot(verticalFov / 2), retail's view-plane-distance value. /// public void Begin(Matrix4x4 gameProjection) { float distance = gameProjection.M22; if (!float.IsFinite(distance) || distance <= 0f) distance = 1f; _gameViewPlaneDistance = distance; CurrentViewPlaneDistance = distance; Enabled = false; } /// /// 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; EndTeleportAnimation /// (0x004D65D5) releases it when entering TunnelContinue. /// 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; } /// /// Rebuild a perspective projection with retail's active view-plane /// distance while preserving the source projection's aspect and far /// plane. Render::set_vdst uses /// FOV = 2 * atan(1 / distance) and /// znear = max(0.1, distance * 0.25). /// 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); } /// /// Decorate the active camera with the same projection returned by /// . Retail's Render::set_vdst 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. /// 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; } } }