A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Reflection;
5using UnityEditor;
6
7namespace UnityEngine.Rendering
8{
9 /// <summary>
10 /// Debug Display Settings Volume
11 /// </summary>
12 public class DebugDisplaySettingsVolume : IDebugDisplaySettingsData
13 {
14 /// <summary>Current volume debug settings.</summary>
15 public IVolumeDebugSettings volumeDebugSettings { get; }
16
17 /// <summary>
18 /// Constructor with the settings
19 /// </summary>
20 /// <param name="volumeDebugSettings">The volume debug settings object used for configuration.</param>
21 public DebugDisplaySettingsVolume(IVolumeDebugSettings volumeDebugSettings)
22 {
23 this.volumeDebugSettings = volumeDebugSettings;
24 }
25
26 internal int volumeComponentEnumIndex;
27
28 internal Dictionary<string, VolumeComponent> debugState = new Dictionary<string, VolumeComponent>();
29
30 static class Styles
31 {
32 public static readonly GUIContent none = new GUIContent("None");
33 public static readonly GUIContent editorCamera = new GUIContent("Editor Camera");
34 }
35
36 static class Strings
37 {
38 public static readonly string none = "None";
39 public static readonly string camera = "Camera";
40 public static readonly string parameter = "Parameter";
41 public static readonly string component = "Component";
42 public static readonly string debugViewNotSupported = "Debug view not supported";
43 public static readonly string volumeInfo = "Volume Info";
44 public static readonly string resultValue = "Result";
45 public static readonly string resultValueTooltip = "The interpolated result value of the parameter. This value is used to render the camera.";
46 public static readonly string globalDefaultValue = "Default";
47 public static readonly string globalDefaultValueTooltip = "Default value for this parameter, defined by the Default Volume Profile in Global Settings.";
48 public static readonly string qualityLevelValue = "SRP Asset";
49 public static readonly string qualityLevelValueTooltip = "Override value for this parameter, defined by the Volume Profile in the current SRP Asset.";
50 public static readonly string global = "Global";
51 public static readonly string local = "Local";
52 }
53
54 const string k_PanelTitle = "Volume";
55
56#if UNITY_EDITOR
57 internal static void OpenInRenderingDebugger()
58 {
59 EditorApplication.ExecuteMenuItem("Window/Analysis/Rendering Debugger");
60 var idx = DebugManager.instance.FindPanelIndex(k_PanelTitle);
61 if (idx != -1)
62 DebugManager.instance.RequestEditorWindowPanelIndex(idx);
63 }
64#endif
65
66 internal static class WidgetFactory
67 {
68 public static DebugUI.EnumField CreateComponentSelector(SettingsPanel panel, Action<DebugUI.Field<int>, int> refresh)
69 {
70 int componentIndex = 0;
71 var componentNames = new List<GUIContent>() { Styles.none };
72 var componentValues = new List<int>() { componentIndex++ };
73
74 var volumesAndTypes = VolumeManager.instance.GetVolumeComponentsForDisplay(GraphicsSettings.currentRenderPipelineAssetType);
75 foreach (var type in volumesAndTypes)
76 {
77 componentNames.Add(new GUIContent() { text = type.Item1 });
78 componentValues.Add(componentIndex++);
79 }
80
81 return new DebugUI.EnumField
82 {
83 displayName = Strings.component,
84 getter = () => panel.data.volumeDebugSettings.selectedComponent,
85 setter = value => panel.data.volumeDebugSettings.selectedComponent = value,
86 enumNames = componentNames.ToArray(),
87 enumValues = componentValues.ToArray(),
88 getIndex = () => panel.data.volumeComponentEnumIndex,
89 setIndex = value => { panel.data.volumeComponentEnumIndex = value; },
90 onValueChanged = refresh
91 };
92 }
93
94 public static DebugUI.ObjectPopupField CreateCameraSelector(SettingsPanel panel, Action<DebugUI.Field<Object>, Object> refresh)
95 {
96 return new DebugUI.ObjectPopupField
97 {
98 displayName = Strings.camera,
99 getter = () => panel.data.volumeDebugSettings.selectedCamera,
100 setter = value =>
101 {
102 var c = panel.data.volumeDebugSettings.cameras.ToArray();
103 panel.data.volumeDebugSettings.selectedCameraIndex = Array.IndexOf(c, value as Camera);
104 },
105 getObjects = () => panel.data.volumeDebugSettings.cameras,
106 onValueChanged = refresh
107 };
108 }
109
110 static DebugUI.Widget CreateVolumeParameterWidget(string name, VolumeParameter param, Func<bool> isHiddenCallback = null)
111 {
112 if (param == null)
113 return new DebugUI.Value() { displayName = name, getter = () => "-" };
114
115 var parameterType = param.GetType();
116
117 // Special overrides
118 if (parameterType == typeof(ColorParameter))
119 {
120 var p = (ColorParameter)param;
121 return new DebugUI.ColorField()
122 {
123 displayName = name,
124 hdr = p.hdr,
125 showAlpha = p.showAlpha,
126 getter = () => p.value,
127 setter = value => p.value = value,
128 isHiddenCallback = isHiddenCallback
129 };
130 }
131 else if (parameterType == typeof(BoolParameter))
132 {
133 var p = (BoolParameter)param;
134 return new DebugUI.BoolField()
135 {
136 displayName = name,
137 getter = () => p.value,
138 setter = value => p.value = value,
139 isHiddenCallback = isHiddenCallback
140 };
141 }
142 else
143 {
144 var typeInfo = parameterType.GetTypeInfo();
145 var genericArguments = typeInfo.BaseType.GenericTypeArguments;
146 if (genericArguments.Length > 0 && genericArguments[0].IsArray)
147 {
148 return new DebugUI.ObjectListField()
149 {
150 displayName = name,
151 getter = () => (Object[])parameterType.GetProperty("value").GetValue(param, null),
152 type = parameterType
153 };
154 }
155 }
156
157 // For parameters that do not override `ToString`
158 var property = param.GetType().GetProperty("value");
159 var toString = property.PropertyType.GetMethod("ToString", Type.EmptyTypes);
160 if ((toString == null) || (toString.DeclaringType == typeof(object)) || (toString.DeclaringType == typeof(UnityEngine.Object)))
161 {
162 // Check if the parameter has a name
163 var nameProp = property.PropertyType.GetProperty("name");
164 if (nameProp == null)
165 return new DebugUI.Value() { displayName = name, getter = () => Strings.debugViewNotSupported };
166
167 // Return the parameter name
168 return new DebugUI.Value()
169 {
170 displayName = name,
171 getter = () =>
172 {
173 var value = property.GetValue(param);
174 if (value == null || value.Equals(null))
175 return Strings.none;
176 var valueString = nameProp.GetValue(value);
177 return valueString ?? Strings.none;
178 },
179 isHiddenCallback = isHiddenCallback
180 };
181 }
182
183 // Call the ToString method
184 return new DebugUI.Value()
185 {
186 displayName = name,
187 getter = () =>
188 {
189 var value = property.GetValue(param);
190 return value == null ? Strings.none : value.ToString();
191 },
192 isHiddenCallback = isHiddenCallback
193 };
194 }
195
196 static DebugUI.Value s_EmptyDebugUIValue = new DebugUI.Value { getter = () => string.Empty };
197
198 public static DebugUI.Table CreateVolumeTable(DebugDisplaySettingsVolume data)
199 {
200 var table = new DebugUI.Table()
201 {
202 displayName = Strings.parameter,
203 isReadOnly = true,
204 isHiddenCallback = () => data.volumeDebugSettings.selectedComponent == 0
205 };
206
207 Type selectedType = data.volumeDebugSettings.selectedComponentType;
208 if (selectedType == null)
209 return table;
210
211 var volumeManager = VolumeManager.instance;
212 var stack = data.volumeDebugSettings.selectedCameraVolumeStack ?? volumeManager.stack;
213 var stackComponent = stack.GetComponent(selectedType);
214 if (stackComponent == null)
215 return table;
216
217 var volumes = data.volumeDebugSettings.GetVolumes();
218
219 // First row for volume info
220 var row1 = new DebugUI.Table.Row()
221 {
222 displayName = Strings.volumeInfo,
223 opened = true, // Open by default for the in-game view
224 children =
225 {
226 new DebugUI.Value()
227 {
228 displayName = Strings.resultValue,
229 tooltip = Strings.resultValueTooltip,
230 getter = () => string.Empty
231 }
232 }
233 };
234
235 // Second row, links to volume gameobjects
236 var row2 = new DebugUI.Table.Row()
237 {
238 displayName = "GameObject",
239 children = { s_EmptyDebugUIValue }
240 };
241
242 // Third row, links to volume profile assets
243 var row3 = new DebugUI.Table.Row()
244 {
245 displayName = "Volume Profile",
246 children = { s_EmptyDebugUIValue }
247 };
248
249 // Fourth row, empty (to separate from actual data)
250 var row4 = new DebugUI.Table.Row()
251 {
252 displayName = string.Empty ,
253 children = { s_EmptyDebugUIValue }
254 };
255
256 foreach (var volume in volumes)
257 {
258 var profile = volume.HasInstantiatedProfile() ? volume.profile : volume.sharedProfile;
259 row1.children.Add(new DebugUI.Value()
260 {
261 displayName = profile.name,
262 tooltip = $"Override value for this parameter, defined by {profile.name}",
263 getter = () =>
264 {
265 var scope = volume.isGlobal ? Strings.global : Strings.local;
266 var weight = data.volumeDebugSettings.GetVolumeWeight(volume);
267 return scope + " (" + (weight * 100f) + "%)";
268 }
269 });
270 row2.children.Add(new DebugUI.ObjectField() { displayName = string.Empty, getter = () => volume });
271 row3.children.Add(new DebugUI.ObjectField() { displayName = string.Empty, getter = () => profile });
272 row4.children.Add(s_EmptyDebugUIValue);
273 }
274
275 // Default value profiles
276 var globalDefaultComponent = GetSelectedVolumeComponent(volumeManager.globalDefaultProfile);
277 var qualityDefaultComponent = GetSelectedVolumeComponent(volumeManager.qualityDefaultProfile);
278 List<(VolumeProfile, VolumeComponent)> customDefaultComponents = new();
279 if (volumeManager.customDefaultProfiles != null)
280 {
281 foreach (var customProfile in volumeManager.customDefaultProfiles)
282 {
283 var customDefaultComponent = GetSelectedVolumeComponent(customProfile);
284 if (customDefaultComponent != null)
285 customDefaultComponents.Add((customProfile, customDefaultComponent));
286 }
287 }
288
289 foreach (var (customProfile, _) in customDefaultComponents)
290 {
291 row1.children.Add(new DebugUI.Value() { displayName = customProfile.name, getter = () => string.Empty });
292 row2.children.Add(s_EmptyDebugUIValue);
293 row3.children.Add(new DebugUI.ObjectField() { displayName = string.Empty, getter = () => customProfile });
294 row4.children.Add(s_EmptyDebugUIValue);
295 }
296
297 row1.children.Add(new DebugUI.Value() { displayName = Strings.qualityLevelValue, tooltip = Strings.qualityLevelValueTooltip, getter = () => string.Empty });
298 row2.children.Add(s_EmptyDebugUIValue);
299 row3.children.Add(new DebugUI.ObjectField() { displayName = string.Empty, getter = () => volumeManager.qualityDefaultProfile });
300 row4.children.Add(s_EmptyDebugUIValue);
301
302 row1.children.Add(new DebugUI.Value() { displayName = Strings.globalDefaultValue, tooltip = Strings.globalDefaultValueTooltip, getter = () => string.Empty });
303 row2.children.Add(s_EmptyDebugUIValue);
304 row3.children.Add(new DebugUI.ObjectField() { displayName = string.Empty, getter = () => volumeManager.globalDefaultProfile });
305 row4.children.Add(s_EmptyDebugUIValue);
306
307 table.children.Add(row1);
308 table.children.Add(row2);
309 table.children.Add(row3);
310 table.children.Add(row4);
311
312 VolumeComponent GetSelectedVolumeComponent(VolumeProfile profile)
313 {
314 if (profile != null)
315 {
316 foreach (var component in profile.components)
317 if (component.GetType() == selectedType)
318 return component;
319 }
320 return null;
321 }
322
323 // Build rows - recursively handles nested parameters
324 var rows = new List<DebugUI.Table.Row>();
325 int AddParameterRows(Type type, string baseName = null, int skip = 0)
326 {
327 void AddRow(FieldInfo f, string prefix, int skip)
328 {
329 var fieldName = prefix + f.Name;
330 var attr = (DisplayInfoAttribute[])f.GetCustomAttributes(typeof(DisplayInfoAttribute), true);
331 if (attr.Length != 0)
332 fieldName = prefix + attr[0].name;
333#if UNITY_EDITOR
334 // Would be nice to have the equivalent for the runtime debug.
335 else
336 fieldName = UnityEditor.ObjectNames.NicifyVariableName(fieldName);
337#endif
338
339 int currentParam = rows.Count + skip;
340 DebugUI.Table.Row row = new DebugUI.Table.Row()
341 {
342 displayName = fieldName,
343 children = { CreateVolumeParameterWidget(Strings.resultValue, stackComponent.parameterList[currentParam]) },
344 };
345
346 foreach (var volume in volumes)
347 {
348 VolumeParameter param = null;
349 var profile = volume.HasInstantiatedProfile() ? volume.profile : volume.sharedProfile;
350 if (profile.TryGet(selectedType, out VolumeComponent component))
351 param = component.parameterList[currentParam];
352 row.children.Add(CreateVolumeParameterWidget(volume.name + " (" + profile.name + ")", param, () => !component.parameterList[currentParam].overrideState));
353 }
354
355 foreach (var (customProfile, customComponent) in customDefaultComponents)
356 row.children.Add(CreateVolumeParameterWidget(customProfile.name,
357 customComponent != null ? customComponent.parameterList[currentParam] : null));
358
359 row.children.Add(CreateVolumeParameterWidget(Strings.qualityLevelValue,
360 qualityDefaultComponent != null ? qualityDefaultComponent.parameterList[currentParam] : null));
361
362 row.children.Add(CreateVolumeParameterWidget(Strings.globalDefaultValue,
363 globalDefaultComponent != null ? globalDefaultComponent.parameterList[currentParam] : null));
364
365 rows.Add(row);
366 }
367
368 var fields = type
369 .GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
370 .OrderBy(t => t.MetadataToken);
371 foreach (var field in fields)
372 {
373 if (field.GetCustomAttributes(typeof(ObsoleteAttribute), false).Length != 0)
374 {
375 skip++;
376 continue;
377 }
378 var fieldType = field.FieldType;
379 if (fieldType.IsSubclassOf(typeof(VolumeParameter)))
380 AddRow(field, baseName ?? string.Empty, skip);
381 else if (!fieldType.IsArray && fieldType.IsClass)
382 skip += AddParameterRows(fieldType, baseName ?? (field.Name + " "), skip);
383 }
384 return skip;
385 }
386
387 AddParameterRows(selectedType);
388 foreach (var r in rows.OrderBy(t => t.displayName))
389 table.children.Add(r);
390
391 data.volumeDebugSettings.RefreshVolumes(volumes);
392 for (int i = 0; i < volumes.Length; i++)
393 table.SetColumnVisibility(i + 1, data.volumeDebugSettings.VolumeHasInfluence(volumes[i]));
394
395 float timer = 0.0f, refreshRate = 0.2f;
396 table.isHiddenCallback = () =>
397 {
398 timer += Time.deltaTime;
399 if (timer >= refreshRate)
400 {
401 if (data.volumeDebugSettings.selectedCamera != null)
402 {
403 var newVolumes = data.volumeDebugSettings.GetVolumes();
404 if (!data.volumeDebugSettings.RefreshVolumes(newVolumes))
405 {
406 for (int i = 0; i < newVolumes.Length; i++)
407 {
408 var visible = data.volumeDebugSettings.VolumeHasInfluence(newVolumes[i]);
409 table.SetColumnVisibility(i + 1, visible);
410 }
411 }
412
413 if (!volumes.SequenceEqual(newVolumes))
414 {
415 volumes = newVolumes;
416 DebugManager.instance.ReDrawOnScreenDebug();
417 }
418 }
419
420 timer = 0.0f;
421 }
422 return false;
423 };
424
425 return table;
426 }
427 }
428
429 [DisplayInfo(name = k_PanelTitle, order = int.MaxValue)]
430 internal class SettingsPanel : DebugDisplaySettingsPanel<DebugDisplaySettingsVolume>
431 {
432 public SettingsPanel(DebugDisplaySettingsVolume data)
433 : base(data)
434 {
435 AddWidget(WidgetFactory.CreateCameraSelector(this, (_, __) => Refresh()));
436 AddWidget(WidgetFactory.CreateComponentSelector(this, (_, __) => Refresh()));
437 m_VolumeTable = WidgetFactory.CreateVolumeTable(m_Data);
438 AddWidget(m_VolumeTable);
439 }
440
441 DebugUI.Table m_VolumeTable = null;
442 void Refresh()
443 {
444 var panel = DebugManager.instance.GetPanel(PanelName);
445 if (panel == null)
446 return;
447
448 bool needsRefresh = false;
449 if (m_VolumeTable != null)
450 {
451 needsRefresh = true;
452 panel.children.Remove(m_VolumeTable);
453 }
454
455 if (m_Data.volumeDebugSettings.selectedComponent > 0 && m_Data.volumeDebugSettings.selectedCamera != null)
456 {
457 needsRefresh = true;
458 m_VolumeTable = WidgetFactory.CreateVolumeTable(m_Data);
459 AddWidget(m_VolumeTable);
460 panel.children.Add(m_VolumeTable);
461 }
462
463 if (needsRefresh)
464 DebugManager.instance.ReDrawOnScreenDebug();
465 }
466 }
467
468 #region IDebugDisplaySettingsData
469 /// <summary>
470 /// Checks whether ANY of the debug settings are currently active.
471 /// </summary>
472 public bool AreAnySettingsActive => false; // Volume Debug Panel doesn't need to modify the renderer data, therefore this property returns false
473
474 /// <inheritdoc/>
475 public IDebugDisplaySettingsPanelDisposable CreatePanel()
476 {
477 return new SettingsPanel(this);
478 }
479
480 #endregion
481 }
482}