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

@ -6922,6 +6922,7 @@ public sealed class GameWindow : IDisposable
// fires Place (materialize) and FireLoginComplete (regain control + ack the server).
// Replaces the old TeleportArrivalController hold/place machine.
private readonly AcDream.Core.World.TeleportAnimSequencer _teleportAnim = new();
private readonly TeleportViewPlaneController _teleportViewPlane = new();
private bool _teleportInProgress;
private System.Numerics.Vector3 _pendingTeleportPos;
private uint _pendingTeleportCell;
@ -7022,11 +7023,12 @@ public sealed class GameWindow : IDisposable
_playerController.Yaw = AcQuaternionToYaw(_pendingTeleportRot);
_chaseCamera?.Update(snappedPos, _playerController.Yaw);
_retailChaseCamera?.Update(snappedPos, _playerController.Yaw,
playerVelocity: System.Numerics.Vector3.Zero,
isOnGround: true,
contactPlaneNormal: System.Numerics.Vector3.UnitZ,
dt: 1f / 60f);
// SmartBox::PlayerPositionUpdated (0x00453870) calls
// set_viewer(player_pos, reset_sought=1): the viewer and sought
// position both restart at the player, then the ordinary per-frame
// camera path re-extends the boom. Updating directly from the stale
// source-world viewer made the destination bend across the reveal.
_retailChaseCamera?.ResetViewerToPlayer(snappedPos, _playerController.Yaw);
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
"PLACED", resolved.CellId, $"forced={forced}");
@ -7055,6 +7057,8 @@ public sealed class GameWindow : IDisposable
_teleportInProgress = true;
_teleportHoldSeconds = 0f;
_teleportForced = false;
_teleportViewPlane.Begin(
_cameraController?.Active.Projection ?? System.Numerics.Matrix4x4.Identity);
_teleportAnim.Begin(AcDream.Core.World.TeleportEntryKind.Portal);
Console.WriteLine($"live: teleport started (seq={sequence})");
}
@ -8900,6 +8904,7 @@ public sealed class GameWindow : IDisposable
int tunnelFrame = _portalTunnel?.CurrentAnimationFrame ?? 0;
var (snap, evts) = _teleportAnim.Tick((float)dt, ready, tunnelFrame);
_teleportViewPlane.Update(snap);
// 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.
@ -8934,6 +8939,7 @@ public sealed class GameWindow : IDisposable
AcDream.Core.Net.Messages.GameActionLoginComplete.Build());
_teleportInProgress = false;
_pendingTeleportCell = 0u;
_teleportViewPlane.Reset();
_teleportFadeAlpha = 0f;
break;
default:
@ -9570,7 +9576,8 @@ public sealed class GameWindow : IDisposable
if (_cameraController is not null)
{
var camera = _cameraController.Active;
var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * camera.Projection);
var worldProjection = _teleportViewPlane.Apply(camera.Projection);
var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * worldProjection);
// Extract camera world position from the inverse of the view
// matrix — needed by the scene-lighting UBO (for fog distance)
@ -9799,7 +9806,7 @@ public sealed class GameWindow : IDisposable
// Phase A8: update EnvCellRenderer's frustum. The per-frame shell snapshot
// is prepared after the portal-visible cell filter is known.
var envCellViewProj = camera.View * camera.Projection;
var envCellViewProj = camera.View * worldProjection;
_envCellFrustum?.Update(envCellViewProj);
// MP-Alloc: reuse _animatedIdsScratch instead of `new`ing a
@ -10422,7 +10429,7 @@ public sealed class GameWindow : IDisposable
}
_debugDrawLogOnce++;
}
_debugLines.Flush(camera.View, camera.Projection);
_debugLines.Flush(camera.View, worldProjection);
}
// Count visible vs total for the perf overlay.
@ -10484,10 +10491,12 @@ public sealed class GameWindow : IDisposable
// 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.
var portalProjection = _teleportViewPlane.Apply(
_cameraController!.Active.Projection);
_portalTunnel?.Draw(
_window!.Size.X,
_window.Size.Y,
_cameraController!.Active.Projection);
portalProjection);
_fadeOverlay?.Draw(_teleportFadeAlpha);
// Phase D.2b Sub-phase C Slice 2 — paperdoll 3-D doll: render the re-dressed player clone into the

