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

@ -16,6 +16,7 @@ Named Sept-2013 retail symbols:
- `gmSmartBoxUI::UseTime` `0x004D6E30`
- `CreatureMode::CreatureMode` `0x00454390`
- `CreatureMode::SetCameraDirection_Degrees` `0x00453760`
- `Frame::rotate` `0x004525B0`
- `CreatureMode::AddObject` `0x004556D0`
- `UIGlobals::Init` `0x004EE470`
- `UIGlobals::GetAnimLevel` `0x004EE540`
@ -24,6 +25,9 @@ Named Sept-2013 retail symbols:
- `SmartBox::PlayerPositionUpdated` `0x00453870`
- `CPhysicsObj::teleport_hook` `0x00514ED0`
- `CommandInterpreter::PlayerTeleported` `0x006B32B0`
- `SmartBox::SetOverrideFovDistance` `0x00451BC0`
- `SmartBox::GetOverrideFovDistance` `0x00451BE0`
- `Render::set_vdst` `0x0054B240`
Constants were checked against static x86 disassembly. The Binary Ninja
pseudocode loses several x87 operands in `UseTime`; disassembly is authoritative
@ -93,7 +97,12 @@ sequence animations, plays `Sound_UI_ExitPortal`, and enters `WorldFadeIn`.
## Camera motion
The camera rotates around AC's vertical `+Z` axis. Each segment is independent:
The camera rolls around its own AC `+Y` forward axis. It does not yaw around
world `+Z`. `SetCameraDirection_Degrees` resets the frame to identity, converts
the vector to radians, then passes `(0, angle, 0)` to `Frame::rotate`, whose
input is an axis-angle rotation vector. The forward direction therefore stays
down the portal passage while local `+Z` up rolls around it. Each segment is
independent:
```text
if now >= rotationStart + rotationDuration:
@ -184,6 +193,39 @@ World/tunnel fades use `GetAnimLevel(elapsed / FadeTime)`. The fade belongs
between the 3-D viewport and the retained UI. It must never cover or disable
the retained UI.
## View-plane warp
Retail couples every fade state to a projection transition. On begin it
captures the current SmartBox view-plane distance; for a perspective
projection this is `cot(verticalFov / 2)`. The transition endpoint is the
named constant `TRANSITION_VIEW_PLANE_DISTANCE = 0.001` at `0x007BD260`.
```text
fadeLevel = GetAnimLevel(elapsed / FadeTime) / 1024
WorldFadeOut or TunnelFadeOut:
currentDistance = gameDistance
+ (0.001 - gameDistance) * fadeLevel
TunnelFadeIn or WorldFadeIn:
currentDistance = 0.001
+ (gameDistance - 0.001) * fadeLevel
SmartBox.SetOverrideFovDistance(true, currentDistance)
```
`Render::set_vdst` turns the distance back into projection values:
```text
verticalFov = 2 * atan(1 / currentDistance)
znear = max(0.1, currentDistance * 0.25)
```
At `0.001`, the view is nearly 180 degrees wide. Black covers the singular
endpoint; as `WorldFadeIn` clears, the FOV eases back to the captured game
projection. This is retail's characteristic destination-world warp, separate
from both the alpha fade and chase-camera movement.
## Player placement boundary
```text
@ -195,7 +237,7 @@ PlayerPositionUpdated(isTeleport=true):
clear position-update/teleport bookkeeping
player.teleport_hook()
commandInterpreter.PlayerTeleported()
move world viewer to player
SmartBox.set_viewer(player.position, reset_sought=true)
update CellManager position
player.teleport_hook():
@ -212,6 +254,11 @@ PlayerTeleported():
SendMovementEvent()
```
The viewer reset copies the player's complete position into both `viewer` and
`viewer_sought_position`. Subsequent ordinary camera updates extend the chase
boom outward through the same damped and swept path used after camera
collision. Reusing the source-world viewer is not retail behavior.
The retail teleport hook does not clear the character's animation sequence.
The arrival presentation is instead hidden by the portal scene and its final
world fade while the normal motion/update stream settles. Therefore acdream
@ -227,6 +274,11 @@ must not add an arrival-only animation reset.
- During tunnel states the portal scene replaces world pixels before retained
UI draws. The black fade also draws before retained UI. Input dispatch and
`RetailUiRuntime.Tick` continue unchanged.
- The projection owner captures the active camera's M22 view-plane distance at
teleport begin and applies `Render::set_vdst` semantics to both the tunnel
and world viewport during the four fade states.
- Destination placement resets the retail chase camera's published and sought
positions to the player before the normal update path resumes.
- Destination residency remains acdream's asynchronous adaptation. It supplies
the same `EndTeleportAnimation` edge retail receives when its blocking cell
load completes; it does not alter presentation ordering.