using System.Numerics; namespace AcDream.App.Rendering; /// /// Retail portal-space camera from gmSmartBoxUI::PostInit /// (0x004D6D8A) and CreatureMode::SetCameraDirection_Degrees /// (0x00453760). Identity looks along AC +Y with +Z up; the animated /// angle rolls that view around its own +Y forward axis. /// public sealed class PortalTunnelCamera : ICamera { public static readonly Vector3 RetailEye = new(0.24f, -2.7f, 0.88f); public float DirectionDegrees { get; set; } public float FovRadians { get; set; } = MathF.PI / 4f; public float Near { get; set; } = 0.1f; // Render::zfar defaults to 4000 in retail (0x0081EC88). CreatureMode::Render // does not install a private far plane: PrimD3DRender::SetFOVInternal // (0x0059AB40) rebuilds the portal projection with that shared value. public float Far { get; set; } = 4000f; public float Aspect { get; set; } = 1f; /// /// Retail CreatureMode::UseSmartboxFOV reads the active SmartBox /// projection each draw. Recover its vertical field of view and near /// and far planes from that perspective matrix. Retail's CreatureMode /// changes the FOV through the SmartBox path while retaining the renderer's /// shared near/far clip range. /// public void UseSmartBoxFov(Matrix4x4 smartBoxProjection) { float verticalScale = smartBoxProjection.M22; if (!float.IsFinite(verticalScale) || verticalScale <= 0f) return; 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; float far = smartBoxProjection.M33 != -1f ? smartBoxProjection.M43 / (smartBoxProjection.M33 + 1f) : float.NaN; if (float.IsFinite(far) && far > 0f) Far = far; if (float.IsFinite(near) && near > 0f && near < Far) Near = near; } public Matrix4x4 View { get { float radians = DirectionDegrees * (MathF.PI / 180f); // 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); } } public Matrix4x4 Projection => Matrix4x4.CreatePerspectiveFieldOfView( FovRadians, Aspect <= 0f ? 1f : Aspect, Near, Far); }