testss
@@ -0,0 +1,535 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEditor.ShortcutManagement;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
class CameraController : Manipulator
|
||||
{
|
||||
[Flags]
|
||||
enum Direction
|
||||
{
|
||||
None = 0,
|
||||
Up = 1 << 0,
|
||||
Down = 1 << 1,
|
||||
Left = 1 << 2,
|
||||
Right = 1 << 3,
|
||||
Forward = 1 << 4,
|
||||
Backward = 1 << 5,
|
||||
All = Up | Down | Left | Right | Forward | Backward
|
||||
}
|
||||
Direction m_DirectionKeyPressed = Direction.None;
|
||||
|
||||
float m_StartZoom = 0.0f;
|
||||
float m_ZoomSpeed = 0.0f;
|
||||
|
||||
float m_TotalMotion = 0.0f;
|
||||
Vector3 m_MotionDirection = new Vector3();
|
||||
|
||||
float m_FlySpeedNormalized = .5f;
|
||||
float m_FlySpeed = 1f;
|
||||
float m_FlySpeedAccelerated = 0f;
|
||||
const float m_FlySpeedMin = .01f;
|
||||
const float m_FlySpeedMax = 2f;
|
||||
//[TODO: check if necessary to add hability to deactivate acceleration]
|
||||
const float k_FlyAcceleration = 1.1f;
|
||||
bool m_ShiftBoostedFly = false;
|
||||
bool m_InFlyMotion;
|
||||
|
||||
bool m_IsDragging;
|
||||
static TimeHelper s_Timer = new TimeHelper();
|
||||
|
||||
ViewTool m_BehaviorState;
|
||||
ViewTool behaviorState
|
||||
{
|
||||
get { return m_BehaviorState; }
|
||||
set
|
||||
{
|
||||
if (value != m_BehaviorState && m_BehaviorState == ViewTool.FPS)
|
||||
{
|
||||
isDragging = false;
|
||||
inFlyMotion = false;
|
||||
m_DirectionKeyPressed = Direction.None;
|
||||
}
|
||||
m_BehaviorState = value;
|
||||
}
|
||||
}
|
||||
|
||||
protected CameraState m_CameraState;
|
||||
DisplayWindow m_Window;
|
||||
protected Action m_Focused;
|
||||
|
||||
Rect screen => target.contentRect;
|
||||
|
||||
bool inFlyMotion
|
||||
{
|
||||
get => m_InFlyMotion;
|
||||
set
|
||||
{
|
||||
if (value ^ m_InFlyMotion)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
s_Timer.Begin();
|
||||
EditorApplication.update += UpdateFPSMotion;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_FlySpeedAccelerated = 0f;
|
||||
m_MotionDirection = Vector3.zero;
|
||||
m_ShiftBoostedFly = false;
|
||||
EditorApplication.update -= UpdateFPSMotion;
|
||||
}
|
||||
m_InFlyMotion = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float flySpeedNormalized
|
||||
{
|
||||
get => m_FlySpeedNormalized;
|
||||
set
|
||||
{
|
||||
m_FlySpeedNormalized = Mathf.Clamp01(value);
|
||||
float speed = Mathf.Lerp(m_FlySpeedMin, m_FlySpeedMax, m_FlySpeedNormalized);
|
||||
// Round to nearest decimal: 2 decimal points when between [0.01, 0.1]; 1 decimal point when between [0.1, 10]; integral between [10, 99]
|
||||
speed = (float)(System.Math.Round((double)speed, speed < 0.1f ? 2 : speed < 10f ? 1 : 0));
|
||||
m_FlySpeed = Mathf.Clamp(speed, m_FlySpeedMin, m_FlySpeedMax);
|
||||
}
|
||||
}
|
||||
float flySpeed
|
||||
{
|
||||
get => m_FlySpeed;
|
||||
set => flySpeedNormalized = Mathf.InverseLerp(m_FlySpeedMin, m_FlySpeedMax, value);
|
||||
}
|
||||
|
||||
virtual protected bool isDragging
|
||||
{
|
||||
get => m_IsDragging;
|
||||
set
|
||||
{
|
||||
//As in scene view, stop dragging as first button is release in case of multiple button down
|
||||
if (value ^ m_IsDragging)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
target.RegisterCallback<MouseMoveEvent>(OnMouseDrag);
|
||||
target.CaptureMouse();
|
||||
EditorGUIUtility.SetWantsMouseJumping(1); //through screen edges
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorGUIUtility.SetWantsMouseJumping(0);
|
||||
target.ReleaseMouse();
|
||||
target.UnregisterCallback<MouseMoveEvent>(OnMouseDrag);
|
||||
}
|
||||
m_IsDragging = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CameraController(DisplayWindow window, Action focused)
|
||||
{
|
||||
m_Window = window;
|
||||
m_Focused = focused;
|
||||
}
|
||||
|
||||
public void UpdateCameraState(Context context, ViewIndex index)
|
||||
{
|
||||
m_CameraState = context.GetViewContent(index).camera;
|
||||
}
|
||||
|
||||
private void ResetCameraControl()
|
||||
{
|
||||
isDragging = false;
|
||||
inFlyMotion = false;
|
||||
behaviorState = ViewTool.None;
|
||||
}
|
||||
|
||||
protected virtual void OnScrollWheel(WheelEvent evt)
|
||||
{
|
||||
// See UnityEditor.SceneViewMotion.HandleScrollWheel
|
||||
switch (behaviorState)
|
||||
{
|
||||
case ViewTool.FPS: OnChangeFPSCameraSpeed(evt); break;
|
||||
default: OnZoom(evt); break;
|
||||
}
|
||||
}
|
||||
|
||||
void OnMouseDrag(MouseMoveEvent evt)
|
||||
{
|
||||
switch (behaviorState)
|
||||
{
|
||||
case ViewTool.Orbit: OnMouseDragOrbit(evt); break;
|
||||
case ViewTool.FPS: OnMouseDragFPS(evt); break;
|
||||
case ViewTool.Pan: OnMouseDragPan(evt); break;
|
||||
case ViewTool.Zoom: OnMouseDragZoom(evt); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void OnKeyDown(KeyDownEvent evt)
|
||||
{
|
||||
OnKeyUpOrDownFPS(evt);
|
||||
OnKeyDownReset(evt);
|
||||
}
|
||||
|
||||
void OnChangeFPSCameraSpeed(WheelEvent evt)
|
||||
{
|
||||
float scrollWheelDelta = evt.delta.y;
|
||||
flySpeedNormalized -= scrollWheelDelta * .01f;
|
||||
string cameraSpeedDisplayValue = flySpeed.ToString(flySpeed < 0.1f ? "F2" : flySpeed < 10f ? "F1" : "F0");
|
||||
if (flySpeed < 0.1f)
|
||||
cameraSpeedDisplayValue = cameraSpeedDisplayValue.TrimStart(new Char[] { '0' });
|
||||
GUIContent cameraSpeedContent = EditorGUIUtility.TrTempContent(
|
||||
$"{cameraSpeedDisplayValue}x");
|
||||
m_Window.ShowNotification(cameraSpeedContent, .5f);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
void OnZoom(WheelEvent evt)
|
||||
{
|
||||
const float deltaCutoff = .3f;
|
||||
const float minZoom = .003f;
|
||||
float scrollWheelDelta = evt.delta.y;
|
||||
float relativeDelta = m_CameraState.viewSize * scrollWheelDelta * .015f;
|
||||
if (relativeDelta > 0 && relativeDelta < deltaCutoff)
|
||||
relativeDelta = deltaCutoff;
|
||||
else if (relativeDelta <= 0 && relativeDelta > -deltaCutoff)
|
||||
relativeDelta = -deltaCutoff;
|
||||
m_CameraState.viewSize += relativeDelta;
|
||||
if (m_CameraState.viewSize < minZoom)
|
||||
m_CameraState.viewSize = minZoom;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
void OnMouseDragOrbit(MouseMoveEvent evt)
|
||||
{
|
||||
Quaternion rotation = m_CameraState.rotation;
|
||||
rotation = Quaternion.AngleAxis(evt.mouseDelta.y * .003f * Mathf.Rad2Deg, rotation * Vector3.right) * rotation;
|
||||
rotation = Quaternion.AngleAxis(evt.mouseDelta.x * .003f * Mathf.Rad2Deg, Vector3.up) * rotation;
|
||||
m_CameraState.rotation = rotation;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
void OnMouseDragFPS(MouseMoveEvent evt)
|
||||
{
|
||||
Vector3 camPos = m_CameraState.pivot - m_CameraState.rotation * Vector3.forward * m_CameraState.distanceFromPivot;
|
||||
Quaternion rotation = m_CameraState.rotation;
|
||||
rotation = Quaternion.AngleAxis(evt.mouseDelta.y * .003f * Mathf.Rad2Deg, rotation * Vector3.right) * rotation;
|
||||
rotation = Quaternion.AngleAxis(evt.mouseDelta.x * .003f * Mathf.Rad2Deg, Vector3.up) * rotation;
|
||||
m_CameraState.rotation = rotation;
|
||||
m_CameraState.pivot = camPos + rotation * Vector3.forward * m_CameraState.distanceFromPivot;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
void OnMouseDragPan(MouseMoveEvent evt)
|
||||
{
|
||||
//[TODO: fix WorldToScreenPoint and ScreenToWorldPoint
|
||||
var screenPos = m_CameraState.QuickProjectPivotInScreen(screen);
|
||||
screenPos += new Vector3(evt.mouseDelta.x, -evt.mouseDelta.y, 0);
|
||||
//Vector3 newWorldPos = m_CameraState.ScreenToWorldPoint(screen, screenPos);
|
||||
Vector3 newWorldPos = m_CameraState.QuickReprojectionWithFixedFOVOnPivotPlane(screen, screenPos);
|
||||
Vector3 worldDelta = newWorldPos - m_CameraState.pivot;
|
||||
worldDelta *= EditorGUIUtility.pixelsPerPoint;
|
||||
if (evt.shiftKey)
|
||||
worldDelta *= 4;
|
||||
m_CameraState.pivot += worldDelta;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
void OnMouseDragZoom(MouseMoveEvent evt)
|
||||
{
|
||||
float zoomDelta = HandleUtility.niceMouseDeltaZoom * (evt.shiftKey ? 9 : 3);
|
||||
m_TotalMotion += zoomDelta;
|
||||
if (m_TotalMotion < 0)
|
||||
m_CameraState.viewSize = m_StartZoom * (1 + m_TotalMotion * .001f);
|
||||
else
|
||||
m_CameraState.viewSize = m_CameraState.viewSize + zoomDelta * m_ZoomSpeed * .003f;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
void OnKeyDownReset(KeyDownEvent evt)
|
||||
{
|
||||
if (evt.keyCode == KeyCode.Escape)
|
||||
ResetCameraControl();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
void OnKeyUpOrDownFPS<T>(KeyboardEventBase<T> evt)
|
||||
where T : KeyboardEventBase<T>, new()
|
||||
{
|
||||
if (behaviorState != ViewTool.FPS)
|
||||
return;
|
||||
|
||||
//Note: Keydown is called in loop but between first occurence of the
|
||||
// loop and laters, there is a small pause. To deal with this, we
|
||||
// need to register the UpdateMovement function to the Editor update
|
||||
KeyCombination combination;
|
||||
if (GetKeyCombinationByID("3D Viewport/Fly Mode Forward", out combination) && combination.Match(evt))
|
||||
RegisterMotionChange(Direction.Forward, evt);
|
||||
if (GetKeyCombinationByID("3D Viewport/Fly Mode Backward", out combination) && combination.Match(evt))
|
||||
RegisterMotionChange(Direction.Backward, evt);
|
||||
if (GetKeyCombinationByID("3D Viewport/Fly Mode Left", out combination) && combination.Match(evt))
|
||||
RegisterMotionChange(Direction.Left, evt);
|
||||
if (GetKeyCombinationByID("3D Viewport/Fly Mode Right", out combination) && combination.Match(evt))
|
||||
RegisterMotionChange(Direction.Right, evt);
|
||||
if (GetKeyCombinationByID("3D Viewport/Fly Mode Up", out combination) && combination.Match(evt))
|
||||
RegisterMotionChange(Direction.Up, evt);
|
||||
if (GetKeyCombinationByID("3D Viewport/Fly Mode Down", out combination) && combination.Match(evt))
|
||||
RegisterMotionChange(Direction.Down, evt);
|
||||
}
|
||||
|
||||
void RegisterMotionChange<T>(Direction direction, KeyboardEventBase<T> evt)
|
||||
where T : KeyboardEventBase<T>, new()
|
||||
{
|
||||
m_ShiftBoostedFly = evt.shiftKey;
|
||||
Direction formerDirection = m_DirectionKeyPressed;
|
||||
bool keyUp = evt is KeyUpEvent;
|
||||
bool keyDown = evt is KeyDownEvent;
|
||||
if (keyDown)
|
||||
m_DirectionKeyPressed |= direction;
|
||||
else if (keyUp)
|
||||
m_DirectionKeyPressed &= (Direction.All & ~direction);
|
||||
if (formerDirection != m_DirectionKeyPressed)
|
||||
{
|
||||
m_MotionDirection = new Vector3(
|
||||
((m_DirectionKeyPressed & Direction.Right) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Left) > 0 ? 1 : 0),
|
||||
((m_DirectionKeyPressed & Direction.Up) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Down) > 0 ? 1 : 0),
|
||||
((m_DirectionKeyPressed & Direction.Forward) > 0 ? 1 : 0) - ((m_DirectionKeyPressed & Direction.Backward) > 0 ? 1 : 0));
|
||||
|
||||
inFlyMotion = m_DirectionKeyPressed != Direction.None;
|
||||
}
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
Vector3 GetMotionDirection()
|
||||
{
|
||||
var deltaTime = s_Timer.Update();
|
||||
Vector3 result;
|
||||
float speed = (m_ShiftBoostedFly ? 5 * flySpeed : flySpeed);
|
||||
if (m_FlySpeedAccelerated == 0)
|
||||
m_FlySpeedAccelerated = 9;
|
||||
else
|
||||
m_FlySpeedAccelerated *= Mathf.Pow(k_FlyAcceleration, deltaTime);
|
||||
result = m_MotionDirection.normalized * m_FlySpeedAccelerated * speed * deltaTime;
|
||||
return result;
|
||||
}
|
||||
|
||||
void UpdateFPSMotion()
|
||||
{
|
||||
m_CameraState.pivot += m_CameraState.rotation * GetMotionDirection();
|
||||
m_Window.Repaint(); //this prevent hich on key down as in CameraFlyModeContext.cs
|
||||
}
|
||||
|
||||
bool GetKeyCombinationByID(string ID, out KeyCombination combination)
|
||||
{
|
||||
var sequence = ShortcutManager.instance.GetShortcutBinding(ID).keyCombinationSequence.GetEnumerator();
|
||||
if (sequence.MoveNext()) //have a first entry
|
||||
{
|
||||
combination = new KeyCombination(sequence.Current);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
combination = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
ViewTool GetBehaviorTool<T>(MouseEventBase<T> evt, bool onMac) where T : MouseEventBase<T>, new()
|
||||
{
|
||||
if (evt.button == 2)
|
||||
return ViewTool.Pan;
|
||||
else if (evt.button == 0 && evt.ctrlKey && onMac || evt.button == 1 && evt.altKey)
|
||||
return ViewTool.Zoom;
|
||||
else if (evt.button == 0)
|
||||
return ViewTool.Orbit;
|
||||
else if (evt.button == 1 && !evt.altKey)
|
||||
return ViewTool.FPS;
|
||||
return ViewTool.None;
|
||||
}
|
||||
|
||||
void OnMouseUp(MouseUpEvent evt)
|
||||
{
|
||||
bool onMac = Application.platform == RuntimePlatform.OSXEditor;
|
||||
var state = GetBehaviorTool(evt, onMac);
|
||||
|
||||
if (state == behaviorState)
|
||||
ResetCameraControl();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
|
||||
void OnMouseDown(MouseDownEvent evt)
|
||||
{
|
||||
bool onMac = Application.platform == RuntimePlatform.OSXEditor;
|
||||
behaviorState = GetBehaviorTool(evt, onMac);
|
||||
|
||||
if (behaviorState == ViewTool.Zoom)
|
||||
{
|
||||
m_StartZoom = m_CameraState.viewSize;
|
||||
m_ZoomSpeed = Mathf.Max(Mathf.Abs(m_StartZoom), .3f);
|
||||
m_TotalMotion = 0;
|
||||
}
|
||||
|
||||
// see also SceneView.HandleClickAndDragToFocus()
|
||||
if (evt.button == 1 && onMac)
|
||||
m_Window.Focus();
|
||||
|
||||
target.Focus(); //required for keyboard event
|
||||
isDragging = true;
|
||||
evt.StopPropagation();
|
||||
|
||||
m_Focused?.Invoke();
|
||||
}
|
||||
|
||||
protected override void RegisterCallbacksOnTarget()
|
||||
{
|
||||
target.focusable = true; //prerequisite for being focusable and recerive keydown events
|
||||
target.RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
target.RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
target.RegisterCallback<WheelEvent>(OnScrollWheel);
|
||||
target.RegisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
target.RegisterCallback<KeyUpEvent>(OnKeyUpOrDownFPS);
|
||||
}
|
||||
|
||||
protected override void UnregisterCallbacksFromTarget()
|
||||
{
|
||||
target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
target.UnregisterCallback<WheelEvent>(OnScrollWheel);
|
||||
target.UnregisterCallback<KeyDownEvent>(OnKeyDown);
|
||||
target.UnregisterCallback<KeyUpEvent>(OnKeyUpOrDownFPS);
|
||||
}
|
||||
|
||||
struct KeyCombination
|
||||
{
|
||||
KeyCode key;
|
||||
EventModifiers modifier;
|
||||
public bool shiftOnLastMatch;
|
||||
|
||||
public KeyCombination(UnityEditor.ShortcutManagement.KeyCombination shortcutCombination)
|
||||
{
|
||||
key = shortcutCombination.keyCode;
|
||||
modifier = EventModifiers.None;
|
||||
if ((shortcutCombination.modifiers & ShortcutModifiers.Shift) != 0)
|
||||
modifier |= EventModifiers.Shift;
|
||||
if ((shortcutCombination.modifiers & ShortcutModifiers.Alt) != 0)
|
||||
modifier |= EventModifiers.Alt;
|
||||
if ((shortcutCombination.modifiers & ShortcutModifiers.Action) != 0)
|
||||
{
|
||||
if (Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.OSXPlayer)
|
||||
modifier |= EventModifiers.Command;
|
||||
else
|
||||
modifier |= EventModifiers.Control;
|
||||
}
|
||||
shiftOnLastMatch = false;
|
||||
}
|
||||
|
||||
//atLeastModifier allow case were A is required but event provide shift+A
|
||||
public bool Match(IKeyboardEvent evt, bool atLeastForModifier = true)
|
||||
{
|
||||
shiftOnLastMatch = evt.shiftKey;
|
||||
if (atLeastForModifier)
|
||||
return key == evt.keyCode && modifier == (evt.modifiers & modifier);
|
||||
else
|
||||
return key == evt.keyCode && modifier == evt.modifiers;
|
||||
}
|
||||
}
|
||||
|
||||
struct TimeHelper
|
||||
{
|
||||
long lastTime;
|
||||
|
||||
public void Begin() => lastTime = System.DateTime.Now.Ticks;
|
||||
|
||||
public float Update()
|
||||
{
|
||||
float deltaTime = (System.DateTime.Now.Ticks - lastTime) / 10000000.0f;
|
||||
lastTime = System.DateTime.Now.Ticks;
|
||||
return deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SwitchableCameraController : CameraController
|
||||
{
|
||||
CameraState m_FirstView;
|
||||
CameraState m_SecondView;
|
||||
ViewIndex m_CurrentViewIndex;
|
||||
|
||||
bool switchedDrag = false;
|
||||
bool switchedWheel = false;
|
||||
|
||||
public SwitchableCameraController(DisplayWindow window, Action<ViewIndex> focused)
|
||||
: base(window, null)
|
||||
{
|
||||
m_CurrentViewIndex = ViewIndex.First;
|
||||
|
||||
m_Focused = () => focused?.Invoke(m_CurrentViewIndex);
|
||||
}
|
||||
|
||||
public void UpdateCameraState(Context context)
|
||||
{
|
||||
m_FirstView = context.GetViewContent(ViewIndex.First).camera;
|
||||
m_SecondView = context.GetViewContent(ViewIndex.Second).camera;
|
||||
|
||||
m_CameraState = m_CurrentViewIndex == ViewIndex.First ? m_FirstView : m_SecondView;
|
||||
}
|
||||
|
||||
void SwitchTo(ViewIndex index)
|
||||
{
|
||||
CameraState stateToSwitch;
|
||||
switch (index)
|
||||
{
|
||||
case ViewIndex.First:
|
||||
stateToSwitch = m_FirstView;
|
||||
break;
|
||||
case ViewIndex.Second:
|
||||
stateToSwitch = m_SecondView;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown ViewIndex");
|
||||
}
|
||||
|
||||
if (stateToSwitch != m_CameraState)
|
||||
m_CameraState = stateToSwitch;
|
||||
|
||||
m_CurrentViewIndex = index;
|
||||
}
|
||||
|
||||
public void SwitchUntilNextEndOfDrag()
|
||||
{
|
||||
switchedDrag = true;
|
||||
SwitchTo(ViewIndex.Second);
|
||||
}
|
||||
|
||||
override protected bool isDragging
|
||||
{
|
||||
get => base.isDragging;
|
||||
set
|
||||
{
|
||||
bool switchBack = false;
|
||||
if (switchedDrag && base.isDragging && !value)
|
||||
switchBack = true;
|
||||
base.isDragging = value;
|
||||
if (switchBack)
|
||||
SwitchTo(ViewIndex.First);
|
||||
}
|
||||
}
|
||||
|
||||
public void SwitchUntilNextWheelEvent()
|
||||
{
|
||||
switchedWheel = true;
|
||||
SwitchTo(ViewIndex.Second);
|
||||
}
|
||||
|
||||
protected override void OnScrollWheel(WheelEvent evt)
|
||||
{
|
||||
base.OnScrollWheel(evt);
|
||||
if (switchedWheel)
|
||||
SwitchTo(ViewIndex.First);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,119 @@
|
||||
using UnityEditor.AnimatedValues;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface to comunicate with simple <see cref="Renderer"/>
|
||||
/// </summary>
|
||||
public interface ICameraUpdater
|
||||
{
|
||||
/// <summary>Method called To update the LookDev camera position</summary>
|
||||
/// <param name="camera">The camera</param>
|
||||
void UpdateCamera(Camera camera);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class containing data regarding position, rotation and viewport size of a camera
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class CameraState : ICameraUpdater
|
||||
{
|
||||
private static readonly Quaternion k_DefaultRotation = Quaternion.LookRotation(new Vector3(0.0f, 0.0f, 1.0f));
|
||||
private const float k_DefaultViewSize = 10f;
|
||||
private static readonly Vector3 k_DefaultPivot = Vector3.zero;
|
||||
private const float k_DefaultFoV = 90f;
|
||||
private const float k_NearFactor = 0.000005f;
|
||||
private const float k_MaxFar = 1000;
|
||||
|
||||
/// <summary>The position of the camera pivot</summary>
|
||||
[field: SerializeField]
|
||||
public Vector3 pivot { get; set; } = k_DefaultPivot;
|
||||
|
||||
/// <summary>The rotation of the camera arround the pivot</summary>
|
||||
[field: SerializeField]
|
||||
public Quaternion rotation { get; set; } = k_DefaultRotation;
|
||||
|
||||
/// <summary>The size of the view</summary>
|
||||
[field: SerializeField]
|
||||
public float viewSize { get; set; } = k_DefaultViewSize;
|
||||
|
||||
/// <summary>The distance from pivot</summary>
|
||||
public float distanceFromPivot
|
||||
// distance coeficient from vertical FOV should be
|
||||
// 1f / Mathf.Tan(kDefaultFoV * 0.5f * Mathf.Deg2Rad)
|
||||
// but with fixed FoV of 90, this coef is always equal to 1f
|
||||
=> viewSize;
|
||||
|
||||
/// <summary>The position of the camera</summary>
|
||||
public Vector3 position
|
||||
=> pivot + rotation * new Vector3(0, 0, -distanceFromPivot);
|
||||
|
||||
/// <summary>The field of view of the camera</summary>
|
||||
public float fieldOfView => k_DefaultFoV;
|
||||
|
||||
/// <summary>The far clip distance from camera</summary>
|
||||
public float farClip => Mathf.Max(k_MaxFar, 2 * k_MaxFar * viewSize);
|
||||
|
||||
/// <summary>The near clip distance from camera</summary>
|
||||
public float nearClip => farClip * k_NearFactor;
|
||||
|
||||
/// <summary>The Forward vector in world space</summary>
|
||||
public Vector3 forward => rotation * Vector3.forward;
|
||||
|
||||
/// <summary>The Up vector in world space</summary>
|
||||
public Vector3 up => rotation * Vector3.up;
|
||||
|
||||
/// <summary>The Right vector in world space</summary>
|
||||
public Vector3 right => rotation * Vector3.right;
|
||||
|
||||
internal Vector3 QuickReprojectionWithFixedFOVOnPivotPlane(Rect screen, Vector2 screenPoint)
|
||||
{
|
||||
if (screen.height == 0)
|
||||
return Vector3.zero;
|
||||
float aspect = screen.width / screen.height;
|
||||
//Note: verticalDistance is same than distance from pivot with fixed FoV 90°
|
||||
float verticalDistance = distanceFromPivot;
|
||||
Vector2 normalizedScreenPoint = new Vector2(
|
||||
screenPoint.x * 2f / screen.width - 1f,
|
||||
screenPoint.y * 2f / screen.height - 1f);
|
||||
return pivot
|
||||
- up * verticalDistance * normalizedScreenPoint.y
|
||||
- right * verticalDistance * aspect * normalizedScreenPoint.x;
|
||||
}
|
||||
|
||||
//Pivot is always on center axis by construction
|
||||
internal Vector3 QuickProjectPivotInScreen(Rect screen)
|
||||
=> new Vector3(screen.width * .5f, screen.height * .5f, distanceFromPivot);
|
||||
|
||||
/// <summary>
|
||||
/// Update a Camera component and its transform with this state values
|
||||
/// </summary>
|
||||
/// <param name="camera">The camera to update</param>
|
||||
public void UpdateCamera(Camera camera)
|
||||
{
|
||||
camera.transform.rotation = rotation;
|
||||
camera.transform.position = position;
|
||||
camera.nearClipPlane = nearClip;
|
||||
camera.farClipPlane = farClip;
|
||||
camera.fieldOfView = fieldOfView;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset the State to its default values
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
pivot = k_DefaultPivot;
|
||||
rotation = k_DefaultRotation;
|
||||
viewSize = k_DefaultViewSize;
|
||||
}
|
||||
|
||||
internal void SynchronizeFrom(CameraState other)
|
||||
{
|
||||
pivot = other.pivot;
|
||||
rotation = other.rotation;
|
||||
viewSize = other.viewSize;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,244 @@
|
||||
using System;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
//TODO: clamps to always have both node on screen
|
||||
class ComparisonGizmoController : Manipulator
|
||||
{
|
||||
const float k_DragPadding = 0.05f;
|
||||
const float k_ReferenceScale = 1080f;
|
||||
|
||||
ComparisonGizmoState m_State;
|
||||
SwitchableCameraController m_Switcher;
|
||||
|
||||
enum Selected
|
||||
{
|
||||
None,
|
||||
NodeFirstView,
|
||||
NodeSecondView,
|
||||
PlaneSeparator,
|
||||
Fader
|
||||
}
|
||||
Selected m_Selected;
|
||||
|
||||
Vector2 m_SavedRelativePositionOnMouseDown;
|
||||
bool m_IsDragging;
|
||||
|
||||
bool isDragging
|
||||
{
|
||||
get => m_IsDragging;
|
||||
set
|
||||
{
|
||||
//As in scene view, stop dragging as first button is release in case of multiple button down
|
||||
if (value ^ m_IsDragging)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
target.RegisterCallback<MouseMoveEvent>(OnMouseDrag);
|
||||
target.CaptureMouse();
|
||||
}
|
||||
else
|
||||
{
|
||||
target.ReleaseMouse();
|
||||
target.UnregisterCallback<MouseMoveEvent>(OnMouseDrag);
|
||||
}
|
||||
m_IsDragging = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ComparisonGizmoController(SwitchableCameraController switcher)
|
||||
{
|
||||
m_Switcher = switcher;
|
||||
}
|
||||
|
||||
public void UpdateGizmoState(ComparisonGizmoState state)
|
||||
{
|
||||
m_State = state;
|
||||
}
|
||||
|
||||
protected override void RegisterCallbacksOnTarget()
|
||||
{
|
||||
target.RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
target.RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
target.RegisterCallback<WheelEvent>(OnScrollWheel);
|
||||
}
|
||||
|
||||
protected override void UnregisterCallbacksFromTarget()
|
||||
{
|
||||
target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
target.UnregisterCallback<WheelEvent>(OnScrollWheel);
|
||||
}
|
||||
|
||||
void OnScrollWheel(WheelEvent evt)
|
||||
{
|
||||
if (LookDev.currentContext.layout.viewLayout != Layout.CustomSplit)
|
||||
return;
|
||||
if (GetViewFromComposition(evt.localMousePosition) == ViewIndex.Second)
|
||||
m_Switcher.SwitchUntilNextWheelEvent();
|
||||
//let event be propagated to views
|
||||
}
|
||||
|
||||
void OnMouseDown(MouseDownEvent evt)
|
||||
{
|
||||
if (LookDev.currentContext.layout.viewLayout != Layout.CustomSplit)
|
||||
return;
|
||||
|
||||
Rect displayRect = target.contentRect;
|
||||
SelectGizmoZone(GetNormalizedCoordinates(evt.localMousePosition, displayRect));
|
||||
if (m_Selected != Selected.None)
|
||||
{
|
||||
m_SavedRelativePositionOnMouseDown = GetNormalizedCoordinates(evt.localMousePosition, displayRect) - m_State.center;
|
||||
isDragging = true;
|
||||
//We do not want to move camera and gizmo at the same time.
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
else
|
||||
{
|
||||
//else let event be propagated to views
|
||||
if (GetViewFromComposition(evt.localMousePosition) == ViewIndex.Second)
|
||||
m_Switcher.SwitchUntilNextEndOfDrag();
|
||||
}
|
||||
}
|
||||
|
||||
void OnMouseUp(MouseUpEvent evt)
|
||||
{
|
||||
if (LookDev.currentContext.layout.viewLayout != Layout.CustomSplit
|
||||
|| m_Selected == Selected.None)
|
||||
return;
|
||||
|
||||
// deadzone in fader gizmo
|
||||
if (m_Selected == Selected.Fader && Mathf.Abs(m_State.blendFactor) < ComparisonGizmoState.circleRadiusSelected / (m_State.length - ComparisonGizmoState.circleRadius))
|
||||
m_State.blendFactor = 0f;
|
||||
|
||||
m_Selected = Selected.None;
|
||||
isDragging = false;
|
||||
//We do not want to move camera and gizmo at the same time.
|
||||
evt.StopImmediatePropagation();
|
||||
|
||||
LookDev.SaveConfig();
|
||||
}
|
||||
|
||||
void OnMouseDrag(MouseMoveEvent evt)
|
||||
{
|
||||
if (m_Selected == Selected.None)
|
||||
return;
|
||||
|
||||
switch (m_Selected)
|
||||
{
|
||||
case Selected.PlaneSeparator: OnDragPlaneSeparator(evt); break;
|
||||
case Selected.NodeFirstView:
|
||||
case Selected.NodeSecondView: OnDragPlaneNodeExtremity(evt); break;
|
||||
case Selected.Fader: OnDragFader(evt); break;
|
||||
default: throw new ArgumentException("Unknown kind of Selected");
|
||||
}
|
||||
}
|
||||
|
||||
void OnDragPlaneSeparator(MouseMoveEvent evt)
|
||||
{
|
||||
//TODO: handle case when resizing window (clamping)
|
||||
Vector2 newPosition = GetNormalizedCoordinates(evt.localMousePosition, target.contentRect) - m_SavedRelativePositionOnMouseDown;
|
||||
|
||||
// We clamp the center of the gizmo to the border of the screen in order to avoid being able to put it out of the screen.
|
||||
// The safe band is here to ensure that you always see at least part of the gizmo in order to be able to grab it again.
|
||||
//Vector2 extends = GetNormalizedCoordinates(new Vector2(displayRect.width, displayRect.height), displayRect);
|
||||
//newPosition.x = Mathf.Clamp(newPosition.x, -extends.x + k_DragPadding, extends.x - k_DragPadding);
|
||||
//newPosition.y = Mathf.Clamp(newPosition.y, -extends.y + k_DragPadding, extends.y - k_DragPadding);
|
||||
|
||||
m_State.Update(newPosition, m_State.length, m_State.angle);
|
||||
//We do not want to move camera and gizmo at the same time.
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
void OnDragPlaneNodeExtremity(MouseMoveEvent evt)
|
||||
{
|
||||
Vector2 normalizedCoord = GetNormalizedCoordinates(evt.localMousePosition, target.contentRect);
|
||||
Vector2 basePoint, newPoint;
|
||||
float angleSnapping = Mathf.Deg2Rad * 45.0f * 0.5f;
|
||||
|
||||
newPoint = normalizedCoord;
|
||||
basePoint = m_Selected == Selected.NodeFirstView ? m_State.point2 : m_State.point1;
|
||||
|
||||
// Snap to a multiple of "angleSnapping"
|
||||
if ((evt.modifiers & EventModifiers.Shift) != 0)
|
||||
{
|
||||
Vector3 verticalPlane = new Vector3(-1.0f, 0.0f, basePoint.x);
|
||||
float side = Vector3.Dot(new Vector3(normalizedCoord.x, normalizedCoord.y, 1.0f), verticalPlane);
|
||||
|
||||
float angle = Mathf.Deg2Rad * Vector2.Angle(new Vector2(0.0f, 1.0f), normalizedCoord - basePoint);
|
||||
if (side > 0.0f)
|
||||
angle = 2.0f * Mathf.PI - angle;
|
||||
angle = (int)(angle / angleSnapping) * angleSnapping;
|
||||
Vector2 dir = normalizedCoord - basePoint;
|
||||
float length = dir.magnitude;
|
||||
newPoint = basePoint + new Vector2(Mathf.Sin(angle), Mathf.Cos(angle)) * length;
|
||||
}
|
||||
|
||||
if (m_Selected == Selected.NodeFirstView)
|
||||
m_State.Update(newPoint, basePoint);
|
||||
else
|
||||
m_State.Update(basePoint, newPoint);
|
||||
//We do not want to move camera and gizmo at the same time.
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
void OnDragFader(MouseMoveEvent evt)
|
||||
{
|
||||
Vector2 mousePosition = GetNormalizedCoordinates(evt.localMousePosition, target.contentRect);
|
||||
float distanceToOrthoPlane = -Vector3.Dot(new Vector3(mousePosition.x, mousePosition.y, 1.0f), m_State.planeOrtho) / m_State.blendFactorMaxGizmoDistance;
|
||||
m_State.blendFactor = Mathf.Clamp(distanceToOrthoPlane, -1.0f, 1.0f);
|
||||
//We do not want to move camera and gizmo at the same time.
|
||||
evt.StopImmediatePropagation();
|
||||
}
|
||||
|
||||
void SelectGizmoZone(Vector2 normalizedMousePosition)
|
||||
{
|
||||
//TODO: Optimize
|
||||
Vector3 normalizedMousePositionZ1 = new Vector3(normalizedMousePosition.x, normalizedMousePosition.y, 1.0f);
|
||||
float distanceToPlane = Vector3.Dot(normalizedMousePositionZ1, m_State.plane);
|
||||
float absDistanceToPlane = Mathf.Abs(distanceToPlane);
|
||||
float distanceFromCenter = Vector2.Distance(normalizedMousePosition, m_State.center);
|
||||
float distanceToOrtho = Vector3.Dot(normalizedMousePositionZ1, m_State.planeOrtho);
|
||||
float side = (distanceToOrtho > 0.0f) ? 1.0f : -1.0f;
|
||||
Vector2 orthoPlaneNormal = new Vector2(m_State.planeOrtho.x, m_State.planeOrtho.y);
|
||||
|
||||
Selected selected = Selected.None;
|
||||
if (absDistanceToPlane < ComparisonGizmoState.circleRadiusSelected && (distanceFromCenter < (m_State.length + ComparisonGizmoState.circleRadiusSelected)))
|
||||
{
|
||||
if (absDistanceToPlane < ComparisonGizmoState.thicknessSelected)
|
||||
selected = Selected.PlaneSeparator;
|
||||
|
||||
Vector2 circleCenter = m_State.center + side * orthoPlaneNormal * m_State.length;
|
||||
float d = Vector2.Distance(normalizedMousePosition, circleCenter);
|
||||
if (d <= ComparisonGizmoState.circleRadiusSelected)
|
||||
selected = side > 0.0f ? Selected.NodeFirstView : Selected.NodeSecondView;
|
||||
|
||||
float maxBlendCircleDistanceToCenter = m_State.blendFactorMaxGizmoDistance;
|
||||
float blendCircleDistanceToCenter = m_State.blendFactor * maxBlendCircleDistanceToCenter;
|
||||
Vector2 blendCircleCenter = m_State.center - orthoPlaneNormal * blendCircleDistanceToCenter;
|
||||
float blendCircleSelectionRadius = Mathf.Lerp(ComparisonGizmoState.blendFactorCircleRadius, ComparisonGizmoState.blendFactorCircleRadiusSelected, Mathf.Clamp((maxBlendCircleDistanceToCenter - Mathf.Abs(blendCircleDistanceToCenter)) / (ComparisonGizmoState.blendFactorCircleRadiusSelected - ComparisonGizmoState.blendFactorCircleRadius), 0.0f, 1.0f));
|
||||
if ((normalizedMousePosition - blendCircleCenter).magnitude < blendCircleSelectionRadius)
|
||||
selected = Selected.Fader;
|
||||
}
|
||||
|
||||
m_Selected = selected;
|
||||
}
|
||||
|
||||
//normalize in [-1,1]^2 for a 1080^2. Can be above 1 for higher than 1080.
|
||||
internal static Vector2 GetNormalizedCoordinates(Vector2 localMousePosition, Rect rect)
|
||||
=> new Vector2(
|
||||
(2f * localMousePosition.x - rect.width) / k_ReferenceScale,
|
||||
(-2f * localMousePosition.y + rect.height) / k_ReferenceScale);
|
||||
|
||||
ViewIndex GetViewFromComposition(Vector2 localCoordinate)
|
||||
{
|
||||
Vector2 normalizedLocalCoordinate = GetNormalizedCoordinates(localCoordinate, target.contentRect);
|
||||
return Vector3.Dot(new Vector3(normalizedLocalCoordinate.x, normalizedLocalCoordinate.y, 1), m_State.plane) >= 0
|
||||
? ViewIndex.First
|
||||
: ViewIndex.Second;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using UnityEditor.AnimatedValues;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>state of the comparison gizmo of the LookDev</summary>
|
||||
[Serializable]
|
||||
public class ComparisonGizmoState
|
||||
{
|
||||
internal const float thickness = 0.0028f;
|
||||
internal const float thicknessSelected = 0.015f;
|
||||
internal const float circleRadius = 0.014f;
|
||||
internal const float circleRadiusSelected = 0.03f;
|
||||
internal const float blendFactorCircleRadius = 0.01f;
|
||||
internal const float blendFactorCircleRadiusSelected = 0.03f;
|
||||
|
||||
/// <summary>Position of the first extremity</summary>
|
||||
public Vector2 point1 { get; private set; }
|
||||
/// <summary>Position of the second extremity</summary>
|
||||
public Vector2 point2 { get; private set; }
|
||||
/// <summary>Position of the center</summary>
|
||||
[field: SerializeField]
|
||||
public Vector2 center { get; private set; } = Vector2.zero;
|
||||
/// <summary>Angle from vertical in radian</summary>
|
||||
[field: SerializeField]
|
||||
public float angle { get; private set; }
|
||||
/// <summary>Length between extremity</summary>
|
||||
[field: SerializeField]
|
||||
public float length { get; private set; } = 0.2f;
|
||||
internal Vector4 plane { get; private set; }
|
||||
internal Vector4 planeOrtho { get; private set; }
|
||||
/// <summary>
|
||||
/// The position of the blending slider.
|
||||
/// From value -1 on first extremity to value 1 on second extremity.
|
||||
/// </summary>
|
||||
[field: SerializeField]
|
||||
public float blendFactor { get; set; }
|
||||
|
||||
internal float blendFactorMaxGizmoDistance
|
||||
=> length - circleRadius - blendFactorCircleRadius;
|
||||
|
||||
internal float blendFactorMinGizmoDistance
|
||||
=> length - circleRadius - blendFactorCircleRadiusSelected;
|
||||
|
||||
internal void Init()
|
||||
=> Update(center, length, angle);
|
||||
|
||||
//TODO: optimize
|
||||
private Vector4 Get2DPlane(Vector2 firstPoint, float angle)
|
||||
{
|
||||
Vector4 result = new Vector4();
|
||||
angle = angle % (2.0f * (float)Math.PI);
|
||||
Vector2 secondPoint = new Vector2(firstPoint.x + Mathf.Sin(angle), firstPoint.y + Mathf.Cos(angle));
|
||||
Vector2 diff = secondPoint - firstPoint;
|
||||
if (Mathf.Abs(diff.x) < 1e-5)
|
||||
{
|
||||
result.Set(-1.0f, 0.0f, firstPoint.x, 0.0f);
|
||||
float sign = Mathf.Cos(angle) > 0.0f ? 1.0f : -1.0f;
|
||||
result *= sign;
|
||||
}
|
||||
else
|
||||
{
|
||||
float slope = diff.y / diff.x;
|
||||
result.Set(-slope, 1.0f, -(firstPoint.y - slope * firstPoint.x), 0.0f);
|
||||
}
|
||||
|
||||
if (angle > Mathf.PI)
|
||||
result = -result;
|
||||
|
||||
float length = Mathf.Sqrt(result.x * result.x + result.y * result.y);
|
||||
result = result / length;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all fields while moving one extremity
|
||||
/// </summary>
|
||||
/// <param name="point1">The new first extremity position</param>
|
||||
/// <param name="point2">The new second extremity position</param>
|
||||
public void Update(Vector2 point1, Vector2 point2)
|
||||
{
|
||||
this.point1 = point1;
|
||||
this.point2 = point2;
|
||||
center = (point1 + point2) * 0.5f;
|
||||
length = (point2 - point1).magnitude * 0.5f;
|
||||
|
||||
Vector3 verticalPlane = Get2DPlane(center, 0.0f);
|
||||
float side = Vector3.Dot(new Vector3(point1.x, point1.y, 1.0f), verticalPlane);
|
||||
angle = (Mathf.Deg2Rad * Vector2.Angle(new Vector2(0.0f, 1.0f), (point1 - point2).normalized));
|
||||
if (side > 0.0f)
|
||||
angle = 2.0f * Mathf.PI - angle;
|
||||
|
||||
plane = Get2DPlane(center, angle);
|
||||
planeOrtho = Get2DPlane(center, angle + 0.5f * (float)Mathf.PI);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update all fields while moving the bar
|
||||
/// </summary>
|
||||
/// <param name="center">The new center position</param>
|
||||
/// <param name="length">Tne new length of this gizmo</param>
|
||||
/// <param name="angle">The new angle of this gizmo</param>
|
||||
public void Update(Vector2 center, float length, float angle)
|
||||
{
|
||||
this.center = center;
|
||||
this.length = length;
|
||||
this.angle = angle;
|
||||
|
||||
plane = Get2DPlane(center, angle);
|
||||
planeOrtho = Get2DPlane(center, angle + 0.5f * (float)Mathf.PI);
|
||||
|
||||
Vector2 dir = new Vector2(planeOrtho.x, planeOrtho.y);
|
||||
point1 = center + dir * length;
|
||||
point2 = center - dir * length;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,404 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Experimental.Rendering;
|
||||
using IDataProvider = UnityEngine.Rendering.LookDev.IDataProvider;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
enum ShadowCompositionPass
|
||||
{
|
||||
MainView,
|
||||
ShadowMask
|
||||
}
|
||||
|
||||
enum CompositionFinal
|
||||
{
|
||||
First,
|
||||
Second
|
||||
}
|
||||
|
||||
class RenderTextureCache : IDisposable
|
||||
{
|
||||
const int k_PassPerViewCount = 3;
|
||||
const int k_ViewCount = 2;
|
||||
const int k_TextureCacheSize = k_PassPerViewCount * k_ViewCount;
|
||||
//RenderTextures are packed this way:
|
||||
//0: ViewIndex.First, ShadowCompositionPass.MainView
|
||||
//1: ViewIndex.First, ShadowCompositionPass.ShadowMask
|
||||
//2: CompositionFinal.First
|
||||
//3: ViewIndex.Second, ShadowCompositionPass.MainView
|
||||
//4: ViewIndex.Second, ShadowCompositionPass.ShadowMask
|
||||
//5: CompositionFinal.Second
|
||||
RenderTexture[] m_RTs = new RenderTexture[k_TextureCacheSize];
|
||||
|
||||
public RenderTexture this[ViewIndex index, ShadowCompositionPass passIndex]
|
||||
{
|
||||
get => m_RTs[computeIndex(index, passIndex)];
|
||||
set => m_RTs[computeIndex(index, passIndex)] = value;
|
||||
}
|
||||
|
||||
public RenderTexture this[CompositionFinal index]
|
||||
{
|
||||
get => m_RTs[computeIndex(index)];
|
||||
set => m_RTs[computeIndex(index)] = value;
|
||||
}
|
||||
|
||||
int computeIndex(ViewIndex index, ShadowCompositionPass passIndex)
|
||||
=> (int)index * k_PassPerViewCount + (int)(passIndex);
|
||||
int computeIndex(CompositionFinal index)
|
||||
=> (k_PassPerViewCount - 1) + (int)(index) * k_PassPerViewCount;
|
||||
|
||||
void UpdateSize(int index, Rect rect, bool pixelPerfect, Camera renderingCamera, string renderDocName = "LookDevRT")
|
||||
{
|
||||
bool nullRect = rect.IsNullOrInverted();
|
||||
|
||||
GraphicsFormat format = SystemInfo.IsFormatSupported(GraphicsFormat.R16G16B16A16_SFloat, FormatUsage.Render)
|
||||
? GraphicsFormat.R16G16B16A16_SFloat
|
||||
: SystemInfo.GetGraphicsFormat(DefaultFormat.LDR);
|
||||
if (m_RTs[index] != null && (nullRect || m_RTs[index].graphicsFormat != format))
|
||||
{
|
||||
m_RTs[index].Release();
|
||||
UnityEngine.Object.DestroyImmediate(m_RTs[index]);
|
||||
m_RTs[index] = null;
|
||||
}
|
||||
if (nullRect)
|
||||
return;
|
||||
|
||||
int width = (int)rect.width;
|
||||
int height = (int)rect.height;
|
||||
|
||||
if (m_RTs[index] == null)
|
||||
{
|
||||
m_RTs[index] = new RenderTexture(0, 0, 24, format);
|
||||
m_RTs[index].name = renderDocName;
|
||||
m_RTs[index].antiAliasing = 1;
|
||||
m_RTs[index].hideFlags = HideFlags.HideAndDontSave;
|
||||
}
|
||||
|
||||
if (m_RTs[index].width != width || m_RTs[index].height != height)
|
||||
{
|
||||
m_RTs[index].Release();
|
||||
m_RTs[index].width = width;
|
||||
m_RTs[index].height = height;
|
||||
m_RTs[index].Create();
|
||||
}
|
||||
|
||||
if (renderingCamera != null)
|
||||
renderingCamera.targetTexture = m_RTs[index];
|
||||
}
|
||||
|
||||
public void UpdateSize(Rect rect, ViewIndex index, bool pixelPerfect, Camera renderingCamera)
|
||||
{
|
||||
UpdateSize(computeIndex(index, ShadowCompositionPass.MainView), rect, pixelPerfect, renderingCamera, $"LookDevRT-{index}-MainView");
|
||||
UpdateSize(computeIndex(index, ShadowCompositionPass.ShadowMask), rect, pixelPerfect, renderingCamera, $"LookDevRT-{index}-ShadowMask");
|
||||
}
|
||||
|
||||
public void UpdateSize(Rect rect, CompositionFinal index, bool pixelPerfect, Camera renderingCamera)
|
||||
=> UpdateSize(computeIndex(index), rect, pixelPerfect, renderingCamera, $"LookDevRT-Final-{index}");
|
||||
|
||||
bool m_Disposed = false;
|
||||
public void Dispose()
|
||||
{
|
||||
if (m_Disposed)
|
||||
return;
|
||||
m_Disposed = true;
|
||||
|
||||
for (int index = 0; index < k_TextureCacheSize; ++index)
|
||||
{
|
||||
if (m_RTs[index] == null || m_RTs[index].Equals(null))
|
||||
continue;
|
||||
|
||||
UnityEngine.Object.DestroyImmediate(m_RTs[index]);
|
||||
m_RTs[index] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Compositer : IDisposable
|
||||
{
|
||||
public static readonly Color firstViewGizmoColor = new Color32(0, 154, 154, 255);
|
||||
public static readonly Color secondViewGizmoColor = new Color32(255, 37, 4, 255);
|
||||
static Material s_Material;
|
||||
static Material material
|
||||
{
|
||||
get
|
||||
{
|
||||
if (s_Material == null || s_Material.Equals(null))
|
||||
s_Material = new Material(Shader.Find("Hidden/LookDev/Compositor"));
|
||||
return s_Material;
|
||||
}
|
||||
}
|
||||
|
||||
IViewDisplayer m_Displayer;
|
||||
Context m_Contexts;
|
||||
RenderTextureCache m_RenderTextures = new RenderTextureCache();
|
||||
Renderer m_Renderer = new Renderer();
|
||||
RenderingData[] m_RenderDataCache;
|
||||
|
||||
bool m_pixelPerfect;
|
||||
bool m_Disposed;
|
||||
|
||||
public bool pixelPerfect
|
||||
{
|
||||
get => m_pixelPerfect;
|
||||
set => m_Renderer.pixelPerfect = m_pixelPerfect = value;
|
||||
}
|
||||
|
||||
Color m_AmbientColor = new Color(0.0f, 0.0f, 0.0f, 0.0f);
|
||||
|
||||
bool m_RenderDocAcquisitionRequested;
|
||||
|
||||
public Compositer(
|
||||
IViewDisplayer displayer,
|
||||
IDataProvider dataProvider,
|
||||
StageCache stages)
|
||||
{
|
||||
m_Displayer = displayer;
|
||||
|
||||
m_RenderDataCache = new RenderingData[2]
|
||||
{
|
||||
new RenderingData() { stage = stages[ViewIndex.First] },
|
||||
new RenderingData() { stage = stages[ViewIndex.Second] }
|
||||
};
|
||||
|
||||
m_Displayer.OnRenderDocAcquisitionTriggered += RenderDocAcquisitionRequested;
|
||||
m_Displayer.OnUpdateRequested += Render;
|
||||
}
|
||||
|
||||
void RenderDocAcquisitionRequested()
|
||||
=> m_RenderDocAcquisitionRequested = true;
|
||||
|
||||
void CleanUp()
|
||||
{
|
||||
for (int index = 0; index < 2; ++index)
|
||||
{
|
||||
m_RenderDataCache[index]?.Dispose();
|
||||
m_RenderDataCache[index] = null;
|
||||
}
|
||||
|
||||
m_RenderTextures.Dispose();
|
||||
|
||||
m_Displayer.OnRenderDocAcquisitionTriggered -= RenderDocAcquisitionRequested;
|
||||
m_Displayer.OnUpdateRequested -= Render;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (m_Disposed)
|
||||
return;
|
||||
m_Disposed = true;
|
||||
CleanUp();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
~Compositer() => CleanUp();
|
||||
|
||||
public void Render()
|
||||
{
|
||||
// This can happen when entering/leaving playmode.
|
||||
if (LookDev.dataProvider == null)
|
||||
return;
|
||||
|
||||
m_Contexts = LookDev.currentContext;
|
||||
|
||||
//TODO: make integration EditorWindow agnostic!
|
||||
if (UnityEditorInternal.RenderDoc.IsLoaded() && UnityEditorInternal.RenderDoc.IsSupported() && m_RenderDocAcquisitionRequested)
|
||||
UnityEditorInternal.RenderDoc.BeginCaptureRenderDoc(m_Displayer as EditorWindow);
|
||||
|
||||
switch (m_Contexts.layout.viewLayout)
|
||||
{
|
||||
case Layout.FullFirstView:
|
||||
RenderSingleAndOutput(ViewIndex.First);
|
||||
break;
|
||||
case Layout.FullSecondView:
|
||||
RenderSingleAndOutput(ViewIndex.Second);
|
||||
break;
|
||||
case Layout.HorizontalSplit:
|
||||
case Layout.VerticalSplit:
|
||||
RenderSingleAndOutput(ViewIndex.First);
|
||||
RenderSingleAndOutput(ViewIndex.Second);
|
||||
break;
|
||||
case Layout.CustomSplit:
|
||||
RenderCompositeAndOutput();
|
||||
break;
|
||||
}
|
||||
|
||||
//TODO: make integration EditorWindow agnostic!
|
||||
if (UnityEditorInternal.RenderDoc.IsLoaded() && UnityEditorInternal.RenderDoc.IsSupported() && m_RenderDocAcquisitionRequested)
|
||||
UnityEditorInternal.RenderDoc.EndCaptureRenderDoc(m_Displayer as EditorWindow);
|
||||
|
||||
//stating that RenderDoc do not need to acquire anymore should
|
||||
//allows to gather both view and composition in render doc at once
|
||||
m_RenderDocAcquisitionRequested = false;
|
||||
}
|
||||
|
||||
void AcquireDataForView(ViewIndex index, Rect viewport)
|
||||
{
|
||||
var renderingData = m_RenderDataCache[(int)index];
|
||||
renderingData.viewPort = viewport;
|
||||
ViewContext view = m_Contexts.GetViewContent(index);
|
||||
|
||||
m_RenderTextures.UpdateSize(renderingData.viewPort, index, m_Renderer.pixelPerfect, renderingData.stage.camera);
|
||||
|
||||
int debugMode = view.debug.viewMode;
|
||||
if (debugMode != -1)
|
||||
LookDev.dataProvider.UpdateDebugMode(debugMode);
|
||||
|
||||
renderingData.output = m_RenderTextures[index, ShadowCompositionPass.MainView];
|
||||
renderingData.updater = view.camera;
|
||||
|
||||
m_Renderer.BeginRendering(renderingData, LookDev.dataProvider);
|
||||
m_Renderer.Acquire(renderingData);
|
||||
|
||||
if (view.debug.shadow)
|
||||
{
|
||||
RenderTexture tmp = m_RenderTextures[index, ShadowCompositionPass.ShadowMask];
|
||||
view.environment?.UpdateSunPosition(renderingData.stage.sunLight);
|
||||
renderingData.stage.sunLight.intensity = 1f;
|
||||
LookDev.dataProvider.GetShadowMask(ref tmp, renderingData.stage.runtimeInterface);
|
||||
renderingData.stage.sunLight.intensity = 0f;
|
||||
m_RenderTextures[index, ShadowCompositionPass.ShadowMask] = tmp;
|
||||
}
|
||||
|
||||
m_Renderer.EndRendering(renderingData, LookDev.dataProvider);
|
||||
|
||||
if (debugMode != -1)
|
||||
LookDev.dataProvider.UpdateDebugMode(-1);
|
||||
}
|
||||
|
||||
void RenderSingleAndOutput(ViewIndex index)
|
||||
{
|
||||
Rect viewport = m_Displayer.GetRect((ViewCompositionIndex)index);
|
||||
AcquireDataForView(index, viewport);
|
||||
Compositing(viewport, (int)index, (CompositionFinal)index);
|
||||
m_Displayer.SetTexture((ViewCompositionIndex)index, m_RenderTextures[(CompositionFinal)index]);
|
||||
}
|
||||
|
||||
void RenderCompositeAndOutput()
|
||||
{
|
||||
Rect viewport = m_Displayer.GetRect(ViewCompositionIndex.Composite);
|
||||
AcquireDataForView(ViewIndex.First, viewport);
|
||||
AcquireDataForView(ViewIndex.Second, viewport);
|
||||
Compositing(viewport, 2 /*split*/, CompositionFinal.First);
|
||||
m_Displayer.SetTexture(ViewCompositionIndex.Composite, m_RenderTextures[CompositionFinal.First]);
|
||||
}
|
||||
|
||||
void Compositing(Rect rect, int pass, CompositionFinal finalBufferIndex)
|
||||
{
|
||||
bool skipShadowComposition0 = !m_Contexts.GetViewContent(ViewIndex.First).debug.shadow;
|
||||
bool skipShadowComposition1 = !m_Contexts.GetViewContent(ViewIndex.Second).debug.shadow;
|
||||
|
||||
if (rect.IsNullOrInverted()
|
||||
|| (m_Contexts.layout.viewLayout != Layout.FullSecondView
|
||||
&& (m_RenderTextures[ViewIndex.First, ShadowCompositionPass.MainView] == null
|
||||
|| (!skipShadowComposition0
|
||||
&& m_RenderTextures[ViewIndex.First, ShadowCompositionPass.ShadowMask] == null)))
|
||||
|| (m_Contexts.layout.viewLayout != Layout.FullFirstView
|
||||
&& (m_RenderTextures[ViewIndex.Second, ShadowCompositionPass.MainView] == null
|
||||
|| (!skipShadowComposition1
|
||||
&& m_RenderTextures[ViewIndex.Second, ShadowCompositionPass.ShadowMask] == null))))
|
||||
{
|
||||
m_RenderTextures[finalBufferIndex] = null;
|
||||
return;
|
||||
}
|
||||
|
||||
m_RenderTextures.UpdateSize(rect, finalBufferIndex, m_pixelPerfect, null);
|
||||
|
||||
ComparisonGizmoState gizmo = m_Contexts.layout.gizmoState;
|
||||
|
||||
Vector4 gizmoPosition = new Vector4(gizmo.center.x, gizmo.center.y, 0.0f, 0.0f);
|
||||
Vector4 gizmoZoneCenter = new Vector4(gizmo.point2.x, gizmo.point2.y, 0.0f, 0.0f);
|
||||
Vector4 gizmoThickness = new Vector4(ComparisonGizmoState.thickness, ComparisonGizmoState.thicknessSelected, 0.0f, 0.0f);
|
||||
Vector4 gizmoCircleRadius = new Vector4(ComparisonGizmoState.circleRadius, ComparisonGizmoState.circleRadiusSelected, 0.0f, 0.0f);
|
||||
|
||||
Environment env0 = m_Contexts.GetViewContent(ViewIndex.First).environment;
|
||||
Environment env1 = m_Contexts.GetViewContent(ViewIndex.Second).environment;
|
||||
|
||||
float exposureValue0 = env0?.exposure ?? 0f;
|
||||
float exposureValue1 = env1?.exposure ?? 0f;
|
||||
float dualViewBlendFactor = gizmo.blendFactor;
|
||||
float isCurrentlyLeftEditting = m_Contexts.layout.lastFocusedView == ViewIndex.First ? 1f : -1f;
|
||||
float dragAndDropContext = 0f; //1f left, -1f right, 0f neither
|
||||
float toneMapEnabled = -1f; //1f true, -1f false
|
||||
float shadowMultiplier0 = skipShadowComposition0 ? -1f : 1.0f;
|
||||
float shadowMultiplier1 = skipShadowComposition1 ? -1f : 1.0f;
|
||||
Color shadowColor0 = env0?.shadowColor ?? Color.white;
|
||||
Color shadowColor1 = env1?.shadowColor ?? Color.white;
|
||||
|
||||
//TODO: handle shadow not at compositing step but in rendering
|
||||
Texture texMainView0 = m_RenderTextures[ViewIndex.First, ShadowCompositionPass.MainView];
|
||||
Texture texShadowsMask0 = m_RenderTextures[ViewIndex.First, ShadowCompositionPass.ShadowMask];
|
||||
|
||||
Texture texMainView1 = m_RenderTextures[ViewIndex.Second, ShadowCompositionPass.MainView];
|
||||
Texture texShadowsMask1 = m_RenderTextures[ViewIndex.Second, ShadowCompositionPass.ShadowMask];
|
||||
|
||||
Vector4 compositingParams = new Vector4(dualViewBlendFactor, exposureValue0, exposureValue1, isCurrentlyLeftEditting);
|
||||
Vector4 compositingParams2 = new Vector4(dragAndDropContext, toneMapEnabled, shadowMultiplier0, shadowMultiplier1);
|
||||
|
||||
// Those could be tweakable for the neutral tonemapper, but in the case of the LookDev we don't need that
|
||||
const float k_BlackIn = 0.02f;
|
||||
const float k_WhiteIn = 10.0f;
|
||||
const float k_BlackOut = 0.0f;
|
||||
const float k_WhiteOut = 10.0f;
|
||||
const float k_WhiteLevel = 5.3f;
|
||||
const float k_WhiteClip = 10.0f;
|
||||
const float k_DialUnits = 20.0f;
|
||||
const float k_HalfDialUnits = k_DialUnits * 0.5f;
|
||||
const float k_GizmoRenderMode = 4f; //display all
|
||||
|
||||
// converting from artist dial units to easy shader-lerps (0-1)
|
||||
//TODO: to compute one time only
|
||||
Vector4 tonemapCoeff1 = new Vector4((k_BlackIn * k_DialUnits) + 1.0f, (k_BlackOut * k_HalfDialUnits) + 1.0f, (k_WhiteIn / k_DialUnits), (1.0f - (k_WhiteOut / k_DialUnits)));
|
||||
Vector4 tonemapCoeff2 = new Vector4(0.0f, 0.0f, k_WhiteLevel, k_WhiteClip / k_HalfDialUnits);
|
||||
|
||||
const float k_ReferenceScale = 1080.0f;
|
||||
Vector4 screenRatio = new Vector4(rect.width / k_ReferenceScale, rect.height / k_ReferenceScale, rect.width, rect.height);
|
||||
|
||||
RenderTexture oldActive = RenderTexture.active;
|
||||
RenderTexture.active = m_RenderTextures[finalBufferIndex];
|
||||
material.SetTexture("_Tex0MainView", texMainView0);
|
||||
material.SetTexture("_Tex0Shadows", texShadowsMask0);
|
||||
material.SetColor("_ShadowColor0", shadowColor0);
|
||||
material.SetTexture("_Tex1MainView", texMainView1);
|
||||
material.SetTexture("_Tex1Shadows", texShadowsMask1);
|
||||
material.SetColor("_ShadowColor1", shadowColor1);
|
||||
material.SetVector("_CompositingParams", compositingParams);
|
||||
material.SetVector("_CompositingParams2", compositingParams2);
|
||||
material.SetColor("_FirstViewColor", firstViewGizmoColor);
|
||||
material.SetColor("_SecondViewColor", secondViewGizmoColor);
|
||||
material.SetVector("_GizmoPosition", gizmoPosition);
|
||||
material.SetVector("_GizmoZoneCenter", gizmoZoneCenter);
|
||||
material.SetVector("_GizmoSplitPlane", gizmo.plane);
|
||||
material.SetVector("_GizmoSplitPlaneOrtho", gizmo.planeOrtho);
|
||||
material.SetFloat("_GizmoLength", gizmo.length);
|
||||
material.SetVector("_GizmoThickness", gizmoThickness);
|
||||
material.SetVector("_GizmoCircleRadius", gizmoCircleRadius);
|
||||
material.SetFloat("_BlendFactorCircleRadius", ComparisonGizmoState.blendFactorCircleRadius);
|
||||
material.SetFloat("_GetBlendFactorMaxGizmoDistance", gizmo.blendFactorMaxGizmoDistance);
|
||||
material.SetFloat("_GizmoRenderMode", k_GizmoRenderMode);
|
||||
material.SetVector("_ScreenRatio", screenRatio);
|
||||
material.SetVector("_ToneMapCoeffs1", tonemapCoeff1);
|
||||
material.SetVector("_ToneMapCoeffs2", tonemapCoeff2);
|
||||
material.SetPass(pass);
|
||||
|
||||
Renderer.DrawFullScreenQuad(new Rect(0, 0, rect.width, rect.height));
|
||||
|
||||
RenderTexture.active = oldActive;
|
||||
}
|
||||
|
||||
public ViewIndex GetViewFromComposition(Vector2 localCoordinate)
|
||||
{
|
||||
Rect compositionRect = m_Displayer.GetRect(ViewCompositionIndex.Composite);
|
||||
Vector2 normalizedLocalCoordinate = ComparisonGizmoController.GetNormalizedCoordinates(localCoordinate, compositionRect);
|
||||
switch (m_Contexts.layout.viewLayout)
|
||||
{
|
||||
case Layout.CustomSplit:
|
||||
return Vector3.Dot(new Vector3(normalizedLocalCoordinate.x, normalizedLocalCoordinate.y, 1), m_Contexts.layout.gizmoState.plane) >= 0
|
||||
? ViewIndex.First
|
||||
: ViewIndex.Second;
|
||||
default:
|
||||
throw new Exception("GetViewFromComposition call when not inside a Composition");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,460 @@
|
||||
Shader "Hidden/LookDev/Compositor"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
_Tex0MainView("Main View", 2D) = "white" {}
|
||||
_Tex0Shadows("First View shadow mask", 2D) = "white" {}
|
||||
_ShadowColor0("Shadow Color for first view", Color) = (1.0, 1.0, 1.0, 1.0)
|
||||
_Tex1MainView("Second View", 2D) = "white" {}
|
||||
_Tex1Shadows("Second View shadow mask", 2D) = "white" {}
|
||||
_ShadowColor1("Shadow Color for second view", Color) = (1.0, 1.0, 1.0, 1.0)
|
||||
_CompositingParams("Blend Factor, exposure for first and second view, and current selected side", Vector) = (0.0, 1.0, 1.0, 1.0)
|
||||
_CompositingParams2("Drag and drop zone and shadow multipliers", Vector) = (0.0, 1.0, 1.0, 1.0) // Drag and Drop zone Left == 1.0, Right == -1.0, None == 0.0
|
||||
_FirstViewColor("Gizmo Color for first view", Color) = (0.5, 0.5, 0.5, 0.5)
|
||||
_SecondViewColor("Gizmo Color for second view", Color) = (0.5, 0.5, 0.5, 0.5)
|
||||
_GizmoPosition("Position of split view gizmo", Vector) = (0.5, 0.5, 0.0, 0.0)
|
||||
_GizmoZoneCenter("Center of Zone view gizmo", Vector) = (0.5, 0.5, 0.0, 0.0)
|
||||
_GizmoSplitPlane("2D plane of the gizmo", Vector) = (1.0, 1.0, 0.0, 0.0)
|
||||
_GizmoSplitPlaneOrtho("2D plane orthogonal to the gizmo", Vector) = (1.0, 1.0, 0.0, 0.0)
|
||||
_GizmoLength("Gizmo Length", Float) = 0.2
|
||||
_GizmoThickness("Gizmo Thickness", Vector) = (0.01, 0.08, 0.0, 0.0)
|
||||
_GizmoCircleRadius("Gizmo extremities radius", Vector) = (0.05, 0.4, 0.0, 0.0)
|
||||
_GizmoRenderMode("Render gizmo mode", Float) = 0.0
|
||||
_GetBlendFactorMaxGizmoDistance("Distance on the gizmo where the blend circle stops", Float) = 0.2
|
||||
_BlendFactorCircleRadius("Visual radius of the blend factor gizmo", Float) = 0.01
|
||||
_ScreenRatio("Screen ratio", Vector) = (1.0, 1.0, 0.0, 0.0) // xy screen ratio, zw screen size
|
||||
_ToneMapCoeffs1("Parameters for neutral tonemap", Vector) = (0.0, 0.0, 0.0, 0.0)
|
||||
_ToneMapCoeffs2("Parameters for neutral tonemap", Vector) = (0.0, 0.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
CGINCLUDE
|
||||
#include "UnityCG.cginc"
|
||||
#pragma vertex vert
|
||||
|
||||
// Enum matching GizmoOperationType in LookDevViews.cs
|
||||
#define kNone 0.0f
|
||||
#define kTranslation 1.0f
|
||||
#define kRotationZone1 2.0f
|
||||
#define kRotationZone2 3.0f
|
||||
#define kAll 4.0f
|
||||
|
||||
|
||||
sampler2D _Tex0MainView;
|
||||
sampler2D _Tex0Shadows;
|
||||
float4 _ShadowColor0;
|
||||
sampler2D _Tex1MainView;
|
||||
sampler2D _Tex1Shadows;
|
||||
float4 _ShadowColor1;
|
||||
float4 _CompositingParams; // x BlendFactor, yz ExposureValue (first/second view), w current selected side
|
||||
float4 _CompositingParams2; // x current drag context, y apply tonemap (bool), z shadow multiplier
|
||||
float4 _FirstViewColor;
|
||||
float4 _SecondViewColor;
|
||||
float4 _GizmoPosition;
|
||||
float4 _GizmoZoneCenter;
|
||||
float4 _GizmoThickness;
|
||||
float4 _GizmoCircleRadius;
|
||||
float4 _GizmoSplitPlane;
|
||||
float4 _GizmoSplitPlaneOrtho;
|
||||
float _GizmoLength;
|
||||
float _GizmoRenderMode;
|
||||
float _GetBlendFactorMaxGizmoDistance;
|
||||
float _BlendFactorCircleRadius;
|
||||
float4 _ScreenRatio;
|
||||
float4 _ToneMapCoeffs1;
|
||||
float4 _ToneMapCoeffs2;
|
||||
|
||||
float4 _Tex0MainView_ST;
|
||||
|
||||
#define ShadowMultiplier0 _CompositingParams2.z
|
||||
#define ShadowMultiplier1 _CompositingParams2.w
|
||||
|
||||
#define ExposureValue1 _CompositingParams.y
|
||||
#define ExposureValue2 _CompositingParams.z
|
||||
|
||||
#define InBlack _ToneMapCoeffs1.x
|
||||
#define OutBlack _ToneMapCoeffs1.y
|
||||
#define InWhite _ToneMapCoeffs1.z
|
||||
#define OutWhite _ToneMapCoeffs1.w
|
||||
#define WhiteLevel _ToneMapCoeffs2.z
|
||||
#define WhiteClip _ToneMapCoeffs2.w
|
||||
|
||||
struct appdata_t
|
||||
{
|
||||
float4 vertex : POSITION;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float2 texcoord : TEXCOORD0;
|
||||
float4 vertex : SV_POSITION;
|
||||
};
|
||||
|
||||
float DistanceToSplit(float2 pos, float3 splitPlane)
|
||||
{
|
||||
return dot(float3(pos, 1), splitPlane);
|
||||
}
|
||||
|
||||
bool IsInsideGizmo(float2 normalizedCoord, float absDistanceToPlane, float distanceFromCenter, float side, float3 orthoPlane, float gizmoCircleRadius, float gizmoThickness, out float outSmoothing, float mode)
|
||||
{
|
||||
bool result = false;
|
||||
outSmoothing = 0.0;
|
||||
if (absDistanceToPlane < gizmoCircleRadius) // First "thick" bar, as large as the radius at extremities.
|
||||
{
|
||||
if (distanceFromCenter < (_GizmoLength + gizmoCircleRadius))
|
||||
{
|
||||
// side < 0 is cyan circle, side > 0 is orange widget
|
||||
if (mode == kAll ||
|
||||
(mode == kRotationZone1 && side > 0) ||
|
||||
(mode == kRotationZone2 && side < 0))
|
||||
{
|
||||
if (distanceFromCenter >= (_GizmoLength - gizmoCircleRadius)) // Inside circle at the extremities ?
|
||||
{
|
||||
float2 circleCenter = _GizmoPosition.xy + side * orthoPlane.xy * _GizmoLength;
|
||||
float d = length(normalizedCoord - circleCenter);
|
||||
if (d <= gizmoCircleRadius)
|
||||
{
|
||||
outSmoothing = smoothstep(1.0, 0.8, d / gizmoCircleRadius);
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == kAll || mode == kTranslation)
|
||||
{
|
||||
if (absDistanceToPlane < gizmoThickness && distanceFromCenter < _GizmoLength)
|
||||
{
|
||||
outSmoothing = max(outSmoothing, smoothstep(1.0, 0.0, absDistanceToPlane / gizmoThickness));
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
float4 GetGizmoColor(float2 normalizedCoord, float3 splitPlane, float3 orthoPlane)
|
||||
{
|
||||
float distanceToPlane = DistanceToSplit(normalizedCoord, splitPlane);
|
||||
float absDistanceToPlane = abs(distanceToPlane);
|
||||
float distanceFromCenter = length(normalizedCoord.xy - _GizmoPosition.xy);
|
||||
float distanceToOrtho = DistanceToSplit(normalizedCoord, orthoPlane);
|
||||
|
||||
float4 result = float4(0.0, 0.0, 0.0, 0.0);
|
||||
float side = 0.0;
|
||||
if (distanceToOrtho > 0.0)
|
||||
{
|
||||
result.rgb = _FirstViewColor.rgb;
|
||||
side = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
result.rgb = _SecondViewColor.rgb;
|
||||
side = -1.0;
|
||||
}
|
||||
|
||||
result.a = 0.0;
|
||||
|
||||
// "normal" gizmo
|
||||
float smoothing = 1.0;
|
||||
if (IsInsideGizmo(normalizedCoord, absDistanceToPlane, distanceFromCenter, side, orthoPlane, _GizmoCircleRadius.x, _GizmoThickness.x, smoothing, kAll))
|
||||
{
|
||||
result.a = 1.0 * smoothing;
|
||||
}
|
||||
|
||||
// large gizmo when in translation mode
|
||||
if (IsInsideGizmo(normalizedCoord, absDistanceToPlane, distanceFromCenter, side, orthoPlane, _GizmoCircleRadius.y, _GizmoThickness.y, smoothing, _GizmoRenderMode))
|
||||
{
|
||||
result.a = max(result.a, 0.25 * smoothing);
|
||||
}
|
||||
|
||||
// Blend factor selection disc
|
||||
float2 blendCircleCenter = _GizmoPosition.xy - _CompositingParams.x * orthoPlane.xy * _GetBlendFactorMaxGizmoDistance;
|
||||
float distanceToBlendCircle = length(normalizedCoord.xy - blendCircleCenter);
|
||||
if (distanceToBlendCircle < _BlendFactorCircleRadius)
|
||||
{
|
||||
float alpha = smoothstep(1.0, 0.6, distanceToBlendCircle / _BlendFactorCircleRadius);
|
||||
result = lerp(result, float4(1.0, 1.0, 1.0, alpha), alpha);
|
||||
}
|
||||
|
||||
// Display transparent disc if near the center where the blend factor selection disc will automatically snap back
|
||||
if (abs(_CompositingParams.x) < _GizmoCircleRadius.y / _GetBlendFactorMaxGizmoDistance)
|
||||
{
|
||||
if (distanceFromCenter < _BlendFactorCircleRadius)
|
||||
{
|
||||
float alpha = smoothstep(1.0, 0.6, distanceFromCenter / _BlendFactorCircleRadius) * 0.75;
|
||||
result = lerp(result, float4(1.0, 1.0, 1.0, alpha), alpha);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
float GetZoneViewFeedbackCircleFactor(float2 normalizedCoord, float radius, float circleSize)
|
||||
{
|
||||
float distanceToCenter = abs(length(_GizmoZoneCenter.xy - normalizedCoord) - radius);
|
||||
return saturate((circleSize - distanceToCenter) / circleSize);
|
||||
}
|
||||
|
||||
float ComputeBorderFactor(float borderSize, float2 screenPos, bool sideBySideView)
|
||||
{
|
||||
float4 borderSize4 = float4(borderSize, borderSize, borderSize, borderSize);
|
||||
float4 distanceToBorder = float4(screenPos.x, screenPos.y, abs(_ScreenRatio.z - screenPos.x), abs(_ScreenRatio.w - screenPos.y));
|
||||
|
||||
float4 factors = saturate((borderSize4 - distanceToBorder) / borderSize4); // Lerp from 1.0 to 0.0 alpha from screen border to border size
|
||||
float factor = max(factors.x, max(factors.y, max(factors.z, factors.w)));
|
||||
|
||||
// Add middle of the screen for side by side view
|
||||
if (sideBySideView)
|
||||
{
|
||||
float distanceToCenterLine = abs(_ScreenRatio.z * 0.5 - screenPos.x);
|
||||
float factorForCenterLine = saturate((borderSize - distanceToCenterLine) / borderSize);
|
||||
factor = max(factor, factorForCenterLine);
|
||||
}
|
||||
|
||||
return factor;
|
||||
}
|
||||
|
||||
float ComputeSelectedSideColorFactor(float side, float2 screenPos, float2 normalizedCoord, bool sideBySideView, bool zoneView)
|
||||
{
|
||||
float borderSize = 2.0;
|
||||
bool selectedSide = side * _CompositingParams.w > 0.0;
|
||||
|
||||
float factor = ComputeBorderFactor(borderSize, screenPos, sideBySideView);
|
||||
|
||||
// Add circle for zone view
|
||||
if (zoneView)
|
||||
{
|
||||
float selectionCircleFeedbackFactor = GetZoneViewFeedbackCircleFactor(normalizedCoord, _GizmoCircleRadius.y, 0.002);
|
||||
factor = max(factor, selectionCircleFeedbackFactor);
|
||||
}
|
||||
|
||||
// If not on the selected side, make it more transparent
|
||||
if (!selectedSide)
|
||||
{
|
||||
factor = factor * 0.2;
|
||||
}
|
||||
|
||||
return factor;
|
||||
}
|
||||
|
||||
float4 ComputeDragColorFactor(float side, float2 screenPos, float2 normalizedCoord, bool sideBySideView, bool zoneView)
|
||||
{
|
||||
float factor = 0;
|
||||
float borderSize = 40.0;
|
||||
bool sideIsDragZone = (side > 0.0 && _CompositingParams2.x > 0.0) || (side < 0.0 && _CompositingParams2.x < 0.0);
|
||||
if (sideIsDragZone)
|
||||
{
|
||||
factor = ComputeBorderFactor(borderSize, screenPos, sideBySideView);
|
||||
|
||||
// Add circle for zone view
|
||||
if (zoneView && side < 0.0)
|
||||
{
|
||||
float feedbackRadius = _GizmoLength * 2.0 * 0.3; // make it proprtional to selection zone
|
||||
factor = max(factor, GetZoneViewFeedbackCircleFactor(normalizedCoord, _GizmoLength * 2.0, feedbackRadius));
|
||||
}
|
||||
|
||||
factor = pow(factor, 8) * 0.7; // Casimir magics values for optimum fadeout :)
|
||||
}
|
||||
|
||||
return factor;
|
||||
}
|
||||
|
||||
float4 ComputeFeedbackColor(float4 inputColor, float side, float2 screenPos, float2 normalizedCoord, bool sideBySideView, bool zoneView)
|
||||
{
|
||||
float factor = ComputeSelectedSideColorFactor(side, screenPos, normalizedCoord, sideBySideView, zoneView);
|
||||
factor = max(factor, ComputeDragColorFactor(side, screenPos, normalizedCoord, sideBySideView, zoneView));
|
||||
|
||||
float4 result = float4(0.0, 0.0, 0.0, 0.0);
|
||||
if (side > 0.0)
|
||||
{
|
||||
result = lerp(inputColor, _FirstViewColor, factor);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = lerp(inputColor, _SecondViewColor, factor);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
v2f vert(appdata_t IN)
|
||||
{
|
||||
v2f OUT;
|
||||
OUT.vertex = UnityObjectToClipPos(IN.vertex);
|
||||
OUT.texcoord = TRANSFORM_TEX(IN.texcoord, _Tex0MainView);
|
||||
return OUT;
|
||||
}
|
||||
|
||||
float3 evalCurve(float3 x, float A, float B, float C, float D, float E, float F)
|
||||
{
|
||||
return ((x*(A*x + C*B) + D*E) / (x*(A*x + B) + D*F)) - E / F;
|
||||
}
|
||||
|
||||
float3 applyTonemapFilmicAD(float3 linearColor)
|
||||
{
|
||||
float blackRatio = InBlack / OutBlack;
|
||||
float whiteRatio = InWhite / OutWhite;
|
||||
|
||||
// blend tunable coefficients
|
||||
float B = lerp(0.57, 0.37, blackRatio);
|
||||
float C = lerp(0.01, 0.24, whiteRatio);
|
||||
float D = lerp(0.02, 0.20, blackRatio);
|
||||
|
||||
// constants
|
||||
float A = 0.2;
|
||||
float E = 0.02;
|
||||
float F = 0.30;
|
||||
|
||||
// eval and correct for white point
|
||||
float3 whiteScale = 1.0f / evalCurve(WhiteLevel, A, B, C, D, E, F);
|
||||
float3 curr = evalCurve(linearColor *whiteScale, A, B, C, D, E, F);
|
||||
|
||||
return curr*whiteScale;
|
||||
}
|
||||
|
||||
float3 remapWhite(float3 inPixel, float whitePt)
|
||||
{
|
||||
// var breakout for readability
|
||||
const float inBlack = 0;
|
||||
const float outBlack = 0;
|
||||
float inWhite = whitePt;
|
||||
const float outWhite = 1;
|
||||
|
||||
// remap input range to output range
|
||||
float3 outPixel = ((inPixel.rgb) - inBlack.xxx) / (inWhite.xxx - inBlack.xxx) * (outWhite.xxx - outBlack.xxx) + outBlack.xxx;
|
||||
return (outPixel.rgb);
|
||||
}
|
||||
|
||||
float3 NeutralTonemap(float3 x)
|
||||
{
|
||||
float3 finalColor = applyTonemapFilmicAD(x); // curve (dynamic coeffs differ per level)
|
||||
finalColor = remapWhite(finalColor, WhiteClip); // post-curve white point adjustment
|
||||
finalColor = saturate(finalColor);
|
||||
return finalColor;
|
||||
}
|
||||
|
||||
float3 ApplyToneMap(float3 color)
|
||||
{
|
||||
if (_CompositingParams2.y > 0.0)
|
||||
{
|
||||
return NeutralTonemap(color);
|
||||
}
|
||||
else
|
||||
{
|
||||
return saturate(color);
|
||||
}
|
||||
}
|
||||
|
||||
float3 ComputeColor(sampler2D texNormal, sampler2D texShadowMask, float shadowMultiplier, float4 shadowColor, float2 texcoord)
|
||||
{
|
||||
// Explanation of how this work:
|
||||
// To simulate the shadow of a directional light, we want to interpolate between two environments. One environment being the regular cubemap and the other a darkened version of the same cubemap
|
||||
// To create the lerp mask we render the scene with a white diffuse material and a single shadow casting directional light.
|
||||
// This will create a mask where the shadowed area is 0 and the lit area is 1 with a smooth NDotL transition in-between.
|
||||
// However, the DNotL will create an unwanted darkening of the scene (it's not actually part of the lighting equation)
|
||||
// so we sort it in order to avoid too much darkening.
|
||||
float3 color = tex2D(texNormal, texcoord).rgb;
|
||||
if (shadowMultiplier < 0.0)
|
||||
{
|
||||
// no need to composite as we do not want shadow in this case
|
||||
return color;
|
||||
}
|
||||
else
|
||||
{
|
||||
float3 shadowMask = sqrt(tex2D(texShadowMask, texcoord).rgb);
|
||||
return lerp(color * shadowColor.rgb * shadowMultiplier, color, saturate(shadowMask.r));
|
||||
}
|
||||
}
|
||||
|
||||
ENDCG
|
||||
|
||||
SubShader
|
||||
{
|
||||
Tags
|
||||
{
|
||||
"ForceSupported" = "True"
|
||||
}
|
||||
|
||||
Lighting Off
|
||||
Cull Off
|
||||
ZTest Always
|
||||
ZWrite Off
|
||||
Blend One Zero
|
||||
|
||||
// Single view 1
|
||||
Pass
|
||||
{
|
||||
CGPROGRAM
|
||||
#pragma fragment frag
|
||||
#pragma target 3.0
|
||||
|
||||
float4 frag(float2 texcoord : TEXCOORD0,
|
||||
UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
|
||||
{
|
||||
float4 color = float4(ComputeColor(_Tex0MainView, _Tex0Shadows, ShadowMultiplier0, _ShadowColor0, texcoord) * exp2(ExposureValue1), 1.0);
|
||||
color.rgb = ApplyToneMap(color.rgb);
|
||||
color = ComputeFeedbackColor(color, 1.0, vpos.xy, float2(0.0, 0.0), false, false);
|
||||
return color;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
|
||||
// Single view 2
|
||||
Pass
|
||||
{
|
||||
CGPROGRAM
|
||||
#pragma fragment frag
|
||||
#pragma target 3.0
|
||||
|
||||
float4 frag(float2 texcoord : TEXCOORD0,
|
||||
UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
|
||||
{
|
||||
float4 color = float4(ComputeColor(_Tex1MainView, _Tex1Shadows, ShadowMultiplier1, _ShadowColor1, texcoord) * exp2(ExposureValue2), 1.0);
|
||||
color.rgb = ApplyToneMap(color.rgb);
|
||||
color = ComputeFeedbackColor(color, -1.0, vpos.xy, float2(0.0, 0.0), false, false);
|
||||
return color;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
|
||||
// split
|
||||
Pass
|
||||
{
|
||||
CGPROGRAM
|
||||
#pragma fragment frag
|
||||
#pragma target 3.0
|
||||
|
||||
float4 frag(float2 texcoord : TEXCOORD0,
|
||||
UNITY_VPOS_TYPE vpos : VPOS) : SV_Target
|
||||
{
|
||||
float3 color1 = ComputeColor(_Tex0MainView, _Tex0Shadows, ShadowMultiplier0, _ShadowColor0, texcoord) * exp2(ExposureValue1);
|
||||
float3 color2 = ComputeColor(_Tex1MainView, _Tex1Shadows, ShadowMultiplier1, _ShadowColor1, texcoord) * exp2(ExposureValue2);
|
||||
|
||||
float2 normalizedCoord = ((texcoord * 2.0 - 1.0) * _ScreenRatio.xy);
|
||||
|
||||
float side = DistanceToSplit(normalizedCoord, _GizmoSplitPlane) < 0.0f ? -1.0f : 1.0f;
|
||||
float blendFactor = 0.0f;
|
||||
if (side < 0.0)
|
||||
{
|
||||
blendFactor = 1.0 - saturate(side * _CompositingParams.x);
|
||||
}
|
||||
else
|
||||
{
|
||||
blendFactor = saturate(side * _CompositingParams.x);
|
||||
}
|
||||
|
||||
float4 finalColor = float4(lerp(color1, color2, blendFactor), 1.0);
|
||||
finalColor.rgb = ApplyToneMap(finalColor.rgb);
|
||||
|
||||
float4 gizmoColor = GetGizmoColor(normalizedCoord, _GizmoSplitPlane, _GizmoSplitPlaneOrtho);
|
||||
finalColor = lerp(finalColor, gizmoColor, gizmoColor.a);
|
||||
finalColor = ComputeFeedbackColor(finalColor, side, vpos.xy, float2(0.0, 0.0), false, false);
|
||||
|
||||
return finalColor;
|
||||
}
|
||||
ENDCG
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,497 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>
|
||||
/// Different working views in LookDev
|
||||
/// </summary>
|
||||
public enum ViewIndex
|
||||
{
|
||||
/// <summary>First view</summary>
|
||||
First,
|
||||
/// <summary>Second view</summary>
|
||||
Second
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Same as <see cref="ViewIndex"/> plus a compound value
|
||||
/// </summary>
|
||||
public enum ViewCompositionIndex
|
||||
{
|
||||
/// <summary>First view</summary>
|
||||
First = ViewIndex.First,
|
||||
/// <summary>Second view</summary>
|
||||
Second = ViewIndex.Second,
|
||||
/// <summary>Composite view (Several view on screen)</summary>
|
||||
Composite
|
||||
};
|
||||
|
||||
// /!\ WARNING: these value name are used as uss file too.
|
||||
// if your rename here, rename in the uss too.
|
||||
/// <summary>
|
||||
/// Different layout supported in LookDev
|
||||
/// </summary>
|
||||
public enum Layout
|
||||
{
|
||||
/// <summary>First view display fully</summary>
|
||||
FullFirstView,
|
||||
/// <summary>Second view display fully</summary>
|
||||
FullSecondView,
|
||||
/// <summary>First and second views displayed splitted horizontally</summary>
|
||||
HorizontalSplit,
|
||||
/// <summary>First and second views displayed splitted vertically</summary>
|
||||
VerticalSplit,
|
||||
/// <summary>First and second views displayed with stacking and orientation customizable split</summary>
|
||||
CustomSplit
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Status of the side panel of the LookDev window
|
||||
/// </summary>
|
||||
public enum SidePanel
|
||||
{
|
||||
/// <summary>No side panel</summary>
|
||||
None = -1,
|
||||
/// <summary>Environment side panel</summary>
|
||||
Environment,
|
||||
/// <summary>Debug side panel</summary>
|
||||
Debug,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The target views of the debug panel
|
||||
/// </summary>
|
||||
public enum TargetDebugView
|
||||
{
|
||||
/// <summary>First Debug view</summary>
|
||||
First,
|
||||
/// <summary>Both Debug view</summary>
|
||||
Both,
|
||||
/// <summary>Second Debug view</summary>
|
||||
Second
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Class containing all data used by the LookDev Window to render
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class Context : ScriptableObject, IDisposable
|
||||
{
|
||||
[SerializeField]
|
||||
string m_EnvironmentLibraryGUID = ""; //Empty GUID
|
||||
|
||||
[SerializeField]
|
||||
bool m_CameraSynced = true;
|
||||
|
||||
EnvironmentLibrary m_EnvironmentLibrary;
|
||||
|
||||
/// <summary>The currently used Environment</summary>
|
||||
public EnvironmentLibrary environmentLibrary
|
||||
{
|
||||
get
|
||||
{
|
||||
//check if asset deleted by user
|
||||
if (m_EnvironmentLibrary != null && AssetDatabase.Contains(m_EnvironmentLibrary))
|
||||
return m_EnvironmentLibrary;
|
||||
|
||||
if (!String.IsNullOrEmpty(m_EnvironmentLibraryGUID))
|
||||
{
|
||||
//user deleted the EnvironmentLibrary asset
|
||||
m_EnvironmentLibraryGUID = ""; //Empty GUID
|
||||
LookDev.currentEnvironmentDisplayer.Repaint();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private set => m_EnvironmentLibrary = value;
|
||||
}
|
||||
|
||||
/// <summary>The currently used layout</summary>
|
||||
[field: SerializeField]
|
||||
public LayoutContext layout { get; private set; } = new LayoutContext();
|
||||
|
||||
/// <summary>
|
||||
/// State if both views camera movement are synced or not
|
||||
/// </summary>
|
||||
public bool cameraSynced
|
||||
{
|
||||
get => m_CameraSynced;
|
||||
set
|
||||
{
|
||||
if (m_CameraSynced ^ value)
|
||||
{
|
||||
if (value)
|
||||
EditorApplication.update += SynchronizeCameraStates;
|
||||
else
|
||||
EditorApplication.update -= SynchronizeCameraStates;
|
||||
m_CameraSynced = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[SerializeField]
|
||||
ViewContext[] m_Views = new ViewContext[2]
|
||||
{
|
||||
new ViewContext(),
|
||||
new ViewContext()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to iterate on views
|
||||
/// </summary>
|
||||
public struct ViewIterator : IEnumerable<ViewContext>
|
||||
{
|
||||
ViewContext[] m_Views;
|
||||
internal ViewIterator(ViewContext[] views)
|
||||
=> m_Views = views;
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to enumerates on ViewContexts
|
||||
/// </summary>
|
||||
/// <returns>Enumerator on ViewContext</returns>
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
=> m_Views.GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to enumerates on ViewContexts
|
||||
/// </summary>
|
||||
/// <returns>Enumerator on ViewContext</returns>
|
||||
IEnumerator<ViewContext> IEnumerable<ViewContext>.GetEnumerator()
|
||||
=> ((IEnumerable<ViewContext>)m_Views).GetEnumerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper function to get ViewIterator on ViewContexts
|
||||
/// </summary>
|
||||
public ViewIterator viewContexts
|
||||
=> new ViewIterator(m_Views);
|
||||
|
||||
/// <summary>
|
||||
/// Get datas relative to a view
|
||||
/// </summary>
|
||||
/// <param name="index">The view index to look at</param>
|
||||
/// <returns>Datas for the selected view</returns>
|
||||
public ViewContext GetViewContent(ViewIndex index)
|
||||
=> m_Views[(int)index];
|
||||
|
||||
internal void Init()
|
||||
{
|
||||
LoadEnvironmentLibraryFromGUID();
|
||||
|
||||
//recompute non serialized computes states
|
||||
layout.gizmoState.Init();
|
||||
|
||||
if (cameraSynced)
|
||||
EditorApplication.update += SynchronizeCameraStates;
|
||||
}
|
||||
|
||||
/// <summary>Update the environment library used.</summary>
|
||||
/// <param name="library">The new EnvironmentLibrary</param>
|
||||
public void UpdateEnvironmentLibrary(EnvironmentLibrary library)
|
||||
{
|
||||
m_EnvironmentLibraryGUID = "";
|
||||
environmentLibrary = null;
|
||||
if (library == null || library.Equals(null))
|
||||
return;
|
||||
|
||||
m_EnvironmentLibraryGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(library));
|
||||
environmentLibrary = library;
|
||||
}
|
||||
|
||||
void LoadEnvironmentLibraryFromGUID()
|
||||
{
|
||||
environmentLibrary = null;
|
||||
|
||||
GUID storedGUID;
|
||||
GUID.TryParse(m_EnvironmentLibraryGUID, out storedGUID);
|
||||
if (storedGUID.Empty())
|
||||
return;
|
||||
|
||||
string path = AssetDatabase.GUIDToAssetPath(m_EnvironmentLibraryGUID);
|
||||
environmentLibrary = AssetDatabase.LoadAssetAtPath<EnvironmentLibrary>(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Synchronize cameras from both view using data from the baseCameraState
|
||||
/// </summary>
|
||||
/// <param name="baseCameraState">The <see cref="ViewIndex"/> to be used as reference</param>
|
||||
public void SynchronizeCameraStates(ViewIndex baseCameraState)
|
||||
{
|
||||
switch (baseCameraState)
|
||||
{
|
||||
case ViewIndex.First:
|
||||
m_Views[1].camera.SynchronizeFrom(m_Views[0].camera);
|
||||
break;
|
||||
case ViewIndex.Second:
|
||||
m_Views[0].camera.SynchronizeFrom(m_Views[1].camera);
|
||||
break;
|
||||
default:
|
||||
throw new System.ArgumentException("Unknow ViewIndex given in parameter.");
|
||||
}
|
||||
}
|
||||
|
||||
void SynchronizeCameraStates()
|
||||
=> SynchronizeCameraStates(layout.lastFocusedView);
|
||||
|
||||
/// <summary>
|
||||
/// Change focused view.
|
||||
/// Focused view is the base view to copy data when syncing views' cameras
|
||||
/// </summary>
|
||||
/// <param name="index">The index of the view</param>
|
||||
public void SetFocusedCamera(ViewIndex index)
|
||||
=> layout.lastFocusedView = index;
|
||||
|
||||
|
||||
private bool disposedValue = false; // To detect redundant calls
|
||||
/// <summary>Disposable behaviour</summary>
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (cameraSynced)
|
||||
EditorApplication.update -= SynchronizeCameraStates;
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool HasLibraryAssetChanged(EnvironmentLibrary environmentLibrary)
|
||||
{
|
||||
if (environmentLibrary == null)
|
||||
return !String.IsNullOrEmpty(m_EnvironmentLibraryGUID);
|
||||
|
||||
return m_EnvironmentLibraryGUID != AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(environmentLibrary));
|
||||
}
|
||||
|
||||
internal void FullReimportEnvironmentLibrary()
|
||||
{
|
||||
if (environmentLibrary == null)
|
||||
return;
|
||||
|
||||
// refresh AssetDatabase in case of undo/redo creating/destructing environment subasset
|
||||
string libraryPath = AssetDatabase.GetAssetPath(environmentLibrary);
|
||||
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(environmentLibrary), ImportAssetOptions.DontDownloadFromCacheServer | ImportAssetOptions.ForceSynchronousImport | ImportAssetOptions.ForceUpdate | ImportAssetOptions.ImportRecursive);
|
||||
UpdateEnvironmentLibrary(AssetDatabase.LoadAssetAtPath<EnvironmentLibrary>(libraryPath));
|
||||
EditorUtility.SetDirty(environmentLibrary);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data regarding the layout currently used in LookDev
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class LayoutContext
|
||||
{
|
||||
/// <summary>The layout used</summary>
|
||||
public Layout viewLayout;
|
||||
/// <summary>The last focused view</summary>
|
||||
public ViewIndex lastFocusedView = ViewIndex.First;
|
||||
/// <summary>The state of the side panel</summary>
|
||||
public SidePanel showedSidePanel;
|
||||
/// <summary>The view to change when manipulating the Debug side panel</summary>
|
||||
[NonSerialized]
|
||||
public TargetDebugView debugPanelSource = TargetDebugView.Both;
|
||||
|
||||
[SerializeField]
|
||||
internal ComparisonGizmoState gizmoState = new ComparisonGizmoState();
|
||||
|
||||
internal bool isSimpleView => viewLayout == Layout.FullFirstView || viewLayout == Layout.FullSecondView;
|
||||
internal bool isMultiView => viewLayout == Layout.HorizontalSplit || viewLayout == Layout.VerticalSplit;
|
||||
internal bool isCombinedView => viewLayout == Layout.CustomSplit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Data container containing content of a view
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class ViewContext
|
||||
{
|
||||
/// <summary>The position and rotation of the camera</summary>
|
||||
[field: SerializeField]
|
||||
public CameraState camera { get; private set; } = new CameraState();
|
||||
|
||||
/// <summary>The currently viewed debugState</summary>
|
||||
public DebugContext debug { get; private set; } = new DebugContext();
|
||||
|
||||
//Environment asset, sub-asset (under a library) or cubemap
|
||||
[SerializeField]
|
||||
string m_EnvironmentGUID = ""; //Empty GUID
|
||||
|
||||
/// <summary>
|
||||
/// Check if an Environment is registered for this view.
|
||||
/// The result will be accurate even if the Environment have not been reloaded yet.
|
||||
/// </summary>
|
||||
public bool hasEnvironment => !String.IsNullOrEmpty(m_EnvironmentGUID);
|
||||
|
||||
/// <summary>The currently used Environment</summary>
|
||||
public Environment environment { get; private set; }
|
||||
|
||||
[SerializeField]
|
||||
string viewedObjectAssetGUID = ""; //Empty GUID
|
||||
|
||||
// Careful here: we want to keep it while reloading script.
|
||||
// But from one unity editor to an other, ID are not kept.
|
||||
// So, only use it when reloading from script update.
|
||||
[SerializeField]
|
||||
int viewedObjecHierarchytInstanceID;
|
||||
|
||||
/// <summary>
|
||||
/// Check if an Environment is registered for this view.
|
||||
/// The result will be accurate even if the object have not been reloaded yet.
|
||||
/// </summary>
|
||||
public bool hasViewedObject =>
|
||||
!String.IsNullOrEmpty(viewedObjectAssetGUID)
|
||||
|| viewedObjecHierarchytInstanceID != 0;
|
||||
|
||||
/// <summary>Reference to the object given for instantiation.</summary>
|
||||
public GameObject viewedObjectReference { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The currently displayed instance of <see cref="viewedObjectReference"/>.
|
||||
/// It will be instantiated when pushing changes to renderer.
|
||||
/// See <see cref="LookDev.SaveContextChangeAndApply(ViewIndex)"/>
|
||||
/// </summary>
|
||||
public GameObject viewedInstanceInPreview { get; internal set; }
|
||||
|
||||
/// <summary>Update the environment used.</summary>
|
||||
/// <param name="environmentOrCubemapAsset">
|
||||
/// The new <see cref="Environment"/> to use.
|
||||
/// Or the <see cref="Cubemap"/> to use to build a new one.
|
||||
/// Other types will raise an ArgumentException.
|
||||
/// </param>
|
||||
public void UpdateEnvironment(UnityEngine.Object environmentOrCubemapAsset)
|
||||
{
|
||||
m_EnvironmentGUID = "";
|
||||
environment = null;
|
||||
if (environmentOrCubemapAsset == null || environmentOrCubemapAsset.Equals(null))
|
||||
return;
|
||||
|
||||
if (!(environmentOrCubemapAsset is Environment)
|
||||
&& !(environmentOrCubemapAsset is Cubemap))
|
||||
throw new System.ArgumentException("Only Environment or Cubemap accepted for environmentOrCubemapAsset parameter");
|
||||
|
||||
string GUID;
|
||||
long localIDInFile;
|
||||
AssetDatabase.TryGetGUIDAndLocalFileIdentifier(environmentOrCubemapAsset, out GUID, out localIDInFile);
|
||||
m_EnvironmentGUID = $"{GUID},{localIDInFile}";
|
||||
|
||||
if (environmentOrCubemapAsset is Environment)
|
||||
environment = environmentOrCubemapAsset as Environment;
|
||||
else //Cubemap
|
||||
environment = Environment.GetTemporaryEnvironmentForCubemap(environmentOrCubemapAsset as Cubemap);
|
||||
}
|
||||
|
||||
void LoadEnvironmentFromGUID()
|
||||
{
|
||||
environment = null;
|
||||
|
||||
GUID storedGUID;
|
||||
string[] GUIDAndLocalIDInFile = m_EnvironmentGUID.Split(new[] { ',' });
|
||||
GUID.TryParse(GUIDAndLocalIDInFile[0], out storedGUID);
|
||||
if (storedGUID.Empty())
|
||||
return;
|
||||
long localIDInFile = GUIDAndLocalIDInFile.Length < 2 ? 0L : long.Parse(GUIDAndLocalIDInFile[1]);
|
||||
|
||||
string path = AssetDatabase.GUIDToAssetPath(GUIDAndLocalIDInFile[0]);
|
||||
|
||||
Type savedType = AssetDatabase.GetMainAssetTypeAtPath(path);
|
||||
if (savedType == typeof(EnvironmentLibrary))
|
||||
{
|
||||
object[] loaded = AssetDatabase.LoadAllAssetsAtPath(path);
|
||||
for (int i = 0; i < loaded.Length; ++i)
|
||||
{
|
||||
string garbage;
|
||||
long testedLocalIndex;
|
||||
if (AssetDatabase.TryGetGUIDAndLocalFileIdentifier((UnityEngine.Object)loaded[i], out garbage, out testedLocalIndex)
|
||||
&& testedLocalIndex == localIDInFile)
|
||||
{
|
||||
environment = loaded[i] as Environment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (savedType == typeof(Environment))
|
||||
environment = AssetDatabase.LoadAssetAtPath<Environment>(path);
|
||||
else if (savedType == typeof(Cubemap))
|
||||
{
|
||||
Cubemap cubemap = AssetDatabase.LoadAssetAtPath<Cubemap>(path);
|
||||
environment = Environment.GetTemporaryEnvironmentForCubemap(cubemap);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Update the object reference used for instantiation.</summary>
|
||||
/// <param name="viewedObject">The new reference.</param>
|
||||
public void UpdateViewedObject(GameObject viewedObject)
|
||||
{
|
||||
viewedObjectAssetGUID = "";
|
||||
viewedObjecHierarchytInstanceID = 0;
|
||||
viewedObjectReference = null;
|
||||
if (viewedObject == null || viewedObject.Equals(null))
|
||||
return;
|
||||
|
||||
bool fromHierarchy = viewedObject.scene.IsValid();
|
||||
if (fromHierarchy)
|
||||
viewedObjecHierarchytInstanceID = viewedObject.GetInstanceID();
|
||||
else
|
||||
viewedObjectAssetGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(viewedObject));
|
||||
viewedObjectReference = viewedObject;
|
||||
}
|
||||
|
||||
//WARNING: only for script reloading
|
||||
void LoadViewedObject()
|
||||
{
|
||||
viewedObjectReference = null;
|
||||
|
||||
GUID storedGUID;
|
||||
GUID.TryParse(viewedObjectAssetGUID, out storedGUID);
|
||||
if (!storedGUID.Empty())
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(viewedObjectAssetGUID);
|
||||
viewedObjectReference = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
||||
}
|
||||
else if (viewedObjecHierarchytInstanceID != 0)
|
||||
{
|
||||
viewedObjectReference = EditorUtility.InstanceIDToObject(viewedObjecHierarchytInstanceID) as GameObject;
|
||||
}
|
||||
}
|
||||
|
||||
internal void LoadAll(bool reloadWithTemporaryID)
|
||||
{
|
||||
if (!reloadWithTemporaryID)
|
||||
CleanTemporaryObjectIndexes();
|
||||
LoadEnvironmentFromGUID();
|
||||
LoadViewedObject();
|
||||
}
|
||||
|
||||
internal void CleanTemporaryObjectIndexes()
|
||||
=> viewedObjecHierarchytInstanceID = 0;
|
||||
|
||||
/// <summary>Reset the camera state to default values</summary>
|
||||
public void ResetCameraState()
|
||||
=> camera.Reset();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Class that will contain debug value used.
|
||||
/// </summary>
|
||||
public class DebugContext
|
||||
{
|
||||
/// <summary>Display shadows in view.</summary>
|
||||
public bool shadow = true;
|
||||
|
||||
/// <summary>Debug mode displayed. -1 means none.</summary>
|
||||
public int viewMode = -1;
|
||||
|
||||
///// <summary>Display the debug grey balls</summary>
|
||||
//public bool greyBalls;
|
||||
|
||||
//[SerializeField]
|
||||
//string colorChartGUID = ""; //Empty GUID
|
||||
|
||||
///// <summary>The currently used color chart</summary>
|
||||
//public Texture2D colorChart { get; private set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,133 @@
|
||||
Shader "Hidden/LookDev/CubeToLatlong"
|
||||
{
|
||||
Properties
|
||||
{
|
||||
[NoScaleOffset] _MainTex ("Cubemap", Any) = "grey" {}
|
||||
_CubeToLatLongParams ("Parameters", Vector) = (0.0, 0.0, 0.0, 0.0)
|
||||
_WindowParams("Window params", Vector) = (0.0, 0.0, 0.0, 0.0)
|
||||
}
|
||||
|
||||
CGINCLUDE
|
||||
|
||||
#include "UnityCG.cginc"
|
||||
|
||||
uniform float4 _MainTex_HDR;
|
||||
uniform float4 _MainTex_ST;
|
||||
UNITY_DECLARE_TEXCUBE(_MainTex);
|
||||
uniform float4 _CubeToLatLongParams; // x angle offset, y alpha, z intensity w lod to use
|
||||
uniform float4 _WindowParams; // x Editor windows height, y Environment windows posY, z margin (constant of 2), w PixelsPerPoint
|
||||
uniform bool _ManualTex2SRGB;
|
||||
|
||||
#define OutputAlpha _CubeToLatLongParams.y
|
||||
#define Intensity _CubeToLatLongParams.z
|
||||
#define CurrentLOD _CubeToLatLongParams.w
|
||||
|
||||
struct appdata_t
|
||||
{
|
||||
float4 vertex : POSITION;
|
||||
float2 texcoord : TEXCOORD0;
|
||||
};
|
||||
|
||||
struct v2f
|
||||
{
|
||||
float2 texcoord : TEXCOORD0;
|
||||
float4 vertex : SV_POSITION;
|
||||
};
|
||||
|
||||
v2f vert(appdata_t IN)
|
||||
{
|
||||
v2f OUT;
|
||||
OUT.vertex = UnityObjectToClipPos(IN.vertex);
|
||||
OUT.texcoord = TRANSFORM_TEX(IN.texcoord, _MainTex);
|
||||
|
||||
return OUT;
|
||||
}
|
||||
|
||||
float4 frag( float2 texcoord : TEXCOORD0,
|
||||
UNITY_VPOS_TYPE vpos : VPOS
|
||||
) : SV_Target
|
||||
{
|
||||
float2 texCoord = texcoord.xy;
|
||||
float theta = texCoord.y * UNITY_PI;
|
||||
float phi = (texCoord.x * 2.f * UNITY_PI - UNITY_PI*0.5f) - _CubeToLatLongParams.x;
|
||||
|
||||
float cosTheta = cos(theta);
|
||||
float sinTheta = sqrt(1.0f - min(1.0f, cosTheta*cosTheta));
|
||||
float cosPhi = cos(phi);
|
||||
float sinPhi = sin(phi);
|
||||
|
||||
float3 direction = float3(sinTheta*cosPhi, cosTheta, sinTheta*sinPhi);
|
||||
direction.xy *= -1.0;
|
||||
float4 ret = float4(DecodeHDR(UNITY_SAMPLE_TEXCUBE_LOD(_MainTex, direction, CurrentLOD), _MainTex_HDR) * Intensity, OutputAlpha);
|
||||
if (_ManualTex2SRGB)
|
||||
ret.rgb = LinearToGammaSpace(ret.rgb);
|
||||
|
||||
// Clip outside of the library window
|
||||
|
||||
// Editor windows is like this:
|
||||
//------
|
||||
// Margin (2)
|
||||
// Scene - Game - Asset Store <= What we call tab size
|
||||
//------
|
||||
// Settings - Views <= what we call menu size
|
||||
//----
|
||||
// View size with Environment windows)
|
||||
//
|
||||
// _WindowParams.x contain the height of the editor windows
|
||||
// _WindowParams.y contain the start of the windows environment in the windows editor, i.e the menu size + tab size
|
||||
// _WindowParams.z contain a constant margin of 2 (don't know how to retrieve that)
|
||||
// _WindowParams.w is PixelsPerPoin (To handle retina display on OSX))
|
||||
|
||||
// We use VPOS register to clip, VPOS is dependent on the API. It is reversed in openGL.
|
||||
// There is no need to clip when y is above height because the editor windows will clip it
|
||||
// vertex.y is relative to editor windows
|
||||
#if UNITY_UV_STARTS_AT_TOP
|
||||
if ((vpos.y / _WindowParams.w) < (_WindowParams.y + _WindowParams.z))
|
||||
#else
|
||||
// vertex.y is reversed (start from bottom of the editor windsows)
|
||||
vpos.y = _WindowParams.x - (vpos.y / _WindowParams.w);
|
||||
if (vpos.y < _WindowParams.z)
|
||||
#endif
|
||||
{
|
||||
clip(-1);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
ENDCG
|
||||
|
||||
SubShader
|
||||
{
|
||||
Tags
|
||||
{
|
||||
"ForceSupported"="True"
|
||||
}
|
||||
|
||||
Lighting Off
|
||||
Cull Off
|
||||
ZTest Always
|
||||
ZWrite Off
|
||||
|
||||
Pass
|
||||
{
|
||||
Blend One Zero
|
||||
|
||||
CGPROGRAM
|
||||
#pragma fragment frag
|
||||
#pragma vertex vert
|
||||
#pragma target 3.0
|
||||
ENDCG
|
||||
}
|
||||
|
||||
Pass
|
||||
{
|
||||
Blend SrcAlpha OneMinusSrcAlpha
|
||||
|
||||
CGPROGRAM
|
||||
#pragma fragment frag
|
||||
#pragma vertex vert
|
||||
#pragma target 3.0
|
||||
ENDCG
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,69 @@
|
||||
|
||||
/* ******************************* */
|
||||
/* override for personal skin them */
|
||||
/* ******************************* */
|
||||
#environmentContainer > EnvironmentElement
|
||||
{
|
||||
border-color: #999999;
|
||||
}
|
||||
|
||||
#separator
|
||||
{
|
||||
border-color: #999999;
|
||||
}
|
||||
|
||||
.list-environment-overlay > ToolbarButton
|
||||
{
|
||||
background-color: #CBCBCB;
|
||||
}
|
||||
|
||||
.list-environment-overlay > ToolbarButton:hover
|
||||
{
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
#inspector-header
|
||||
{
|
||||
background-color: #CBCBCB;
|
||||
border-color: #999999;
|
||||
}
|
||||
|
||||
#separator-line
|
||||
{
|
||||
background-color: #CBCBCB;
|
||||
}
|
||||
|
||||
Image.unity-list-view__item:selected
|
||||
{
|
||||
border-color: #3A72B0;
|
||||
}
|
||||
|
||||
#environmentContainer
|
||||
{
|
||||
border-color: #999999;
|
||||
}
|
||||
|
||||
#debugContainer
|
||||
{
|
||||
border-color: #999999;
|
||||
}
|
||||
|
||||
#debugToolbar
|
||||
{
|
||||
border-color: #999999;
|
||||
}
|
||||
|
||||
MultipleSourcePopupField > MultipleDifferentValue
|
||||
{
|
||||
background-color: #DFDFDF;
|
||||
}
|
||||
|
||||
MultipleSourcePopupField > MultipleDifferentValue:hover
|
||||
{
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
#sunToBrightestButton:hover
|
||||
{
|
||||
background-color: #e8e8e8;
|
||||
}
|
@@ -0,0 +1,318 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
partial class DisplayWindow
|
||||
{
|
||||
static partial class Style
|
||||
{
|
||||
internal const string k_DebugViewLabel = "Selected View";
|
||||
internal const string k_DebugShadowLabel = "Display Shadows";
|
||||
internal const string k_DebugViewMode = "View Mode";
|
||||
|
||||
internal static readonly Texture2D k_LockOpen = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Unlocked", forceLowRes: true);
|
||||
internal static readonly Texture2D k_LockClose = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Locked", forceLowRes: true);
|
||||
|
||||
// /!\ WARNING:
|
||||
//The following const are used in the uss.
|
||||
//If you change them, update the uss file too.
|
||||
internal const string k_DebugToolbarName = "debugToolbar";
|
||||
}
|
||||
|
||||
MultipleSourceToggle m_Shadow;
|
||||
MultipleSourcePopupField m_DebugView;
|
||||
List<string> listDebugMode;
|
||||
|
||||
bool cameraSynced
|
||||
=> LookDev.currentContext.cameraSynced;
|
||||
|
||||
ViewContext lastFocusedViewContext
|
||||
=> LookDev.currentContext.GetViewContent(LookDev.currentContext.layout.lastFocusedView);
|
||||
|
||||
TargetDebugView targetDebugView
|
||||
{
|
||||
get => layout.debugPanelSource;
|
||||
set
|
||||
{
|
||||
if (layout.debugPanelSource != value)
|
||||
{
|
||||
layout.debugPanelSource = value;
|
||||
UpdateSideDebugPanelProperties();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool debugView1SidePanel
|
||||
=> targetDebugView == TargetDebugView.First
|
||||
|| targetDebugView == TargetDebugView.Both;
|
||||
|
||||
bool debugView2SidePanel
|
||||
=> targetDebugView == TargetDebugView.Second
|
||||
|| targetDebugView == TargetDebugView.Both;
|
||||
|
||||
void ApplyInFilteredViewsContext(Action<ViewContext> action)
|
||||
{
|
||||
if (debugView1SidePanel)
|
||||
action.Invoke(LookDev.currentContext.GetViewContent(ViewIndex.First));
|
||||
if (debugView2SidePanel)
|
||||
action.Invoke(LookDev.currentContext.GetViewContent(ViewIndex.Second));
|
||||
}
|
||||
|
||||
T GetInFilteredViewsContext<T>(Func<ViewContext, T> getter, out bool multipleDifferentValue)
|
||||
{
|
||||
T res1 = default;
|
||||
T res2 = default;
|
||||
multipleDifferentValue = false;
|
||||
bool view1 = debugView1SidePanel;
|
||||
bool view2 = debugView2SidePanel;
|
||||
|
||||
if (view1)
|
||||
res1 = getter.Invoke(LookDev.currentContext.GetViewContent(ViewIndex.First));
|
||||
if (view2)
|
||||
res2 = getter.Invoke(LookDev.currentContext.GetViewContent(ViewIndex.Second));
|
||||
if (view1 && view2 && !res1.Equals(res2))
|
||||
{
|
||||
multipleDifferentValue = true;
|
||||
return default;
|
||||
}
|
||||
else
|
||||
return view1 ? res1 : res2;
|
||||
}
|
||||
|
||||
void ReadValueFromSourcesWithoutNotify<T, K>(K element, Func<ViewContext, T> from)
|
||||
where K : BaseField<T>, IMultipleSource
|
||||
{
|
||||
bool multipleDifferentValue;
|
||||
T newValue = GetInFilteredViewsContext(from, out multipleDifferentValue);
|
||||
if (element is MultipleSourcePopupField)
|
||||
element.inMultipleValueState = multipleDifferentValue;
|
||||
element.SetValueWithoutNotify(newValue);
|
||||
element.inMultipleValueState = multipleDifferentValue;
|
||||
}
|
||||
|
||||
#region Hack_Support_UIElement_MixedValueState
|
||||
|
||||
class MultipleDifferentValue : TextElement
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<MultipleDifferentValue, UxmlTraits> {}
|
||||
|
||||
public new class UxmlTraits : TextElement.UxmlTraits {}
|
||||
|
||||
public new static readonly string ussClassName = "unity-multipledifferentevalue";
|
||||
|
||||
public MultipleDifferentValue()
|
||||
{
|
||||
AddToClassList(ussClassName);
|
||||
text = "-";
|
||||
}
|
||||
}
|
||||
|
||||
interface IMultipleSource
|
||||
{
|
||||
bool inMultipleValueState { get; set; }
|
||||
}
|
||||
|
||||
class MultipleSourceToggle : Toggle, IMultipleSource
|
||||
{
|
||||
MultipleDifferentValue m_MultipleOverlay;
|
||||
bool m_InMultipleValueState = false;
|
||||
|
||||
public bool inMultipleValueState
|
||||
{
|
||||
get => m_InMultipleValueState;
|
||||
set
|
||||
{
|
||||
if (value != m_InMultipleValueState)
|
||||
{
|
||||
m_MultipleOverlay.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
m_InMultipleValueState = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MultipleSourceToggle() : base()
|
||||
{
|
||||
m_MultipleOverlay = new MultipleDifferentValue();
|
||||
this.Q(name: "unity-checkmark").Add(m_MultipleOverlay);
|
||||
m_MultipleOverlay.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
public MultipleSourceToggle(string label) : base(label)
|
||||
{
|
||||
m_MultipleOverlay = new MultipleDifferentValue();
|
||||
this.Q(name: "unity-checkmark").Add(m_MultipleOverlay);
|
||||
m_MultipleOverlay.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
public override void SetValueWithoutNotify(bool newValue)
|
||||
{
|
||||
if (inMultipleValueState)
|
||||
inMultipleValueState = false;
|
||||
base.SetValueWithoutNotify(newValue);
|
||||
}
|
||||
|
||||
public override bool value
|
||||
{
|
||||
get => inMultipleValueState ? default : base.value;
|
||||
set
|
||||
{
|
||||
if (inMultipleValueState)
|
||||
inMultipleValueState = false;
|
||||
base.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleSourcePopupField : PopupField<string>, IMultipleSource
|
||||
{
|
||||
internal readonly int count;
|
||||
MultipleDifferentValue m_MultipleOverlay;
|
||||
bool m_InMultipleValueState = false;
|
||||
|
||||
public bool inMultipleValueState
|
||||
{
|
||||
get => m_InMultipleValueState;
|
||||
set
|
||||
{
|
||||
if (value != m_InMultipleValueState)
|
||||
{
|
||||
m_MultipleOverlay.style.display = value ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
m_InMultipleValueState = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public MultipleSourcePopupField(string label, List<string> choices, int defaultIndex = 0)
|
||||
: base(
|
||||
label,
|
||||
choices,
|
||||
defaultIndex,
|
||||
null,
|
||||
null)
|
||||
{
|
||||
count = choices.Count;
|
||||
m_MultipleOverlay = new MultipleDifferentValue();
|
||||
Add(m_MultipleOverlay);
|
||||
m_MultipleOverlay.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
public override void SetValueWithoutNotify(string newValue)
|
||||
{
|
||||
// forbid change from not direct selection as it can be find different value at opening
|
||||
if (!inMultipleValueState)
|
||||
base.SetValueWithoutNotify(newValue);
|
||||
}
|
||||
|
||||
public override string value
|
||||
{
|
||||
get => inMultipleValueState ? default : base.value;
|
||||
set
|
||||
{
|
||||
//when actively changing in the drop down, quit mixed value state
|
||||
if (inMultipleValueState)
|
||||
inMultipleValueState = false;
|
||||
base.value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
void CreateDebug()
|
||||
{
|
||||
if (m_MainContainer == null || m_MainContainer.Equals(null))
|
||||
throw new System.MemberAccessException("m_MainContainer should be assigned prior CreateEnvironment()");
|
||||
|
||||
m_DebugContainer = new VisualElement() { name = Style.k_DebugContainerName };
|
||||
m_MainContainer.Add(m_DebugContainer);
|
||||
if (sidePanel == SidePanel.Debug)
|
||||
m_MainContainer.AddToClassList(Style.k_ShowDebugPanelClass);
|
||||
|
||||
AddDebugViewSelector();
|
||||
|
||||
AddDebugShadow();
|
||||
AddDebugViewMode();
|
||||
|
||||
//[TODO: finish]
|
||||
//Toggle greyBalls = new Toggle("Grey balls");
|
||||
//greyBalls.SetValueWithoutNotify(LookDev.currentContext.GetViewContent(LookDev.currentContext.layout.lastFocusedView).debug.greyBalls);
|
||||
//greyBalls.RegisterValueChangedCallback(evt =>
|
||||
//{
|
||||
// LookDev.currentContext.GetViewContent(LookDev.currentContext.layout.lastFocusedView).debug.greyBalls = evt.newValue;
|
||||
//});
|
||||
//m_DebugContainer.Add(greyBalls);
|
||||
|
||||
//[TODO: debug why list sometimes empty on resource reloading]
|
||||
//[TODO: display only per view]
|
||||
|
||||
if (sidePanel == SidePanel.Debug)
|
||||
UpdateSideDebugPanelProperties();
|
||||
}
|
||||
|
||||
void AddDebugViewSelector()
|
||||
{
|
||||
ToolbarRadio viewSelector = new ToolbarRadio()
|
||||
{
|
||||
name = Style.k_DebugToolbarName
|
||||
};
|
||||
viewSelector.AddRadios(new[]
|
||||
{
|
||||
Style.k_Camera1Icon,
|
||||
Style.k_LinkIcon,
|
||||
Style.k_Camera2Icon
|
||||
});
|
||||
viewSelector.SetValueWithoutNotify((int)targetDebugView);
|
||||
viewSelector.RegisterValueChangedCallback(evt
|
||||
=> targetDebugView = (TargetDebugView)evt.newValue);
|
||||
m_DebugContainer.Add(viewSelector);
|
||||
}
|
||||
|
||||
void AddDebugShadow()
|
||||
{
|
||||
m_Shadow = new MultipleSourceToggle(Style.k_DebugShadowLabel);
|
||||
ReadValueFromSourcesWithoutNotify(m_Shadow, view => view.debug.shadow);
|
||||
m_Shadow.RegisterValueChangedCallback(evt
|
||||
=> ApplyInFilteredViewsContext(view => view.debug.shadow = evt.newValue));
|
||||
m_DebugContainer.Add(m_Shadow);
|
||||
}
|
||||
|
||||
void AddDebugViewMode(int pos = -1)
|
||||
{
|
||||
//if debugPanel is open on script reload, at this time
|
||||
//RenderPipelineManager.currentPipeline is still null.
|
||||
//So compute the list on next frame only.
|
||||
if (LookDev.dataProvider == null)
|
||||
{
|
||||
EditorApplication.delayCall += () => AddDebugViewMode(2); //2 = hardcoded position of this field
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_DebugView != null && m_DebugContainer.Contains(m_DebugView))
|
||||
m_DebugContainer.Remove(m_DebugView);
|
||||
|
||||
listDebugMode = new List<string>(LookDev.dataProvider?.supportedDebugModes ?? Enumerable.Empty<string>());
|
||||
listDebugMode.Insert(0, "None");
|
||||
m_DebugView = new MultipleSourcePopupField(Style.k_DebugViewMode, listDebugMode);
|
||||
if (sidePanel == SidePanel.Debug)
|
||||
ReadValueFromSourcesWithoutNotify(m_DebugView, view => listDebugMode[view.debug.viewMode + 1]);
|
||||
m_DebugView.RegisterValueChangedCallback(evt
|
||||
=> ApplyInFilteredViewsContext(view => view.debug.viewMode = listDebugMode.IndexOf(evt.newValue) - 1));
|
||||
if (pos == -1)
|
||||
m_DebugContainer.Add(m_DebugView);
|
||||
else
|
||||
m_DebugContainer.Insert(pos, m_DebugView);
|
||||
}
|
||||
|
||||
void UpdateSideDebugPanelProperties()
|
||||
{
|
||||
ReadValueFromSourcesWithoutNotify(m_Shadow, view => view.debug.shadow);
|
||||
if (m_DebugView != null)
|
||||
ReadValueFromSourcesWithoutNotify(m_DebugView, view => listDebugMode[view.debug.viewMode + 1]);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,482 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>Interface that must implement the EnvironmentLibrary view to communicate with the data management</summary>
|
||||
public interface IEnvironmentDisplayer
|
||||
{
|
||||
/// <summary>Repaint the UI</summary>
|
||||
void Repaint();
|
||||
|
||||
/// <summary>Callback on Environment change in the Library</summary>
|
||||
event Action<EnvironmentLibrary> OnChangingEnvironmentLibrary;
|
||||
}
|
||||
|
||||
partial class DisplayWindow : IEnvironmentDisplayer
|
||||
{
|
||||
static partial class Style
|
||||
{
|
||||
internal static readonly Texture2D k_AddIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Add", forceLowRes: true);
|
||||
internal static readonly Texture2D k_RemoveIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Remove", forceLowRes: true);
|
||||
internal static readonly Texture2D k_DuplicateIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Duplicate", forceLowRes: true);
|
||||
|
||||
internal const string k_DragAndDropLibrary = "Drag and drop EnvironmentLibrary here";
|
||||
}
|
||||
|
||||
VisualElement m_EnvironmentContainer;
|
||||
ListView m_EnvironmentList;
|
||||
EnvironmentElement m_EnvironmentInspector;
|
||||
UIElements.Toolbar m_EnvironmentListToolbar;
|
||||
UIElements.ObjectField m_LibraryField;
|
||||
|
||||
//event Action<UnityEngine.Object> OnAddingEnvironmentInternal;
|
||||
//event Action<UnityEngine.Object> IEnvironmentDisplayer.OnAddingEnvironment
|
||||
//{
|
||||
// add => OnAddingEnvironmentInternal += value;
|
||||
// remove => OnAddingEnvironmentInternal -= value;
|
||||
//}
|
||||
|
||||
//event Action<int> OnRemovingEnvironmentInternal;
|
||||
//event Action<int> IEnvironmentDisplayer.OnRemovingEnvironment
|
||||
//{
|
||||
// add => OnRemovingEnvironmentInternal += value;
|
||||
// remove => OnRemovingEnvironmentInternal -= value;
|
||||
//}
|
||||
|
||||
event Action<EnvironmentLibrary> OnChangingEnvironmentLibraryInternal;
|
||||
event Action<EnvironmentLibrary> IEnvironmentDisplayer.OnChangingEnvironmentLibrary
|
||||
{
|
||||
add => OnChangingEnvironmentLibraryInternal += value;
|
||||
remove => OnChangingEnvironmentLibraryInternal -= value;
|
||||
}
|
||||
|
||||
static int FirstVisibleIndex(ListView listView)
|
||||
=> (int)(listView.Q<ScrollView>().scrollOffset.y / listView.itemHeight);
|
||||
|
||||
void CreateEnvironment()
|
||||
{
|
||||
if (m_MainContainer == null || m_MainContainer.Equals(null))
|
||||
throw new System.MemberAccessException("m_MainContainer should be assigned prior CreateEnvironment()");
|
||||
|
||||
m_EnvironmentContainer = new VisualElement() { name = Style.k_EnvironmentContainerName };
|
||||
m_MainContainer.Add(m_EnvironmentContainer);
|
||||
if (sidePanel == SidePanel.Environment)
|
||||
m_MainContainer.AddToClassList(Style.k_ShowEnvironmentPanelClass);
|
||||
|
||||
m_EnvironmentInspector = new EnvironmentElement(withPreview: false, () =>
|
||||
{
|
||||
LookDev.SaveContextChangeAndApply(ViewIndex.First);
|
||||
LookDev.SaveContextChangeAndApply(ViewIndex.Second);
|
||||
});
|
||||
m_EnvironmentList = new ListView();
|
||||
m_EnvironmentList.AddToClassList("list-environment");
|
||||
m_EnvironmentList.selectionType = SelectionType.Single;
|
||||
m_EnvironmentList.itemHeight = EnvironmentElement.k_SkyThumbnailHeight;
|
||||
m_EnvironmentList.makeItem = () =>
|
||||
{
|
||||
var preview = new Image();
|
||||
preview.AddManipulator(new EnvironmentPreviewDragger(this, m_ViewContainer));
|
||||
return preview;
|
||||
};
|
||||
m_EnvironmentList.bindItem = (e, i) =>
|
||||
{
|
||||
if (LookDev.currentContext.environmentLibrary == null)
|
||||
return;
|
||||
|
||||
(e as Image).image = EnvironmentElement.GetLatLongThumbnailTexture(
|
||||
LookDev.currentContext.environmentLibrary[i],
|
||||
EnvironmentElement.k_SkyThumbnailWidth);
|
||||
};
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
m_EnvironmentList.onSelectionChange += objects =>
|
||||
{
|
||||
bool empty = !objects.GetEnumerator().MoveNext();
|
||||
if (empty || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
|
||||
#else
|
||||
m_EnvironmentList.onSelectionChanged += objects =>
|
||||
|
||||
{
|
||||
if (objects.Count == 0 || (LookDev.currentContext.environmentLibrary?.Count ?? 0) == 0)
|
||||
#endif
|
||||
{
|
||||
m_EnvironmentInspector.style.visibility = Visibility.Hidden;
|
||||
m_EnvironmentInspector.style.height = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EnvironmentInspector.style.visibility = Visibility.Visible;
|
||||
m_EnvironmentInspector.style.height = new StyleLength(StyleKeyword.Auto);
|
||||
int firstVisibleIndex = FirstVisibleIndex(m_EnvironmentList);
|
||||
Environment environment = LookDev.currentContext.environmentLibrary[m_EnvironmentList.selectedIndex];
|
||||
var container = m_EnvironmentList.Q("unity-content-container");
|
||||
if (m_EnvironmentList.selectedIndex - firstVisibleIndex >= container.childCount || m_EnvironmentList.selectedIndex < firstVisibleIndex)
|
||||
{
|
||||
m_EnvironmentList.ScrollToItem(m_EnvironmentList.selectedIndex);
|
||||
firstVisibleIndex = FirstVisibleIndex(m_EnvironmentList);
|
||||
}
|
||||
Image deportedLatLong = container[m_EnvironmentList.selectedIndex - firstVisibleIndex] as Image;
|
||||
m_EnvironmentInspector.Bind(environment, deportedLatLong);
|
||||
}
|
||||
};
|
||||
#if UNITY_2020_1_OR_NEWER
|
||||
m_EnvironmentList.onItemsChosen += objCollection =>
|
||||
{
|
||||
foreach (var obj in objCollection)
|
||||
EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary ? [(int)obj]);
|
||||
};
|
||||
#else
|
||||
m_EnvironmentList.onItemChosen += obj =>
|
||||
EditorGUIUtility.PingObject(LookDev.currentContext.environmentLibrary ? [(int)obj]);
|
||||
#endif
|
||||
m_NoEnvironmentList = new Label(Style.k_DragAndDropLibrary);
|
||||
m_NoEnvironmentList.style.flexGrow = 1;
|
||||
m_NoEnvironmentList.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
m_EnvironmentContainer.Add(m_EnvironmentInspector);
|
||||
|
||||
m_EnvironmentListToolbar = new UIElements.Toolbar();
|
||||
ToolbarButton addEnvironment = new ToolbarButton(() =>
|
||||
{
|
||||
if (LookDev.currentContext.environmentLibrary == null)
|
||||
return;
|
||||
|
||||
LookDev.currentContext.environmentLibrary.Add();
|
||||
RefreshLibraryDisplay();
|
||||
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
|
||||
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.Count - 1;
|
||||
ScrollToEnd();
|
||||
})
|
||||
{
|
||||
name = "add",
|
||||
tooltip = "Add new empty environment"
|
||||
};
|
||||
addEnvironment.Add(new Image() { image = Style.k_AddIcon });
|
||||
ToolbarButton removeEnvironment = new ToolbarButton(() =>
|
||||
{
|
||||
if (m_EnvironmentList.selectedIndex == -1 || LookDev.currentContext.environmentLibrary == null)
|
||||
return;
|
||||
LookDev.currentContext.environmentLibrary?.Remove(m_EnvironmentList.selectedIndex);
|
||||
RefreshLibraryDisplay();
|
||||
m_EnvironmentList.selectedIndex = -1;
|
||||
})
|
||||
{
|
||||
name = "remove",
|
||||
tooltip = "Remove environment currently selected"
|
||||
};
|
||||
removeEnvironment.Add(new Image() { image = Style.k_RemoveIcon });
|
||||
ToolbarButton duplicateEnvironment = new ToolbarButton(() =>
|
||||
{
|
||||
if (m_EnvironmentList.selectedIndex == -1 || LookDev.currentContext.environmentLibrary == null)
|
||||
return;
|
||||
LookDev.currentContext.environmentLibrary.Duplicate(m_EnvironmentList.selectedIndex);
|
||||
RefreshLibraryDisplay();
|
||||
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
|
||||
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.Count - 1;
|
||||
ScrollToEnd();
|
||||
})
|
||||
{
|
||||
name = "duplicate",
|
||||
tooltip = "Duplicate environment currently selected"
|
||||
};
|
||||
duplicateEnvironment.Add(new Image() { image = Style.k_DuplicateIcon });
|
||||
m_EnvironmentListToolbar.Add(addEnvironment);
|
||||
m_EnvironmentListToolbar.Add(removeEnvironment);
|
||||
m_EnvironmentListToolbar.Add(duplicateEnvironment);
|
||||
m_EnvironmentListToolbar.AddToClassList("list-environment-overlay");
|
||||
|
||||
var m_EnvironmentInspectorSeparator = new VisualElement() { name = "separator-line" };
|
||||
m_EnvironmentInspectorSeparator.Add(new VisualElement() { name = "separator" });
|
||||
m_EnvironmentContainer.Add(m_EnvironmentInspectorSeparator);
|
||||
|
||||
VisualElement listContainer = new VisualElement();
|
||||
listContainer.AddToClassList("list-environment");
|
||||
listContainer.Add(m_EnvironmentList);
|
||||
listContainer.Add(m_EnvironmentListToolbar);
|
||||
|
||||
m_LibraryField = new ObjectField("Library")
|
||||
{
|
||||
tooltip = "The currently used library"
|
||||
};
|
||||
m_LibraryField.allowSceneObjects = false;
|
||||
m_LibraryField.objectType = typeof(EnvironmentLibrary);
|
||||
m_LibraryField.SetValueWithoutNotify(LookDev.currentContext.environmentLibrary);
|
||||
m_LibraryField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
m_EnvironmentList.selectedIndex = -1;
|
||||
OnChangingEnvironmentLibraryInternal?.Invoke(evt.newValue as EnvironmentLibrary);
|
||||
RefreshLibraryDisplay();
|
||||
});
|
||||
|
||||
var environmentListCreationToolbar = new UIElements.Toolbar()
|
||||
{
|
||||
name = "environmentListCreationToolbar"
|
||||
};
|
||||
environmentListCreationToolbar.Add(m_LibraryField);
|
||||
environmentListCreationToolbar.Add(new ToolbarButton(()
|
||||
=> EnvironmentLibraryCreator.CreateAndAssignTo(m_LibraryField))
|
||||
{
|
||||
text = "New",
|
||||
tooltip = "Create a new EnvironmentLibrary"
|
||||
});
|
||||
|
||||
m_EnvironmentContainer.Add(listContainer);
|
||||
m_EnvironmentContainer.Add(m_NoEnvironmentList);
|
||||
m_EnvironmentContainer.Add(environmentListCreationToolbar);
|
||||
|
||||
//add ability to unselect
|
||||
m_EnvironmentList.RegisterCallback<MouseDownEvent>(evt =>
|
||||
{
|
||||
var clickedIndex = (int)(evt.localMousePosition.y / m_EnvironmentList.itemHeight);
|
||||
if (clickedIndex >= m_EnvironmentList.itemsSource.Count)
|
||||
{
|
||||
m_EnvironmentList.selectedIndex = -1;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
RefreshLibraryDisplay();
|
||||
}
|
||||
|
||||
//necessary as the scrollview need to be updated which take some editor frames.
|
||||
void ScrollToEnd(int attemptRemaining = 5)
|
||||
{
|
||||
m_EnvironmentList.ScrollToItem(-1); //-1: scroll to end
|
||||
if (attemptRemaining > 0)
|
||||
EditorApplication.delayCall += () => ScrollToEnd(--attemptRemaining);
|
||||
}
|
||||
|
||||
void RefreshLibraryDisplay()
|
||||
{
|
||||
if (m_LibraryField != null)
|
||||
m_LibraryField.SetValueWithoutNotify(LookDev.currentContext.environmentLibrary);
|
||||
|
||||
if (m_EnvironmentInspector != null && m_EnvironmentList != null)
|
||||
{
|
||||
int itemMax = LookDev.currentContext.environmentLibrary?.Count ?? 0;
|
||||
if (itemMax == 0 || m_EnvironmentList.selectedIndex == -1)
|
||||
{
|
||||
m_EnvironmentInspector.style.visibility = Visibility.Hidden;
|
||||
m_EnvironmentInspector.style.height = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EnvironmentInspector.style.visibility = Visibility.Visible;
|
||||
m_EnvironmentInspector.style.height = new StyleLength(StyleKeyword.Auto);
|
||||
}
|
||||
|
||||
var items = new List<int>(itemMax);
|
||||
for (int i = 0; i < itemMax; i++)
|
||||
items.Add(i);
|
||||
m_EnvironmentList.itemsSource = items;
|
||||
if (LookDev.currentContext.environmentLibrary == null)
|
||||
{
|
||||
m_EnvironmentList
|
||||
.Q(className: "unity-scroll-view__vertical-scroller")
|
||||
.Q("unity-dragger")
|
||||
.style.visibility = Visibility.Hidden;
|
||||
m_EnvironmentListToolbar.style.visibility = Visibility.Hidden;
|
||||
m_NoEnvironmentList.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_EnvironmentList
|
||||
.Q(className: "unity-scroll-view__vertical-scroller")
|
||||
.Q("unity-dragger")
|
||||
.style.visibility = itemMax == 0
|
||||
? Visibility.Hidden
|
||||
: Visibility.Visible;
|
||||
m_EnvironmentListToolbar.style.visibility = Visibility.Visible;
|
||||
m_NoEnvironmentList.style.display = DisplayStyle.None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DraggingContext StartDragging(VisualElement item, Vector2 worldPosition)
|
||||
=> new DraggingContext(
|
||||
rootVisualElement,
|
||||
item as Image,
|
||||
//note: this even can come before the selection event of the
|
||||
//ListView. Reconstruct index by looking at target of the event.
|
||||
(int)item.layout.y / m_EnvironmentList.itemHeight,
|
||||
worldPosition);
|
||||
|
||||
void EndDragging(DraggingContext context, Vector2 mouseWorldPosition)
|
||||
{
|
||||
Environment environment = LookDev.currentContext.environmentLibrary ? [context.draggedIndex];
|
||||
if (environment == null)
|
||||
return;
|
||||
|
||||
if (m_Views[(int)ViewIndex.First].ContainsPoint(mouseWorldPosition))
|
||||
{
|
||||
if (viewLayout == Layout.CustomSplit)
|
||||
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.Composite, mouseWorldPosition);
|
||||
else
|
||||
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.First, mouseWorldPosition);
|
||||
m_NoEnvironment1.style.visibility = environment == null || environment.Equals(null) ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
else
|
||||
{
|
||||
OnChangingEnvironmentInViewInternal?.Invoke(environment, ViewCompositionIndex.Second, mouseWorldPosition);
|
||||
m_NoEnvironment2.style.visibility = environment == null || environment.Equals(null) ? Visibility.Visible : Visibility.Hidden;
|
||||
}
|
||||
}
|
||||
|
||||
class DraggingContext : IDisposable
|
||||
{
|
||||
const string k_CursorFollowerName = "cursorFollower";
|
||||
public readonly int draggedIndex;
|
||||
readonly Image cursorFollower;
|
||||
readonly Vector2 cursorOffset;
|
||||
readonly VisualElement windowContent;
|
||||
|
||||
public DraggingContext(VisualElement windowContent, Image draggedElement, int draggedIndex, Vector2 worldPosition)
|
||||
{
|
||||
this.windowContent = windowContent;
|
||||
this.draggedIndex = draggedIndex;
|
||||
cursorFollower = new Image()
|
||||
{
|
||||
name = k_CursorFollowerName,
|
||||
image = draggedElement.image
|
||||
};
|
||||
cursorFollower.tintColor = new Color(1f, 1f, 1f, .5f);
|
||||
windowContent.Add(cursorFollower);
|
||||
cursorOffset = draggedElement.WorldToLocal(worldPosition);
|
||||
|
||||
cursorFollower.style.position = Position.Absolute;
|
||||
UpdateCursorFollower(worldPosition);
|
||||
}
|
||||
|
||||
public void UpdateCursorFollower(Vector2 mouseWorldPosition)
|
||||
{
|
||||
Vector2 windowLocalPosition = windowContent.WorldToLocal(mouseWorldPosition);
|
||||
cursorFollower.style.left = windowLocalPosition.x - cursorOffset.x;
|
||||
cursorFollower.style.top = windowLocalPosition.y - cursorOffset.y;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (windowContent.Contains(cursorFollower))
|
||||
windowContent.Remove(cursorFollower);
|
||||
}
|
||||
}
|
||||
|
||||
class EnvironmentPreviewDragger : Manipulator
|
||||
{
|
||||
VisualElement m_DropArea;
|
||||
DisplayWindow m_Window;
|
||||
|
||||
//Note: static as only one drag'n'drop at a time
|
||||
static DraggingContext s_Context;
|
||||
|
||||
public EnvironmentPreviewDragger(DisplayWindow window, VisualElement dropArea)
|
||||
{
|
||||
m_Window = window;
|
||||
m_DropArea = dropArea;
|
||||
}
|
||||
|
||||
protected override void RegisterCallbacksOnTarget()
|
||||
{
|
||||
target.RegisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
target.RegisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
}
|
||||
|
||||
protected override void UnregisterCallbacksFromTarget()
|
||||
{
|
||||
target.UnregisterCallback<MouseDownEvent>(OnMouseDown);
|
||||
target.UnregisterCallback<MouseUpEvent>(OnMouseUp);
|
||||
}
|
||||
|
||||
void Release()
|
||||
{
|
||||
target.UnregisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||
s_Context?.Dispose();
|
||||
target.ReleaseMouse();
|
||||
s_Context = null;
|
||||
}
|
||||
|
||||
void OnMouseDown(MouseDownEvent evt)
|
||||
{
|
||||
if (evt.button == 0)
|
||||
{
|
||||
target.CaptureMouse();
|
||||
target.RegisterCallback<MouseMoveEvent>(OnMouseMove);
|
||||
s_Context = m_Window.StartDragging(target, evt.mousePosition);
|
||||
//do not stop event as we still need to propagate it to the ListView for selection
|
||||
}
|
||||
}
|
||||
|
||||
void OnMouseUp(MouseUpEvent evt)
|
||||
{
|
||||
if (evt.button != 0)
|
||||
return;
|
||||
if (m_DropArea.ContainsPoint(m_DropArea.WorldToLocal(Event.current.mousePosition)))
|
||||
{
|
||||
m_Window.EndDragging(s_Context, evt.mousePosition);
|
||||
evt.StopPropagation();
|
||||
}
|
||||
Release();
|
||||
}
|
||||
|
||||
void OnMouseMove(MouseMoveEvent evt)
|
||||
{
|
||||
evt.StopPropagation();
|
||||
s_Context.UpdateCursorFollower(evt.mousePosition);
|
||||
}
|
||||
}
|
||||
|
||||
void IEnvironmentDisplayer.Repaint()
|
||||
{
|
||||
//can be unsync if library asset is destroy by user, so if null force sync
|
||||
if (LookDev.currentContext.environmentLibrary == null)
|
||||
m_LibraryField.value = null;
|
||||
|
||||
RefreshLibraryDisplay();
|
||||
}
|
||||
|
||||
void OnFocus()
|
||||
{
|
||||
//OnFocus is called before OnEnable that open backend if not already opened, so only sync if backend is open
|
||||
if (LookDev.open)
|
||||
{
|
||||
//If EnvironmentLibrary asset as been edited by the user (deletion),
|
||||
//update all view to use null environment if it was not temporary ones
|
||||
if (LookDev.currentContext.HasLibraryAssetChanged(m_LibraryField.value as EnvironmentLibrary))
|
||||
{
|
||||
ViewContext viewContext = LookDev.currentContext.GetViewContent(ViewIndex.First);
|
||||
if (!(viewContext.environment?.isCubemapOnly ?? false))
|
||||
OnChangingEnvironmentInViewInternal?.Invoke(viewContext.environment, ViewCompositionIndex.First, default);
|
||||
viewContext = LookDev.currentContext.GetViewContent(ViewIndex.Second);
|
||||
if (!(viewContext.environment?.isCubemapOnly ?? false))
|
||||
OnChangingEnvironmentInViewInternal?.Invoke(viewContext.environment, ViewCompositionIndex.Second, default);
|
||||
}
|
||||
|
||||
//If Cubemap asset as been edited by the user (deletion),
|
||||
//update all views to use null environment if it was temporary ones
|
||||
//and update all other views' environment to not use cubemap anymore
|
||||
foreach (ViewContext viewContext in LookDev.currentContext.viewContexts)
|
||||
{
|
||||
if (viewContext.environment == null || !viewContext.environment.HasCubemapAssetChanged(viewContext.environment.cubemap))
|
||||
continue;
|
||||
|
||||
if (viewContext.environment.isCubemapOnly)
|
||||
viewContext.UpdateEnvironment(null);
|
||||
else
|
||||
viewContext.environment.cubemap = null;
|
||||
}
|
||||
|
||||
((IEnvironmentDisplayer)this).Repaint();
|
||||
}
|
||||
}
|
||||
|
||||
void FullRefreshEnvironmentList()
|
||||
{
|
||||
if (LookDev.currentContext.environmentLibrary != null)
|
||||
LookDev.currentContext.FullReimportEnvironmentLibrary();
|
||||
|
||||
((IEnvironmentDisplayer)this).Repaint();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,689 @@
|
||||
using System;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
using RenderPipelineManager = UnityEngine.Rendering.RenderPipelineManager;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>Interface that must implement the viewer to communicate with the compositor and data management</summary>
|
||||
public interface IViewDisplayer
|
||||
{
|
||||
/// <summary>Get the displayed rect to use</summary>
|
||||
/// <param name="index">Index of this view</param>
|
||||
/// <returns>The Rect to draw</returns>
|
||||
Rect GetRect(ViewCompositionIndex index);
|
||||
/// <summary>Set the computed texture in the view</summary>
|
||||
/// <param name="index">Index of this view</param>
|
||||
/// <param name="texture">The texture used</param>
|
||||
void SetTexture(ViewCompositionIndex index, Texture texture);
|
||||
|
||||
/// <summary>Repaint the UI</summary>
|
||||
void Repaint();
|
||||
|
||||
/// <summary>Callback on layout changed</summary>
|
||||
event Action<Layout, SidePanel> OnLayoutChanged;
|
||||
|
||||
/// <summary>Callback on RenderDoc acquisition is triggered</summary>
|
||||
event Action OnRenderDocAcquisitionTriggered;
|
||||
|
||||
/// <summary>Callback on ;ouse events in the view</summary>
|
||||
event Action<IMouseEvent> OnMouseEventInView;
|
||||
|
||||
/// <summary>Callback on object changed in the view</summary>
|
||||
event Action<GameObject, ViewCompositionIndex, Vector2> OnChangingObjectInView;
|
||||
/// <summary>Callback on environment changed in the view</summary>
|
||||
event Action<UnityEngine.Object, ViewCompositionIndex, Vector2> OnChangingEnvironmentInView;
|
||||
|
||||
/// <summary>Callback on closed</summary>
|
||||
event Action OnClosed;
|
||||
|
||||
/// <summary>Callback on update requested</summary>
|
||||
event Action OnUpdateRequested;
|
||||
}
|
||||
|
||||
partial class DisplayWindow : EditorWindow, IViewDisplayer
|
||||
{
|
||||
static partial class Style
|
||||
{
|
||||
internal const string k_IconFolder = @"Packages/com.unity.render-pipelines.core/Editor/LookDev/Icons/";
|
||||
internal const string k_uss = @"Packages/com.unity.render-pipelines.core/Editor/LookDev/DisplayWindow.uss";
|
||||
internal const string k_uss_personal_overload = @"Packages/com.unity.render-pipelines.core/Editor/LookDev/DisplayWindow-PersonalSkin.uss";
|
||||
|
||||
internal static readonly GUIContent k_WindowTitleAndIcon = EditorGUIUtility.TrTextContentWithIcon("Look Dev", CoreEditorUtils.LoadIcon(k_IconFolder, "LookDev", forceLowRes: true));
|
||||
|
||||
internal static readonly (Texture2D icon, string tooltip)k_Layout1Icon =
|
||||
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Layout1", forceLowRes: true),
|
||||
"First view");
|
||||
internal static readonly (Texture2D icon, string tooltip)k_Layout2Icon =
|
||||
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Layout2", forceLowRes: true),
|
||||
"Second view");
|
||||
internal static readonly (Texture2D icon, string tooltip)k_LayoutVerticalIcon =
|
||||
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "LayoutVertical", forceLowRes: true),
|
||||
"Both views split vertically");
|
||||
internal static readonly (Texture2D icon, string tooltip)k_LayoutHorizontalIcon =
|
||||
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "LayoutHorizontal", forceLowRes: true),
|
||||
"Both views split horizontally");
|
||||
internal static readonly (Texture2D icon, string tooltip)k_LayoutStackIcon =
|
||||
(CoreEditorUtils.LoadIcon(Style.k_IconFolder, "LayoutCustom", forceLowRes: true),
|
||||
"Both views stacked");
|
||||
|
||||
internal static readonly Texture2D k_Camera1Icon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Camera1", forceLowRes: true);
|
||||
internal static readonly Texture2D k_Camera2Icon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Camera2", forceLowRes: true);
|
||||
internal static readonly Texture2D k_LinkIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "Link", forceLowRes: true);
|
||||
internal static readonly Texture2D k_RightIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "RightArrow", forceLowRes: true);
|
||||
internal static readonly Texture2D k_LeftIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "LeftArrow", forceLowRes: true);
|
||||
|
||||
internal static readonly Texture2D k_RenderdocIcon = CoreEditorUtils.LoadIcon(Style.k_IconFolder, "RenderDoc", forceLowRes: true);
|
||||
internal const string k_RenderDocLabel = " Content";
|
||||
|
||||
internal const string k_CameraSyncTooltip = "Synchronize camera movement amongst views";
|
||||
internal const string k_CameraMenuSync1On2 = "Align Camera 1 with Camera 2";
|
||||
internal const string k_CameraMenuSync2On1 = "Align Camera 2 with Camera 1";
|
||||
internal const string k_CameraMenuReset = "Reset Cameras";
|
||||
|
||||
internal const string k_EnvironmentSidePanelName = "Environment";
|
||||
internal const string k_DebugSidePanelName = "Debug";
|
||||
|
||||
internal const string k_DragAndDropObject = "Drag and drop object here";
|
||||
internal const string k_DragAndDropEnvironment = "Drag and drop environment from side panel here";
|
||||
|
||||
// /!\ WARNING:
|
||||
//The following const are used in the uss.
|
||||
//If you change them, update the uss file too.
|
||||
internal const string k_MainContainerName = "mainContainer";
|
||||
internal const string k_ViewContainerName = "viewContainer";
|
||||
internal const string k_FirstViewName = "firstView";
|
||||
internal const string k_SecondViewName = "secondView";
|
||||
internal const string k_ToolbarName = "toolbar";
|
||||
internal const string k_ToolbarRadioName = "toolbarRadio";
|
||||
internal const string k_TabsRadioName = "tabsRadio";
|
||||
internal const string k_SideToolbarName = "sideToolbar";
|
||||
internal const string k_SharedContainerClass = "container";
|
||||
internal const string k_FirstViewClass = "firstView";
|
||||
internal const string k_SecondViewsClass = "secondView";
|
||||
internal const string k_VerticalViewsClass = "verticalSplit";
|
||||
internal const string k_DebugContainerName = "debugContainer";
|
||||
internal const string k_ShowDebugPanelClass = "showDebugPanel";
|
||||
|
||||
internal const string k_EnvironmentContainerName = "environmentContainer";
|
||||
internal const string k_ShowEnvironmentPanelClass = "showEnvironmentPanel";
|
||||
|
||||
internal const string k_CameraMenuName = "cameraMenu";
|
||||
internal const string k_CameraButtonName = "cameraButton";
|
||||
internal const string k_CameraSeparatorName = "cameraSeparator";
|
||||
|
||||
internal const string k_RenderDocContentName = "renderdoc-content";
|
||||
}
|
||||
|
||||
VisualElement m_MainContainer;
|
||||
VisualElement m_ViewContainer;
|
||||
VisualElement m_DebugContainer;
|
||||
Label m_NoEnvironmentList;
|
||||
Label m_NoObject1;
|
||||
Label m_NoEnvironment1;
|
||||
Label m_NoObject2;
|
||||
Label m_NoEnvironment2;
|
||||
|
||||
Image[] m_Views = new Image[2];
|
||||
|
||||
LayoutContext layout
|
||||
=> LookDev.currentContext.layout;
|
||||
|
||||
Layout viewLayout
|
||||
{
|
||||
get => layout.viewLayout;
|
||||
set
|
||||
{
|
||||
if (layout.viewLayout != value)
|
||||
{
|
||||
OnLayoutChangedInternal?.Invoke(value, sidePanel);
|
||||
ApplyLayout(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SidePanel sidePanel
|
||||
{
|
||||
get => layout.showedSidePanel;
|
||||
set
|
||||
{
|
||||
if (layout.showedSidePanel != value)
|
||||
{
|
||||
OnLayoutChangedInternal?.Invoke(viewLayout, value);
|
||||
ApplySidePanelChange(layout.showedSidePanel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event Action<Layout, SidePanel> OnLayoutChangedInternal;
|
||||
event Action<Layout, SidePanel> IViewDisplayer.OnLayoutChanged
|
||||
{
|
||||
add => OnLayoutChangedInternal += value;
|
||||
remove => OnLayoutChangedInternal -= value;
|
||||
}
|
||||
|
||||
event Action OnRenderDocAcquisitionTriggeredInternal;
|
||||
event Action IViewDisplayer.OnRenderDocAcquisitionTriggered
|
||||
{
|
||||
add => OnRenderDocAcquisitionTriggeredInternal += value;
|
||||
remove => OnRenderDocAcquisitionTriggeredInternal -= value;
|
||||
}
|
||||
|
||||
event Action<IMouseEvent> OnMouseEventInViewPortInternal;
|
||||
event Action<IMouseEvent> IViewDisplayer.OnMouseEventInView
|
||||
{
|
||||
add => OnMouseEventInViewPortInternal += value;
|
||||
remove => OnMouseEventInViewPortInternal -= value;
|
||||
}
|
||||
|
||||
event Action<GameObject, ViewCompositionIndex, Vector2> OnChangingObjectInViewInternal;
|
||||
event Action<GameObject, ViewCompositionIndex, Vector2> IViewDisplayer.OnChangingObjectInView
|
||||
{
|
||||
add => OnChangingObjectInViewInternal += value;
|
||||
remove => OnChangingObjectInViewInternal -= value;
|
||||
}
|
||||
|
||||
//event Action<Material, ViewCompositionIndex, Vector2> OnChangingMaterialInViewInternal;
|
||||
//event Action<Material, ViewCompositionIndex, Vector2> IViewDisplayer.OnChangingMaterialInView
|
||||
//{
|
||||
// add => OnChangingMaterialInViewInternal += value;
|
||||
// remove => OnChangingMaterialInViewInternal -= value;
|
||||
//}
|
||||
|
||||
event Action<UnityEngine.Object, ViewCompositionIndex, Vector2> OnChangingEnvironmentInViewInternal;
|
||||
event Action<UnityEngine.Object, ViewCompositionIndex, Vector2> IViewDisplayer.OnChangingEnvironmentInView
|
||||
{
|
||||
add => OnChangingEnvironmentInViewInternal += value;
|
||||
remove => OnChangingEnvironmentInViewInternal -= value;
|
||||
}
|
||||
|
||||
event Action OnClosedInternal;
|
||||
event Action IViewDisplayer.OnClosed
|
||||
{
|
||||
add => OnClosedInternal += value;
|
||||
remove => OnClosedInternal -= value;
|
||||
}
|
||||
|
||||
event Action OnUpdateRequestedInternal;
|
||||
event Action IViewDisplayer.OnUpdateRequested
|
||||
{
|
||||
add => OnUpdateRequestedInternal += value;
|
||||
remove => OnUpdateRequestedInternal -= value;
|
||||
}
|
||||
|
||||
StyleSheet styleSheet = null;
|
||||
StyleSheet styleSheetLight = null;
|
||||
|
||||
SwitchableCameraController m_FirstOrCompositeManipulator;
|
||||
CameraController m_SecondManipulator;
|
||||
ComparisonGizmoController m_GizmoManipulator;
|
||||
|
||||
void ReloadStyleSheets()
|
||||
{
|
||||
if (styleSheet == null || styleSheet.Equals(null))
|
||||
{
|
||||
styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(Style.k_uss);
|
||||
if (styleSheet == null || styleSheet.Equals(null))
|
||||
{
|
||||
//Debug.LogWarning("[LookDev] Could not load Stylesheet.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rootVisualElement.styleSheets.Contains(styleSheet))
|
||||
rootVisualElement.styleSheets.Add(styleSheet);
|
||||
|
||||
//Additively load Light Skin
|
||||
if (!EditorGUIUtility.isProSkin)
|
||||
{
|
||||
if (styleSheetLight == null || styleSheetLight.Equals(null))
|
||||
{
|
||||
styleSheetLight = AssetDatabase.LoadAssetAtPath<StyleSheet>(Style.k_uss_personal_overload);
|
||||
if (styleSheetLight == null || styleSheetLight.Equals(null))
|
||||
{
|
||||
//Debug.LogWarning("[LookDev] Could not load Light skin.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rootVisualElement.styleSheets.Contains(styleSheetLight))
|
||||
rootVisualElement.styleSheets.Add(styleSheetLight);
|
||||
}
|
||||
}
|
||||
|
||||
void CreateGUI()
|
||||
{
|
||||
ReloadStyleSheets();
|
||||
|
||||
//Call the open function to configure LookDev
|
||||
// in case the window where open when last editor session finished.
|
||||
// (Else it will open at start and has nothing to display).
|
||||
if (!LookDev.open)
|
||||
LookDev.Initialize(this);
|
||||
|
||||
titleContent = Style.k_WindowTitleAndIcon;
|
||||
|
||||
// /!\ be sure to have a minSize that will allow a non negative sized viewport even with side panel open
|
||||
this.minSize = new Vector2(600, 400);
|
||||
|
||||
CreateToolbar();
|
||||
|
||||
m_MainContainer = new VisualElement() { name = Style.k_MainContainerName };
|
||||
m_MainContainer.AddToClassList(Style.k_SharedContainerClass);
|
||||
rootVisualElement.Add(m_MainContainer);
|
||||
|
||||
CreateViews();
|
||||
CreateEnvironment();
|
||||
CreateDebug();
|
||||
CreateDropAreas();
|
||||
|
||||
ApplyLayout(viewLayout);
|
||||
ApplySidePanelChange(layout.showedSidePanel);
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
Undo.undoRedoPerformed += FullRefreshEnvironmentList;
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
Undo.undoRedoPerformed -= FullRefreshEnvironmentList;
|
||||
OnClosedInternal?.Invoke();
|
||||
}
|
||||
|
||||
void CreateToolbar()
|
||||
{
|
||||
// Layout swapper part
|
||||
var layoutRadio = new ToolbarRadio() { name = Style.k_ToolbarRadioName };
|
||||
layoutRadio.AddRadios(new[]
|
||||
{
|
||||
Style.k_Layout1Icon,
|
||||
Style.k_Layout2Icon,
|
||||
Style.k_LayoutVerticalIcon,
|
||||
Style.k_LayoutHorizontalIcon,
|
||||
Style.k_LayoutStackIcon,
|
||||
});
|
||||
layoutRadio.RegisterCallback((ChangeEvent<int> evt)
|
||||
=> viewLayout = (Layout)evt.newValue);
|
||||
layoutRadio.SetValueWithoutNotify((int)viewLayout);
|
||||
|
||||
var cameraMenu = new ToolbarMenu() { name = Style.k_CameraMenuName };
|
||||
cameraMenu.variant = ToolbarMenu.Variant.Popup;
|
||||
var cameraToggle = new ToolbarToggle() { name = Style.k_CameraButtonName };
|
||||
cameraToggle.value = LookDev.currentContext.cameraSynced;
|
||||
cameraToggle.tooltip = Style.k_CameraSyncTooltip;
|
||||
|
||||
//Note: when having Image on top of the Toggle nested in the Menu, RegisterValueChangedCallback is not called
|
||||
//cameraToggle.RegisterValueChangedCallback(evt => LookDev.currentContext.cameraSynced = evt.newValue);
|
||||
cameraToggle.RegisterCallback<MouseUpEvent>(evt =>
|
||||
{
|
||||
LookDev.currentContext.cameraSynced ^= true;
|
||||
cameraToggle.SetValueWithoutNotify(LookDev.currentContext.cameraSynced);
|
||||
});
|
||||
|
||||
var cameraSeparator = new ToolbarToggle() { name = Style.k_CameraSeparatorName };
|
||||
cameraToggle.Add(new Image() { image = Style.k_Camera1Icon });
|
||||
cameraToggle.Add(new Image() { image = Style.k_LinkIcon });
|
||||
cameraToggle.Add(new Image() { image = Style.k_Camera2Icon });
|
||||
cameraMenu.Add(cameraSeparator);
|
||||
cameraMenu.Add(cameraToggle);
|
||||
cameraMenu.menu.AppendAction(Style.k_CameraMenuSync1On2,
|
||||
(DropdownMenuAction a) => LookDev.currentContext.SynchronizeCameraStates(ViewIndex.Second),
|
||||
DropdownMenuAction.AlwaysEnabled);
|
||||
cameraMenu.menu.AppendAction(Style.k_CameraMenuSync2On1,
|
||||
(DropdownMenuAction a) => LookDev.currentContext.SynchronizeCameraStates(ViewIndex.First),
|
||||
DropdownMenuAction.AlwaysEnabled);
|
||||
cameraMenu.menu.AppendAction(Style.k_CameraMenuReset,
|
||||
(DropdownMenuAction a) =>
|
||||
{
|
||||
LookDev.currentContext.GetViewContent(ViewIndex.First).ResetCameraState();
|
||||
LookDev.currentContext.GetViewContent(ViewIndex.Second).ResetCameraState();
|
||||
},
|
||||
DropdownMenuAction.AlwaysEnabled);
|
||||
|
||||
// Side part
|
||||
var sideRadio = new ToolbarRadio(canDeselectAll: true)
|
||||
{
|
||||
name = Style.k_TabsRadioName
|
||||
};
|
||||
sideRadio.AddRadios(new[]
|
||||
{
|
||||
Style.k_EnvironmentSidePanelName,
|
||||
Style.k_DebugSidePanelName,
|
||||
});
|
||||
sideRadio.SetValueWithoutNotify((int)sidePanel);
|
||||
sideRadio.RegisterCallback((ChangeEvent<int> evt)
|
||||
=> sidePanel = (SidePanel)evt.newValue);
|
||||
|
||||
// Aggregate parts
|
||||
var toolbar = new UIElements.Toolbar() { name = Style.k_ToolbarName };
|
||||
toolbar.Add(layoutRadio);
|
||||
toolbar.Add(new ToolbarSpacer());
|
||||
toolbar.Add(cameraMenu);
|
||||
|
||||
toolbar.Add(new ToolbarSpacer() { flex = true });
|
||||
if (UnityEditorInternal.RenderDoc.IsInstalled() && UnityEditorInternal.RenderDoc.IsLoaded())
|
||||
{
|
||||
var renderDocButton = new ToolbarButton(() => OnRenderDocAcquisitionTriggeredInternal?.Invoke())
|
||||
{
|
||||
name = Style.k_RenderDocContentName
|
||||
};
|
||||
renderDocButton.Add(new Image() { image = Style.k_RenderdocIcon });
|
||||
renderDocButton.Add(new Label() { text = Style.k_RenderDocLabel });
|
||||
toolbar.Add(renderDocButton);
|
||||
toolbar.Add(new ToolbarSpacer());
|
||||
}
|
||||
toolbar.Add(sideRadio);
|
||||
rootVisualElement.Add(toolbar);
|
||||
}
|
||||
|
||||
void CreateViews()
|
||||
{
|
||||
if (m_MainContainer == null || m_MainContainer.Equals(null))
|
||||
throw new System.MemberAccessException("m_MainContainer should be assigned prior CreateViews()");
|
||||
|
||||
m_ViewContainer = new VisualElement() { name = Style.k_ViewContainerName };
|
||||
m_ViewContainer.AddToClassList(LookDev.currentContext.layout.isMultiView ? Style.k_SecondViewsClass : Style.k_FirstViewClass);
|
||||
m_ViewContainer.AddToClassList(Style.k_SharedContainerClass);
|
||||
m_MainContainer.Add(m_ViewContainer);
|
||||
m_ViewContainer.RegisterCallback<MouseDownEvent>(evt => OnMouseEventInViewPortInternal?.Invoke(evt));
|
||||
m_ViewContainer.RegisterCallback<MouseUpEvent>(evt => OnMouseEventInViewPortInternal?.Invoke(evt));
|
||||
m_ViewContainer.RegisterCallback<MouseMoveEvent>(evt => OnMouseEventInViewPortInternal?.Invoke(evt));
|
||||
|
||||
m_Views[(int)ViewIndex.First] = new Image() { name = Style.k_FirstViewName, image = Texture2D.blackTexture };
|
||||
m_ViewContainer.Add(m_Views[(int)ViewIndex.First]);
|
||||
m_Views[(int)ViewIndex.Second] = new Image() { name = Style.k_SecondViewName, image = Texture2D.blackTexture };
|
||||
m_ViewContainer.Add(m_Views[(int)ViewIndex.Second]);
|
||||
|
||||
m_FirstOrCompositeManipulator = new SwitchableCameraController(
|
||||
this,
|
||||
index =>
|
||||
{
|
||||
LookDev.currentContext.SetFocusedCamera(index);
|
||||
var environment = LookDev.currentContext.GetViewContent(index).environment;
|
||||
if (sidePanel == SidePanel.Environment && environment != null && LookDev.currentContext.environmentLibrary != null)
|
||||
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.IndexOf(environment);
|
||||
});
|
||||
m_SecondManipulator = new CameraController(
|
||||
this,
|
||||
() =>
|
||||
{
|
||||
LookDev.currentContext.SetFocusedCamera(ViewIndex.Second);
|
||||
var environment = LookDev.currentContext.GetViewContent(ViewIndex.Second).environment;
|
||||
if (sidePanel == SidePanel.Environment && environment != null && LookDev.currentContext.environmentLibrary != null)
|
||||
m_EnvironmentList.selectedIndex = LookDev.currentContext.environmentLibrary.IndexOf(environment);
|
||||
});
|
||||
m_GizmoManipulator = new ComparisonGizmoController(m_FirstOrCompositeManipulator);
|
||||
m_Views[(int)ViewIndex.First].AddManipulator(m_GizmoManipulator); //must take event first to switch the firstOrCompositeManipulator
|
||||
m_Views[(int)ViewIndex.First].AddManipulator(m_FirstOrCompositeManipulator);
|
||||
m_Views[(int)ViewIndex.Second].AddManipulator(m_SecondManipulator);
|
||||
|
||||
m_NoObject1 = new Label(Style.k_DragAndDropObject);
|
||||
m_NoObject1.style.flexGrow = 1;
|
||||
m_NoObject1.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
m_NoObject2 = new Label(Style.k_DragAndDropObject);
|
||||
m_NoObject2.style.flexGrow = 1;
|
||||
m_NoObject2.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
m_NoEnvironment1 = new Label(Style.k_DragAndDropEnvironment);
|
||||
m_NoEnvironment1.style.flexGrow = 1;
|
||||
m_NoEnvironment1.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
m_NoEnvironment1.style.whiteSpace = WhiteSpace.Normal;
|
||||
m_NoEnvironment2 = new Label(Style.k_DragAndDropEnvironment);
|
||||
m_NoEnvironment2.style.flexGrow = 1;
|
||||
m_NoEnvironment2.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||
m_NoEnvironment2.style.whiteSpace = WhiteSpace.Normal;
|
||||
m_Views[(int)ViewIndex.First].Add(m_NoObject1);
|
||||
m_Views[(int)ViewIndex.First].Add(m_NoEnvironment1);
|
||||
m_Views[(int)ViewIndex.Second].Add(m_NoObject2);
|
||||
m_Views[(int)ViewIndex.Second].Add(m_NoEnvironment2);
|
||||
}
|
||||
|
||||
void CreateDropAreas()
|
||||
{
|
||||
// GameObject or Prefab in view
|
||||
new DropArea(new[] { typeof(GameObject) }, m_Views[(int)ViewIndex.First], (obj, localPos) =>
|
||||
{
|
||||
if (viewLayout == Layout.CustomSplit)
|
||||
OnChangingObjectInViewInternal?.Invoke(obj as GameObject, ViewCompositionIndex.Composite, localPos);
|
||||
else
|
||||
OnChangingObjectInViewInternal?.Invoke(obj as GameObject, ViewCompositionIndex.First, localPos);
|
||||
m_NoObject1.style.visibility = obj == null || obj.Equals(null) ? Visibility.Visible : Visibility.Hidden;
|
||||
});
|
||||
new DropArea(new[] { typeof(GameObject) }, m_Views[(int)ViewIndex.Second], (obj, localPos) =>
|
||||
{
|
||||
OnChangingObjectInViewInternal?.Invoke(obj as GameObject, ViewCompositionIndex.Second, localPos);
|
||||
m_NoObject2.style.visibility = obj == null || obj.Equals(null) ? Visibility.Visible : Visibility.Hidden;
|
||||
});
|
||||
|
||||
// Material in view
|
||||
//new DropArea(new[] { typeof(GameObject) }, m_Views[(int)ViewIndex.First], (obj, localPos) =>
|
||||
//{
|
||||
// if (layout == Layout.CustomSplit || layout == Layout.CustomCircular)
|
||||
// OnChangingMaterialInViewInternal?.Invoke(obj as Material, ViewCompositionIndex.Composite, localPos);
|
||||
// else
|
||||
// OnChangingMaterialInViewInternal?.Invoke(obj as Material, ViewCompositionIndex.First, localPos);
|
||||
//});
|
||||
//new DropArea(new[] { typeof(Material) }, m_Views[(int)ViewIndex.Second], (obj, localPos)
|
||||
// => OnChangingMaterialInViewInternal?.Invoke(obj as Material, ViewCompositionIndex.Second, localPos));
|
||||
|
||||
// Environment in view
|
||||
new DropArea(new[] { typeof(Environment), typeof(Cubemap) }, m_Views[(int)ViewIndex.First], (obj, localPos) =>
|
||||
{
|
||||
if (viewLayout == Layout.CustomSplit)
|
||||
OnChangingEnvironmentInViewInternal?.Invoke(obj, ViewCompositionIndex.Composite, localPos);
|
||||
else
|
||||
OnChangingEnvironmentInViewInternal?.Invoke(obj, ViewCompositionIndex.First, localPos);
|
||||
m_NoEnvironment1.style.visibility = obj == null || obj.Equals(null) ? Visibility.Visible : Visibility.Hidden;
|
||||
});
|
||||
new DropArea(new[] { typeof(Environment), typeof(Cubemap) }, m_Views[(int)ViewIndex.Second], (obj, localPos) =>
|
||||
{
|
||||
OnChangingEnvironmentInViewInternal?.Invoke(obj, ViewCompositionIndex.Second, localPos);
|
||||
m_NoEnvironment2.style.visibility = obj == null || obj.Equals(null) ? Visibility.Visible : Visibility.Hidden;
|
||||
});
|
||||
|
||||
// Environment in library
|
||||
//new DropArea(new[] { typeof(Environment), typeof(Cubemap) }, m_EnvironmentContainer, (obj, localPos) =>
|
||||
//{
|
||||
// //[TODO: check if this come from outside of library]
|
||||
// OnAddingEnvironmentInternal?.Invoke(obj);
|
||||
//});
|
||||
new DropArea(new[] { typeof(EnvironmentLibrary) }, m_EnvironmentContainer, (obj, localPos) =>
|
||||
{
|
||||
OnChangingEnvironmentLibraryInternal?.Invoke(obj as EnvironmentLibrary);
|
||||
RefreshLibraryDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
Rect IViewDisplayer.GetRect(ViewCompositionIndex index)
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case ViewCompositionIndex.First:
|
||||
case ViewCompositionIndex.Composite: //display composition on first rect
|
||||
return m_Views[(int)ViewIndex.First].contentRect;
|
||||
case ViewCompositionIndex.Second:
|
||||
return m_Views[(int)ViewIndex.Second].contentRect;
|
||||
default:
|
||||
throw new ArgumentException("Unknown ViewCompositionIndex: " + index);
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 m_LastFirstViewSize = new Vector2();
|
||||
Vector2 m_LastSecondViewSize = new Vector2();
|
||||
void IViewDisplayer.SetTexture(ViewCompositionIndex index, Texture texture)
|
||||
{
|
||||
if (texture == null)
|
||||
return;
|
||||
|
||||
bool updated = false;
|
||||
switch (index)
|
||||
{
|
||||
case ViewCompositionIndex.First:
|
||||
case ViewCompositionIndex.Composite: //display composition on first rect
|
||||
if (updated |= m_Views[(int)ViewIndex.First].image != texture)
|
||||
m_Views[(int)ViewIndex.First].image = texture;
|
||||
else if (updated |= (m_LastFirstViewSize.x != texture.width
|
||||
|| m_LastFirstViewSize.y != texture.height))
|
||||
{
|
||||
m_Views[(int)ViewIndex.First].image = null; //force refresh else it will appear zoomed
|
||||
m_Views[(int)ViewIndex.First].image = texture;
|
||||
}
|
||||
if (updated)
|
||||
{
|
||||
m_LastFirstViewSize.x = texture?.width ?? 0;
|
||||
m_LastFirstViewSize.y = texture?.height ?? 0;
|
||||
}
|
||||
break;
|
||||
case ViewCompositionIndex.Second:
|
||||
if (m_Views[(int)ViewIndex.Second].image != texture)
|
||||
m_Views[(int)ViewIndex.Second].image = texture;
|
||||
else if (updated |= (m_LastSecondViewSize.x != texture.width
|
||||
|| m_LastSecondViewSize.y != texture.height))
|
||||
{
|
||||
m_Views[(int)ViewIndex.Second].image = null; //force refresh else it will appear zoomed
|
||||
m_Views[(int)ViewIndex.Second].image = texture;
|
||||
}
|
||||
if (updated)
|
||||
{
|
||||
m_LastSecondViewSize.x = texture?.width ?? 0;
|
||||
m_LastSecondViewSize.y = texture?.height ?? 0;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown ViewCompositionIndex: " + index);
|
||||
}
|
||||
}
|
||||
|
||||
void IViewDisplayer.Repaint() => Repaint();
|
||||
|
||||
void ApplyLayout(Layout value)
|
||||
{
|
||||
m_NoObject1.style.visibility = LookDev.currentContext.GetViewContent(ViewIndex.First).hasViewedObject ? Visibility.Hidden : Visibility.Visible;
|
||||
m_NoObject2.style.visibility = LookDev.currentContext.GetViewContent(ViewIndex.Second).hasViewedObject ? Visibility.Hidden : Visibility.Visible;
|
||||
m_NoEnvironment1.style.visibility = LookDev.currentContext.GetViewContent(ViewIndex.First).hasEnvironment ? Visibility.Hidden : Visibility.Visible;
|
||||
m_NoEnvironment2.style.visibility = LookDev.currentContext.GetViewContent(ViewIndex.Second).hasEnvironment ? Visibility.Hidden : Visibility.Visible;
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case Layout.HorizontalSplit:
|
||||
case Layout.VerticalSplit:
|
||||
if (!m_ViewContainer.ClassListContains(Style.k_FirstViewClass))
|
||||
m_ViewContainer.AddToClassList(Style.k_FirstViewClass);
|
||||
if (!m_ViewContainer.ClassListContains(Style.k_SecondViewsClass))
|
||||
m_ViewContainer.AddToClassList(Style.k_SecondViewsClass);
|
||||
if (value == Layout.VerticalSplit)
|
||||
{
|
||||
m_ViewContainer.AddToClassList(Style.k_VerticalViewsClass);
|
||||
if (!m_ViewContainer.ClassListContains(Style.k_VerticalViewsClass))
|
||||
m_ViewContainer.AddToClassList(Style.k_FirstViewClass);
|
||||
}
|
||||
for (int i = 0; i < 2; ++i)
|
||||
m_Views[i].style.display = DisplayStyle.Flex;
|
||||
break;
|
||||
case Layout.FullFirstView:
|
||||
case Layout.CustomSplit: //display composition on first rect
|
||||
if (!m_ViewContainer.ClassListContains(Style.k_FirstViewClass))
|
||||
m_ViewContainer.AddToClassList(Style.k_FirstViewClass);
|
||||
if (m_ViewContainer.ClassListContains(Style.k_SecondViewsClass))
|
||||
m_ViewContainer.RemoveFromClassList(Style.k_SecondViewsClass);
|
||||
m_Views[0].style.display = DisplayStyle.Flex;
|
||||
m_Views[1].style.display = DisplayStyle.None;
|
||||
break;
|
||||
case Layout.FullSecondView:
|
||||
if (m_ViewContainer.ClassListContains(Style.k_FirstViewClass))
|
||||
m_ViewContainer.RemoveFromClassList(Style.k_FirstViewClass);
|
||||
if (!m_ViewContainer.ClassListContains(Style.k_SecondViewsClass))
|
||||
m_ViewContainer.AddToClassList(Style.k_SecondViewsClass);
|
||||
m_Views[0].style.display = DisplayStyle.None;
|
||||
m_Views[1].style.display = DisplayStyle.Flex;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown Layout");
|
||||
}
|
||||
|
||||
//Add flex direction here
|
||||
if (value == Layout.VerticalSplit)
|
||||
m_ViewContainer.AddToClassList(Style.k_VerticalViewsClass);
|
||||
else if (m_ViewContainer.ClassListContains(Style.k_VerticalViewsClass))
|
||||
m_ViewContainer.RemoveFromClassList(Style.k_VerticalViewsClass);
|
||||
}
|
||||
|
||||
void ApplySidePanelChange(SidePanel sidePanel)
|
||||
{
|
||||
IStyle GetEnvironmentContenairDraggerStyle()
|
||||
=> m_EnvironmentContainer.Q(className: "unity-base-slider--vertical").Q("unity-dragger").style;
|
||||
|
||||
if (sidePanel == SidePanel.Environment)
|
||||
{
|
||||
if (!m_MainContainer.ClassListContains(Style.k_ShowEnvironmentPanelClass))
|
||||
m_MainContainer.AddToClassList(Style.k_ShowEnvironmentPanelClass);
|
||||
|
||||
if (m_EnvironmentList.selectedIndex != -1)
|
||||
m_EnvironmentContainer.Q<EnvironmentElement>().style.visibility = Visibility.Visible;
|
||||
GetEnvironmentContenairDraggerStyle().display = DisplayStyle.Flex;
|
||||
m_EnvironmentContainer.style.display = DisplayStyle.Flex;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_MainContainer.ClassListContains(Style.k_ShowEnvironmentPanelClass))
|
||||
m_MainContainer.RemoveFromClassList(Style.k_ShowEnvironmentPanelClass);
|
||||
|
||||
m_EnvironmentContainer.Q<EnvironmentElement>().style.visibility = Visibility.Hidden;
|
||||
GetEnvironmentContenairDraggerStyle().display = DisplayStyle.None;
|
||||
m_EnvironmentContainer.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
if (sidePanel == SidePanel.Debug)
|
||||
{
|
||||
if (!m_MainContainer.ClassListContains(Style.k_ShowDebugPanelClass))
|
||||
m_MainContainer.AddToClassList(Style.k_ShowDebugPanelClass);
|
||||
UpdateSideDebugPanelProperties();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_MainContainer.ClassListContains(Style.k_ShowDebugPanelClass))
|
||||
m_MainContainer.RemoveFromClassList(Style.k_ShowDebugPanelClass);
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (LookDev.waitingConfigure)
|
||||
return;
|
||||
|
||||
// [case 1245086] Guard in case the SRP asset is set to null (or to a not supported SRP) when the lookdev window is already open
|
||||
// Note: After an editor reload, we might get a null SRP for a couple of frames, hence the check.
|
||||
if (!LookDev.supported)
|
||||
{
|
||||
// Print an error and close the Lookdev window (to avoid spamming the console)
|
||||
if (RenderPipelineManager.currentPipeline != null)
|
||||
Debug.LogError("LookDev is not supported by this Scriptable Render Pipeline: " + RenderPipelineManager.currentPipeline.ToString());
|
||||
else if (GraphicsSettings.currentRenderPipeline != null)
|
||||
Debug.LogError("LookDev is not available until a camera render occurs.");
|
||||
else
|
||||
Debug.LogError("LookDev is not supported: No SRP detected.");
|
||||
LookDev.Close();
|
||||
}
|
||||
|
||||
// All those states coming from the Contexts can become invalid after a domain reload so we need to update them.
|
||||
m_FirstOrCompositeManipulator.UpdateCameraState(LookDev.currentContext);
|
||||
m_SecondManipulator.UpdateCameraState(LookDev.currentContext, ViewIndex.Second);
|
||||
m_GizmoManipulator.UpdateGizmoState(LookDev.currentContext.layout.gizmoState);
|
||||
}
|
||||
|
||||
void OnGUI()
|
||||
{
|
||||
if (EditorApplication.isUpdating)
|
||||
return;
|
||||
|
||||
//deal with missing style on domain reload...
|
||||
ReloadStyleSheets();
|
||||
|
||||
OnUpdateRequestedInternal?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,414 @@
|
||||
.container
|
||||
{
|
||||
margin: 0px;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
/* ///// */
|
||||
/* VIEWS */
|
||||
/* ///// */
|
||||
|
||||
/* override as later in document */
|
||||
.verticalSplit
|
||||
{
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#viewContainer
|
||||
{
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#firstView,
|
||||
#secondView
|
||||
{
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
/* override as later in document */
|
||||
.firstView > #firstView,
|
||||
.secondView > #secondView
|
||||
{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
|
||||
/* /////////// */
|
||||
/* ENVIRONMENT */
|
||||
/* /////////// */
|
||||
|
||||
#environmentContainer
|
||||
{
|
||||
width: 0px;
|
||||
visibility: hidden;
|
||||
flex-direction: column-reverse;
|
||||
border-color: #232323;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
#debugContainer
|
||||
{
|
||||
width: 0px;
|
||||
visibility: hidden;
|
||||
border-color: #232323;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
#environmentContainer > EnvironmentElement
|
||||
{
|
||||
border-color: #232323;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.showEnvironmentPanel > #environmentContainer
|
||||
{
|
||||
width: 255px;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.showDebugPanel > #debugContainer
|
||||
{
|
||||
width: 256px; /*219px;*/
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.unity-label
|
||||
{
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.unity-composite-field__field > .unity-base-field__label
|
||||
{
|
||||
padding-left: 0px;
|
||||
min-width: 10px;
|
||||
max-width: 10px;
|
||||
}
|
||||
|
||||
.unity-composite-field__field > .unity-base-field__input
|
||||
{
|
||||
margin-left: 0px;
|
||||
min-width: 38px;
|
||||
max-width: 38px;
|
||||
}
|
||||
|
||||
#unity-text-input
|
||||
{
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.list-environment
|
||||
{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-environment-overlay
|
||||
{
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
padding-left: 1px;
|
||||
border-bottom-width: 0px;
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
#environmentListCreationToolbar
|
||||
{
|
||||
padding-left: 1px;
|
||||
}
|
||||
|
||||
#environmentListCreationToolbar > *
|
||||
{
|
||||
flex: 1;
|
||||
-unity-text-align: middle-center;
|
||||
}
|
||||
|
||||
#environmentListCreationToolbar > ObjectField > Label
|
||||
{
|
||||
min-width: 60px;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
#environmentListCreationToolbar > ToolbarButton
|
||||
{
|
||||
min-width: 40px;
|
||||
width: 40px;
|
||||
flex: 0;
|
||||
border-left-width: 1px;
|
||||
border-right-width: 0px;
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
ObjectFieldDisplay > Label
|
||||
{
|
||||
-unity-text-align: middle-left;
|
||||
}
|
||||
|
||||
ObjectFieldDisplay > Image
|
||||
{
|
||||
min-width: 12px;
|
||||
}
|
||||
|
||||
ToolbarButton
|
||||
{
|
||||
border-left-width: 0px;
|
||||
}
|
||||
|
||||
.list-environment-overlay > ToolbarButton
|
||||
{
|
||||
border-width: 0px;
|
||||
border-right-width: 1px;
|
||||
width: 20px;
|
||||
min-width: 20px;
|
||||
-unity-text-align: middle-center;
|
||||
background-color: #3c3c3c;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.list-environment-overlay > ToolbarButton:hover
|
||||
{
|
||||
background-color: #585858;
|
||||
}
|
||||
|
||||
.list-environment-overlay > #duplicate
|
||||
{
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
||||
Image.unity-list-view__item
|
||||
{
|
||||
width: 210px;
|
||||
margin: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
Image.unity-list-view__item:selected
|
||||
{
|
||||
border-width: 2px;
|
||||
padding: 3px;
|
||||
border-color: #3d6091;
|
||||
background-color: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.sun-to-brightest-button
|
||||
{
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
#inspector-header
|
||||
{
|
||||
flex-direction: row;
|
||||
border-bottom-width: 1px;
|
||||
border-color: #232323;
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 3px;
|
||||
background-color: #3C3C3C;
|
||||
}
|
||||
|
||||
#inspector-header > Image
|
||||
{
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
#inspector-header > TextField
|
||||
{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#inspector
|
||||
{
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
#separator-line
|
||||
{
|
||||
background-color: #3C3C3C;
|
||||
}
|
||||
|
||||
#separator
|
||||
{
|
||||
flex: 1;
|
||||
width: 188px;
|
||||
border-bottom-width: 1px;
|
||||
border-color: #232323;
|
||||
height: 0;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
#sunToBrightestButton
|
||||
{
|
||||
background-color: rgba(0,0,0,0);
|
||||
border-radius: 0px;
|
||||
border-width: 0px;
|
||||
padding: 0px;
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
#sunToBrightestButton:hover
|
||||
{
|
||||
background-color: #585858;
|
||||
}
|
||||
|
||||
/* /////// */
|
||||
/* /DEBUG/ */
|
||||
/* /////// */
|
||||
|
||||
MultipleDifferentValue
|
||||
{
|
||||
-unity-font-style: bold;
|
||||
font-size: 15px;
|
||||
-unity-text-align: middle-center;
|
||||
margin-bottom: 2px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
MultipleSourcePopupField > MultipleDifferentValue
|
||||
{
|
||||
-unity-text-align: middle-left;
|
||||
width: 120px;
|
||||
background-color: #515151;
|
||||
position: absolute;
|
||||
left: 103px;
|
||||
bottom: 1px;
|
||||
}
|
||||
|
||||
MultipleSourcePopupField > MultipleDifferentValue:hover
|
||||
{
|
||||
background-color: #585858;
|
||||
}
|
||||
|
||||
#debugToolbar
|
||||
{
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
flex-direction: row;
|
||||
align-self: center;
|
||||
border-bottom-width: 1px;
|
||||
border-top-width: 1px;
|
||||
border-color: #232323;
|
||||
}
|
||||
|
||||
#debugToolbar > ToolbarToggle
|
||||
{
|
||||
width: 40px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#debugToolbar > ToolbarToggle > *
|
||||
{
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* /////// */
|
||||
/* TOOLBAR */
|
||||
/* /////// */
|
||||
|
||||
#toolbar
|
||||
{
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#toolbarRadio
|
||||
{
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.unity-toggle__input > .unity-image
|
||||
{
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
#tabsRadio
|
||||
{
|
||||
width: 256px;
|
||||
min-width: 256px;
|
||||
max-width: 256px;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
-unity-text-align: middle-center;
|
||||
}
|
||||
|
||||
#tabsRadio > ToolbarToggle
|
||||
{
|
||||
flex: 1;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
#tabsRadio > ToolbarToggle > * > Label
|
||||
{
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.unity-toolbar-toggle
|
||||
{
|
||||
padding-top: 0px;
|
||||
padding-right: 0px;
|
||||
padding-bottom: 0px;
|
||||
padding-left: 0px;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
border-left-width: 0px;
|
||||
}
|
||||
|
||||
#renderdoc-content
|
||||
{
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
max-width: 80px;
|
||||
flex-shrink: 0;
|
||||
min-width: 24px;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
#renderdoc-content > Label
|
||||
{
|
||||
-unity-text-align: middle-left;
|
||||
min-width: 0px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
#renderdoc-content > Image
|
||||
{
|
||||
flex-shrink: 0;
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
#cameraMenu
|
||||
{
|
||||
flex-direction: row-reverse;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
#cameraButton
|
||||
{
|
||||
border-radius: 0px;
|
||||
border-width: 0px;
|
||||
border-left-width: 1px;
|
||||
padding: 0px;
|
||||
padding-right: 4px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#cameraSeparator
|
||||
{
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
#cameraButton > *
|
||||
{
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* /////////// */
|
||||
/* DRAG'N'DROP */
|
||||
/* /////////// */
|
||||
|
||||
#cursorFollower
|
||||
{
|
||||
position: absolute;
|
||||
}
|
@@ -0,0 +1,103 @@
|
||||
using System.Collections;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
class DropArea
|
||||
{
|
||||
readonly Type[] k_AcceptedTypes;
|
||||
bool droppable;
|
||||
|
||||
public DropArea(Type[] acceptedTypes, VisualElement area, Action<UnityEngine.Object, Vector2> OnDrop)
|
||||
{
|
||||
k_AcceptedTypes = acceptedTypes;
|
||||
area.RegisterCallback<DragPerformEvent>(evt => Drop(evt, OnDrop));
|
||||
area.RegisterCallback<DragEnterEvent>(evt => DragEnter(evt));
|
||||
area.RegisterCallback<DragLeaveEvent>(evt => DragLeave(evt));
|
||||
area.RegisterCallback<DragExitedEvent>(evt => DragExit(evt));
|
||||
area.RegisterCallback<DragUpdatedEvent>(evt => DragUpdate(evt));
|
||||
}
|
||||
|
||||
void DragEnter(DragEnterEvent evt)
|
||||
{
|
||||
droppable = false;
|
||||
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
|
||||
{
|
||||
if (!IsInAcceptedTypes(obj.GetType()))
|
||||
continue;
|
||||
|
||||
droppable = true;
|
||||
evt.StopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void DragLeave(DragLeaveEvent evt)
|
||||
{
|
||||
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
|
||||
{
|
||||
if (!IsInAcceptedTypes(obj.GetType()))
|
||||
continue;
|
||||
|
||||
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
|
||||
evt.StopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void DragExit(DragExitedEvent evt)
|
||||
{
|
||||
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
|
||||
{
|
||||
if (!IsInAcceptedTypes(obj.GetType()))
|
||||
continue;
|
||||
|
||||
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
|
||||
evt.StopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void DragUpdate(DragUpdatedEvent evt)
|
||||
{
|
||||
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
|
||||
{
|
||||
if (!IsInAcceptedTypes(obj.GetType()))
|
||||
continue;
|
||||
|
||||
DragAndDrop.visualMode = droppable ? DragAndDropVisualMode.Link : DragAndDropVisualMode.Rejected;
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
void Drop(DragPerformEvent evt, Action<UnityEngine.Object, Vector2> OnDrop)
|
||||
{
|
||||
bool atLeastOneAccepted = false;
|
||||
foreach (UnityEngine.Object obj in DragAndDrop.objectReferences)
|
||||
{
|
||||
if (!IsInAcceptedTypes(obj.GetType()))
|
||||
continue;
|
||||
|
||||
OnDrop.Invoke(obj, evt.localMousePosition);
|
||||
atLeastOneAccepted = true;
|
||||
}
|
||||
if (atLeastOneAccepted)
|
||||
{
|
||||
DragAndDrop.AcceptDrag();
|
||||
evt.StopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
bool IsInAcceptedTypes(Type testedType)
|
||||
{
|
||||
foreach (Type type in k_AcceptedTypes)
|
||||
{
|
||||
if (testedType.IsAssignableFrom(type))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,467 @@
|
||||
using UnityEngine;
|
||||
using System;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor.UIElements;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>
|
||||
/// Lighting environment used in LookDev
|
||||
/// </summary>
|
||||
public class Environment : ScriptableObject
|
||||
{
|
||||
[SerializeField]
|
||||
string m_CubemapGUID;
|
||||
Cubemap m_Cubemap;
|
||||
|
||||
internal bool isCubemapOnly { get; private set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Offset on the longitude. Affect both sky and sun position in Shadow part
|
||||
/// </summary>
|
||||
public float rotation = 0.0f;
|
||||
/// <summary>
|
||||
/// Exposure to use with this Sky
|
||||
/// </summary>
|
||||
public float exposure = 0f;
|
||||
|
||||
// Setup default position to be on the sun in the default HDRI.
|
||||
// This is important as the defaultHDRI don't call the set brightest spot function on first call.
|
||||
[SerializeField]
|
||||
float m_Latitude = 60.0f; // [-90..90]
|
||||
[SerializeField]
|
||||
float m_Longitude = 299.0f; // [0..360[
|
||||
|
||||
/// <summary>
|
||||
/// The shading tint to used when computing shadow from sun
|
||||
/// </summary>
|
||||
public Color shadowColor = new Color(0.7f, 0.7f, 0.7f);
|
||||
|
||||
/// <summary>
|
||||
/// The cubemap used for this part of the lighting environment
|
||||
/// </summary>
|
||||
public Cubemap cubemap
|
||||
{
|
||||
get
|
||||
{
|
||||
if (m_Cubemap == null || m_Cubemap.Equals(null))
|
||||
LoadCubemap();
|
||||
return m_Cubemap;
|
||||
}
|
||||
set
|
||||
{
|
||||
m_Cubemap = value;
|
||||
m_CubemapGUID = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(m_Cubemap));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Latitude position of the sun casting shadows
|
||||
/// </summary>
|
||||
public float sunLatitude
|
||||
{
|
||||
get => m_Latitude;
|
||||
set => m_Latitude = ClampLatitude(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The Longitude position of the sun casting shadows
|
||||
/// </summary>
|
||||
public float sunLongitude
|
||||
{
|
||||
get => m_Longitude;
|
||||
set => m_Longitude = ClampLongitude(value);
|
||||
}
|
||||
|
||||
internal static float ClampLatitude(float value) => Mathf.Clamp(value, -90, 90);
|
||||
|
||||
internal static float ClampLongitude(float value)
|
||||
{
|
||||
value = value % 360f;
|
||||
if (value < 0.0)
|
||||
value += 360f;
|
||||
return value;
|
||||
}
|
||||
|
||||
internal void UpdateSunPosition(Light sun)
|
||||
=> sun.transform.rotation = Quaternion.Euler(sunLatitude, rotation + sunLongitude, 0f);
|
||||
|
||||
/// <summary>
|
||||
/// Compute sun position to be brightest spot of the sky
|
||||
/// </summary>
|
||||
public void ResetToBrightestSpot()
|
||||
=> EnvironmentElement.ResetToBrightestSpot(this);
|
||||
|
||||
void LoadCubemap()
|
||||
{
|
||||
m_Cubemap = null;
|
||||
|
||||
GUID storedGUID;
|
||||
GUID.TryParse(m_CubemapGUID, out storedGUID);
|
||||
if (!storedGUID.Empty())
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(m_CubemapGUID);
|
||||
m_Cubemap = AssetDatabase.LoadAssetAtPath<Cubemap>(path);
|
||||
}
|
||||
}
|
||||
|
||||
internal void CopyTo(Environment other)
|
||||
{
|
||||
other.cubemap = cubemap;
|
||||
other.exposure = exposure;
|
||||
other.rotation = rotation;
|
||||
other.sunLatitude = sunLatitude;
|
||||
other.sunLongitude = sunLongitude;
|
||||
other.shadowColor = shadowColor;
|
||||
other.name = name + " (copy)";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Implicit conversion operator to runtime version of sky datas
|
||||
/// </summary>
|
||||
/// <param name="sky">Editor version of the datas</param>
|
||||
public UnityEngine.Rendering.LookDev.Sky sky
|
||||
=> new UnityEngine.Rendering.LookDev.Sky()
|
||||
{
|
||||
cubemap = cubemap,
|
||||
longitudeOffset = rotation,
|
||||
exposure = exposure
|
||||
};
|
||||
|
||||
internal static Environment GetTemporaryEnvironmentForCubemap(Cubemap cubemap)
|
||||
{
|
||||
Environment result = ScriptableObject.CreateInstance<Environment>();
|
||||
result.cubemap = cubemap;
|
||||
result.isCubemapOnly = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
internal bool HasCubemapAssetChanged(Cubemap cubemap)
|
||||
{
|
||||
if (cubemap == null)
|
||||
return !String.IsNullOrEmpty(m_CubemapGUID);
|
||||
|
||||
return m_CubemapGUID != AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(cubemap));
|
||||
}
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(Environment))]
|
||||
class EnvironmentEditor : Editor
|
||||
{
|
||||
//display nothing
|
||||
public sealed override VisualElement CreateInspectorGUI() => null;
|
||||
|
||||
// Don't use ImGUI
|
||||
public sealed override void OnInspectorGUI() {}
|
||||
|
||||
//but make preview in Project window
|
||||
override public Texture2D RenderStaticPreview(string assetPath, UnityEngine.Object[] subAssets, int width, int height)
|
||||
=> EnvironmentElement.GetLatLongThumbnailTexture(target as Environment, width);
|
||||
}
|
||||
|
||||
interface IBendable<T>
|
||||
{
|
||||
void Bind(T data);
|
||||
}
|
||||
|
||||
class EnvironmentElement : VisualElement, IBendable<Environment>
|
||||
{
|
||||
internal const int k_SkyThumbnailWidth = 200;
|
||||
internal const int k_SkyThumbnailHeight = 100;
|
||||
static Material s_cubeToLatlongMaterial;
|
||||
static Material cubeToLatlongMaterial
|
||||
{
|
||||
get
|
||||
{
|
||||
if (s_cubeToLatlongMaterial == null || s_cubeToLatlongMaterial.Equals(null))
|
||||
{
|
||||
s_cubeToLatlongMaterial = new Material(Shader.Find("Hidden/LookDev/CubeToLatlong"));
|
||||
}
|
||||
return s_cubeToLatlongMaterial;
|
||||
}
|
||||
}
|
||||
|
||||
VisualElement environmentParams;
|
||||
Environment environment;
|
||||
|
||||
Image latlong;
|
||||
ObjectField skyCubemapField;
|
||||
FloatField skyRotationOffset;
|
||||
FloatField skyExposureField;
|
||||
Vector2Field sunPosition;
|
||||
ColorField shadowColor;
|
||||
TextField environmentName;
|
||||
|
||||
Action OnChangeCallback;
|
||||
|
||||
public Environment target => environment;
|
||||
|
||||
public EnvironmentElement() => Create(withPreview : true);
|
||||
public EnvironmentElement(bool withPreview, Action OnChangeCallback = null)
|
||||
{
|
||||
this.OnChangeCallback = OnChangeCallback;
|
||||
Create(withPreview);
|
||||
}
|
||||
|
||||
public EnvironmentElement(Environment environment)
|
||||
{
|
||||
Create(withPreview: true);
|
||||
Bind(environment);
|
||||
}
|
||||
|
||||
void Create(bool withPreview)
|
||||
{
|
||||
if (withPreview)
|
||||
{
|
||||
latlong = new Image();
|
||||
latlong.style.width = k_SkyThumbnailWidth;
|
||||
latlong.style.height = k_SkyThumbnailHeight;
|
||||
Add(latlong);
|
||||
}
|
||||
|
||||
environmentParams = GetDefaultInspector();
|
||||
Add(environmentParams);
|
||||
}
|
||||
|
||||
public void Bind(Environment environment)
|
||||
{
|
||||
this.environment = environment;
|
||||
if (environment == null || environment.Equals(null))
|
||||
return;
|
||||
|
||||
if (latlong != null && !latlong.Equals(null))
|
||||
latlong.image = GetLatLongThumbnailTexture();
|
||||
skyCubemapField.SetValueWithoutNotify(environment.cubemap);
|
||||
skyRotationOffset.SetValueWithoutNotify(environment.rotation);
|
||||
skyExposureField.SetValueWithoutNotify(environment.exposure);
|
||||
sunPosition.SetValueWithoutNotify(new Vector2(environment.sunLongitude, environment.sunLatitude));
|
||||
shadowColor.SetValueWithoutNotify(environment.shadowColor);
|
||||
environmentName.SetValueWithoutNotify(environment.name);
|
||||
}
|
||||
|
||||
public void Bind(Environment environment, Image deportedLatlong)
|
||||
{
|
||||
latlong = deportedLatlong;
|
||||
Bind(environment);
|
||||
}
|
||||
|
||||
static public Vector2 PositionToLatLong(Vector2 position)
|
||||
{
|
||||
Vector2 result = new Vector2();
|
||||
result.x = position.y * Mathf.PI * 0.5f * Mathf.Rad2Deg;
|
||||
result.y = (position.x * 0.5f + 0.5f) * 2f * Mathf.PI * Mathf.Rad2Deg;
|
||||
|
||||
if (result.x < -90.0f) result.x = -90f;
|
||||
if (result.x > 90.0f) result.x = 90f;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void ResetToBrightestSpot(Environment environment)
|
||||
{
|
||||
cubeToLatlongMaterial.SetTexture("_MainTex", environment.cubemap);
|
||||
cubeToLatlongMaterial.SetVector("_WindowParams", new Vector4(10000, -1000.0f, 2, 0.0f)); // Neutral value to not clip
|
||||
cubeToLatlongMaterial.SetVector("_CubeToLatLongParams", new Vector4(Mathf.Deg2Rad * environment.rotation, 0.5f, 1.0f, 3.0f)); // We use LOD 3 to take a region rather than a single pixel in the map
|
||||
cubeToLatlongMaterial.SetPass(0);
|
||||
|
||||
int width = k_SkyThumbnailWidth;
|
||||
int height = width >> 1;
|
||||
|
||||
RenderTexture temporaryRT = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||
Texture2D brightestPointTexture = new Texture2D(width, height, TextureFormat.RGBAHalf, false);
|
||||
|
||||
// Convert cubemap to a 2D LatLong to read on CPU
|
||||
Graphics.Blit(environment.cubemap, temporaryRT, cubeToLatlongMaterial);
|
||||
brightestPointTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
|
||||
brightestPointTexture.Apply();
|
||||
|
||||
// CPU read back
|
||||
// From Doc: The returned array is a flattened 2D array, where pixels are laid out left to right, bottom to top (i.e. row after row)
|
||||
Color[] color = brightestPointTexture.GetPixels();
|
||||
RenderTexture.active = null;
|
||||
temporaryRT.Release();
|
||||
|
||||
float maxLuminance = 0.0f;
|
||||
int maxIndex = 0;
|
||||
for (int index = height * width - 1; index >= 0; --index)
|
||||
{
|
||||
Color pixel = color[index];
|
||||
float luminance = pixel.r * 0.2126729f + pixel.g * 0.7151522f + pixel.b * 0.0721750f;
|
||||
if (maxLuminance < luminance)
|
||||
{
|
||||
maxLuminance = luminance;
|
||||
maxIndex = index;
|
||||
}
|
||||
}
|
||||
Vector2 sunPosition = PositionToLatLong(new Vector2(((maxIndex % width) / (float)(width - 1)) * 2f - 1f, ((maxIndex / width) / (float)(height - 1)) * 2f - 1f));
|
||||
environment.sunLatitude = sunPosition.x;
|
||||
environment.sunLongitude = sunPosition.y - environment.rotation;
|
||||
}
|
||||
|
||||
public Texture2D GetLatLongThumbnailTexture()
|
||||
=> GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
|
||||
|
||||
public static Texture2D GetLatLongThumbnailTexture(Environment environment, int width)
|
||||
{
|
||||
int height = width >> 1;
|
||||
RenderTexture oldActive = RenderTexture.active;
|
||||
RenderTexture temporaryRT = new RenderTexture(width, height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||||
RenderTexture.active = temporaryRT;
|
||||
cubeToLatlongMaterial.SetTexture("_MainTex", environment.cubemap);
|
||||
cubeToLatlongMaterial.SetVector("_WindowParams",
|
||||
new Vector4(
|
||||
height, //height
|
||||
-1000f, //y position, -1000f to be sure to not have clipping issue (we should not clip normally but don't want to create a new shader)
|
||||
2f, //margin value
|
||||
1f)); //Pixel per Point
|
||||
cubeToLatlongMaterial.SetVector("_CubeToLatLongParams",
|
||||
new Vector4(
|
||||
Mathf.Deg2Rad * environment.rotation, //rotation of the environment in radian
|
||||
1f, //alpha
|
||||
1f, //intensity
|
||||
0f)); //LOD
|
||||
cubeToLatlongMaterial.SetPass(0);
|
||||
GL.LoadPixelMatrix(0, width, height, 0);
|
||||
GL.Clear(true, true, Color.black);
|
||||
Rect skyRect = new Rect(0, 0, width, height);
|
||||
Renderer.DrawFullScreenQuad(skyRect);
|
||||
|
||||
Texture2D result = new Texture2D(width, height, TextureFormat.ARGB32, false);
|
||||
result.ReadPixels(new Rect(0, 0, width, height), 0, 0, false);
|
||||
result.Apply(false);
|
||||
RenderTexture.active = oldActive;
|
||||
UnityEngine.Object.DestroyImmediate(temporaryRT);
|
||||
return result;
|
||||
}
|
||||
|
||||
public VisualElement GetDefaultInspector()
|
||||
{
|
||||
VisualElement inspector = new VisualElement() { name = "inspector" };
|
||||
|
||||
VisualElement header = new VisualElement() { name = "inspector-header" };
|
||||
header.Add(new Image()
|
||||
{
|
||||
image = CoreEditorUtils.LoadIcon(@"Packages/com.unity.render-pipelines.core/Editor/LookDev/Icons/", "Environment", forceLowRes: true)
|
||||
});
|
||||
environmentName = new TextField();
|
||||
environmentName.isDelayed = true;
|
||||
environmentName.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
string path = AssetDatabase.GetAssetPath(environment);
|
||||
environment.name = evt.newValue;
|
||||
AssetDatabase.SetLabels(environment, new string[] { evt.newValue });
|
||||
EditorUtility.SetDirty(environment);
|
||||
AssetDatabase.ImportAsset(path);
|
||||
environmentName.name = environment.name;
|
||||
});
|
||||
header.Add(environmentName);
|
||||
inspector.Add(header);
|
||||
|
||||
Foldout foldout = new Foldout()
|
||||
{
|
||||
text = "Environment Settings"
|
||||
};
|
||||
skyCubemapField = new ObjectField("Sky with Sun")
|
||||
{
|
||||
tooltip = "A cubemap that will be used as the sky."
|
||||
};
|
||||
skyCubemapField.allowSceneObjects = false;
|
||||
skyCubemapField.objectType = typeof(Cubemap);
|
||||
skyCubemapField.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
var tmp = environment.cubemap;
|
||||
RegisterChange(ref tmp, evt.newValue as Cubemap, updatePreview: true, customResync: () => environment.cubemap = tmp);
|
||||
});
|
||||
foldout.Add(skyCubemapField);
|
||||
|
||||
skyRotationOffset = new FloatField("Rotation")
|
||||
{
|
||||
tooltip = "Rotation offset on the longitude of the sky."
|
||||
};
|
||||
skyRotationOffset.RegisterValueChangedCallback(evt
|
||||
=> RegisterChange(ref environment.rotation, Environment.ClampLongitude(evt.newValue), skyRotationOffset, updatePreview: true));
|
||||
foldout.Add(skyRotationOffset);
|
||||
|
||||
skyExposureField = new FloatField("Exposure")
|
||||
{
|
||||
tooltip = "The exposure to apply with this sky."
|
||||
};
|
||||
skyExposureField.RegisterValueChangedCallback(evt
|
||||
=> RegisterChange(ref environment.exposure, evt.newValue));
|
||||
foldout.Add(skyExposureField);
|
||||
var style = foldout.Q<Toggle>().style;
|
||||
style.marginLeft = 3;
|
||||
style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
inspector.Add(foldout);
|
||||
|
||||
sunPosition = new Vector2Field("Sun Position")
|
||||
{
|
||||
tooltip = "The sun position as (Longitude, Latitude)\nThe button compute brightest position in the sky with sun."
|
||||
};
|
||||
sunPosition.Q("unity-x-input").Q<FloatField>().formatString = "n1";
|
||||
sunPosition.Q("unity-y-input").Q<FloatField>().formatString = "n1";
|
||||
sunPosition.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
var tmpContainer = new Vector2(
|
||||
environment.sunLongitude,
|
||||
environment.sunLatitude);
|
||||
var tmpNewValue = new Vector2(
|
||||
Environment.ClampLongitude(evt.newValue.x),
|
||||
Environment.ClampLatitude(evt.newValue.y));
|
||||
RegisterChange(ref tmpContainer, tmpNewValue, sunPosition, customResync: () =>
|
||||
{
|
||||
environment.sunLongitude = tmpContainer.x;
|
||||
environment.sunLatitude = tmpContainer.y;
|
||||
});
|
||||
});
|
||||
foldout.Add(sunPosition);
|
||||
|
||||
Button sunToBrightess = new Button(() =>
|
||||
{
|
||||
ResetToBrightestSpot(environment);
|
||||
sunPosition.SetValueWithoutNotify(new Vector2(
|
||||
Environment.ClampLongitude(environment.sunLongitude),
|
||||
Environment.ClampLatitude(environment.sunLatitude)));
|
||||
})
|
||||
{
|
||||
name = "sunToBrightestButton"
|
||||
};
|
||||
sunToBrightess.Add(new Image()
|
||||
{
|
||||
image = CoreEditorUtils.LoadIcon(@"Packages/com.unity.render-pipelines.core/Editor/LookDev/Icons/", "SunPosition", forceLowRes: true)
|
||||
});
|
||||
sunToBrightess.AddToClassList("sun-to-brightest-button");
|
||||
var vector2Input = sunPosition.Q(className: "unity-vector2-field__input");
|
||||
vector2Input.Remove(sunPosition.Q(className: "unity-composite-field__field-spacer"));
|
||||
vector2Input.Add(sunToBrightess);
|
||||
|
||||
shadowColor = new ColorField("Shadow Tint")
|
||||
{
|
||||
tooltip = "The wanted shadow tint to be used when computing shadow."
|
||||
};
|
||||
shadowColor.RegisterValueChangedCallback(evt
|
||||
=> RegisterChange(ref environment.shadowColor, evt.newValue));
|
||||
foldout.Add(shadowColor);
|
||||
|
||||
style = foldout.Q<Toggle>().style;
|
||||
style.marginLeft = 3;
|
||||
style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
inspector.Add(foldout);
|
||||
|
||||
return inspector;
|
||||
}
|
||||
|
||||
void RegisterChange<TValueType>(ref TValueType reflectedVariable, TValueType newValue, BaseField<TValueType> resyncField = null, bool updatePreview = false, Action customResync = null)
|
||||
{
|
||||
if (environment == null || environment.Equals(null))
|
||||
return;
|
||||
reflectedVariable = newValue;
|
||||
resyncField?.SetValueWithoutNotify(newValue);
|
||||
customResync?.Invoke();
|
||||
if (updatePreview && latlong != null && !latlong.Equals(null))
|
||||
latlong.image = GetLatLongThumbnailTexture(environment, k_SkyThumbnailWidth);
|
||||
EditorUtility.SetDirty(environment);
|
||||
OnChangeCallback?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,207 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.UIElements;
|
||||
using System.IO;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>
|
||||
/// Class containing a collection of Environment
|
||||
/// </summary>
|
||||
[HelpURL(Documentation.baseURLHDRP + Documentation.version + Documentation.subURL + "Environment-Library" + Documentation.endURL)]
|
||||
public class EnvironmentLibrary : ScriptableObject
|
||||
{
|
||||
[field: SerializeField]
|
||||
List<Environment> environments { get; set; } = new List<Environment>();
|
||||
|
||||
/// <summary>
|
||||
/// Number of elements in the collection
|
||||
/// </summary>
|
||||
public int Count => environments.Count;
|
||||
/// <summary>
|
||||
/// Indexer giving access to contained Environment
|
||||
/// </summary>
|
||||
public Environment this[int index] => environments[index];
|
||||
|
||||
/// <summary>
|
||||
/// Create a new empty Environment at the end of the collection
|
||||
/// </summary>
|
||||
/// <returns>The created Environment</returns>
|
||||
public Environment Add()
|
||||
{
|
||||
Undo.SetCurrentGroupName("Add Environment");
|
||||
int group = Undo.GetCurrentGroup();
|
||||
|
||||
Environment environment = ScriptableObject.CreateInstance<Environment>();
|
||||
environment.name = "New Environment";
|
||||
Undo.RegisterCreatedObjectUndo(environment, "Add Environment");
|
||||
|
||||
Undo.RecordObject(this, "Add Environment");
|
||||
environments.Add(environment);
|
||||
|
||||
// Store this new environment as a subasset so we can reference it safely afterwards.
|
||||
AssetDatabase.AddObjectToAsset(environment, this);
|
||||
|
||||
Undo.CollapseUndoOperations(group);
|
||||
|
||||
// Force save / refresh. Important to do this last because SaveAssets can cause effect to become null!
|
||||
EditorUtility.SetDirty(this);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove Environment of the collection at given index
|
||||
/// </summary>
|
||||
/// <param name="index">Index where to remove Environment</param>
|
||||
public void Remove(int index)
|
||||
{
|
||||
Undo.SetCurrentGroupName("Remove Environment");
|
||||
int group = Undo.GetCurrentGroup();
|
||||
|
||||
Environment environment = environments[index];
|
||||
Undo.RecordObject(this, "Remove Environment");
|
||||
environments.RemoveAt(index);
|
||||
Undo.DestroyObjectImmediate(environment);
|
||||
|
||||
Undo.CollapseUndoOperations(group);
|
||||
|
||||
// Force save / refresh
|
||||
EditorUtility.SetDirty(this);
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Duplicate the Environment at given index and add it at the end of the Collection
|
||||
/// </summary>
|
||||
/// <param name="fromIndex">Index where to take data for duplication</param>
|
||||
/// <returns>The created Environment</returns>
|
||||
public Environment Duplicate(int fromIndex)
|
||||
{
|
||||
Undo.SetCurrentGroupName("Duplicate Environment");
|
||||
int group = Undo.GetCurrentGroup();
|
||||
|
||||
Environment environment = ScriptableObject.CreateInstance<Environment>();
|
||||
Environment environmentToCopy = environments[fromIndex];
|
||||
environmentToCopy.CopyTo(environment);
|
||||
|
||||
Undo.RegisterCreatedObjectUndo(environment, "Duplicate Environment");
|
||||
Undo.RecordObject(this, "Duplicate Environment");
|
||||
environments.Add(environment);
|
||||
|
||||
// Store this new environment as a subasset so we can reference it safely afterwards.
|
||||
AssetDatabase.AddObjectToAsset(environment, this);
|
||||
|
||||
Undo.CollapseUndoOperations(group);
|
||||
|
||||
// Force save / refresh. Important to do this last because SaveAssets can cause effect to become null!
|
||||
EditorUtility.SetDirty(this);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
return environment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute position of given Environment in the collection
|
||||
/// </summary>
|
||||
/// <param name="environment">Environment to look at</param>
|
||||
/// <returns>Index of the searched environment. If not found, -1.</returns>
|
||||
public int IndexOf(Environment environment)
|
||||
=> environments.IndexOf(environment);
|
||||
}
|
||||
|
||||
[CustomEditor(typeof(EnvironmentLibrary))]
|
||||
class EnvironmentLibraryEditor : Editor
|
||||
{
|
||||
VisualElement m_Root;
|
||||
VisualElement m_OpenButton;
|
||||
|
||||
public sealed override VisualElement CreateInspectorGUI()
|
||||
{
|
||||
var library = target as EnvironmentLibrary;
|
||||
m_Root = new VisualElement();
|
||||
|
||||
m_OpenButton = new Button(() =>
|
||||
{
|
||||
if (!LookDev.open)
|
||||
LookDev.Open();
|
||||
LookDev.currentContext.UpdateEnvironmentLibrary(library);
|
||||
LookDev.currentEnvironmentDisplayer.Repaint();
|
||||
})
|
||||
{
|
||||
text = "Open in Look Dev window"
|
||||
};
|
||||
m_OpenButton.SetEnabled(LookDev.supported);
|
||||
|
||||
m_Root.Add(m_OpenButton);
|
||||
return m_Root;
|
||||
}
|
||||
|
||||
void OnEnable() => EditorApplication.update += Update;
|
||||
void OnDisable() => EditorApplication.update -= Update;
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Current SRP can be changed at any time so we need to do this at every update.
|
||||
if (m_OpenButton != null)
|
||||
m_OpenButton.SetEnabled(LookDev.supported);
|
||||
}
|
||||
|
||||
// Don't use ImGUI
|
||||
public sealed override void OnInspectorGUI() {}
|
||||
}
|
||||
|
||||
class EnvironmentLibraryCreator : ProjectWindowCallback.EndNameEditAction
|
||||
{
|
||||
ObjectField m_Field = null;
|
||||
|
||||
public void SetField(ObjectField field)
|
||||
=> m_Field = field;
|
||||
|
||||
public override void Cancelled(int instanceId, string pathName, string resourceFile)
|
||||
=> m_Field = null;
|
||||
|
||||
public override void Action(int instanceId, string pathName, string resourceFile)
|
||||
{
|
||||
var newAsset = CreateInstance<EnvironmentLibrary>();
|
||||
newAsset.name = Path.GetFileName(pathName);
|
||||
AssetDatabase.CreateAsset(newAsset, pathName);
|
||||
ProjectWindowUtil.ShowCreatedAsset(newAsset);
|
||||
if (m_Field != null)
|
||||
m_Field.value = newAsset;
|
||||
m_Field = null;
|
||||
}
|
||||
|
||||
[MenuItem("Assets/Create/LookDev/Environment Library", priority = 2000)]
|
||||
static void Create()
|
||||
{
|
||||
var icon = EditorGUIUtility.FindTexture("ScriptableObject Icon");
|
||||
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0, ScriptableObject.CreateInstance<EnvironmentLibraryCreator>(), "New EnvironmentLibrary.asset", icon, null);
|
||||
}
|
||||
|
||||
public static void CreateAndAssignTo(ObjectField field)
|
||||
{
|
||||
var icon = EditorGUIUtility.FindTexture("ScriptableObject Icon");
|
||||
var assetCreator = ScriptableObject.CreateInstance<EnvironmentLibraryCreator>();
|
||||
assetCreator.SetField(field);
|
||||
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(assetCreator.GetInstanceID(), assetCreator, "New EnvironmentLibrary.asset", icon, null);
|
||||
}
|
||||
}
|
||||
|
||||
static class EnvironmentLibraryLoader
|
||||
{
|
||||
static Action<UnityEngine.Object> LoadCallback(Action onUpdate)
|
||||
{
|
||||
return (UnityEngine.Object newLibrary) =>
|
||||
{
|
||||
LookDev.currentContext.UpdateEnvironmentLibrary(newLibrary as EnvironmentLibrary);
|
||||
onUpdate?.Invoke();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 195 B |
After Width: | Height: | Size: 284 B |
After Width: | Height: | Size: 332 B |
After Width: | Height: | Size: 537 B |
After Width: | Height: | Size: 415 B |
After Width: | Height: | Size: 740 B |
After Width: | Height: | Size: 351 B |
After Width: | Height: | Size: 618 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 347 B |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 895 B |
After Width: | Height: | Size: 266 B |
After Width: | Height: | Size: 467 B |
After Width: | Height: | Size: 351 B |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 297 B |
After Width: | Height: | Size: 506 B |
After Width: | Height: | Size: 225 B |
After Width: | Height: | Size: 340 B |
After Width: | Height: | Size: 218 B |
After Width: | Height: | Size: 333 B |
After Width: | Height: | Size: 229 B |
After Width: | Height: | Size: 400 B |
After Width: | Height: | Size: 328 B |
After Width: | Height: | Size: 639 B |
After Width: | Height: | Size: 394 B |
After Width: | Height: | Size: 891 B |
After Width: | Height: | Size: 127 B |
After Width: | Height: | Size: 180 B |
After Width: | Height: | Size: 749 B |
After Width: | Height: | Size: 208 B |
After Width: | Height: | Size: 359 B |
After Width: | Height: | Size: 466 B |
After Width: | Height: | Size: 959 B |
After Width: | Height: | Size: 415 B |
After Width: | Height: | Size: 720 B |
After Width: | Height: | Size: 196 B |
After Width: | Height: | Size: 288 B |
After Width: | Height: | Size: 321 B |
After Width: | Height: | Size: 499 B |
After Width: | Height: | Size: 435 B |
After Width: | Height: | Size: 734 B |
After Width: | Height: | Size: 340 B |
After Width: | Height: | Size: 651 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 352 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 903 B |
After Width: | Height: | Size: 274 B |
After Width: | Height: | Size: 454 B |
After Width: | Height: | Size: 359 B |
After Width: | Height: | Size: 667 B |
After Width: | Height: | Size: 309 B |
After Width: | Height: | Size: 506 B |
After Width: | Height: | Size: 228 B |
After Width: | Height: | Size: 345 B |
After Width: | Height: | Size: 206 B |
After Width: | Height: | Size: 318 B |
After Width: | Height: | Size: 203 B |
After Width: | Height: | Size: 365 B |
After Width: | Height: | Size: 325 B |
After Width: | Height: | Size: 640 B |
After Width: | Height: | Size: 384 B |
After Width: | Height: | Size: 856 B |
After Width: | Height: | Size: 127 B |
After Width: | Height: | Size: 181 B |
After Width: | Height: | Size: 735 B |
After Width: | Height: | Size: 199 B |
After Width: | Height: | Size: 345 B |
After Width: | Height: | Size: 449 B |
After Width: | Height: | Size: 953 B |
After Width: | Height: | Size: 379 B |
After Width: | Height: | Size: 752 B |
After Width: | Height: | Size: 367 B |
After Width: | Height: | Size: 361 B |
@@ -0,0 +1,37 @@
|
||||
Container
|
||||
{
|
||||
border-top-width:1px;
|
||||
border-color:#000000;
|
||||
padding-top:10px;
|
||||
padding-bottom:10px;
|
||||
}
|
||||
|
||||
Container.First
|
||||
{
|
||||
border-top-width:0px;
|
||||
}
|
||||
|
||||
Container.Selected > *
|
||||
{
|
||||
margin-left:-5px;
|
||||
border-left-width:5px;
|
||||
border-color:#3d6091;
|
||||
}
|
||||
|
||||
List > .Footer
|
||||
{
|
||||
align-self:flex-end;
|
||||
flex-direction:row;
|
||||
}
|
||||
|
||||
List > .Footer > *
|
||||
{
|
||||
width:25px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.unity-label
|
||||
{
|
||||
min-width:90px;
|
||||
}
|
@@ -0,0 +1,271 @@
|
||||
using UnityEngine.Rendering;
|
||||
using UnityEngine.Rendering.LookDev;
|
||||
using UnityEditorInternal;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>
|
||||
/// Main entry point for scripting LookDev
|
||||
/// </summary>
|
||||
public static class LookDev
|
||||
{
|
||||
const string lastRenderingDataSavePath = "Library/LookDevConfig.asset";
|
||||
|
||||
//TODO: ensure only one displayer at time for the moment
|
||||
static IViewDisplayer s_ViewDisplayer;
|
||||
static IEnvironmentDisplayer s_EnvironmentDisplayer;
|
||||
static Compositer s_Compositor;
|
||||
static StageCache s_Stages;
|
||||
static Context s_CurrentContext;
|
||||
|
||||
internal static IDataProvider dataProvider
|
||||
=> RenderPipelineManager.currentPipeline as IDataProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Get all the data used in LookDev currently (views, layout, debug... )
|
||||
/// </summary>
|
||||
internal static Context currentContext
|
||||
{
|
||||
//Lazy init: load it when needed instead in static even if you do not support lookdev
|
||||
get
|
||||
{
|
||||
if (s_CurrentContext == null || s_CurrentContext.Equals(null))
|
||||
{
|
||||
s_CurrentContext = LoadConfigInternal();
|
||||
if (s_CurrentContext == null)
|
||||
s_CurrentContext = defaultContext;
|
||||
|
||||
ReloadStage(false);
|
||||
}
|
||||
return s_CurrentContext;
|
||||
}
|
||||
private set => s_CurrentContext = value;
|
||||
}
|
||||
|
||||
static Context defaultContext
|
||||
{
|
||||
get
|
||||
{
|
||||
var context = UnityEngine.ScriptableObject.CreateInstance<Context>();
|
||||
context.Init();
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
//[TODO: not compatible with multiple displayer. To rework if needed]
|
||||
internal static IViewDisplayer currentViewDisplayer => s_ViewDisplayer;
|
||||
internal static IEnvironmentDisplayer currentEnvironmentDisplayer => s_EnvironmentDisplayer;
|
||||
|
||||
[MenuItem("Window/Render Pipeline/Look Dev", false, 10200)]
|
||||
static void OpenLookDev() => Open();
|
||||
|
||||
[MenuItem("Window/Render Pipeline/Look Dev", true, 10200)]
|
||||
static bool LookDevAvailable() => supported;
|
||||
|
||||
internal static bool waitingConfigure { get; private set; } = true;
|
||||
|
||||
/// <summary>State of the LookDev window</summary>
|
||||
public static bool open { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Does LookDev is supported with the current render pipeline?
|
||||
/// </summary>
|
||||
public static bool supported => dataProvider != null;
|
||||
|
||||
/// <summary>
|
||||
/// Reset all LookDevs datas to the default configuration
|
||||
/// </summary>
|
||||
public static void ResetConfig()
|
||||
=> currentContext = defaultContext;
|
||||
|
||||
static Context LoadConfigInternal(string path = lastRenderingDataSavePath)
|
||||
{
|
||||
var objs = InternalEditorUtility.LoadSerializedFileAndForget(path);
|
||||
Context context = (objs.Length > 0 ? objs[0] : null) as Context;
|
||||
if (context != null && !context.Equals(null))
|
||||
context.Init();
|
||||
return context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a different set of datas
|
||||
/// </summary>
|
||||
/// <param name="path">Path where to load</param>
|
||||
internal static void LoadConfig(string path = lastRenderingDataSavePath)
|
||||
{
|
||||
var last = LoadConfigInternal(path);
|
||||
if (last != null)
|
||||
currentContext = last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Save the current set of datas
|
||||
/// </summary>
|
||||
/// <param name="path">[optional] Path to save. By default, saved in Library folder</param>
|
||||
internal static void SaveConfig(string path = lastRenderingDataSavePath)
|
||||
{
|
||||
if (currentContext != null && !currentContext.Equals(null))
|
||||
InternalEditorUtility.SaveToSerializedFileAndForget(new[] { currentContext }, path, true);
|
||||
}
|
||||
|
||||
/// <summary>Open the LookDev window</summary>
|
||||
public static void Open()
|
||||
{
|
||||
EditorWindow.GetWindow<DisplayWindow>();
|
||||
}
|
||||
|
||||
/// <summary>Close the LookDev window</summary>
|
||||
public static void Close()
|
||||
{
|
||||
(s_ViewDisplayer as EditorWindow)?.Close();
|
||||
s_ViewDisplayer = null;
|
||||
(s_EnvironmentDisplayer as EditorWindow)?.Close();
|
||||
s_EnvironmentDisplayer = null;
|
||||
}
|
||||
|
||||
internal static void Initialize(DisplayWindow window)
|
||||
{
|
||||
s_ViewDisplayer = window;
|
||||
s_EnvironmentDisplayer = window;
|
||||
open = true;
|
||||
|
||||
// Lookdev Initialize can be called when the window is re-created by the editor layout system.
|
||||
// In that case, the current context won't be null and there might be objects to reload from the temp ID
|
||||
ConfigureLookDev(reloadWithTemporaryID: s_CurrentContext != null);
|
||||
}
|
||||
|
||||
[Callbacks.DidReloadScripts]
|
||||
static void OnEditorReload()
|
||||
{
|
||||
var windows = Resources.FindObjectsOfTypeAll<DisplayWindow>();
|
||||
s_ViewDisplayer = windows.Length > 0 ? windows[0] : null;
|
||||
s_EnvironmentDisplayer = windows.Length > 0 ? windows[0] : null;
|
||||
open = s_ViewDisplayer != null;
|
||||
if (open)
|
||||
ConfigureLookDev(reloadWithTemporaryID: true);
|
||||
}
|
||||
|
||||
static void ConfigureLookDev(bool reloadWithTemporaryID)
|
||||
{
|
||||
open = true;
|
||||
waitingConfigure = true;
|
||||
if (s_CurrentContext == null || s_CurrentContext.Equals(null))
|
||||
LoadConfig();
|
||||
WaitingSRPReloadForConfiguringRenderer(5, reloadWithTemporaryID: reloadWithTemporaryID);
|
||||
}
|
||||
|
||||
static void WaitingSRPReloadForConfiguringRenderer(int maxAttempt, bool reloadWithTemporaryID, int attemptNumber = 0)
|
||||
{
|
||||
if (supported)
|
||||
{
|
||||
waitingConfigure = false;
|
||||
ConfigureRenderer(reloadWithTemporaryID);
|
||||
LinkViewDisplayer();
|
||||
LinkEnvironmentDisplayer();
|
||||
ReloadStage(reloadWithTemporaryID);
|
||||
}
|
||||
else if (attemptNumber < maxAttempt)
|
||||
EditorApplication.delayCall +=
|
||||
() => WaitingSRPReloadForConfiguringRenderer(maxAttempt, reloadWithTemporaryID, ++attemptNumber);
|
||||
else
|
||||
waitingConfigure = false;
|
||||
}
|
||||
|
||||
static void ConfigureRenderer(bool reloadWithTemporaryID)
|
||||
{
|
||||
s_Stages?.Dispose(); //clean previous occurrence on reloading
|
||||
s_Stages = new StageCache(dataProvider);
|
||||
s_Compositor?.Dispose(); //clean previous occurrence on reloading
|
||||
s_Compositor = new Compositer(s_ViewDisplayer, dataProvider, s_Stages);
|
||||
}
|
||||
|
||||
static void LinkViewDisplayer()
|
||||
{
|
||||
s_ViewDisplayer.OnClosed += () =>
|
||||
{
|
||||
s_Compositor?.Dispose();
|
||||
s_Compositor = null;
|
||||
s_Stages?.Dispose();
|
||||
s_Stages = null;
|
||||
s_ViewDisplayer = null;
|
||||
//currentContext = null;
|
||||
|
||||
SaveConfig();
|
||||
|
||||
open = false;
|
||||
};
|
||||
s_ViewDisplayer.OnLayoutChanged += (layout, envPanelOpen) =>
|
||||
{
|
||||
currentContext.layout.viewLayout = layout;
|
||||
currentContext.layout.showedSidePanel = envPanelOpen;
|
||||
SaveConfig();
|
||||
};
|
||||
s_ViewDisplayer.OnChangingObjectInView += (go, index, localPos) =>
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case ViewCompositionIndex.First:
|
||||
case ViewCompositionIndex.Second:
|
||||
currentContext.GetViewContent((ViewIndex)index).UpdateViewedObject(go);
|
||||
SaveContextChangeAndApply((ViewIndex)index);
|
||||
break;
|
||||
case ViewCompositionIndex.Composite:
|
||||
ViewIndex viewIndex = s_Compositor.GetViewFromComposition(localPos);
|
||||
currentContext.GetViewContent(viewIndex).UpdateViewedObject(go);
|
||||
SaveContextChangeAndApply(viewIndex);
|
||||
break;
|
||||
}
|
||||
};
|
||||
s_ViewDisplayer.OnChangingEnvironmentInView += (obj, index, localPos) =>
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case ViewCompositionIndex.First:
|
||||
case ViewCompositionIndex.Second:
|
||||
currentContext.GetViewContent((ViewIndex)index).UpdateEnvironment(obj);
|
||||
SaveContextChangeAndApply((ViewIndex)index);
|
||||
break;
|
||||
case ViewCompositionIndex.Composite:
|
||||
ViewIndex viewIndex = s_Compositor.GetViewFromComposition(localPos);
|
||||
currentContext.GetViewContent(viewIndex).UpdateEnvironment(obj);
|
||||
SaveContextChangeAndApply(viewIndex);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static void LinkEnvironmentDisplayer()
|
||||
{
|
||||
s_EnvironmentDisplayer.OnChangingEnvironmentLibrary += UpdateEnvironmentLibrary;
|
||||
}
|
||||
|
||||
static void UpdateEnvironmentLibrary(EnvironmentLibrary library)
|
||||
{
|
||||
LookDev.currentContext.UpdateEnvironmentLibrary(library);
|
||||
}
|
||||
|
||||
static void ReloadStage(bool reloadWithTemporaryID)
|
||||
{
|
||||
currentContext.GetViewContent(ViewIndex.First).LoadAll(reloadWithTemporaryID);
|
||||
ApplyContextChange(ViewIndex.First);
|
||||
currentContext.GetViewContent(ViewIndex.Second).LoadAll(reloadWithTemporaryID);
|
||||
ApplyContextChange(ViewIndex.Second);
|
||||
}
|
||||
|
||||
static void ApplyContextChange(ViewIndex index)
|
||||
{
|
||||
s_Stages.UpdateSceneObjects(index);
|
||||
s_Stages.UpdateSceneLighting(index, dataProvider);
|
||||
s_ViewDisplayer.Repaint();
|
||||
}
|
||||
|
||||
/// <summary>Update the rendered element with element in the context</summary>
|
||||
/// <param name="index">The index of the stage to update</param>
|
||||
internal static void SaveContextChangeAndApply(ViewIndex index)
|
||||
{
|
||||
SaveConfig();
|
||||
ApplyContextChange(index);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,129 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering.LookDev;
|
||||
using IDataProvider = UnityEngine.Rendering.LookDev.IDataProvider;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
/// <summary>Data container to be used with Renderer class</summary>
|
||||
class RenderingData : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Internally set to true when the given RenderTexture <see cref="output"/> was not the good size regarding <see cref="viewPort"/> and needed to be recreated
|
||||
/// </summary>
|
||||
public bool sizeMissmatched;
|
||||
/// <summary>The stage that possess every object in your view</summary>
|
||||
public Stage stage;
|
||||
/// <summary>Callback to update the Camera position. Only done in First phase.</summary>
|
||||
public ICameraUpdater updater;
|
||||
/// <summary>Viewport size</summary>
|
||||
public Rect viewPort;
|
||||
/// <summary>Render texture handling captured image</summary>
|
||||
public RenderTexture output;
|
||||
|
||||
private bool disposed = false;
|
||||
|
||||
/// <summary>Dispose pattern</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
disposed = true;
|
||||
|
||||
stage = null;
|
||||
updater = null;
|
||||
output?.Release();
|
||||
output = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Basic renderer to draw scene in texture</summary>
|
||||
class Renderer
|
||||
{
|
||||
/// <summary>Use pixel perfect</summary>
|
||||
public bool pixelPerfect { get; set; }
|
||||
|
||||
/// <summary>Constructor</summary>
|
||||
/// <param name="pixelPerfect">[Optional] Use pixel perfect</param>
|
||||
public Renderer(bool pixelPerfect = false)
|
||||
=> this.pixelPerfect = pixelPerfect;
|
||||
|
||||
/// <summary>Init for rendering</summary>
|
||||
/// <param name="data">The data to use</param>
|
||||
public void BeginRendering(RenderingData data, IDataProvider dataProvider)
|
||||
{
|
||||
data.stage.OnBeginRendering(dataProvider);
|
||||
data.updater?.UpdateCamera(data.stage.camera);
|
||||
data.stage.camera.enabled = true;
|
||||
}
|
||||
|
||||
/// <summary>Finish to render</summary>
|
||||
/// <param name="data">The data to use</param>
|
||||
public void EndRendering(RenderingData data, IDataProvider dataProvider)
|
||||
{
|
||||
data.stage.camera.enabled = false;
|
||||
data.stage.OnEndRendering(dataProvider);
|
||||
}
|
||||
|
||||
bool CheckWrongSizeOutput(RenderingData data)
|
||||
{
|
||||
if (data.viewPort.IsNullOrInverted()
|
||||
|| data.viewPort.width != data.output.width
|
||||
|| data.viewPort.height != data.viewPort.height)
|
||||
{
|
||||
data.output = null;
|
||||
data.sizeMissmatched = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
data.sizeMissmatched = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capture image of the scene.
|
||||
/// </summary>
|
||||
/// <param name="data">Datas required to compute the capture</param>
|
||||
/// [Optional] When drawing several time the scene, you can remove First and/or Last to not initialize objects.
|
||||
/// Be careful though to always start your frame with a First and always end with a Last.
|
||||
/// </param>
|
||||
public void Acquire(RenderingData data)
|
||||
{
|
||||
if (CheckWrongSizeOutput(data))
|
||||
return;
|
||||
|
||||
data.stage.camera.targetTexture = data.output;
|
||||
data.stage.camera.Render();
|
||||
}
|
||||
|
||||
internal static void DrawFullScreenQuad(Rect rect)
|
||||
{
|
||||
GL.PushMatrix();
|
||||
GL.LoadOrtho();
|
||||
GL.Viewport(rect);
|
||||
|
||||
GL.Begin(GL.QUADS);
|
||||
GL.TexCoord2(0, 0);
|
||||
GL.Vertex3(0f, 0f, 0);
|
||||
GL.TexCoord2(0, 1);
|
||||
GL.Vertex3(0f, 1f, 0);
|
||||
GL.TexCoord2(1, 1);
|
||||
GL.Vertex3(1f, 1f, 0);
|
||||
GL.TexCoord2(1, 0);
|
||||
GL.Vertex3(1f, 0f, 0);
|
||||
GL.End();
|
||||
GL.PopMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Rect extension</summary>
|
||||
public static partial class RectExtension
|
||||
{
|
||||
/// <summary>Return true if the <see cref="Rect"/> is null sized or inverted.</summary>
|
||||
/// <param name="r">The rect</param>
|
||||
/// <returns>True: null or inverted area</returns>
|
||||
public static bool IsNullOrInverted(this Rect r)
|
||||
=> r.width <= 0f || r.height <= 0f
|
||||
|| float.IsNaN(r.width) || float.IsNaN(r.height);
|
||||
}
|
||||
}
|
@@ -0,0 +1,378 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.SceneManagement;
|
||||
using UnityEngine.Rendering.LookDev;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
//TODO: add undo support
|
||||
/// <summary>
|
||||
/// Class handling object of the scene with isolation from other scene based on culling
|
||||
/// </summary>
|
||||
class Stage : IDisposable
|
||||
{
|
||||
const int k_PreviewCullingLayerIndex = 31; //Camera.PreviewCullingLayer; //TODO: expose or reflection
|
||||
|
||||
private readonly Scene m_PreviewScene;
|
||||
|
||||
// Everything except camera
|
||||
private readonly List<GameObject> m_GameObjects = new List<GameObject>();
|
||||
private readonly List<GameObject> m_PersistentGameObjects = new List<GameObject>();
|
||||
private readonly Camera m_Camera;
|
||||
private readonly Light m_SunLight;
|
||||
|
||||
/// <summary>Get access to the stage's camera</summary>
|
||||
public Camera camera => m_Camera;
|
||||
|
||||
/// <summary>Get access to the stage's light</summary>
|
||||
public Light sunLight => m_SunLight;
|
||||
|
||||
/// <summary>Get access to the stage's scene</summary>
|
||||
public Scene scene => m_PreviewScene;
|
||||
|
||||
private StageRuntimeInterface SRI;
|
||||
/// <summary>The runtime interface on stage</summary>
|
||||
public StageRuntimeInterface runtimeInterface
|
||||
=> SRI ?? (SRI = new StageRuntimeInterface(
|
||||
CreateGameObjectIntoStage,
|
||||
() => camera,
|
||||
() => sunLight));
|
||||
|
||||
/// <summary>
|
||||
/// Construct a new stage to let your object live.
|
||||
/// A stage is a scene with visibility isolation.
|
||||
/// </summary>
|
||||
/// <param name="sceneName">Name of the scene used.</param>
|
||||
public Stage(string sceneName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(sceneName))
|
||||
throw new System.ArgumentNullException("sceneName");
|
||||
|
||||
m_PreviewScene = EditorSceneManager.NewPreviewScene();
|
||||
m_PreviewScene.name = sceneName;
|
||||
|
||||
var camGO = EditorUtility.CreateGameObjectWithHideFlags("Look Dev Camera", HideFlags.HideAndDontSave, typeof(Camera));
|
||||
MoveIntoStage(camGO, true); //position will be updated right before rendering
|
||||
camGO.layer = k_PreviewCullingLayerIndex;
|
||||
|
||||
m_Camera = camGO.GetComponent<Camera>();
|
||||
m_Camera.cameraType = CameraType.Game; //cannot be preview in HDRP: too many things skiped
|
||||
m_Camera.enabled = false;
|
||||
m_Camera.clearFlags = CameraClearFlags.Depth;
|
||||
m_Camera.cullingMask = 1 << k_PreviewCullingLayerIndex;
|
||||
m_Camera.renderingPath = RenderingPath.DeferredShading;
|
||||
m_Camera.useOcclusionCulling = false;
|
||||
m_Camera.scene = m_PreviewScene;
|
||||
|
||||
var lightGO = EditorUtility.CreateGameObjectWithHideFlags("Look Dev Sun", HideFlags.HideAndDontSave, typeof(Light));
|
||||
MoveIntoStage(lightGO, true); //position will be updated right before rendering
|
||||
m_SunLight = lightGO.GetComponent<Light>();
|
||||
m_SunLight.type = LightType.Directional;
|
||||
m_SunLight.shadows = LightShadows.Soft;
|
||||
m_SunLight.intensity = 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move a GameObject into the stage's scene at origin.
|
||||
/// </summary>
|
||||
/// <param name="gameObject">The gameObject to move.</param>
|
||||
/// <param name="persistent">
|
||||
/// [OPTIONAL] If true, the object is not recreated with the scene update.
|
||||
/// Default value: false.
|
||||
/// </param>
|
||||
/// <seealso cref="InstantiateIntoStage"/>
|
||||
public void MoveIntoStage(GameObject gameObject, bool persistent = false)
|
||||
=> MoveIntoStage(gameObject, Vector3.zero, gameObject.transform.rotation, persistent);
|
||||
|
||||
/// <summary>
|
||||
/// Move a GameObject into the stage's scene at specific position and
|
||||
/// rotation.
|
||||
/// </summary>
|
||||
/// <param name="gameObject">The gameObject to move.</param>
|
||||
/// <param name="position">The new world position</param>
|
||||
/// <param name="rotation">The new world rotation</param>
|
||||
/// <param name="persistent">
|
||||
/// [OPTIONAL] If true, the object is not recreated with the scene update.
|
||||
/// Default value: false.
|
||||
/// </param>
|
||||
/// <seealso cref="InstantiateIntoStage"/>
|
||||
public void MoveIntoStage(GameObject gameObject, Vector3 position, Quaternion rotation, bool persistent = false)
|
||||
{
|
||||
if (m_GameObjects.Contains(gameObject))
|
||||
return;
|
||||
|
||||
SceneManager.MoveGameObjectToScene(gameObject, m_PreviewScene);
|
||||
gameObject.transform.position = position;
|
||||
gameObject.transform.rotation = rotation;
|
||||
if (persistent)
|
||||
m_PersistentGameObjects.Add(gameObject);
|
||||
else
|
||||
m_GameObjects.Add(gameObject);
|
||||
|
||||
InitAddedObjectsRecursively(gameObject);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Instantiate a scene GameObject or a prefab into the stage's scene.
|
||||
/// It is instantiated at origin.
|
||||
/// </summary>
|
||||
/// <param name="prefabOrSceneObject">The element to instantiate</param>
|
||||
/// <param name="persistent">
|
||||
/// [OPTIONAL] If true, the object is not recreated with the scene update.
|
||||
/// Default value: false.
|
||||
/// </param>
|
||||
/// <returns>The instance</returns>
|
||||
/// <seealso cref="MoveIntoStage"/>
|
||||
public GameObject InstantiateIntoStage(GameObject prefabOrSceneObject, bool persistent = false)
|
||||
=> InstantiateIntoStage(prefabOrSceneObject, Vector3.zero, prefabOrSceneObject.transform.rotation, persistent);
|
||||
|
||||
/// <summary>
|
||||
/// Instantiate a scene GameObject or a prefab into the stage's scene
|
||||
/// at a specific position and rotation.
|
||||
/// </summary>
|
||||
/// <param name="prefabOrSceneObject">The element to instantiate</param>
|
||||
/// <param name="position">The new world position</param>
|
||||
/// <param name="rotation">The new world rotation</param>
|
||||
/// <param name="persistent">
|
||||
/// [OPTIONAL] If true, the object is not recreated with the scene update.
|
||||
/// Default value: false.
|
||||
/// </param>
|
||||
/// <returns>The instance</returns>
|
||||
/// <seealso cref="MoveIntoStage"/>
|
||||
public GameObject InstantiateIntoStage(GameObject prefabOrSceneObject, Vector3 position, Quaternion rotation, bool persistent = false)
|
||||
{
|
||||
var handle = GameObject.Instantiate(prefabOrSceneObject);
|
||||
MoveIntoStage(handle, position, rotation, persistent);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>Create a GameObject into the stage.</summary>
|
||||
/// <param name="persistent">
|
||||
/// [OPTIONAL] If true, the object is not recreated with the scene update.
|
||||
/// Default value: false.
|
||||
/// </param>
|
||||
/// <returns>The created GameObject</returns>
|
||||
public GameObject CreateGameObjectIntoStage(bool persistent = false)
|
||||
{
|
||||
var handle = new GameObject();
|
||||
MoveIntoStage(handle, persistent);
|
||||
return handle;
|
||||
}
|
||||
|
||||
/// <summary>Clear all scene object except camera.</summary>
|
||||
/// <param name="persistent">
|
||||
/// [OPTIONAL] If true, clears also persistent objects.
|
||||
/// Default value: false.
|
||||
/// </param>
|
||||
public void Clear(bool persistent = false)
|
||||
{
|
||||
foreach (var go in m_GameObjects)
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
m_GameObjects.Clear();
|
||||
|
||||
if (persistent)
|
||||
{
|
||||
foreach (var go in m_PersistentGameObjects)
|
||||
UnityEngine.Object.DestroyImmediate(go);
|
||||
m_PersistentGameObjects.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
static void InitAddedObjectsRecursively(GameObject go)
|
||||
{
|
||||
go.hideFlags = HideFlags.HideAndDontSave;
|
||||
go.layer = k_PreviewCullingLayerIndex;
|
||||
|
||||
var meshRenderer = go.GetComponent<MeshRenderer>();
|
||||
if (meshRenderer != null)
|
||||
meshRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
|
||||
|
||||
var skinnedMeshRenderer = go.GetComponent<SkinnedMeshRenderer>();
|
||||
if (skinnedMeshRenderer != null)
|
||||
skinnedMeshRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
|
||||
|
||||
var lineRenderer = go.GetComponent<LineRenderer>();
|
||||
if (lineRenderer != null)
|
||||
lineRenderer.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
|
||||
|
||||
var volumes = go.GetComponents<UnityEngine.Rendering.Volume>();
|
||||
foreach (var volume in volumes)
|
||||
volume.UpdateLayer(); //force update of layer now as the Update can be called after we unregister volume from manager
|
||||
|
||||
foreach (Transform child in go.transform)
|
||||
InitAddedObjectsRecursively(child.gameObject);
|
||||
}
|
||||
|
||||
/// <summary>Changes stage scene's objects visibility.</summary>
|
||||
/// <param name="visible">
|
||||
/// True: make them visible.
|
||||
/// False: hide them.
|
||||
/// </param>
|
||||
void SetGameObjectVisible(bool visible)
|
||||
{
|
||||
foreach (GameObject go in m_GameObjects)
|
||||
{
|
||||
if (go == null || go.Equals(null))
|
||||
continue;
|
||||
foreach (UnityEngine.Renderer renderer in go.GetComponentsInChildren<UnityEngine.Renderer>())
|
||||
{
|
||||
if ((renderer.hideFlags & HideFlags.HideInInspector) == 0 && ((renderer.hideFlags & HideFlags.HideAndDontSave) == 0))
|
||||
renderer.enabled = visible;
|
||||
}
|
||||
foreach (Light light in go.GetComponentsInChildren<Light>())
|
||||
{
|
||||
if ((light.hideFlags & HideFlags.HideInInspector) == 0 && ((light.hideFlags & HideFlags.HideAndDontSave) == 0))
|
||||
light.enabled = visible;
|
||||
}
|
||||
}
|
||||
|
||||
// in case we add camera frontal light and such
|
||||
foreach (UnityEngine.Renderer renderer in m_Camera.GetComponentsInChildren<UnityEngine.Renderer>())
|
||||
{
|
||||
if ((renderer.hideFlags & HideFlags.HideInInspector) == 0 && ((renderer.hideFlags & HideFlags.HideAndDontSave) == 0))
|
||||
renderer.enabled = visible;
|
||||
}
|
||||
foreach (Light light in m_Camera.GetComponentsInChildren<Light>())
|
||||
{
|
||||
if ((light.hideFlags & HideFlags.HideInInspector) == 0 && ((light.hideFlags & HideFlags.HideAndDontSave) == 0))
|
||||
light.enabled = visible;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnBeginRendering(IDataProvider dataProvider)
|
||||
{
|
||||
SetGameObjectVisible(true);
|
||||
dataProvider.OnBeginRendering(runtimeInterface);
|
||||
}
|
||||
|
||||
public void OnEndRendering(IDataProvider dataProvider)
|
||||
{
|
||||
SetGameObjectVisible(false);
|
||||
dataProvider.OnEndRendering(runtimeInterface);
|
||||
}
|
||||
|
||||
private bool disposedValue = false; // To detect redundant calls
|
||||
|
||||
void CleanUp()
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
if (SRI != null)
|
||||
SRI.SRPData = null;
|
||||
SRI = null;
|
||||
EditorSceneManager.ClosePreviewScene(m_PreviewScene);
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
~Stage() => CleanUp();
|
||||
|
||||
/// <summary>Clear and close the stage's scene.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
CleanUp();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
class StageCache : IDisposable
|
||||
{
|
||||
const string firstStageName = "LookDevFirstView";
|
||||
const string secondStageName = "LookDevSecondView";
|
||||
|
||||
Stage[] m_Stages;
|
||||
IDataProvider m_CurrentDataProvider;
|
||||
|
||||
public Stage this[ViewIndex index]
|
||||
=> m_Stages[(int)index];
|
||||
|
||||
public bool initialized { get; private set; }
|
||||
|
||||
public StageCache(IDataProvider dataProvider)
|
||||
{
|
||||
m_Stages = new Stage[2]
|
||||
{
|
||||
InitStage(ViewIndex.First, dataProvider),
|
||||
InitStage(ViewIndex.Second, dataProvider)
|
||||
};
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
Stage InitStage(ViewIndex index, IDataProvider dataProvider)
|
||||
{
|
||||
Stage stage;
|
||||
switch (index)
|
||||
{
|
||||
case ViewIndex.First:
|
||||
stage = new Stage(firstStageName);
|
||||
stage.camera.backgroundColor = new Color32(5, 5, 5, 255);
|
||||
stage.camera.name += "_1";
|
||||
break;
|
||||
case ViewIndex.Second:
|
||||
stage = new Stage(secondStageName);
|
||||
stage.camera.backgroundColor = new Color32(5, 5, 5, 255);
|
||||
stage.camera.name += "_2";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException("Unknown ViewIndex: " + index);
|
||||
}
|
||||
|
||||
dataProvider.FirstInitScene(stage.runtimeInterface);
|
||||
|
||||
m_CurrentDataProvider = dataProvider;
|
||||
return stage;
|
||||
}
|
||||
|
||||
public void UpdateSceneObjects(ViewIndex index)
|
||||
{
|
||||
Stage stage = this[index];
|
||||
stage.Clear();
|
||||
|
||||
var viewContent = LookDev.currentContext.GetViewContent(index);
|
||||
if (viewContent == null)
|
||||
{
|
||||
viewContent.viewedInstanceInPreview = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (viewContent.viewedObjectReference != null && !viewContent.viewedObjectReference.Equals(null))
|
||||
viewContent.viewedInstanceInPreview = stage.InstantiateIntoStage(viewContent.viewedObjectReference);
|
||||
}
|
||||
|
||||
public void UpdateSceneLighting(ViewIndex index, IDataProvider provider)
|
||||
{
|
||||
Stage stage = this[index];
|
||||
Environment environment = LookDev.currentContext.GetViewContent(index).environment;
|
||||
provider.UpdateSky(stage.camera,
|
||||
environment == null ? default : environment.sky,
|
||||
stage.runtimeInterface);
|
||||
}
|
||||
|
||||
private bool disposedValue = false; // To detect redundant calls
|
||||
|
||||
void CleanUp()
|
||||
{
|
||||
if (!disposedValue)
|
||||
{
|
||||
foreach (Stage stage in m_Stages)
|
||||
{
|
||||
m_CurrentDataProvider.Cleanup(stage.runtimeInterface);
|
||||
stage.Dispose();
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
~StageCache() => CleanUp();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CleanUp();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,158 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace UnityEditor.Rendering.LookDev
|
||||
{
|
||||
class ToolbarRadio : UIElements.Toolbar, INotifyValueChanged<int>
|
||||
{
|
||||
public new class UxmlFactory : UxmlFactory<ToolbarRadio, UxmlTraits> {}
|
||||
public new class UxmlTraits : Button.UxmlTraits {}
|
||||
|
||||
List<ToolbarToggle> radios = new List<ToolbarToggle>();
|
||||
|
||||
public new static readonly string ussClassName = "unity-toolbar-radio";
|
||||
|
||||
bool m_CanDeselectAll = false;
|
||||
|
||||
public int radioLength { get; private set; } = 0;
|
||||
|
||||
int m_Value;
|
||||
public int value
|
||||
{
|
||||
get => m_Value;
|
||||
set
|
||||
{
|
||||
if (value == m_Value)
|
||||
return;
|
||||
|
||||
if (panel != null)
|
||||
{
|
||||
using (ChangeEvent<int> evt = ChangeEvent<int>.GetPooled(m_Value, value))
|
||||
{
|
||||
evt.target = this;
|
||||
SetValueWithoutNotify(value);
|
||||
SendEvent(evt);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
SetValueWithoutNotify(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ToolbarRadio() : this(null, false) {}
|
||||
|
||||
public ToolbarRadio(string label = null, bool canDeselectAll = false)
|
||||
{
|
||||
RemoveFromClassList(UIElements.Toolbar.ussClassName);
|
||||
AddToClassList(ussClassName);
|
||||
|
||||
m_CanDeselectAll = canDeselectAll;
|
||||
if (m_CanDeselectAll)
|
||||
m_Value = -1;
|
||||
if (label != null)
|
||||
Add(new Label() { text = label });
|
||||
}
|
||||
|
||||
public void AddRadio(string text = null, Texture2D icon = null, string tooltip = null)
|
||||
{
|
||||
var toggle = new ToolbarToggle();
|
||||
toggle.RegisterValueChangedCallback(InnerValueChanged(radioLength));
|
||||
toggle.SetValueWithoutNotify(radioLength == (m_CanDeselectAll ? -1 : 0));
|
||||
toggle.tooltip = tooltip;
|
||||
radios.Add(toggle);
|
||||
if (icon != null)
|
||||
{
|
||||
var childsContainer = toggle.Q(null, ToolbarToggle.inputUssClassName);
|
||||
childsContainer.Add(new Image() { image = icon });
|
||||
if (text != null)
|
||||
childsContainer.Add(new Label() { text = text });
|
||||
}
|
||||
else
|
||||
toggle.text = text;
|
||||
Add(toggle);
|
||||
if (radioLength == 0)
|
||||
toggle.style.borderLeftWidth = 1;
|
||||
radioLength++;
|
||||
}
|
||||
|
||||
public void AddRadios(string[] labels)
|
||||
{
|
||||
foreach (var label in labels)
|
||||
AddRadio(label);
|
||||
}
|
||||
|
||||
public void AddRadios((string text, string tooltip)[] labels)
|
||||
{
|
||||
foreach (var label in labels)
|
||||
AddRadio(label.text, null, label.tooltip);
|
||||
}
|
||||
|
||||
public void AddRadios(Texture2D[] icons)
|
||||
{
|
||||
foreach (var icon in icons)
|
||||
AddRadio(null, icon);
|
||||
}
|
||||
|
||||
public void AddRadios((string text, Texture2D icon)[] labels)
|
||||
{
|
||||
foreach (var label in labels)
|
||||
AddRadio(label.text, label.icon);
|
||||
}
|
||||
|
||||
public void AddRadios((Texture2D icon, string tooltip)[] labels)
|
||||
{
|
||||
foreach (var label in labels)
|
||||
AddRadio(null, label.icon, label.tooltip);
|
||||
}
|
||||
|
||||
public void AddRadios((string text, Texture2D icon, string tooltip)[] labels)
|
||||
{
|
||||
foreach (var label in labels)
|
||||
AddRadio(label.text, label.icon, label.tooltip);
|
||||
}
|
||||
|
||||
EventCallback<ChangeEvent<bool>> InnerValueChanged(int radioIndex)
|
||||
{
|
||||
return (ChangeEvent<bool> evt) =>
|
||||
{
|
||||
if (radioIndex == m_Value)
|
||||
{
|
||||
if (!evt.newValue && !m_CanDeselectAll)
|
||||
radios[radioIndex].SetValueWithoutNotify(true);
|
||||
else
|
||||
value = -1;
|
||||
}
|
||||
else
|
||||
value = radioIndex;
|
||||
};
|
||||
}
|
||||
|
||||
public void SetValueWithoutNotify(int newValue)
|
||||
{
|
||||
if (m_Value != newValue)
|
||||
{
|
||||
if (newValue < (m_CanDeselectAll ? -1 : 0) || newValue >= radioLength)
|
||||
throw new System.IndexOutOfRangeException();
|
||||
|
||||
if (m_Value == newValue && m_CanDeselectAll)
|
||||
{
|
||||
if (m_Value > -1)
|
||||
radios[m_Value].SetValueWithoutNotify(false);
|
||||
m_Value = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (m_Value > -1)
|
||||
radios[m_Value].SetValueWithoutNotify(false);
|
||||
if (newValue > -1)
|
||||
radios[newValue].SetValueWithoutNotify(true);
|
||||
m_Value = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|