// 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 { /// [Editor-Only] Utility for rendering previews of animated objects. /// Parts of this class are based on Unity's . /// 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; /// The root object in the preview scene. public Transform PreviewSceneRoot { get; private set; } /// /// An instance of the . /// A child of the . /// public GameObject EnvironmentInstance { get; private set; } /************************************************************************************************************************/ /// [] The object being previewed. public AnimancerPreviewObject PreviewObject { get { InitializePreviewRenderUtility(); return AnimancerPreviewObject.Initialize(ref _PreviewObject, this, PreviewSceneRoot); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ /// Cleans up this renderer. public void Dispose() { _PreviewObject?.Dispose(); CleanupPreviewRenderUtility(); } /************************************************************************************************************************/ /// Calles when the is changed. private void OnEnvironmentPrefabChanged() { Object.DestroyImmediate(EnvironmentInstance); var prefab = TransitionPreviewSettings.SceneEnvironment; if (prefab != null) EnvironmentInstance = Object.Instantiate(prefab, PreviewSceneRoot); } /************************************************************************************************************************/ /// void AnimancerPreviewObject.IEventHandler.OnInstantiateObject() { _PivotPositionOffset = _PreviewObject.InstanceBounds.center; } /// void AnimancerPreviewObject.IEventHandler.OnSetSelectedAnimator() { } /// void AnimancerPreviewObject.IEventHandler.OnCreateGraph() { } /************************************************************************************************************************/ #region GUI /************************************************************************************************************************/ /// Draws the preview. 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); } /************************************************************************************************************************/ /// Shows a warning if the current device doesn't support render textures. 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; } /************************************************************************************************************************/ /// Initializes the . 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(); } /************************************************************************************************************************/ /// Cleans up the . private void CleanupPreviewRenderUtility() { _PreviewRenderUtility?.Cleanup(); _PreviewRenderUtility = null; _PreviewLights = null; } /************************************************************************************************************************/ /// Updates and renders the preview. private void DrawPreview(Rect area, GUIStyle background) { InitializePreviewRenderUtility(); _PreviewRenderUtility.BeginPreview(area, background); UpdatePreviewRenderUtility(); _PreviewRenderUtility.Render(true, true); _PreviewRenderUtility.EndAndDrawPreview(area); } /************************************************************************************************************************/ /// Updates the preview rendering details. 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(); /// Handles GUI events for controlling the preview camera. 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(); /// Handles drag input within a given `area`. /// Copied from Unity's . 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; } /************************************************************************************************************************/ /// Handles a mouse scroll event to zoom the preview camera. 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(); } /************************************************************************************************************************/ /// Handles a mouse drag event to pan the preview camera. 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; } /************************************************************************************************************************/ /// Frames the preview object in the middle of the camera. private void FrameTarget() { _ZoomFactor = 1f; _OrthographicPosition = new(0.5f, 0.5f, -1f); _PivotPositionOffset = default; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } } #endif