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; } } }