//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using BansheeEngine; namespace BansheeEditor { /** @addtogroup Scene-Editor * @{ */ /// /// Handles camera movement in the scene view. /// [RunInEditor] internal sealed class SceneCamera : ManagedComponent { #region Constants public const string MoveForwardBinding = "SceneForward"; public const string MoveLeftBinding = "SceneLeft"; public const string MoveRightBinding = "SceneRight"; public const string MoveBackBinding = "SceneBackward"; public const string MoveUpBinding = "SceneUp"; public const string MoveDownBinding = "SceneDown"; public const string FastMoveBinding = "SceneFastMove"; public const string PanBinding = "ScenePan"; public const string RotateBinding = "SceneRotate"; public const string HorizontalAxisBinding = "SceneHorizontal"; public const string VerticalAxisBinding = "SceneVertical"; public const string ScrollAxisBinding = "SceneScroll"; private const float StartSpeed = 4.0f; private const float TopSpeed = 12.0f; private const float Acceleration = 1.0f; private const float FastModeMultiplier = 2.0f; private const float PanSpeed = 3.0f; private const float ScrollSpeed = 3.0f; private const float RotationalSpeed = 3.0f; private readonly Degree FieldOfView = (Degree)90.0f; #endregion #region Fields private VirtualButton moveForwardBtn; private VirtualButton moveLeftBtn; private VirtualButton moveRightBtn; private VirtualButton moveBackwardBtn; private VirtualButton moveUpBtn; private VirtualButton moveDownBtn; private VirtualButton fastMoveBtn; private VirtualButton activeBtn; private VirtualButton panBtn; private VirtualAxis horizontalAxis; private VirtualAxis verticalAxis; private VirtualAxis scrollAxis; private float currentSpeed; private Degree yaw; private Degree pitch; private bool lastHideCursorState; private Camera camera; private bool inputEnabled = true; // Animating camera transitions private CameraAnimation animation = new CameraAnimation(); private float frustumWidth = 50.0f; private float lerp; private bool isAnimating; #endregion #region Public properties /// /// Type of projection used by camera for rendering the scene. /// public ProjectionType ProjectionType { get { return camera.ProjectionType; } set { if (camera.ProjectionType != value) { CameraState state = new CameraState(); state.Position = camera.SceneObject.Position; state.Rotation = camera.SceneObject.Rotation; state.Ortographic = value == ProjectionType.Orthographic; state.FrustumWidth = frustumWidth; SetState(state); } } } #endregion #region Public methods /// /// Enables or disables camera controls. /// /// True to enable controls, false to disable. public void EnableInput(bool enable) { inputEnabled = enable; } /// /// Focuses the camera on the currently selected object(s). /// public void FrameSelected() { SceneObject[] selectedObjects = Selection.SceneObjects; if (selectedObjects.Length > 0) { AABox box = EditorUtility.CalculateBounds(Selection.SceneObjects); FrameBounds(box); } } /// /// Orients the camera so it looks along the provided axis. /// public void LookAlong(Vector3 axis) { Vector3 up = Vector3.YAxis; if (MathEx.Abs(Vector3.Dot(axis, up)) > 0.9f) up = Vector3.ZAxis; CameraState state = new CameraState(); state.Position = camera.SceneObject.Position; state.Rotation = Quaternion.LookRotation(axis, up); state.Ortographic = camera.ProjectionType == ProjectionType.Orthographic; state.FrustumWidth = frustumWidth; SetState(state); } #endregion #region Private methods private void OnReset() { camera = SceneObject.GetComponent(); moveForwardBtn = new VirtualButton(MoveForwardBinding); moveLeftBtn = new VirtualButton(MoveLeftBinding); moveRightBtn = new VirtualButton(MoveRightBinding); moveBackwardBtn = new VirtualButton(MoveBackBinding); moveUpBtn = new VirtualButton(MoveUpBinding); moveDownBtn = new VirtualButton(MoveDownBinding); fastMoveBtn = new VirtualButton(FastMoveBinding); activeBtn = new VirtualButton(RotateBinding); panBtn = new VirtualButton(PanBinding); horizontalAxis = new VirtualAxis(HorizontalAxisBinding); verticalAxis = new VirtualAxis(VerticalAxisBinding); scrollAxis = new VirtualAxis(ScrollAxisBinding); } private void OnUpdate() { bool isOrtographic = camera.ProjectionType == ProjectionType.Orthographic; if (inputEnabled) { bool goingForward = VirtualInput.IsButtonHeld(moveForwardBtn); bool goingBack = VirtualInput.IsButtonHeld(moveBackwardBtn); bool goingLeft = VirtualInput.IsButtonHeld(moveLeftBtn); bool goingRight = VirtualInput.IsButtonHeld(moveRightBtn); bool goingUp = VirtualInput.IsButtonHeld(moveUpBtn); bool goingDown = VirtualInput.IsButtonHeld(moveDownBtn); bool fastMove = VirtualInput.IsButtonHeld(fastMoveBtn); bool camActive = VirtualInput.IsButtonHeld(activeBtn); bool isPanning = VirtualInput.IsButtonHeld(panBtn); bool hideCursor = camActive || isPanning; if (hideCursor != lastHideCursorState) { if (hideCursor) { Cursor.Hide(); Rect2I clipRect; clipRect.x = Input.PointerPosition.x - 2; clipRect.y = Input.PointerPosition.y - 2; clipRect.width = 4; clipRect.height = 4; Cursor.ClipToRect(clipRect); } else { Cursor.Show(); Cursor.ClipDisable(); } lastHideCursorState = hideCursor; } float frameDelta = Time.FrameDelta; if (camActive) { float horzValue = VirtualInput.GetAxisValue(horizontalAxis); float vertValue = VirtualInput.GetAxisValue(verticalAxis); float rotationAmount = RotationalSpeed * EditorSettings.MouseSensitivity; yaw += new Degree(horzValue * rotationAmount); pitch += new Degree(vertValue * rotationAmount); yaw = MathEx.WrapAngle(yaw); pitch = MathEx.WrapAngle(pitch); Quaternion yRot = Quaternion.FromAxisAngle(Vector3.YAxis, yaw); Quaternion xRot = Quaternion.FromAxisAngle(Vector3.XAxis, pitch); Quaternion camRot = yRot*xRot; camRot.Normalize(); SceneObject.Rotation = camRot; // Handle movement using movement keys Vector3 direction = Vector3.Zero; if (goingForward) direction += SceneObject.Forward; if (goingBack) direction -= SceneObject.Forward; if (goingRight) direction += SceneObject.Right; if (goingLeft) direction -= SceneObject.Right; if (goingUp) direction += SceneObject.Up; if (goingDown) direction -= SceneObject.Up; if (direction.SqrdLength != 0) { direction.Normalize(); float multiplier = 1.0f; if (fastMove) multiplier = FastModeMultiplier; currentSpeed = MathEx.Clamp(currentSpeed + Acceleration*frameDelta, StartSpeed, TopSpeed); currentSpeed *= multiplier; } else { currentSpeed = 0.0f; } const float tooSmall = 0.0001f; if (currentSpeed > tooSmall) { Vector3 velocity = direction*currentSpeed; SceneObject.Move(velocity*frameDelta); } } // Pan if (isPanning) { float horzValue = VirtualInput.GetAxisValue(horizontalAxis); float vertValue = VirtualInput.GetAxisValue(verticalAxis); Vector3 direction = new Vector3(horzValue, -vertValue, 0.0f); direction = camera.SceneObject.Rotation.Rotate(direction); SceneObject.Move(direction*PanSpeed*frameDelta); } } else { if (lastHideCursorState) { Cursor.Show(); Cursor.ClipDisable(); lastHideCursorState = false; } } SceneWindow sceneWindow = EditorWindow.GetWindow(); if ((sceneWindow.Active && sceneWindow.HasFocus) || sceneWindow.IsPointerHovering) { Rect2I bounds = sceneWindow.Bounds; // Move using scroll wheel if (bounds.Contains(Input.PointerPosition)) { float scrollAmount = VirtualInput.GetAxisValue(scrollAxis); if (!isOrtographic) { SceneObject.Move(SceneObject.Forward*scrollAmount*ScrollSpeed); } else { float orthoHeight = MathEx.Max(1.0f, camera.OrthoHeight - scrollAmount); camera.OrthoHeight = orthoHeight; } } } UpdateAnim(); } /// /// Moves and orients a camera so that the provided bounds end covering the camera's viewport. /// /// Bounds to frame in camera's view. /// Amount of padding to leave on the borders of the viewport, in percent [0, 1]. private void FrameBounds(AABox bounds, float padding = 0.0f) { // TODO - Use AABox bounds directly instead of a sphere to be more accurate float worldWidth = bounds.Size.Length; float worldHeight = worldWidth; if (worldWidth == 0.0f) worldWidth = 1.0f; if (worldHeight == 0.0f) worldHeight = 1.0f; float boundsAspect = worldWidth / worldHeight; float paddingScale = MathEx.Clamp01(padding) + 1.0f; float frustumWidth; // If camera has wider aspect than bounds then height will be the limiting dimension if (camera.AspectRatio > boundsAspect) frustumWidth = worldHeight * camera.AspectRatio * paddingScale; else // Otherwise width frustumWidth = worldWidth * paddingScale; float distance = CalcDistanceForFrustumWidth(frustumWidth); Vector3 forward = bounds.Center - SceneObject.Position; forward.Normalize(); CameraState state = new CameraState(); state.Position = bounds.Center - forward * distance; state.Rotation = Quaternion.LookRotation(forward, Vector3.YAxis); state.Ortographic = camera.ProjectionType == ProjectionType.Orthographic; state.FrustumWidth = frustumWidth; SetState(state); } /// /// Changes the state of the camera, either instantly or animated over several frames. The state includes /// camera position, rotation, type and possibly other parameters. /// /// New state of the camera. /// Should the state be linearly interpolated over a course of several frames. private void SetState(CameraState state, bool animated = true) { CameraState startState = new CameraState(); startState.Position = SceneObject.Position; startState.Rotation = SceneObject.Rotation; startState.Ortographic = camera.ProjectionType == ProjectionType.Orthographic; startState.FrustumWidth = frustumWidth; animation.Start(startState, state); if (!animated) { ApplyState(1.0f); isAnimating = false; } else { isAnimating = true; lerp = 0.0f; } } /// /// Applies the animation target state depending on the interpolation parameter. . /// /// Interpolation parameter ranging [0, 1] that interpolated between the start state and the /// target state. private void ApplyState(float t) { animation.Update(t); SceneObject.Position = animation.State.Position; SceneObject.Rotation = animation.State.Rotation; frustumWidth = animation.State.FrustumWidth; Vector3 eulerAngles = SceneObject.Rotation.ToEuler(); pitch = (Degree)eulerAngles.x; yaw = (Degree)eulerAngles.y; Degree FOV = (Degree)(1.0f - animation.State.OrtographicPct)*FieldOfView; if (FOV < (Degree)5.0f) { camera.ProjectionType = ProjectionType.Orthographic; camera.OrthoHeight = frustumWidth * 0.5f / camera.AspectRatio; } else { camera.ProjectionType = ProjectionType.Perspective; camera.FieldOfView = FOV; } // Note: Consider having a global setting for near/far planes as changing it here might confuse the user float distance = CalcDistanceForFrustumWidth(frustumWidth); if (distance < 1) { camera.NearClipPlane = 0.005f; camera.FarClipPlane = 1000f; } if (distance < 100) { camera.NearClipPlane = 0.05f; camera.FarClipPlane = 2500f; } else if (distance < 1000) { camera.NearClipPlane = 0.5f; camera.FarClipPlane = 10000f; } else { camera.NearClipPlane = 5.0f; camera.FarClipPlane = 1000000f; } } /// /// Calculates distance at which the camera's frustum width is equal to the provided width. /// /// Frustum width to find the distance for, in world units. /// Distance at which the camera's frustum is the specified width, in world units. private float CalcDistanceForFrustumWidth(float frustumWidth) { if (camera.ProjectionType == ProjectionType.Perspective) return (frustumWidth*0.5f)/MathEx.Tan(camera.FieldOfView*0.5f); else return frustumWidth * 2.0f; } /// /// Updates camera state transition animation. Should be called every frame. /// private void UpdateAnim() { if (!isAnimating) return; const float ANIM_TIME = 0.5f; // 0.5f seconds lerp += Time.FrameDelta * (1.0f / ANIM_TIME); if (lerp >= 1.0f) { lerp = 1.0f; isAnimating = false; } ApplyState(lerp); } /// /// Contains data for a possible camera state. Camera states can be interpolated between each other as needed. /// private struct CameraState { private float _ortographic; public Vector3 Position { get; set; } public Quaternion Rotation { get; set; } public float FrustumWidth { get; set; } public bool Ortographic { get { return _ortographic > 0.5; } set { _ortographic = value ? 1.0f : 0.0f; } } public float OrtographicPct { get { return _ortographic; } set { _ortographic = value; } } } /// /// Helper class that performs linear interpolation between two camera states. /// private struct CameraAnimation { private CameraState start; private CameraState target; private CameraState interpolated; /// /// Returns currently interpolated animation state. /// public CameraState State { get { return interpolated; } } /// /// Initializes the animation with initial and target states. /// /// Initial state to animate from. /// Target state to animate towards. public void Start(CameraState start, CameraState target) { this.start = start; this.target = target; } /// /// Updates the animation by interpolating between the start and target states. /// /// Interpolation parameter in range [0, 1] that determines how much to interpolate between /// start and target states. public void Update(float t) { interpolated = new CameraState(); interpolated.Position = start.Position * (1.0f - t) + target.Position * t; interpolated.Rotation = Quaternion.Slerp(start.Rotation, target.Rotation, t); interpolated.OrtographicPct = start.OrtographicPct * (1.0f - t) + target.OrtographicPct * t; interpolated.FrustumWidth = start.FrustumWidth * (1.0f - t) + target.FrustumWidth * t; } }; #endregion } /** @} */ }