Singularity/Library/PackageCache/com.unity.render-pipelines..../Editor/Lighting/Shadow/ShadowCascadeGUI.cs

497 lines
21 KiB
C#
Raw Normal View History

2024-05-06 14:45:45 -04:00
using System.Runtime.CompilerServices;
using UnityEngine;
namespace UnityEditor.Rendering
{
/// <summary>
/// Helper class for drawing shadow cascade with GUI.
/// </summary>
public static class ShadowCascadeGUI
{
private const string kPathToHorizontalGradientTexture = "Packages/com.unity.render-pipelines.core/Editor/Lighting/Icons/HorizontalGradient.png";
private const string kPathToUpSnatchTexture = "Packages/com.unity.render-pipelines.core/Editor/Lighting/Icons/UpSnatch.png";
private const string kPathToUpSnatchFocusedTexture = "Packages/com.unity.render-pipelines.core/Editor/Lighting/Icons/UpSnatchFocused.png";
private const string kPathToDownSnatchTexture = "Packages/com.unity.render-pipelines.core/Editor/Lighting/Icons/DownSnatch.png";
private const string kPathTDownSnatchFocusedTexture = "Packages/com.unity.render-pipelines.core/Editor/Lighting/Icons/DownSnatchFocused.png";
private const float kSliderbarMargin = 2.0f;
private const float kSliderbarHeight = 28.0f;
//Value that used in LODSliderRange in normal background texture
private const float kLODSliderRangeModifier = 0.78824f;
// Keep in sync with the ones in Debug.hlsl
private static readonly Color[] kCascadeColors =
{
new Color(0.5f, 0.5f, 0.7f, 1.0f),
new Color(0.5f, 0.7f, 0.5f, 1.0f),
new Color(0.7f, 0.7f, 0.5f, 1.0f),
new Color(0.7f, 0.5f, 0.5f, 1.0f),
};
private static readonly Color kDisabledColor = new Color(0.5f, 0.5f, 0.5f, 0.4f); //Works with both personal and pro skin
private static Vector2 s_DragLastMousePosition;
private static readonly int s_CascadeSliderId = "s_CascadeSliderId".GetHashCode();
private static GUIStyle s_HorizontalGradient = null; // Lazy init
private static GUIStyle s_UpSnatch = null; // Lazy init
private static GUIStyle s_DownSnatch = null; // Lazy init
private static readonly GUIStyle s_CascadeSliderBG = "LODSliderRange"; // Using a LODGroup skin
private static readonly GUIStyle s_TextCenteredStyle = new GUIStyle(EditorStyles.whiteMiniLabel)
{
alignment = TextAnchor.MiddleCenter
};
/// <summary>
/// Represents the state of the cascade handle.
/// </summary>
public enum HandleState
{
/// <summary>
/// Handle will not be drawn.
/// </summary>
Hidden,
/// <summary>
/// Handle will be disabled.
/// </summary>
Disabled,
/// <summary>
/// Handle will be enabled.
/// </summary>
Enabled,
}
/// <summary>
/// Data of single cascade for drawing in GUI.
/// </summary>
public struct Cascade
{
/// <summary>
/// Cascade normalized size that ranges from 0 to 1.
/// Sum of all cascade sizes can not exceed 1.
/// </summary>
public float size;
/// <summary>
/// Cascade border size that ranges from 0 to 1.
/// Border represents the width of shadow blend.
/// Where 0 value result in no blend and 1 will blend from cascade beginning.
/// </summary>
public float borderSize;
/// <summary>
/// Current state of cascade handle that will be used for drawing it.
/// </summary>
public HandleState cascadeHandleState;
/// <summary>
/// Current state of border handle that will be used for drawing it.
/// </summary>
public HandleState borderHandleState;
}
/// <summary>
/// Draw cascades using editor GUI. This also includes handles
/// </summary>
/// <param name="cascades">Array of cascade data.</param>
/// <param name="useMetric">True if numbers should be presented with metric system, otherwise procentage.</param>
/// <param name="baseMetric">The base of the metric system. In most cases it is maximum shadow distance.</param>
public static void DrawCascades(ref Cascade[] cascades, bool useMetric, float baseMetric)
{
// Validate arguments
if (cascades == null || cascades.Length == 0)
{
Debug.LogError($"No cascades passed.");
return;
}
// Validate cascade sizes
float cascadeSizeSum = 0;
for (int i = 0; i < cascades.Length; ++i)
{
cascadeSizeSum += cascades[i].size;
}
if (Mathf.Abs(cascadeSizeSum - 1f) > 0.01f)
{
Debug.LogError($"Cascade total sum of size must be 1.0 (Currently it is {cascadeSizeSum}).");
// Normalize
for (int i = 0; i < cascades.Length; ++i)
{
if (cascadeSizeSum > 0)
cascades[i].size /= cascadeSizeSum;
else
cascades[i].size = (1f / cascades.Length);
}
}
// Validate cascade border sizes
for (int i = 0; i < cascades.Length; ++i)
{
cascades[i].borderSize = Mathf.Clamp01(cascades[i].borderSize);
}
EditorGUILayout.BeginVertical();
// Space for cascade handles
GUILayout.Space(13f);
EditorGUILayout.BeginHorizontal();
// Correctly handle indents
GUILayout.Space(EditorGUI.indentLevel * 15f);
var sliderRect = GUILayoutUtility.GetRect(
GUIContent.none,
s_CascadeSliderBG,
GUILayout.Height(kSliderbarMargin + kSliderbarHeight + kSliderbarMargin),
GUILayout.ExpandWidth(true));
DrawBackgroundBoxGUI(sliderRect, Color.gray);
var formatSymbol = useMetric ? 'm' : '%';
var usableRect = new Rect(sliderRect.x + kSliderbarMargin, sliderRect.y + kSliderbarMargin, sliderRect.width - kSliderbarMargin * 2, sliderRect.height - kSliderbarMargin * 2);
var partitionWidth = 2.0f / EditorGUIUtility.pixelsPerPoint;
var partitionHalfWidth = partitionWidth * 0.5f;
// Calculate pixel perfect cascade widths
float widthForCascades = usableRect.width;
float[] cascadeWidths = new float[cascades.Length];
float sumOfCascadeWidthsWithoutLast = 0;
float startX = 0;
for (int i = 0; i < cascades.Length - 1; ++i)
{
float endX = startX + cascades[i].size * widthForCascades;
float pixelPerfectStartX = Mathf.Round(startX * EditorGUIUtility.pixelsPerPoint) / EditorGUIUtility.pixelsPerPoint;
float pixelPerfectEndX = Mathf.Round(endX * EditorGUIUtility.pixelsPerPoint) / EditorGUIUtility.pixelsPerPoint;
float pixelPerfectCascadeWidth = pixelPerfectEndX - pixelPerfectStartX;
cascadeWidths[i] = pixelPerfectCascadeWidth;
sumOfCascadeWidthsWithoutLast += cascadeWidths[i];
startX = endX;
}
cascadeWidths[cascades.Length - 1] = widthForCascades - sumOfCascadeWidthsWithoutLast;
float currentX = usableRect.x;
for (int i = 0; i < cascades.Length; ++i)
{
ref var cascade = ref cascades[i];
var cascadeWidth = cascadeWidths[i];
bool isLastCascade = (i == cascades.Length - 1);
// Split cascade into cascade without border and border
float borderValue;
float cascadeValue;
float borderWidth;
float cascadeWithoutBorderWidth;
if (cascade.borderHandleState != HandleState.Hidden)
{
borderValue = cascade.size * cascade.borderSize;
cascadeValue = cascade.size - borderValue;
var cascadeWidthWithoutPartition = cascadeWidth;
cascadeWithoutBorderWidth = Mathf.Round(cascadeWidthWithoutPartition * (1 - cascade.borderSize) * EditorGUIUtility.pixelsPerPoint) / EditorGUIUtility.pixelsPerPoint;
borderWidth = cascadeWidth - cascadeWithoutBorderWidth;
}
else
{
borderValue = 0;
cascadeValue = cascade.size;
borderWidth = 0;
cascadeWithoutBorderWidth = cascadeWidth;
}
// Draw cascade
var cascadeRect = new Rect(currentX, usableRect.y, cascadeWithoutBorderWidth, usableRect.height);
currentX += DrawBoxGUI(cascadeRect, kCascadeColors[i]);
// Draw cascade text
float cascadeValueForText = useMetric ? cascadeValue * baseMetric : cascadeValue * 100;
string cascadeText = $"{i}\n{cascadeValueForText:F1}{formatSymbol}";
DrawLabelGUI(cascadeRect, cascadeText, Color.black);
if (cascade.borderHandleState != HandleState.Hidden)
{
// As we are rounding everything against pixel per point and subtracting from total it might result in fractions for the last cascade border
if (isLastCascade && cascade.borderSize == 0.0)
borderWidth = 0;
// Draw border snatch handle
var borderPartitionHandleRect = new Rect(
currentX - 6 - partitionHalfWidth,
usableRect.y + usableRect.height - 1,
12,
18);
var enabled = cascade.borderHandleState == HandleState.Enabled;
var borderPartitionColor = enabled ? kCascadeColors[i] : kDisabledColor;
var delta = DrawSnatchWithHandle(borderPartitionHandleRect, cascadeWidth, borderPartitionColor, GetUpSnatchStyle(), enabled);
cascade.borderSize = Mathf.Clamp01(cascade.borderSize - delta);
// Draw border partition
DrawBoxGUI(new Rect(currentX - partitionWidth, usableRect.y, partitionWidth, usableRect.height), Color.black);
// Draw border
var borderRect = new Rect(currentX, usableRect.y, borderWidth, usableRect.height);
var gradientLeftColor = kCascadeColors[i];
var gradientRightColor = isLastCascade ? Color.black : kCascadeColors[i + 1];
currentX += DrawGradientBoxGUI(borderRect, gradientLeftColor, gradientRightColor);
// Draw border text
float borderValueForText = useMetric ? borderValue * baseMetric : borderValue * 100;
string borderText;
if (isLastCascade)
{
string fallbackText = (borderWidth < 57) ? "F." : "Fallback";
borderText = $"{i}\u2192{fallbackText}\n{borderValueForText:F1}{formatSymbol}";
}
else
{
borderText = $"{i}\u2192{i + 1}\n{borderValueForText:F1}{formatSymbol}";
}
DrawLabelGUI(borderRect, borderText, Color.black);
}
if (!isLastCascade) // Don't draw partition for last cascade
{
if (cascade.cascadeHandleState != HandleState.Hidden)
{
// Draw cascade partition snatch handle
var cascadeHandleRect = new Rect(
currentX - 6 - partitionHalfWidth,
usableRect.y - 19 + 1,
12,
18);
var enabled = cascade.cascadeHandleState == HandleState.Enabled;
var cascadePartitionColor = enabled ? kCascadeColors[i + 1] : kDisabledColor;
var delta = DrawSnatchWithHandle(cascadeHandleRect, usableRect.width, cascadePartitionColor, GetDownSnatchStyle(), enabled);
if (delta != 0)
{
ref var nextCascade = ref cascades[i + 1];
// We want to resize only the current cascade and next cascade
// Lets convert this problem to the slider
var sliderMinimum = 0;
var sliderMaximum = cascade.size + nextCascade.size;
var sliderPosition = cascade.size + delta;
// Force minimum cascade size and prevent cascade going out of bounds
var cascadeMinimumSize = 0.001f;
var sliderPositionPixelPerfectClamped = Mathf.Clamp(sliderPosition,
sliderMinimum + cascadeMinimumSize, sliderMaximum - cascadeMinimumSize);
cascade.size = sliderPositionPixelPerfectClamped;
nextCascade.size = sliderMaximum - sliderPositionPixelPerfectClamped;
}
}
// Draw cascade partition
DrawBoxGUI(new Rect(currentX - partitionWidth, usableRect.y, partitionWidth, usableRect.height), Color.black);
}
}
EditorGUILayout.EndHorizontal();
// Space for border handles
GUILayout.Space(15f);
EditorGUILayout.EndVertical();
}
private static float DrawBackgroundBoxGUI(Rect rect, Color color)
{
var cachedColor = GUI.backgroundColor;
GUI.backgroundColor = color;
GUI.Box(rect, GUIContent.none);
GUI.backgroundColor = cachedColor;
return rect.width;
}
private static float DrawGradientBoxGUI(Rect rect, Color leftColor, Color rightColor)
{
if (s_HorizontalGradient == null)
{
var horizontalGradientTexture = AssetDatabase.LoadAssetAtPath<Texture2D>(kPathToHorizontalGradientTexture);
Debug.Assert(horizontalGradientTexture != null);
s_HorizontalGradient = new GUIStyle();
s_HorizontalGradient.normal.background = horizontalGradientTexture;
}
var cachedColor = GUI.backgroundColor;
// Draw right color as background
GUI.backgroundColor = rightColor;
GUI.Box(rect, GUIContent.none, s_CascadeSliderBG);
// Draw left color as gradient overlay
// Tune the color of overlay gradient to reflect color darkening from s_CascadeSliderBG (LODSliderRange) style which use AnimationRowOdd (LightSkin) texture for that
GUI.backgroundColor = RGBMultiplied(kLODSliderRangeModifier, leftColor);
GUI.Box(rect, GUIContent.none, s_HorizontalGradient);
GUI.backgroundColor = cachedColor;
return rect.width;
}
private static float DrawBoxGUI(Rect rect, Color color)
{
var cachedColor = GUI.backgroundColor;
GUI.backgroundColor = color;
GUI.Box(rect, GUIContent.none, s_CascadeSliderBG);
GUI.backgroundColor = cachedColor;
return rect.width;
}
private static float DrawLabelGUI(Rect rect, string text, Color color)
{
var cachedColor = GUI.backgroundColor;
var oldColor = GUI.color;
GUI.color = color;
GUI.Label(rect, text, s_TextCenteredStyle);
GUI.backgroundColor = cachedColor;
GUI.color = oldColor;
return rect.width;
}
private static float DrawSnatchWithHandle(Rect rect, float distance, Color color, GUIStyle snatch, bool enabled = true)
{
// check for user input on any of the partition handles
// this mechanism gets the current event in the queue... make sure that the mouse is over our control before consuming the event
int sliderControlId = GUIUtility.GetControlID(s_CascadeSliderId, FocusType.Keyboard, rect);
Event currentEvent = Event.current;
EventType eventType = currentEvent.GetTypeForControl(sliderControlId);
if (eventType == EventType.Repaint)
{
bool isFocused = GUIUtility.keyboardControl == sliderControlId && enabled;
bool isHovered = rect.Contains(currentEvent.mousePosition) && enabled;
var cachedColor = GUI.backgroundColor;
// Draw focused with white color as we want to keep original one in texture
GUI.backgroundColor = Color.white;
if (isFocused)
snatch.Draw(rect, false, false, false, isFocused);
// Draw on top of the snatch texture
GUI.backgroundColor = color * (isFocused || isHovered ? 1.4f : 1.0f);
snatch.Draw(rect, false, false, false, false);
GUI.backgroundColor = cachedColor;
}
float delta = 0;
if (enabled)
{
EditorGUIUtility.AddCursorRect(rect, MouseCursor.ResizeHorizontal, sliderControlId);
switch (eventType)
{
case EventType.KeyDown:
if (GUIUtility.keyboardControl != sliderControlId)
break;
if (currentEvent.keyCode == KeyCode.RightArrow)
{
delta = 0.01f;
GUI.changed = true;
currentEvent.Use();
}
else if (currentEvent.keyCode == KeyCode.LeftArrow)
{
delta = -0.01f;
GUI.changed = true;
currentEvent.Use();
}
break;
case EventType.MouseDown:
if (!rect.Contains(currentEvent.mousePosition))
break;
// We do not consume event on purpose.
// In case there is overlapping snatch, this way the last one will be hot control
GUIUtility.hotControl = sliderControlId;
GUIUtility.keyboardControl = sliderControlId;
s_DragLastMousePosition = currentEvent.mousePosition;
break;
case EventType.MouseUp:
// mouseUp event anywhere should release the hotcontrol (if it belongs to us), drags (if any)
if (GUIUtility.hotControl == sliderControlId)
{
GUIUtility.hotControl = 0;
currentEvent.Use();
}
break;
case EventType.MouseDrag:
if (GUIUtility.hotControl != sliderControlId)
break;
delta = (currentEvent.mousePosition - s_DragLastMousePosition).x / (distance);
GUI.changed = true;
s_DragLastMousePosition = currentEvent.mousePosition;
currentEvent.Use();
break;
}
}
return delta;
}
private static GUIStyle GetDownSnatchStyle()
{
if (s_DownSnatch == null)
{
var downSnatch = AssetDatabase.LoadAssetAtPath<Texture2D>(kPathToDownSnatchTexture);
Debug.Assert(downSnatch != null);
var downSnatchFocused = AssetDatabase.LoadAssetAtPath<Texture2D>(kPathTDownSnatchFocusedTexture);
Debug.Assert(downSnatchFocused != null);
s_DownSnatch = new GUIStyle();
s_DownSnatch.normal.background = downSnatch;
s_DownSnatch.hover.background = downSnatch; // We will simulate hover with brighter color
s_DownSnatch.focused.background = downSnatchFocused;
}
return s_DownSnatch;
}
private static GUIStyle GetUpSnatchStyle()
{
if (s_UpSnatch == null)
{
var downSnatch = AssetDatabase.LoadAssetAtPath<Texture2D>(kPathToUpSnatchTexture);
Debug.Assert(downSnatch != null);
var downSnatchFocused = AssetDatabase.LoadAssetAtPath<Texture2D>(kPathToUpSnatchFocusedTexture);
Debug.Assert(downSnatchFocused != null);
s_UpSnatch = new GUIStyle();
s_UpSnatch.normal.background = downSnatch;
s_UpSnatch.hover.background = downSnatch; // We will simulate hover with brighter color
s_UpSnatch.focused.background = downSnatchFocused;
}
return s_UpSnatch;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static Color RGBMultiplied(float multiplier, Color color)
{
return new Color(color.r * multiplier, color.g * multiplier, color.b * multiplier, color.a);
}
}
}