494 lines
18 KiB
C#
494 lines
18 KiB
C#
|
using System;
|
||
|
using System.Linq;
|
||
|
using System.Collections.Generic;
|
||
|
using UnityEngine;
|
||
|
using UnityEngine.Accessibility;
|
||
|
using UnityEngine.Rendering.Universal;
|
||
|
using UnityEngine.SceneManagement;
|
||
|
using UnityEngine.UIElements;
|
||
|
|
||
|
namespace UnityEditor.Rendering.Universal
|
||
|
{
|
||
|
internal class LightBatchingDebugger : EditorWindow
|
||
|
{
|
||
|
private const string ResourcePath = "Packages/com.unity.render-pipelines.universal/Editor/2D/LightBatchingDebugger/";
|
||
|
|
||
|
private class LayerBatch
|
||
|
{
|
||
|
public List<string> LayerNames = new List<string>();
|
||
|
public List<UnityEngine.Object> Lights = new List<UnityEngine.Object>();
|
||
|
public List<UnityEngine.Object> Shadows = new List<UnityEngine.Object>();
|
||
|
public int batchId;
|
||
|
}
|
||
|
|
||
|
[MenuItem("Window/2D/Light Batching Debugger")]
|
||
|
public static void ShowExample()
|
||
|
{
|
||
|
// Open Game View
|
||
|
EditorApplication.ExecuteMenuItem("Window/General/Game");
|
||
|
|
||
|
LightBatchingDebugger wnd = GetWindow<LightBatchingDebugger>();
|
||
|
wnd.titleContent = new GUIContent("Light Batching Debugger");
|
||
|
}
|
||
|
|
||
|
VisualElement root => rootVisualElement;
|
||
|
|
||
|
private static Color[] batchColors = new Color[10];
|
||
|
private List<LayerBatch> batchList = new List<LayerBatch>();
|
||
|
private List<int> selectedIndices = new List<int>();
|
||
|
private ListView batchListView;
|
||
|
private int lightCount = 0;
|
||
|
private int shadowCount = 0;
|
||
|
|
||
|
// Variables used for refresh view
|
||
|
private bool doRefresh;
|
||
|
private int cachedSceneHandle;
|
||
|
private int totalLightCount;
|
||
|
private int totalShadowCount;
|
||
|
private Vector3 cachedCamPos;
|
||
|
|
||
|
ILight2DCullResult lightCullResult
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
// Game view main camera
|
||
|
var renderer = Camera.main?.GetUniversalAdditionalCameraData().scriptableRenderer as Renderer2D;
|
||
|
var data = renderer?.GetRenderer2DData();
|
||
|
if (data != null && data.lightCullResult.IsGameView())
|
||
|
return data?.lightCullResult;
|
||
|
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private bool PopulateData()
|
||
|
{
|
||
|
if (lightCullResult == null)
|
||
|
return false;
|
||
|
|
||
|
batchList.Clear();
|
||
|
|
||
|
var layers = Light2DManager.GetCachedSortingLayer();
|
||
|
var batches = LayerUtility.CalculateBatches(lightCullResult, out var batchCount);
|
||
|
|
||
|
for (var i = 0; i < batchCount; i++)
|
||
|
{
|
||
|
var batchInfo = new LayerBatch
|
||
|
{
|
||
|
batchId = i
|
||
|
};
|
||
|
|
||
|
var batch = batches[i];
|
||
|
|
||
|
// Get the lights
|
||
|
foreach (var light in lightCullResult.visibleLights)
|
||
|
{
|
||
|
// If the lit layers are different, or if they are lit but this is a shadow casting light then don't batch.
|
||
|
if (light.IsLitLayer(batch.startLayerID))
|
||
|
{
|
||
|
batchInfo.Lights.Add(light);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Get the shadows
|
||
|
var visibleShadows = lightCullResult.visibleShadows.SelectMany(x => x.GetShadowCasters());
|
||
|
foreach (var shadowCaster in visibleShadows)
|
||
|
{
|
||
|
if (shadowCaster.IsShadowedLayer(batch.startLayerID))
|
||
|
batchInfo.Shadows.Add(shadowCaster);
|
||
|
}
|
||
|
|
||
|
for (var batchIndex = batch.startIndex; batchIndex <= batch.endIndex; batchIndex++)
|
||
|
{
|
||
|
batchInfo.LayerNames.Add(layers[batchIndex].name);
|
||
|
}
|
||
|
|
||
|
batchList.Add(batchInfo);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
private VisualElement MakePill(UnityEngine.Object obj)
|
||
|
{
|
||
|
var bubble = new Button();
|
||
|
bubble.AddToClassList("Pill");
|
||
|
bubble.text = obj.name;
|
||
|
|
||
|
bubble.clicked += () =>
|
||
|
{
|
||
|
Selection.activeObject = obj;
|
||
|
};
|
||
|
|
||
|
return bubble;
|
||
|
}
|
||
|
|
||
|
private VisualElement GetInfoView()
|
||
|
{
|
||
|
// Hide initial prompt
|
||
|
DisplayInitialPrompt(false);
|
||
|
|
||
|
return root.Query<VisualElement>("InfoView").First();
|
||
|
}
|
||
|
|
||
|
private void ViewBatch(int index)
|
||
|
{
|
||
|
if (index >= batchList.Count())
|
||
|
return;
|
||
|
|
||
|
var infoView = GetInfoView();
|
||
|
|
||
|
var batch1 = batchList[index];
|
||
|
|
||
|
var title = root.Query<Label>("InfoTitle").First();
|
||
|
title.text = $"<b>Batch {batch1.batchId}</b>" + " selected. Select any two adjacent batches to compare.";
|
||
|
|
||
|
var title2 = root.Query<Label>("InfoTitle2").First();
|
||
|
title2.text = "";
|
||
|
|
||
|
// Add Light Pill VisualElements
|
||
|
var lightLabel1 = infoView.Query<Label>("LightLabel1").First();
|
||
|
lightLabel1.text = $"Lights in <b>Batch {batch1.batchId}:</b>";
|
||
|
|
||
|
if (batch1.Lights.Count() == 0)
|
||
|
lightLabel1.text += "\n\nNo lights found.";
|
||
|
|
||
|
var lightBubble1 = infoView.Query<VisualElement>("LightBubble1").First();
|
||
|
lightBubble1.Clear();
|
||
|
|
||
|
foreach (var obj in batch1.Lights)
|
||
|
{
|
||
|
if(obj != null)
|
||
|
lightBubble1.Add(MakePill(obj));
|
||
|
}
|
||
|
|
||
|
var lightLabel2 = infoView.Query<Label>("LightLabel2").First();
|
||
|
lightLabel2.text = "";
|
||
|
|
||
|
var lightBubble2 = infoView.Query<VisualElement>("LightBubble2").First();
|
||
|
lightBubble2.Clear();
|
||
|
|
||
|
// Add Shadow Caster Pill VisualElements
|
||
|
var shadowLabel1 = infoView.Query<Label>("ShadowLabel1").First();
|
||
|
shadowLabel1.text = $"Shadow Casters in <b>Batch {batch1.batchId}:</b>";
|
||
|
|
||
|
if (batch1.Shadows.Count() == 0)
|
||
|
shadowLabel1.text += "\n\nNo shadow casters found.";
|
||
|
|
||
|
var shadowBubble1 = infoView.Query<VisualElement>("ShadowBubble1").First();
|
||
|
shadowBubble1.Clear();
|
||
|
|
||
|
foreach (var obj in batch1.Shadows)
|
||
|
{
|
||
|
if (obj != null)
|
||
|
shadowBubble1.Add(MakePill(obj));
|
||
|
}
|
||
|
|
||
|
var shadowLabel2 = infoView.Query<Label>("ShadowLabel2").First();
|
||
|
shadowLabel2.text = "";
|
||
|
|
||
|
var shadowBubble2 = infoView.Query<VisualElement>("ShadowBubble2").First();
|
||
|
shadowBubble2.Clear();
|
||
|
|
||
|
lightCount = batch1.Lights.Count;
|
||
|
shadowCount = batch1.Shadows.Count;
|
||
|
}
|
||
|
|
||
|
private void CompareBatch(int index1, int index2)
|
||
|
{
|
||
|
// Each editor window contains a root VisualElement object
|
||
|
var infoView = GetInfoView();
|
||
|
|
||
|
LayerBatch batch1;
|
||
|
LayerBatch batch2;
|
||
|
|
||
|
if (batchList[index1].batchId < batchList[index2].batchId)
|
||
|
{
|
||
|
batch1 = batchList[index1];
|
||
|
batch2 = batchList[index2];
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
batch1 = batchList[index2];
|
||
|
batch2 = batchList[index1];
|
||
|
}
|
||
|
|
||
|
// Do batch comparisons
|
||
|
var lightSet1 = batch1.Lights.Except(batch2.Lights);
|
||
|
var lightSet2 = batch2.Lights.Except(batch1.Lights);
|
||
|
var shadowSet1 = batch1.Shadows.Except(batch2.Shadows);
|
||
|
var shadowSet2 = batch2.Shadows.Except(batch1.Shadows);
|
||
|
|
||
|
// Change InfoTitle description when comparing batches
|
||
|
var title = root.Query<Label>("InfoTitle").First();
|
||
|
title.text = $"Comparing <b>Batch {batch1.batchId}</b> and <b>Batch {batch2.batchId}</b>.";
|
||
|
|
||
|
var title2 = root.Query<Label>("InfoTitle2").First();
|
||
|
title2.text = $"To batch <b>Batch {batch1.batchId}</b> and <b>Batch {batch2.batchId}</b>, ensure that the Sorting Layers in both batches share the same set of Lights and Shadow Casters.";
|
||
|
|
||
|
// Light batch comparison
|
||
|
var lightLabel1 = infoView.Query<Label>("LightLabel1").First();
|
||
|
lightLabel1.text = $"Lights only in <b>Batch {batch1.batchId}:</b>";
|
||
|
|
||
|
if (lightSet1.Count() == 0)
|
||
|
lightLabel1.text += "\n\nNo lights found.";
|
||
|
|
||
|
var lightBubble1 = infoView.Query<VisualElement>("LightBubble1").First();
|
||
|
lightBubble1.Clear();
|
||
|
foreach (var obj in lightSet1)
|
||
|
{
|
||
|
if(obj != null)
|
||
|
lightBubble1.Add(MakePill(obj));
|
||
|
}
|
||
|
|
||
|
var lightLabel2 = infoView.Query<Label>("LightLabel2").First();
|
||
|
lightLabel2.text = $"Lights only in <b>Batch {batch2.batchId}:</b>";
|
||
|
|
||
|
if (lightSet2.Count() == 0)
|
||
|
lightLabel2.text += "\n\nNo lights found.";
|
||
|
|
||
|
var lightBubble2 = infoView.Query<VisualElement>("LightBubble2").First();
|
||
|
lightBubble2.Clear();
|
||
|
foreach (var obj in lightSet2)
|
||
|
{
|
||
|
if(obj != null)
|
||
|
lightBubble2.Add(MakePill(obj));
|
||
|
}
|
||
|
|
||
|
// Shadow caster batch comparison
|
||
|
var shadowLabel1 = infoView.Query<Label>("ShadowLabel1").First();
|
||
|
shadowLabel1.text = $"Shadow Casters only in <b>Batch {batch1.batchId}:</b>";
|
||
|
|
||
|
if (shadowSet1.Count() == 0)
|
||
|
shadowLabel1.text += "\n\nNo shadow casters found.";
|
||
|
|
||
|
var shadowBubble1 = infoView.Query<VisualElement>("ShadowBubble1").First();
|
||
|
shadowBubble1.Clear();
|
||
|
foreach (var obj in shadowSet1)
|
||
|
{
|
||
|
if (obj != null)
|
||
|
shadowBubble1.Add(MakePill(obj));
|
||
|
}
|
||
|
|
||
|
var shadowLabel2 = infoView.Query<Label>("ShadowLabel2").First();
|
||
|
shadowLabel2.text = $"Shadow Casters only in <b>Batch {batch2.batchId}:</b>";
|
||
|
|
||
|
if (shadowSet2.Count() == 0)
|
||
|
shadowLabel2.text += "\n\nNo shadow casters found.";
|
||
|
|
||
|
var shadowBubble2 = infoView.Query<VisualElement>("ShadowBubble2").First();
|
||
|
shadowBubble2.Clear();
|
||
|
foreach (var obj in shadowSet2)
|
||
|
{
|
||
|
if (obj != null)
|
||
|
shadowBubble2.Add(MakePill(obj));
|
||
|
}
|
||
|
|
||
|
lightCount = lightSet1.Count() + lightSet2.Count();
|
||
|
shadowCount = shadowSet1.Count() + shadowSet2.Count();
|
||
|
}
|
||
|
|
||
|
// Create once, initialize
|
||
|
private void CreateGUI()
|
||
|
{
|
||
|
// Generate color-blind friendly colors
|
||
|
VisionUtility.GetColorBlindSafePalette(batchColors, 0.51f, 1.0f);
|
||
|
|
||
|
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(ResourcePath + "LightBatchingDebugger.uxml");
|
||
|
var templateRoot = visualTree.Instantiate();
|
||
|
templateRoot.style.flexGrow = 1;
|
||
|
templateRoot.Q("ParentElement").StretchToParentSize();
|
||
|
root.Add(templateRoot);
|
||
|
|
||
|
var batchElement = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(ResourcePath + "LayerBatch.uxml");
|
||
|
Func<VisualElement> makeItem = () => batchElement.Instantiate();
|
||
|
|
||
|
Action<VisualElement, int> bindItem = (e, i) =>
|
||
|
{
|
||
|
if (i >= batchList.Count())
|
||
|
return;
|
||
|
|
||
|
// This is required to make the child of the ListView vary in heights
|
||
|
e.style.height = StyleKeyword.Auto;
|
||
|
|
||
|
var batch = batchList[i];
|
||
|
var batchIndex = e.Query<Label>("BatchIndex").First();
|
||
|
batchIndex.text = batch.batchId.ToString();
|
||
|
|
||
|
var layers = e.Query<VisualElement>("LayerNames").First();
|
||
|
layers.Clear();
|
||
|
foreach (var layerName in batchList[i].LayerNames)
|
||
|
{
|
||
|
var label = new Label { text = layerName };
|
||
|
label.AddToClassList("LayerNameLabel");
|
||
|
layers.Add(label);
|
||
|
}
|
||
|
|
||
|
var color = e.Query<VisualElement>("BatchColor").First();
|
||
|
color.style.backgroundColor = new StyleColor(batchColors[i % batchColors.Length]);
|
||
|
};
|
||
|
|
||
|
DisplayInitialPrompt(true);
|
||
|
|
||
|
batchListView = root.Query<ListView>("BatchList").First();
|
||
|
batchListView.itemsSource = batchList;
|
||
|
batchListView.makeItem = makeItem;
|
||
|
batchListView.bindItem = bindItem;
|
||
|
batchListView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
|
||
|
batchListView.showAlternatingRowBackgrounds = AlternatingRowBackground.ContentOnly;
|
||
|
batchListView.selectionType = SelectionType.Multiple;
|
||
|
|
||
|
batchListView.selectionChanged += objects =>
|
||
|
{
|
||
|
OnSelectionChanged();
|
||
|
};
|
||
|
}
|
||
|
|
||
|
private void OnEnable()
|
||
|
{
|
||
|
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
||
|
}
|
||
|
|
||
|
private void OnDisable()
|
||
|
{
|
||
|
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
||
|
}
|
||
|
|
||
|
void OnPlayModeStateChanged(PlayModeStateChange playModeState)
|
||
|
{
|
||
|
if (PlayModeStateChange.EnteredEditMode == playModeState)
|
||
|
QueueRefresh();
|
||
|
}
|
||
|
|
||
|
void DisplayInitialPrompt(bool display)
|
||
|
{
|
||
|
var initialPrompt = root.Query<Label>("InitialPrompt").First();
|
||
|
initialPrompt.style.display = display ? DisplayStyle.Flex : DisplayStyle.None;
|
||
|
|
||
|
var infoView = root.Query<VisualElement>("InfoView").First();
|
||
|
infoView.style.display = display ? DisplayStyle.None : DisplayStyle.Flex;
|
||
|
}
|
||
|
|
||
|
private void OnSelectionChanged()
|
||
|
{
|
||
|
if (batchListView == null)
|
||
|
return;
|
||
|
|
||
|
switch (batchListView.selectedIndices.Count())
|
||
|
{
|
||
|
case 1:
|
||
|
selectedIndices.Clear();
|
||
|
selectedIndices.Add(batchListView.selectedIndex);
|
||
|
ViewBatch(batchListView.selectedIndex);
|
||
|
break;
|
||
|
|
||
|
case 2:
|
||
|
selectedIndices.Clear();
|
||
|
var firstIndex = batchListView.selectedIndices.First();
|
||
|
var secondIndex = batchListView.selectedIndices.Last();
|
||
|
|
||
|
if(secondIndex > firstIndex + 1 || secondIndex < firstIndex - 1)
|
||
|
{
|
||
|
// Clamp since we do adjacent batch comparisons
|
||
|
secondIndex = Mathf.Clamp(secondIndex, firstIndex - 1, firstIndex + 1);
|
||
|
selectedIndices.Add(firstIndex);
|
||
|
selectedIndices.Add(secondIndex);
|
||
|
batchListView.SetSelection(selectedIndices);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
CompareBatch(firstIndex, secondIndex);
|
||
|
selectedIndices.AddRange(batchListView.selectedIndices);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
// Account for multiple select either with shift or ctrl keys
|
||
|
if(batchListView.selectedIndices.Count() > 2)
|
||
|
{
|
||
|
if (selectedIndices.Count == 1)
|
||
|
{
|
||
|
firstIndex = secondIndex = selectedIndices.First();
|
||
|
|
||
|
if (batchListView.selectedIndices.First() > firstIndex)
|
||
|
secondIndex = firstIndex + 1;
|
||
|
else if (batchListView.selectedIndices.First() < firstIndex)
|
||
|
secondIndex = firstIndex - 1;
|
||
|
|
||
|
selectedIndices.Add(secondIndex);
|
||
|
batchListView.SetSelection(selectedIndices);
|
||
|
}
|
||
|
else if (selectedIndices.Count == 2)
|
||
|
{
|
||
|
batchListView.SetSelection(selectedIndices);
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Update counts
|
||
|
Label lightHeader = root.Query<Label>("LightHeader");
|
||
|
lightHeader.text = $"Lights ({lightCount})";
|
||
|
Label shadowHeader = root.Query<Label>("ShadowHeader");
|
||
|
shadowHeader.text = $"Shadow Casters ({shadowCount})";
|
||
|
}
|
||
|
|
||
|
private void RefreshView()
|
||
|
{
|
||
|
PopulateData();
|
||
|
batchListView.RefreshItems();
|
||
|
OnSelectionChanged();
|
||
|
|
||
|
ResetDirty();
|
||
|
}
|
||
|
|
||
|
private void Update()
|
||
|
{
|
||
|
if (IsDirty())
|
||
|
QueueRefresh();
|
||
|
|
||
|
if (doRefresh)
|
||
|
RefreshView();
|
||
|
}
|
||
|
|
||
|
private bool IsDirty()
|
||
|
{
|
||
|
bool isDirty = false;
|
||
|
|
||
|
// Refresh if layers are added or removed
|
||
|
isDirty |= Light2DManager.GetCachedSortingLayer().Count() != batchList.Sum(x => x.LayerNames.Count());
|
||
|
isDirty |= cachedSceneHandle != SceneManager.GetActiveScene().handle;
|
||
|
isDirty |= cachedCamPos != Camera.main?.transform.position;
|
||
|
|
||
|
if (lightCullResult != null)
|
||
|
{
|
||
|
isDirty |= totalLightCount != lightCullResult.visibleLights.Count();
|
||
|
isDirty |= totalShadowCount != lightCullResult.visibleShadows.Count();
|
||
|
}
|
||
|
|
||
|
return isDirty;
|
||
|
}
|
||
|
|
||
|
private void ResetDirty()
|
||
|
{
|
||
|
cachedSceneHandle = SceneManager.GetActiveScene().handle;
|
||
|
|
||
|
if (Camera.main != null)
|
||
|
cachedCamPos = Camera.main.transform.position;
|
||
|
|
||
|
if (lightCullResult != null)
|
||
|
{
|
||
|
totalLightCount = lightCullResult.visibleLights.Count();
|
||
|
totalShadowCount = lightCullResult.visibleShadows.Count();
|
||
|
}
|
||
|
|
||
|
doRefresh = false;
|
||
|
}
|
||
|
|
||
|
public void QueueRefresh()
|
||
|
{
|
||
|
doRefresh = true;
|
||
|
}
|
||
|
}
|
||
|
}
|