fix(camera): complete retail in-head adjustment behavior
This commit is contained in:
parent
11e68aad82
commit
4d0379744b
2 changed files with 86 additions and 7 deletions
|
|
@ -36,6 +36,8 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
private const float RetailFirstPersonForward = 0.18f;
|
private const float RetailFirstPersonForward = 0.18f;
|
||||||
private const float RetailLeaveHeadBack = 0.6f;
|
private const float RetailLeaveHeadBack = 0.6f;
|
||||||
private const float RetailLeaveHeadUp = 0.5f;
|
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
|
// CameraSet::{Farther,Closer,Raise,Lower,Rotate} constants. The named
|
||||||
// retail decomp carries CAMERA_MOUSELOOK_INC at 0x0079BC04 and the
|
// 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.
|
// Keep in View snaps the camera behind the target and disables RMB orbit.
|
||||||
float viewerYawOffset = trackedHeading.HasValue ? YawOffset : 0f;
|
float viewerYawOffset = trackedHeading.HasValue ? YawOffset : 0f;
|
||||||
(Vector3 targetEye, Vector3 targetForward) = _inHead
|
(Vector3 targetEye, Vector3 targetForward) = _inHead
|
||||||
? ComputeInHeadPose(pivotWorld, heading)
|
? ComputeInHeadPose(pivotWorld, heading, _targetDirectionLocal)
|
||||||
: _targetDirectionLocal is { } localDirection
|
: _targetDirectionLocal is { } localDirection
|
||||||
? ComputeTargetDirectionPose(
|
? ComputeTargetDirectionPose(
|
||||||
pivotWorld, heading, Distance, Pitch, localDirection)
|
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
|
// Farther's exact in-head branch replaces (0,+0.18,0) with
|
||||||
// (0,-0.6,+0.5) and returns without applying the ordinary scale.
|
// (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;
|
_inHead = false;
|
||||||
|
_targetDirectionLocal = null;
|
||||||
SetViewerOffset(RetailLeaveHeadBack, RetailLeaveHeadUp);
|
SetViewerOffset(RetailLeaveHeadBack, RetailLeaveHeadUp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -366,8 +371,16 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
|
|
||||||
if (_inHead)
|
if (_inHead)
|
||||||
{
|
{
|
||||||
_inHead = false;
|
// CameraSet::{Raise,Lower} modify CameraManager::direction.z by
|
||||||
SetViewerOffset(RetailLeaveHeadBack, RetailLeaveHeadUp);
|
// 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;
|
float candidatePitch = Pitch + adjustment * OffsetAngleRadians;
|
||||||
|
|
@ -674,10 +687,22 @@ public sealed class RetailChaseCamera : ICamera
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static (Vector3 eye, Vector3 forward) ComputeInHeadPose(
|
internal static (Vector3 eye, Vector3 forward) ComputeInHeadPose(
|
||||||
Vector3 pivotWorld,
|
Vector3 pivotWorld,
|
||||||
Vector3 heading)
|
Vector3 heading,
|
||||||
|
Vector3? targetDirectionLocal = null)
|
||||||
{
|
{
|
||||||
Vector3 forward = Vector3.Normalize(heading);
|
Vector3 headingForward = Vector3.Normalize(heading);
|
||||||
return (pivotWorld + forward * RetailFirstPersonForward, forward);
|
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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -618,6 +618,60 @@ public class RetailChaseCameraTests
|
||||||
Assert.Equal(MathF.Sqrt(0.6f * 0.6f + 0.5f * 0.5f), cam.Distance, 5);
|
Assert.Equal(MathF.Sqrt(0.6f * 0.6f + 0.5f * 0.5f), cam.Distance, 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AdjustingCloserInRetailFirstPerson_RefusesNearWriteAndStaysInHead()
|
||||||
|
{
|
||||||
|
var cam = new RetailChaseCamera();
|
||||||
|
cam.SetRetailFirstPersonView();
|
||||||
|
|
||||||
|
cam.AdjustDistance(-1f);
|
||||||
|
|
||||||
|
Assert.True(cam.IsInHead);
|
||||||
|
Assert.Equal(0.18f, cam.Distance, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AdjustPitchInRetailFirstPerson_ChangesDirectionWithoutMovingEye()
|
||||||
|
{
|
||||||
|
var cam = new RetailChaseCamera();
|
||||||
|
cam.SetRetailFirstPersonView();
|
||||||
|
|
||||||
|
cam.AdjustPitch(+1f);
|
||||||
|
cam.Update(
|
||||||
|
playerPosition: Vector3.Zero,
|
||||||
|
playerYaw: 0f,
|
||||||
|
playerVelocity: Vector3.Zero,
|
||||||
|
isOnGround: true,
|
||||||
|
contactPlaneNormal: Vector3.UnitZ,
|
||||||
|
dt: 1f / 60f);
|
||||||
|
|
||||||
|
Assert.True(cam.IsInHead);
|
||||||
|
Assert.Equal(new Vector3(0.18f, 0f, 1.5f), cam.Position);
|
||||||
|
Vector3 forward = Vector3.Normalize(new Vector3(
|
||||||
|
-cam.View.M13,
|
||||||
|
-cam.View.M23,
|
||||||
|
-cam.View.M33));
|
||||||
|
Assert.True(forward.Z > 0f);
|
||||||
|
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
cam.AdjustPitch(+1f);
|
||||||
|
cam.Update(
|
||||||
|
playerPosition: Vector3.Zero,
|
||||||
|
playerYaw: 0f,
|
||||||
|
playerVelocity: Vector3.Zero,
|
||||||
|
isOnGround: true,
|
||||||
|
contactPlaneNormal: Vector3.UnitZ,
|
||||||
|
dt: 1f);
|
||||||
|
forward = Vector3.Normalize(new Vector3(
|
||||||
|
-cam.View.M13,
|
||||||
|
-cam.View.M23,
|
||||||
|
-cam.View.M33));
|
||||||
|
Assert.Equal(
|
||||||
|
0.8f / MathF.Sqrt(1f + 0.8f * 0.8f),
|
||||||
|
forward.Z,
|
||||||
|
5);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AdjustPitch_UsesRetailEightDegreeRotationAndPreservesLength()
|
public void AdjustPitch_UsesRetailEightDegreeRotationAndPreservesLength()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue