fix(camera): complete retail in-head adjustment behavior

This commit is contained in:
Erik 2026-08-31 06:18:37 +02:00
parent 11e68aad82
commit 4d0379744b
2 changed files with 86 additions and 7 deletions

View file

@ -36,6 +36,8 @@ public sealed class RetailChaseCamera : ICamera
private const float RetailFirstPersonForward = 0.18f;
private const float RetailLeaveHeadBack = 0.6f;
private const float RetailLeaveHeadUp = 0.5f;
private const float RetailInHeadDirectionStep = 0.200000003f;
private const float RetailInHeadDirectionLimit = 0.800000012f;
// CameraSet::{Farther,Closer,Raise,Lower,Rotate} constants. The named
// retail decomp carries CAMERA_MOUSELOOK_INC at 0x0079BC04 and the
@ -199,7 +201,7 @@ public sealed class RetailChaseCamera : ICamera
// Keep in View snaps the camera behind the target and disables RMB orbit.
float viewerYawOffset = trackedHeading.HasValue ? YawOffset : 0f;
(Vector3 targetEye, Vector3 targetForward) = _inHead
? ComputeInHeadPose(pivotWorld, heading)
? ComputeInHeadPose(pivotWorld, heading, _targetDirectionLocal)
: _targetDirectionLocal is { } localDirection
? ComputeTargetDirectionPose(
pivotWorld, heading, Distance, Pitch, localDirection)
@ -333,9 +335,12 @@ public sealed class RetailChaseCamera : ICamera
// Farther's exact in-head branch replaces (0,+0.18,0) with
// (0,-0.6,+0.5) and returns without applying the ordinary scale.
if (_inHead)
// Closer has no matching escape branch: its scaled 0.18 m offset
// fails the 0.5 m near gate and therefore leaves first-person intact.
if (_inHead && adjustment > 0f)
{
_inHead = false;
_targetDirectionLocal = null;
SetViewerOffset(RetailLeaveHeadBack, RetailLeaveHeadUp);
return;
}
@ -366,8 +371,16 @@ public sealed class RetailChaseCamera : ICamera
if (_inHead)
{
_inHead = false;
SetViewerOffset(RetailLeaveHeadBack, RetailLeaveHeadUp);
// CameraSet::{Raise,Lower} modify CameraManager::direction.z by
// 0.2 while InHead, clamp it to +/-0.8, and return. The viewer
// offset remains the authored +Y*0.18 head position.
Vector3 direction = _targetDirectionLocal ?? Vector3.UnitY;
direction.Z = Math.Clamp(
direction.Z + adjustment * RetailInHeadDirectionStep,
-RetailInHeadDirectionLimit,
RetailInHeadDirectionLimit);
_targetDirectionLocal = direction;
return;
}
float candidatePitch = Pitch + adjustment * OffsetAngleRadians;
@ -674,10 +687,22 @@ public sealed class RetailChaseCamera : ICamera
/// </summary>
internal static (Vector3 eye, Vector3 forward) ComputeInHeadPose(
Vector3 pivotWorld,
Vector3 heading)
Vector3 heading,
Vector3? targetDirectionLocal = null)
{
Vector3 forward = Vector3.Normalize(heading);
return (pivotWorld + forward * RetailFirstPersonForward, forward);
Vector3 headingForward = Vector3.Normalize(heading);
Vector3 targetForward = headingForward;
if (targetDirectionLocal is { } local)
{
var (frameForward, frameRight, frameUp) = BuildBasis(headingForward);
targetForward = Vector3.Normalize(
frameForward * local.Y
- frameRight * local.X
+ frameUp * local.Z);
}
return (
pivotWorld + headingForward * RetailFirstPersonForward,
targetForward);
}
/// <summary>