View file

@ -6,7 +6,7 @@ namespace AcDream.App.Rendering;
/// 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.
/// angle rolls that view around its own +Y forward axis.
/// </summary>
public sealed class PortalTunnelCamera : ICamera
{
@ -20,8 +20,9 @@ public sealed class PortalTunnelCamera : ICamera
/// <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.
/// projection each draw. Recover its vertical field of view and near
/// plane from that perspective matrix while retaining this private
/// scene's bounded far plane.
/// </summary>
public void UseSmartBoxFov(Matrix4x4 smartBoxProjection)
{
@ -32,6 +33,15 @@ public sealed class PortalTunnelCamera : ICamera
float fov = 2f * MathF.Atan(1f / verticalScale);
if (float.IsFinite(fov) && fov > 0f && fov < MathF.PI)
FovRadians = fov;
// System.Numerics perspective matrices encode near as M43/M33.
// During teleport fades this carries Render::set_vdst's
// max(0.1, viewPlaneDistance * 0.25) result.
float near = smartBoxProjection.M33 != 0f
? smartBoxProjection.M43 / smartBoxProjection.M33
: float.NaN;
if (float.IsFinite(near) && near > 0f && near < Far)
Near = near;
}
public Matrix4x4 View
@ -39,9 +49,17 @@ public sealed class PortalTunnelCamera : ICamera
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);
// CreatureMode::SetCameraDirection_Degrees (0x00453760) resets
// the Frame to identity, converts (0, angle, 0) to radians, and
// passes that rotation vector to Frame::rotate (0x004525B0).
// Frame::rotate interprets the vector as axis * angle, so this is
// a roll around AC +Y -- the identity camera's FORWARD axis --
// not a yaw around world +Z. The passage therefore stays ahead
// while the authored tunnel rolls around the viewer.
Quaternion rotation = Quaternion.CreateFromAxisAngle(Vector3.UnitY, radians);
Vector3 forward = Vector3.UnitY;
Vector3 up = Vector3.Transform(Vector3.UnitZ, rotation);
return Matrix4x4.CreateLookAt(RetailEye, RetailEye + forward, up);
}
}

View file

@ -247,6 +247,32 @@ public sealed class RetailChaseCamera : ICamera
PlayerTranslucency = ComputeTranslucency(d);
}
/// <summary>
/// Retail <c>SmartBox::set_viewer(playerPosition, reset_sought: 1)</c>
/// (<c>0x00452C40</c>), called by
/// <c>SmartBox::PlayerPositionUpdated</c> after a teleport
/// (<c>0x00453870</c>). Both the published viewer and its sought
/// position restart at the player; subsequent camera updates then
/// re-extend the boom through the ordinary damped/swept path.
/// </summary>
public void ResetViewerToPlayer(Vector3 playerPosition, float playerYaw)
{
Vector3 playerForward = new(MathF.Cos(playerYaw), MathF.Sin(playerYaw), 0f);
_publishedEye = playerPosition;
_soughtEye = playerPosition;
_dampedForward = playerForward;
_initialised = true;
Position = playerPosition;
ViewerCellId = 0u; // retail set_viewer clears viewer_cell at the teleport boundary
View = Matrix4x4.CreateLookAt(
playerPosition,
playerPosition + playerForward,
Vector3.UnitZ);
PlayerTranslucency = 0f;
}
/// <summary>
/// Adjust the camera distance (zoom) by a delta, clamped to
/// <see cref="DistanceMin"/>..<see cref="DistanceMax"/>. Mirrors

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);
}