Retail's world-camera FOV is not a constant: the applied vertical FOV is m_fGameFOV / (viewportAspect - 0.1), recomputed on every aspect or game-FOV change (CreatureMode smartbox sites 0x00452b2f/0x00453b14), gated by Render::SetFOVRad's open (0, pi) acceptance (0x0054b2d0 - rejected results keep the previous FOV). m_fGameFOV defaults to pi/2 = 90 degrees (0x00454649) and is what the Field of View option sets in degrees (0x00451e6a; registered range [10,160] default 90 - gmClient::InitUIPreferences @0x004035b0). Net effect: the horizontal view stays ~85-90 degrees across aspect ratios; wide screens trim the vertical slice instead of ballooning the sides. acdream hardcoded FovY = pi/3 = 60 degrees on all four world cameras, aspect-independent, and the Config slider wrote raw vertical-FOV degrees. New: RetailFieldOfView (the law + gate, decomp-cited), CameraController.GameFovRadians + SetGameFov + one ApplyProjection chokepoint recomputing every camera on SetAspect/SetGameFov/ EnterChaseMode/RestoreState; ApplyFieldOfView now feeds the law; DisplaySettings.Default.FieldOfView 60 -> 90 (the retail registered default; the stored number changed MEANING with this commit). The same seam closes a second latent bug the 2026-08-13 "squished" gate report exposed: SetAspect only ever updated Orbit/Fly - the CHASE cameras (the ones the player looks through) kept their creation-time aspect across every mid-session resize, drawing the world at the old shape stretched onto the new viewport. The paperdoll camera stays outside the law by design (retail portrait mode is UseSharpMode, not smartbox - DollCamera's own doc). Tests: RetailFieldOfViewTests (golden law values at 4:3/16:9/21:9, the constant-horizontal property, the rejection gate, controller propagation incl. chase attach/restore + rejected-law aspect-still-propagates); DisplaySettingsTests + RuntimeSettingsControllerTests updated to the new semantics. App suite 4,953/3 skips; UI.Abstractions 916/0. AD-89 retired in this commit; user settings.json migrated 60->90 by hand (stale pre-port default). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
81 lines
3 KiB
C#
81 lines
3 KiB
C#
// src/AcDream.App/Rendering/FlyCamera.cs
|
|
using System.Numerics;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
public sealed class FlyCamera : ICamera
|
|
{
|
|
public Vector3 Position { get; set; } = new(96, 96, 150);
|
|
public float Yaw { get; set; } = MathF.PI / 2f; // facing +Y
|
|
public float Pitch { get; set; } = -0.3f; // looking slightly down
|
|
// #389: smartbox law at the 16:9 default aspect — see RetailFieldOfView.
|
|
public float FovY { get; set; } = RetailFieldOfView.DefaultAppliedFovY;
|
|
public float Aspect { get; set; } = 16f / 9f;
|
|
|
|
/// <summary>
|
|
/// Default move speed in world units per second. AC's character walks
|
|
/// at ~4 u/s and runs at ~7 u/s; 12 here is comfortable for navigating
|
|
/// scenery without overshooting and roughly tracks a fast jog. Hold
|
|
/// shift while moving to enter "boost" mode at <see cref="BoostSpeed"/>.
|
|
/// </summary>
|
|
public float MoveSpeed { get; set; } = 12f;
|
|
public float BoostSpeed { get; set; } = 50f;
|
|
public float MouseSensitivity { get; set; } = 0.003f;
|
|
|
|
private const float PitchLimit = 1.5533f; // ~89 degrees
|
|
|
|
public Matrix4x4 View
|
|
{
|
|
get
|
|
{
|
|
var forward = Forward();
|
|
return Matrix4x4.CreateLookAt(Position, Position + forward, Vector3.UnitZ);
|
|
}
|
|
}
|
|
|
|
// Near plane 0.1 m = retail Render::znear (see RetailChaseCamera.Projection).
|
|
public Matrix4x4 Projection
|
|
=> Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 0.1f, 5000f);
|
|
|
|
/// <summary>
|
|
/// Integrate position for one frame based on WASD + vertical keys.
|
|
/// W/S move forward/back in the horizontal plane (ignoring pitch).
|
|
/// A/D strafe left/right. Up/down translate along world Z.
|
|
/// </summary>
|
|
public void Update(double dt, bool w, bool a, bool s, bool d, bool up, bool down, bool boost = false)
|
|
{
|
|
float speed = boost ? BoostSpeed : MoveSpeed;
|
|
float step = (float)(speed * dt);
|
|
|
|
// Forward in the horizontal plane (ignore pitch so W doesn't dive into ground).
|
|
var flatForward = new Vector3(MathF.Cos(Yaw), MathF.Sin(Yaw), 0f);
|
|
var right = new Vector3(MathF.Sin(Yaw), -MathF.Cos(Yaw), 0f);
|
|
|
|
if (w) Position += flatForward * step;
|
|
if (s) Position -= flatForward * step;
|
|
if (a) Position -= right * step;
|
|
if (d) Position += right * step;
|
|
if (up) Position += Vector3.UnitZ * step;
|
|
if (down) Position -= Vector3.UnitZ * step;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply accumulated mouse deltas (pixels since last frame). Positive deltaX
|
|
/// rotates the view to the right (decreases yaw), positive deltaY rotates
|
|
/// down (decreases pitch).
|
|
/// </summary>
|
|
public void Look(float deltaX, float deltaY)
|
|
{
|
|
Yaw -= deltaX * MouseSensitivity;
|
|
Pitch = Math.Clamp(Pitch - deltaY * MouseSensitivity, -PitchLimit, PitchLimit);
|
|
}
|
|
|
|
private Vector3 Forward()
|
|
{
|
|
float cp = MathF.Cos(Pitch);
|
|
return new Vector3(
|
|
cp * MathF.Cos(Yaw),
|
|
cp * MathF.Sin(Yaw),
|
|
MathF.Sin(Pitch));
|
|
}
|
|
}
|