//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using bs;
namespace bs.Editor
{
/** @addtogroup Scene-Editor
* @{
*/
///
/// Handles camera movement in the scene view.
///
[RunInEditor]
internal sealed class SceneCamera : ManagedComponent
{
#region Constants
public const float StartSpeed = 4.0f;
public const float TopSpeed = 12.0f;
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";
public const string ViewSettingsKey = "SceneCamera0_ViewSettings";
public const string MoveSettingsKey = "SceneCamera0_MoveSettings";
public const string RenderSettingsKey = "SceneCamera0_RenderSettings";
#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;
private SceneCameraViewSettings viewSettings;
// Animating camera transitions
private CameraAnimation animation = new CameraAnimation();
private float frustumWidth = 50.0f;
private float lerp;
private bool isAnimating;
#endregion
#region Public properties
///
/// Returns the internal camera component. You should not make modifications to the returned camera.
///
public Camera Camera { get => camera; }
///
/// Counter that increments every frame when the camera transform changes.
///
public ulong UpdateCount { get; private set; }
///
/// Settings for controlling scene camera view.
///
public SceneCameraViewSettings ViewSettings
{
get => viewSettings;
set
{
viewSettings = value;
camera.ProjectionType = value.projectionType;
camera.NearClipPlane = value.nearClipPlane;
camera.FarClipPlane = value.farClipPlane;
camera.OrthoHeight = value.orthographicSize;
camera.FieldOfView = value.fieldOfView;
camera.Viewport.ClearColor = value.backgroundColor;
NotifyNeedsRedraw();
}
}
///
/// Settings for controlling scene camera movement.
///
public SceneCameraMoveSettings MoveSettings { get; set; }
///
/// Options for controlling scene camera rendering.
///
public RenderSettings RenderSettings
{
get => camera.RenderSettings;
set => camera.RenderSettings = value;
}
#endregion
#region Public methods
///
/// Changes the scene camera projection type and animates the transition.
///
/// New projection type.
public void ChangeProjectionType(ProjectionType projectionType)
{
if (camera.ProjectionType != projectionType)
{
CameraState state = new CameraState();
state.Position = camera.SceneObject.Position;
state.Rotation = camera.SceneObject.Rotation;
state.Orthographic = projectionType == ProjectionType.Orthographic;
state.FrustumWidth = frustumWidth;
viewSettings.projectionType = projectionType;
ProjectSettings.SetObject(ViewSettingsKey, viewSettings);
SetState(state);
}
}
///
/// 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.Orthographic = camera.ProjectionType == ProjectionType.Orthographic;
state.FrustumWidth = frustumWidth;
SetState(state);
}
///
/// Notifies the system that the 3D viewport should be redrawn.
///
internal void NotifyNeedsRedraw()
{
camera.NotifyNeedsRedraw();
}
///
/// Enables or disables on-demand drawing. When enabled the 3D viewport will only be redrawn when
/// is called. If disabled the viewport will be redrawn every frame.
/// Normally you always want to keep this disabled unless you know the viewport will require updates
/// every frame (e.g. when a game is running, or when previewing animations).
///
/// True to enable on-demand drawing, false otherwise.
internal void ToggleOnDemandDrawing(bool enabled)
{
camera.Flags = enabled ? CameraFlag.OnDemand : new CameraFlag();
}
#endregion
#region Private methods
private void OnReset()
{
camera = SceneObject.GetComponent();
ViewSettings = ProjectSettings.GetObject(ViewSettingsKey);
MoveSettings = ProjectSettings.GetObject(MoveSettingsKey);
RenderSettings = ProjectSettings.GetObject(RenderSettingsKey);
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);
NotifyNeedsRedraw();
}
private void OnUpdate()
{
bool isOrthographic = camera.ProjectionType == ProjectionType.Orthographic;
float frameDelta = Time.FrameDelta;
bool updated = false;
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;
}
if (camActive)
{
float horzValue = VirtualInput.GetAxisValue(horizontalAxis);
float vertValue = VirtualInput.GetAxisValue(verticalAxis);
float rotationAmount = MoveSettings.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 = MoveSettings.fastModeMultiplier;
currentSpeed = MathEx.Clamp(currentSpeed + MoveSettings.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);
}
updated = true;
}
// 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 * MoveSettings.panSpeed * frameDelta);
updated = true;
}
}
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(scrollAmount != 0)
{
if (!isOrthographic)
{
SceneObject.Move(SceneObject.Forward * scrollAmount * MoveSettings.scrollSpeed * frameDelta);
}
else
{
float oldOrthoHeight = camera.OrthoHeight;
float orthoHeight = MathEx.Max(1.0f, oldOrthoHeight - scrollAmount * frameDelta);
if (oldOrthoHeight != orthoHeight)
camera.OrthoHeight = orthoHeight;
}
updated = true;
}
}
}
UpdateAnim();
if (updated)
{
NotifyNeedsRedraw();
UpdateCount++;
}
}
///
/// 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();
GetNearFarForDistance(distance, out var near, out var far);
viewSettings.nearClipPlane = near;
viewSettings.farClipPlane = far;
ProjectSettings.SetObject(ViewSettingsKey, viewSettings);
CameraState state = new CameraState();
state.Position = bounds.Center - forward * distance;
state.Rotation = Quaternion.LookRotation(forward, Vector3.YAxis);
state.Orthographic = 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.Orthographic = 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.OrthographicPct) * viewSettings.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);
GetNearFarForDistance(distance, out var near, out var far);
camera.NearClipPlane = near;
camera.FarClipPlane = far;
NotifyNeedsRedraw();
UpdateCount++;
}
///
/// 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);
}
///
/// Calculates the camera near and far plane distances used for looking at an object certain distance from the
/// camera.
///
/// Distance of the object from the camera.
/// Output near plane distance.
/// Output far plane distance.
private static void GetNearFarForDistance(float distance, out float near, out float far)
{
if (distance < 1)
{
near = 0.005f;
far = 1000f;
}
if (distance < 100)
{
near = 0.05f;
far = 2500f;
}
else if (distance < 1000)
{
near = 0.5f;
far = 10000f;
}
else
{
near = 5.0f;
far = 1000000f;
}
}
///
/// Contains data for a possible camera state. Camera states can be interpolated between each other as needed.
///
private struct CameraState
{
private float _orthographic;
public Vector3 Position { get; set; }
public Quaternion Rotation { get; set; }
public float FrustumWidth { get; set; }
public bool Orthographic
{
get { return _orthographic > 0.5; }
set { _orthographic = value ? 1.0f : 0.0f; }
}
public float OrthographicPct
{
get { return _orthographic; }
set { _orthographic = 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.OrthographicPct = start.OrthographicPct * (1.0f - t) + target.OrthographicPct * t;
interpolated.FrustumWidth = start.FrustumWidth * (1.0f - t) + target.FrustumWidth * t;
}
};
#endregion
}
///
/// Contains properties used for controlling scene camera movement.
///
[SerializeObject]
internal class SceneCameraMoveSettings
{
[Range(0.1f, 10.0f)]
public float acceleration = 1.0f;
[Range(1.0f, 10.0f)]
public float fastModeMultiplier = 2.0f;
[Range(0.1f, 10.0f)]
public float panSpeed = 3.0f;
[Range(0.1f, 3.0f)]
public float scrollSpeed = 3.0f;
[Range(0.1f, 10.0f)]
public float rotationalSpeed = 3.0f;
}
///
/// Contains properties used for controlling scene camera view.
///
[SerializeObject]
internal class SceneCameraViewSettings
{
public ProjectionType projectionType = ProjectionType.Perspective;
[Range(10.0f, 170.0f, false)]
public Degree fieldOfView = new Degree(90.0f);
[Range(0.001f, 10000.0f, false)]
public float orthographicSize = 10.0f;
[Range(0.0001f, 10, false)]
public float nearClipPlane = 0.05f;
[Range(1.0f, 100000.0f, false)]
public float farClipPlane = 2500.0f;
public Color backgroundColor = new Color(0.282f, 0.341f, 0.478f);
}
/** @} */
}