using BansheeEngine; namespace BansheeEditor { /// /// Handles camera movement in the scene view. /// [RunInEditor] internal sealed class SceneCamera : Component { public const string MoveForwardBinding = "SceneForward"; public const string MoveLeftBinding = "SceneLeft"; public const string MoveRightBinding = "SceneRight"; public const string MoveBackBinding = "SceneBackward"; public const string FastMoveBinding = "SceneFastMove"; public const string RotateBinding = "SceneRotate"; public const string HorizontalAxisBinding = "SceneHorizontal"; public const string VerticalAxisBinding = "SceneVertical"; 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 RotationalSpeed = 360.0f; // Degrees/second private readonly Degree FieldOfView = 90.0f; private VirtualButton moveForwardBtn; private VirtualButton moveLeftBtn; private VirtualButton moveRightBtn; private VirtualButton moveBackwardBtn; private VirtualButton fastMoveBtn; private VirtualButton activeBtn; private VirtualAxis horizontalAxis; private VirtualAxis verticalAxis; private float currentSpeed; private Degree yaw; private Degree pitch; private bool lastButtonState; 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; private void OnReset() { camera = SceneObject.GetComponent(); moveForwardBtn = new VirtualButton(MoveForwardBinding); moveLeftBtn = new VirtualButton(MoveLeftBinding); moveRightBtn = new VirtualButton(MoveRightBinding); moveBackwardBtn = new VirtualButton(MoveBackBinding); fastMoveBtn = new VirtualButton(FastMoveBinding); activeBtn = new VirtualButton(RotateBinding); horizontalAxis = new VirtualAxis(HorizontalAxisBinding); verticalAxis = new VirtualAxis(VerticalAxisBinding); } private void OnUpdate() { bool goingForward = VirtualInput.IsButtonHeld(moveForwardBtn); bool goingBack = VirtualInput.IsButtonHeld(moveBackwardBtn); bool goingLeft = VirtualInput.IsButtonHeld(moveLeftBtn); bool goingRight = VirtualInput.IsButtonHeld(moveRightBtn); bool fastMove = VirtualInput.IsButtonHeld(fastMoveBtn); bool camActive = VirtualInput.IsButtonHeld(activeBtn); if (camActive != lastButtonState) { if (camActive) Cursor.Hide(); else Cursor.Show(); lastButtonState = camActive; } float frameDelta = Time.FrameDelta; if (camActive) { float horzValue = VirtualInput.GetAxisValue(horizontalAxis); float vertValue = VirtualInput.GetAxisValue(verticalAxis); yaw += new Degree(horzValue * RotationalSpeed * frameDelta); pitch += new Degree(vertValue * RotationalSpeed * frameDelta); 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; 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 (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); } } UpdateAnim(); } /// /// Enables or disables camera controls. /// /// True to enable controls, false to disable. public void EnableInput(bool enable) { if (inputEnabled == enable) return; inputEnabled = enable; if (!inputEnabled) { if (VirtualInput.IsButtonHeld(activeBtn)) Cursor.Show(); } } /// /// 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); } } /// /// 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 = eulerAngles.x; yaw = eulerAngles.y; Degree FOV = (1.0f - animation.State.OrtographicPct)*FieldOfView; if (FOV < 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.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; } }; } }