// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. #if USE_LARGE_WORLDS using Real = System.Double; #else using Real = System.Single; #endif using FlaxEditor.Gizmo; using FlaxEngine; namespace FlaxEditor.Viewport.Cameras { /// /// Implementation of that simulated the first-person camera which can fly though the scene. /// /// [HideInEditor] public class FPSCamera : ViewportCamera { private Transform _startMove; private Transform _endMove; private float _moveStartTime = -1; /// /// Gets a value indicating whether this viewport is animating movement. /// public bool IsAnimatingMove => _moveStartTime > Mathf.Epsilon; /// /// The target point location. It's used to orbit around it when user clicks Alt+LMB. /// public Vector3 TargetPoint = new Vector3(-200); /// /// Sets view. /// /// The view position. /// The view direction. public void SetView(Vector3 position, Vector3 direction) { if (IsAnimatingMove) return; // Rotate and move Viewport.ViewPosition = position; Viewport.ViewDirection = direction; } /// /// Sets view. /// /// The view position. /// The view rotation. public void SetView(Vector3 position, Quaternion orientation) { if (IsAnimatingMove) return; // Rotate and move Viewport.ViewPosition = position; Viewport.ViewOrientation = orientation; } /// /// Start animating viewport movement to the target transformation. /// /// The target position. /// The target orientation. public void MoveViewport(Vector3 position, Quaternion orientation) { MoveViewport(new Transform(position, orientation)); } /// /// Start animating viewport movement to the target transformation. /// /// The target transform. public void MoveViewport(Transform target) { _startMove = Viewport.ViewTransform; _endMove = target; _moveStartTime = Time.UnscaledGameTime; } /// public override void ShowSphere(ref BoundingSphere sphere, ref Quaternion orientation) { Vector3 position; if (Viewport.UseOrthographicProjection) { position = sphere.Center + Vector3.Backward * orientation * (sphere.Radius * 5.0f); Viewport.OrthographicScale = (float)Vector3.Distance(position, sphere.Center) / 1000; } else { // calculate the min. distance so that the sphere fits roughly 70% in FOV // clip to far plane as a disappearing big object might be confusing var distance = Mathf.Min(1.4f * sphere.Radius / Mathf.Tan(Mathf.DegreesToRadians * Viewport.FieldOfView / 2), Viewport.FarPlane); position = sphere.Center - Vector3.Forward * orientation * distance; } TargetPoint = sphere.Center; MoveViewport(position, orientation); } /// public override void SetArcBallView(Quaternion orientation, Vector3 orbitCenter, Real orbitRadius) { base.SetArcBallView(orientation, orbitCenter, orbitRadius); TargetPoint = orbitCenter; } /// public override void Update(float deltaTime) { // Update animated movement if (IsAnimatingMove) { // Calculate linear progress float animationDuration = 0.5f; float time = Time.UnscaledGameTime; float progress = (time - _moveStartTime) / animationDuration; // Check for end if (progress >= 1.0f) { // Animation has been finished _moveStartTime = -1; } // Animate camera try { float a = Mathf.Saturate(progress); a = a * a * a; var targetTransform = Transform.Lerp(_startMove, _endMove, a); if (progress >= 1.0f) targetTransform = _endMove; // Be precise targetTransform.Scale = Vector3.Zero; Viewport.ViewPosition = targetTransform.Translation; Viewport.ViewOrientation = targetTransform.Orientation; } catch { // Fix camera if lerp failed (eg. large world with NaNs inside) Viewport.ViewPosition = Vector3.Zero; Viewport.ViewOrientation = Quaternion.Identity; } } } /// public override void UpdateView(float dt, ref Vector3 moveDelta, ref Float2 mouseDelta, out bool centerMouse) { centerMouse = true; if (IsAnimatingMove) return; Viewport.GetInput(out var input); Viewport.GetPrevInput(out var prevInput); var transformGizmo = (Viewport as EditorGizmoViewport)?.Gizmos.Active as TransformGizmoBase; var isUsingGizmo = transformGizmo != null && transformGizmo.ActiveAxis != TransformGizmoBase.Axis.None; // Get current view properties var yaw = Viewport.Yaw; var pitch = Viewport.Pitch; var position = Viewport.ViewPosition; var rotation = Viewport.ViewOrientation; // Compute base vectors for camera movement var forward = Vector3.Forward * rotation; var up = Vector3.Up * rotation; var right = Vector3.Cross(forward, up); // Dolly if (input.IsPanning || input.IsMoving || input.IsRotating) { Vector3.Transform(ref moveDelta, ref rotation, out Vector3 move); position += move; } // Pan if (input.IsPanning) { var panningSpeed = (Viewport.RelativePanning) ? Mathf.Abs((position - TargetPoint).Length) * 0.005f : Viewport.PanningSpeed; if (Viewport.InvertPanning) { position += up * (mouseDelta.Y * panningSpeed); position += right * (mouseDelta.X * panningSpeed); } else { position -= right * (mouseDelta.X * panningSpeed); position -= up * (mouseDelta.Y * panningSpeed); } } // Move if (input.IsMoving) { // Move camera over XZ plane var projectedForward = Vector3.Normalize(new Vector3(forward.X, 0, forward.Z)); position -= projectedForward * mouseDelta.Y; yaw += mouseDelta.X; } // Rotate or orbit if (input.IsRotating || (input.IsOrbiting && !isUsingGizmo && prevInput.IsOrbiting)) { yaw += mouseDelta.X; pitch += mouseDelta.Y; } // Zoom in/out if (input.IsZooming && !input.IsRotating) { position += forward * (Viewport.MouseWheelZoomSpeedFactor * input.MouseWheelDelta * 25.0f); if (input.IsAltDown) { position += forward * (Viewport.MouseSpeed * 40 * Viewport.MousePositionDelta.ValuesSum); } } // Move camera with the gizmo if (input.IsOrbiting && isUsingGizmo) { centerMouse = false; Viewport.ViewPosition += transformGizmo.LastDelta.Translation; return; } // Update view Viewport.Yaw = yaw; Viewport.Pitch = pitch; if (input.IsOrbiting) { float orbitRadius = Mathf.Max((float)Vector3.Distance(ref position, ref TargetPoint), 0.0001f); Vector3 localPosition = Viewport.ViewDirection * (-1 * orbitRadius); Viewport.ViewPosition = TargetPoint + localPosition; } else { TargetPoint += position - Viewport.ViewPosition; Viewport.ViewPosition = position; } } } }