353 lines
15 KiB
C#
353 lines
15 KiB
C#
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
|
|
|
#if UNITY_EDITOR && UNITY_IMGUI
|
|
|
|
using System;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using static Animancer.Editor.AnimancerGUI;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Animancer.Editor.Previews
|
|
{
|
|
/// <summary>[Editor-Only] Utility for rendering previews of animated objects.</summary>
|
|
/// <remarks>Parts of this class are based on Unity's <see cref="MeshPreview"/>.</remarks>
|
|
/// https://kybernetik.com.au/animancer/api/Animancer.Editor.Previews/AnimancerPreviewRenderer
|
|
[Serializable]
|
|
public class AnimancerPreviewRenderer :
|
|
AnimancerPreviewObject.IEventHandler,
|
|
IDisposable
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Fields and Properties
|
|
/************************************************************************************************************************/
|
|
|
|
[NonSerialized] private PreviewRenderUtility _PreviewRenderUtility;
|
|
[NonSerialized] private Light[] _PreviewLights;
|
|
|
|
[SerializeField] private AnimancerPreviewObject _PreviewObject;
|
|
[SerializeField] private Vector3 _OrthographicPosition = new(0.5f, 0.5f, -1);
|
|
[SerializeField] private Vector2 _PreviewDirection = new(135, -30);
|
|
[SerializeField] private Vector2 _LightDirection = new(-40, -40);
|
|
[SerializeField] private Vector3 _PivotPositionOffset;
|
|
[SerializeField] private float _ZoomFactor = 1f;
|
|
|
|
/// <summary>The root object in the preview scene.</summary>
|
|
public Transform PreviewSceneRoot { get; private set; }
|
|
|
|
/// <summary>
|
|
/// An instance of the <see cref="TransitionPreviewSettings.SceneEnvironment"/>.
|
|
/// A child of the <see cref="PreviewSceneRoot"/>.
|
|
/// </summary>
|
|
public GameObject EnvironmentInstance { get; private set; }
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[<see cref="SerializeField"/>] The object being previewed.</summary>
|
|
public AnimancerPreviewObject PreviewObject
|
|
{
|
|
get
|
|
{
|
|
InitializePreviewRenderUtility();
|
|
return AnimancerPreviewObject.Initialize(ref _PreviewObject, this, PreviewSceneRoot);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Cleans up this renderer.</summary>
|
|
public void Dispose()
|
|
{
|
|
_PreviewObject?.Dispose();
|
|
CleanupPreviewRenderUtility();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Calles when the <see cref="TransitionPreviewSettings.SceneEnvironment"/> is changed.</summary>
|
|
private void OnEnvironmentPrefabChanged()
|
|
{
|
|
Object.DestroyImmediate(EnvironmentInstance);
|
|
|
|
var prefab = TransitionPreviewSettings.SceneEnvironment;
|
|
if (prefab != null)
|
|
EnvironmentInstance = Object.Instantiate(prefab, PreviewSceneRoot);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
void AnimancerPreviewObject.IEventHandler.OnInstantiateObject()
|
|
{
|
|
_PivotPositionOffset = _PreviewObject.InstanceBounds.center;
|
|
}
|
|
|
|
/// <inheritdoc/>
|
|
void AnimancerPreviewObject.IEventHandler.OnSetSelectedAnimator() { }
|
|
|
|
/// <inheritdoc/>
|
|
void AnimancerPreviewObject.IEventHandler.OnCreateGraph() { }
|
|
|
|
/************************************************************************************************************************/
|
|
#region GUI
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Draws the preview.</summary>
|
|
public void DoGUI(Rect area, GUIStyle background)
|
|
{
|
|
if (!DrawIsRenderTextureSupported(area))
|
|
return;
|
|
|
|
var currentEvent = Event.current;
|
|
|
|
HandleCameraControlEvent(area, currentEvent);
|
|
|
|
if (currentEvent.type == EventType.Repaint)
|
|
DrawPreview(area, background);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Shows a warning if the current device doesn't support render textures.</summary>
|
|
private bool DrawIsRenderTextureSupported(Rect area)
|
|
{
|
|
if (ShaderUtil.hardwareSupportsRectRenderTexture)
|
|
return true;
|
|
|
|
EditorGUI.DropShadowLabel(
|
|
area,
|
|
"Device doesn't support Render Textures\nUnable to render previews");
|
|
|
|
return false;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Initializes the <see cref="_PreviewRenderUtility"/>.</summary>
|
|
private void InitializePreviewRenderUtility()
|
|
{
|
|
if (_PreviewRenderUtility != null)
|
|
return;
|
|
|
|
_PreviewRenderUtility = new();
|
|
_PreviewRenderUtility.camera.fieldOfView = 30;
|
|
_PreviewRenderUtility.camera.allowHDR = false;
|
|
_PreviewRenderUtility.camera.allowMSAA = false;
|
|
_PreviewRenderUtility.ambientColor = Grey(0.1f, 0);
|
|
|
|
_PreviewLights = _PreviewRenderUtility.lights;
|
|
_PreviewLights[0].intensity = 1.4f;
|
|
_PreviewLights[0].transform.rotation = Quaternion.Euler(40, 40, 0);
|
|
_PreviewLights[1].intensity = 1.4f;
|
|
|
|
var root = AnimancerPreviewObject.CreateEmpty(nameof(AnimancerPreviewRenderer));
|
|
_PreviewRenderUtility.AddSingleGO(root);
|
|
PreviewSceneRoot = root.transform;
|
|
|
|
OnEnvironmentPrefabChanged();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Cleans up the <see cref="_PreviewRenderUtility"/>.</summary>
|
|
private void CleanupPreviewRenderUtility()
|
|
{
|
|
_PreviewRenderUtility?.Cleanup();
|
|
_PreviewRenderUtility = null;
|
|
_PreviewLights = null;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Updates and renders the preview.</summary>
|
|
private void DrawPreview(Rect area, GUIStyle background)
|
|
{
|
|
InitializePreviewRenderUtility();
|
|
_PreviewRenderUtility.BeginPreview(area, background);
|
|
UpdatePreviewRenderUtility();
|
|
_PreviewRenderUtility.Render(true, true);
|
|
_PreviewRenderUtility.EndAndDrawPreview(area);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Updates the preview rendering details.</summary>
|
|
private void UpdatePreviewRenderUtility()
|
|
{
|
|
var previewObject = PreviewObject;
|
|
if (previewObject.InstanceObject == null)
|
|
return;
|
|
|
|
var rotation = Quaternion.Euler(_PreviewDirection.y, 0, 0) * Quaternion.Euler(0, _PreviewDirection.x, 0);
|
|
previewObject.InstanceObject.rotation = rotation;
|
|
|
|
var size = previewObject.InstanceBounds.extents.magnitude;
|
|
var position = _ZoomFactor * -4f * size * Vector3.forward + _PivotPositionOffset;
|
|
|
|
var camera = _PreviewRenderUtility.camera;
|
|
camera.transform.SetPositionAndRotation(position, Quaternion.identity);
|
|
camera.nearClipPlane = 0.0001f;
|
|
camera.farClipPlane = 1000;
|
|
camera.orthographic = false;
|
|
|
|
var lights = _PreviewLights;
|
|
lights[0].intensity = 1.1f;
|
|
lights[0].transform.rotation = Quaternion.Euler(-_LightDirection.y, -_LightDirection.x, 0);
|
|
lights[1].intensity = 1.1f;
|
|
lights[1].transform.rotation = Quaternion.Euler(_LightDirection.y, _LightDirection.x, 0);
|
|
|
|
_PreviewRenderUtility.ambientColor = Grey(0.1f, 0);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Camera Controls
|
|
/************************************************************************************************************************/
|
|
|
|
private static readonly int CameraControlsHint = "CameraControls".GetHashCode();
|
|
|
|
/// <summary>Handles GUI events for controlling the preview camera.</summary>
|
|
private void HandleCameraControlEvent(Rect area, Event currentEvent)
|
|
{
|
|
if (currentEvent.button == 1)
|
|
{
|
|
if (currentEvent.alt)
|
|
_LightDirection = Drag2D(_LightDirection, area);// Could draw some lines to show the light directions.
|
|
else
|
|
_PreviewDirection = Drag2D(_PreviewDirection, area);
|
|
}
|
|
|
|
var control = new GUIControl(area, currentEvent, CameraControlsHint);
|
|
|
|
switch (control.EventType)
|
|
{
|
|
case EventType.ScrollWheel:
|
|
HandleScrollZoomEvent(area, currentEvent);
|
|
break;
|
|
|
|
case EventType.MouseDown:
|
|
if (currentEvent.button <= 0 || currentEvent.button == 2)
|
|
control.TryUseMouseDown();
|
|
break;
|
|
|
|
case EventType.MouseUp:
|
|
control.TryUseMouseUp();
|
|
break;
|
|
|
|
case EventType.MouseDrag:
|
|
if (control.TryUseHotControl())
|
|
HandleDragPanEvent(area, currentEvent);
|
|
break;
|
|
|
|
case EventType.ValidateCommand:
|
|
case EventType.ExecuteCommand:
|
|
switch (currentEvent.commandName)
|
|
{
|
|
case Commands.FrameSelected:
|
|
case Commands.FrameSelectedWithLock:
|
|
FrameTarget();
|
|
currentEvent.Use();
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private static readonly int SliderHash = "Slider".GetHashCode();
|
|
|
|
/// <summary>Handles drag input within a given `area`.</summary>
|
|
/// <remarks>Copied from Unity's <see cref="PreviewGUI.Drag2D"/>.</remarks>
|
|
public static Vector2 Drag2D(Vector2 scrollPosition, Rect area)
|
|
{
|
|
var control = new GUIControl(area, SliderHash);
|
|
|
|
switch (control.EventType)
|
|
{
|
|
case EventType.MouseDown:
|
|
if (control.TryUseMouseDown() && area.width > 50)
|
|
EditorGUIUtility.SetWantsMouseJumping(1);
|
|
|
|
break;
|
|
|
|
case EventType.MouseUp:
|
|
if (control.TryUseMouseUp())
|
|
EditorGUIUtility.SetWantsMouseJumping(0);
|
|
break;
|
|
|
|
case EventType.MouseDrag:
|
|
if (control.TryUseHotControl())
|
|
{
|
|
var multiplier = control.Event.shift ? 3 : 1;
|
|
var size = Mathf.Min(area.width, area.height);
|
|
scrollPosition -= control.Event.delta * multiplier / size * 140;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return scrollPosition;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Handles a mouse scroll event to zoom the preview camera.</summary>
|
|
private void HandleScrollZoomEvent(Rect area, Event currentEvent)
|
|
{
|
|
var delta = HandleUtility.niceMouseDeltaZoom * -0.025f;
|
|
var zoom = _ZoomFactor * (1 + delta);
|
|
zoom = Mathf.Clamp(zoom, 0.1f, 10);
|
|
var vector = new Vector2(
|
|
currentEvent.mousePosition.x / area.width,
|
|
1 - currentEvent.mousePosition.y / area.height);
|
|
var origin = _PreviewRenderUtility.camera.ViewportToWorldPoint(vector);
|
|
var direction = _OrthographicPosition - origin;
|
|
var position = origin + direction * (zoom / _ZoomFactor);
|
|
_PreviewRenderUtility.camera.transform.position = position;
|
|
|
|
_ZoomFactor = zoom;
|
|
currentEvent.Use();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Handles a mouse drag event to pan the preview camera.</summary>
|
|
private void HandleDragPanEvent(Rect area, Event currentEvent)
|
|
{
|
|
var camera = _PreviewRenderUtility.camera;
|
|
var direction = new Vector3(
|
|
-currentEvent.delta.x * camera.pixelWidth / area.width,
|
|
currentEvent.delta.y * camera.pixelHeight / area.height,
|
|
0);
|
|
|
|
var position = camera.WorldToScreenPoint(_PivotPositionOffset);
|
|
position += direction;
|
|
|
|
direction = camera.ScreenToWorldPoint(position) - _PivotPositionOffset;
|
|
_PivotPositionOffset += direction;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Frames the preview object in the middle of the camera.</summary>
|
|
private void FrameTarget()
|
|
{
|
|
_ZoomFactor = 1f;
|
|
_OrthographicPosition = new(0.5f, 0.5f, -1f);
|
|
_PivotPositionOffset = default;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|