A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using UnityEngine;
4using UnityEngine.Rendering;
5using UnityEngine.UIElements;
6
7namespace UnityEditor.Rendering
8{
9 /// <summary>
10 /// Utility functions for handling VolumeProfiles in the editor.
11 /// </summary>
12 public static class VolumeProfileUtils
13 {
14 internal static class Styles
15 {
16 public static readonly GUIContent newVolumeProfile = EditorGUIUtility.TrTextContent("New Volume Profile...");
17 public static readonly GUIContent clone = EditorGUIUtility.TrTextContent("Clone");
18 public static readonly GUIContent collapseAll = EditorGUIUtility.TrTextContent("Collapse All");
19 public static readonly GUIContent expandAll = EditorGUIUtility.TrTextContent("Expand All");
20 public static readonly GUIContent reset = EditorGUIUtility.TrTextContent("Reset");
21 public static readonly GUIContent resetAll = EditorGUIUtility.TrTextContent("Reset All");
22 public static readonly GUIContent openInRenderingDebugger = EditorGUIUtility.TrTextContent("Open In Rendering Debugger");
23 public static readonly GUIContent copySettings = EditorGUIUtility.TrTextContent("Copy Settings");
24 public static readonly GUIContent copyAllSettings = EditorGUIUtility.TrTextContent("Copy All Settings");
25 public static readonly GUIContent pasteSettings = EditorGUIUtility.TrTextContent("Paste Settings");
26 }
27
28 internal static void CopyValuesToProfile(VolumeComponent component, VolumeProfile profile)
29 {
30 var profileComponent = profile.GetVolumeComponentOfType(component.GetType());
31 Undo.RecordObject(profileComponent, "Copy component to profile");
32 CopyValuesToComponent(component, profileComponent, true);
33 VolumeManager.instance.OnVolumeProfileChanged(profile);
34 }
35
36 internal static void CopyValuesToComponent(VolumeComponent component, VolumeComponent targetComponent, bool copyOnlyOverriddenParams)
37 {
38 if (targetComponent == null)
39 return;
40
41 for (int i = 0; i < component.parameters.Count; i++)
42 {
43 var param = component.parameters[i];
44 if (copyOnlyOverriddenParams && !param.overrideState)
45 continue;
46 var targetParam = targetComponent.parameters[i];
47 targetParam.SetValue(param);
48 }
49 }
50
51 internal static void AssignValuesToProfile(VolumeProfile targetProfile, VolumeComponent component, SerializedProperty newPropertyValue)
52 {
53 var defaultComponent = targetProfile.GetVolumeComponentOfType(component.GetType());
54 if (defaultComponent != null)
55 {
56 var defaultObject = new SerializedObject(defaultComponent);
57 var defaultProperty = defaultObject.FindProperty(newPropertyValue.propertyPath);
58 if (defaultProperty != null)
59 {
60 defaultProperty.serializedObject.CopyFromSerializedProperty(newPropertyValue);
61 defaultProperty.serializedObject.ApplyModifiedProperties();
62 VolumeManager.instance.OnVolumeProfileChanged(targetProfile);
63 }
64 }
65 }
66
67 /// <summary>
68 /// Assign the global default default profile to VolumeManager. Ensures that defaultVolumeProfile contains
69 /// overrides for every component. If defaultValueSource is provided, it will be used as the source for
70 /// default values instead of default-constructing them.
71 /// If components will be added to the profile, a confirmation dialog is displayed.
72 /// </summary>
73 /// <param name="globalDefaultVolumeProfile">VolumeProfile asset assigned in pipeline global settings.</param>
74 /// <param name="defaultValueSource">An optional VolumeProfile asset containing default values to use for
75 /// any components that are added to <see cref="globalDefaultVolumeProfile"/>.</param>
76 /// <typeparam name="TRenderPipeline">The type of RenderPipeline that this VolumeProfile is used for. If it is
77 /// not the active pipeline, the function does nothing.</typeparam>
78 /// <returns>Whether the operation was confirmed</returns>
79 public static bool UpdateGlobalDefaultVolumeProfileWithConfirmation<TRenderPipeline>(VolumeProfile globalDefaultVolumeProfile, VolumeProfile defaultValueSource = null)
80 where TRenderPipeline : RenderPipeline
81 {
82 if (RenderPipelineManager.currentPipeline is not TRenderPipeline)
83 return false;
84
85 int numComponentsMissingFromProfile = GetTypesMissingFromDefaultProfile(globalDefaultVolumeProfile).Count;
86 if (numComponentsMissingFromProfile == 0 ||
87 EditorUtility.DisplayDialog(
88 "New Default Volume Profile",
89 $"Assigning {globalDefaultVolumeProfile.name} as the Default Volume Profile will add {numComponentsMissingFromProfile} Volume Components to it. Are you sure?", "Yes", "Cancel"))
90 {
91 UpdateGlobalDefaultVolumeProfile<TRenderPipeline>(globalDefaultVolumeProfile, defaultValueSource);
92 return true;
93 }
94
95 return false;
96 }
97
98 /// <summary>
99 /// Assign the global default default profile to VolumeManager. Ensures that defaultVolumeProfile contains
100 /// overrides for every component. If defaultValueSource is provided, it will be used as the source for
101 /// default values instead of default-constructing them.
102 /// </summary>
103 /// <param name="globalDefaultVolumeProfile">VolumeProfile asset assigned in pipeline global settings.</param>
104 /// <param name="defaultValueSource">An optional VolumeProfile asset containing default values to use for
105 /// any components that are added to <see cref="globalDefaultVolumeProfile"/>.</param>
106 /// <typeparam name="TRenderPipeline">The type of RenderPipeline that this VolumeProfile is used for. If it is
107 /// not the active pipeline, the function does nothing.</typeparam>
108 public static void UpdateGlobalDefaultVolumeProfile<TRenderPipeline>(VolumeProfile globalDefaultVolumeProfile, VolumeProfile defaultValueSource = null)
109 where TRenderPipeline : RenderPipeline
110 {
111 if (RenderPipelineManager.currentPipeline is not TRenderPipeline)
112 return;
113
114 Undo.RecordObject(globalDefaultVolumeProfile, $"Ensure {globalDefaultVolumeProfile.name} has all Volume Components");
115 foreach (var comp in globalDefaultVolumeProfile.components)
116 Undo.RecordObject(comp, $"Save {comp.name} state");
117
118 EnsureAllOverridesForDefaultProfile(globalDefaultVolumeProfile, defaultValueSource);
119 VolumeManager.instance.SetGlobalDefaultProfile(globalDefaultVolumeProfile);
120 }
121
122 // Helper extension method: Returns the VolumeComponent of given type from the profile if present, or null
123 static VolumeComponent GetVolumeComponentOfType(this VolumeProfile profile, Type type)
124 {
125 if (profile != null)
126 {
127 foreach (var component in profile.components)
128 if (component.GetType() == type)
129 return component;
130 }
131 return null;
132 }
133
134 // Helper extension method: Returns the VolumeComponent of given type from the profile if present, or a default-constructed one
135 static VolumeComponent GetVolumeComponentOfTypeOrDefault(this VolumeProfile profile, Type type)
136 {
137 return profile.GetVolumeComponentOfType(type) ?? (VolumeComponent) ScriptableObject.CreateInstance(type);
138 }
139
140 static List<Type> GetTypesMissingFromDefaultProfile(VolumeProfile profile)
141 {
142 List<Type> missingTypes = new List<Type>();
143 var volumeComponentTypes = VolumeManager.instance.baseComponentTypeArray;
144 foreach (var type in volumeComponentTypes)
145 {
146 if (profile.components.Find(c => c.GetType() == type) == null)
147 {
148 if (type.IsDefined(typeof(ObsoleteAttribute), false) ||
149 type.IsDefined(typeof(HideInInspector), false))
150 continue;
151
152 missingTypes.Add(type);
153 }
154 }
155
156 return missingTypes;
157 }
158
159 /// <summary>
160 /// Ensure the provided VolumeProfile contains every VolumeComponent, they are active and overrideState for
161 /// every VolumeParameter is true. Obsolete and hidden components are excluded.
162 /// </summary>
163 /// <param name="profile">VolumeProfile to use.</param>
164 /// <param name="defaultValueSource">An optional VolumeProfile asset containing default values to use for
165 /// any components that are added to <see cref="profile"/>.</param>
166 public static void EnsureAllOverridesForDefaultProfile(VolumeProfile profile, VolumeProfile defaultValueSource = null)
167 {
168 // It's possible that the volume profile is assigned to the default asset inside the HDRP package. In
169 // this case it cannot be modified. User is expected to use HDRP Wizard "Fix" to create a local profile.
170 var path = AssetDatabase.GetAssetPath(profile);
171 if (CoreEditorUtils.IsAssetInReadOnlyPackage(path))
172 return;
173
174 bool changed = false;
175 int numComponentsBefore = profile.components.Count;
176
177 // Remove any obsolete VolumeComponents
178 profile.components.RemoveAll(
179 comp => comp == null || comp.GetType().IsDefined(typeof(ObsoleteAttribute), false));
180
181 changed |= profile.components.Count != numComponentsBefore;
182
183 // Ensure all existing VolumeComponents are active & all overrides enabled
184 foreach (var comp in profile.components)
185 {
186 bool resetAll = false;
187 if (!comp.active)
188 {
189 changed = true;
190 comp.active = true;
191 resetAll = true;
192 }
193
194 VolumeComponent defaultValueComponent = null;
195 for (int i = 0; i < comp.parameters.Count; ++i)
196 {
197 var param = comp.parameters[i];
198 if (resetAll || !param.overrideState)
199 {
200 if (defaultValueComponent == null)
201 defaultValueComponent = defaultValueSource.GetVolumeComponentOfTypeOrDefault(comp.GetType());
202
203 // Because the parameter values for inactive VolumeComponents or non-overriden parameters are
204 // not in effect, we must reset these values to avoid unexpected changes when assigning an
205 // existing profile as a Default Profile.
206 param.SetValue(defaultValueComponent.parameters[i]);
207 }
208 if (!param.overrideState)
209 {
210 changed = true;
211 param.overrideState = true;
212 }
213 }
214 }
215
216 // Add missing VolumeComponents to profile
217 var missingTypes = GetTypesMissingFromDefaultProfile(profile);
218 foreach (var type in missingTypes)
219 {
220 var comp = profile.Add(type, overrides: true);
221 comp.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
222
223 // Copy values from default value source if present & overridden
224 var defaultValueSourceComponent = defaultValueSource.GetVolumeComponentOfType(type);
225 if (defaultValueSourceComponent != null)
226 {
227 for (int i = 0; i < comp.parameters.Count; i++)
228 {
229 var defaultValueSourceParam = defaultValueSourceComponent.parameters[i];
230 if (defaultValueSourceParam.overrideState)
231 comp.parameters[i].SetValue(defaultValueSourceParam);
232 }
233 }
234
235 AssetDatabase.AddObjectToAsset(comp, profile);
236 changed = true;
237 }
238
239 if (changed)
240 {
241 VolumeManager.instance.OnVolumeProfileChanged(profile);
242 EditorUtility.SetDirty(profile);
243 AssetDatabase.SaveAssets();
244 AssetDatabase.Refresh();
245 }
246 }
247
248 /// <summary>
249 /// Adds context menu dropdown items for a Volume Profile.
250 /// </summary>
251 /// <param name="menu">Dropdown menu to add items to</param>
252 /// <param name="volumeProfile">VolumeProfile associated with the context menu</param>
253 /// <param name="componentEditors">List of VolumeComponentEditors associated with the profile</param>
254 /// <param name="overrideStateOnReset">Default override state for components when they are reset</param>
255 /// <param name="defaultVolumeProfilePath">Default path for the new volume profile<</param>
256 /// <param name="onNewVolumeProfileCreated">Callback when new volume profile has been created</param>
257 /// <param name="onComponentEditorsExpandedCollapsed">Callback when all editors are collapsed or expanded</param>
258 /// <param name="canCreateNewProfile">Whether it is allowed to create a new profile</param>
259 public static void AddVolumeProfileContextMenuItems(
260 ref GenericMenu menu,
261 VolumeProfile volumeProfile,
262 List<VolumeComponentEditor> componentEditors,
263 bool overrideStateOnReset,
264 string defaultVolumeProfilePath,
265 Action<VolumeProfile> onNewVolumeProfileCreated,
266 Action onComponentEditorsExpandedCollapsed = null,
267 bool canCreateNewProfile = true)
268 {
269 if (canCreateNewProfile)
270 {
271 menu.AddItem(Styles.newVolumeProfile, false, () =>
272 {
273 VolumeProfileFactory.CreateVolumeProfileWithCallback(defaultVolumeProfilePath,
274 onNewVolumeProfileCreated);
275 });
276 }
277 else
278 {
279 menu.AddDisabledItem(Styles.newVolumeProfile, false);
280 }
281
282 if (volumeProfile != null)
283 {
284 if (canCreateNewProfile)
285 {
286 menu.AddItem(Styles.clone, false, () =>
287 {
288 var pathName = AssetDatabase.GenerateUniqueAssetPath(AssetDatabase.GetAssetPath(volumeProfile));
289 var clone = VolumeProfileFactory.CreateVolumeProfileAtPath(pathName, volumeProfile);
290 onNewVolumeProfileCreated(clone);
291 });
292 }
293 else
294 {
295 menu.AddDisabledItem(Styles.clone, false);
296 }
297
298 menu.AddSeparator(string.Empty);
299
300 menu.AddItem(Styles.collapseAll, false, () =>
301 {
302 SetComponentEditorsExpanded(componentEditors, false);
303 onComponentEditorsExpandedCollapsed?.Invoke();
304 });
305 menu.AddItem(Styles.expandAll, false, () =>
306 {
307 SetComponentEditorsExpanded(componentEditors, true);
308 onComponentEditorsExpandedCollapsed?.Invoke();
309 });
310 }
311
312 menu.AddSeparator(string.Empty);
313
314 menu.AddAdvancedPropertiesBoolMenuItem();
315
316 menu.AddSeparator(string.Empty);
317
318 menu.AddItem(Styles.openInRenderingDebugger, false, DebugDisplaySettingsVolume.OpenInRenderingDebugger);
319
320 if (volumeProfile != null)
321 {
322 menu.AddSeparator(string.Empty);
323
324 menu.AddItem(Styles.copyAllSettings, false,
325 () => VolumeComponentCopyPaste.CopySettings(volumeProfile.components));
326
327 if (VolumeComponentCopyPaste.CanPaste(volumeProfile.components))
328 menu.AddItem(Styles.pasteSettings, false, () =>
329 {
330 VolumeComponentCopyPaste.PasteSettings(volumeProfile.components);
331 VolumeManager.instance.OnVolumeProfileChanged(volumeProfile);
332 });
333 else
334 menu.AddDisabledItem(Styles.pasteSettings, false);
335 }
336 }
337
338 /// <summary>
339 /// Draws the context menu dropdown for a Volume Profile.
340 /// </summary>
341 /// <param name="position">Context menu position</param>
342 /// <param name="volumeProfile">VolumeProfile associated with the context menu</param>
343 /// <param name="componentEditors">List of VolumeComponentEditors associated with the profile</param>
344 /// <param name="defaultVolumeProfilePath">Default path for the new volume profile</param>
345 /// <param name="overrideStateOnReset">Default override state for components when they are reset</param>
346 /// <param name="onNewVolumeProfileCreated">Callback when new volume profile has been created</param>
347 /// <param name="onComponentEditorsExpandedCollapsed">Callback when all editors are collapsed or expanded</param>
348 public static void OnVolumeProfileContextClick(
349 Vector2 position,
350 VolumeProfile volumeProfile,
351 List<VolumeComponentEditor> componentEditors,
352 bool overrideStateOnReset,
353 string defaultVolumeProfilePath,
354 Action<VolumeProfile> onNewVolumeProfileCreated,
355 Action onComponentEditorsExpandedCollapsed = null)
356 {
357 var menu = new GenericMenu();
358 menu.AddItem(Styles.newVolumeProfile, false, () =>
359 {
360 VolumeProfileFactory.CreateVolumeProfileWithCallback(defaultVolumeProfilePath,
361 onNewVolumeProfileCreated);
362 });
363
364 if (volumeProfile != null)
365 {
366 menu.AddItem(Styles.clone, false, () =>
367 {
368 var pathName = AssetDatabase.GenerateUniqueAssetPath(AssetDatabase.GetAssetPath(volumeProfile));
369 var clone = VolumeProfileFactory.CreateVolumeProfileAtPath(pathName, volumeProfile);
370 onNewVolumeProfileCreated(clone);
371 });
372
373 menu.AddSeparator(string.Empty);
374
375 menu.AddItem(Styles.collapseAll, false, () =>
376 {
377 SetComponentEditorsExpanded(componentEditors, false);
378 onComponentEditorsExpandedCollapsed?.Invoke();
379 });
380 menu.AddItem(Styles.expandAll, false, () =>
381 {
382 SetComponentEditorsExpanded(componentEditors, true);
383 onComponentEditorsExpandedCollapsed?.Invoke();
384 });
385
386 menu.AddSeparator(string.Empty);
387
388 menu.AddItem(Styles.resetAll, false, () =>
389 {
390 VolumeComponent[] components = new VolumeComponent[componentEditors.Count];
391 for (int i = 0; i < componentEditors.Count; i++)
392 components[i] = componentEditors[i].volumeComponent;
393
394 ResetComponentsInternal(new SerializedObject(volumeProfile), volumeProfile, components, overrideStateOnReset);
395 });
396 }
397
398 menu.AddSeparator(string.Empty);
399
400 menu.AddAdvancedPropertiesBoolMenuItem();
401
402 menu.AddSeparator(string.Empty);
403
404 menu.AddItem(Styles.openInRenderingDebugger, false, DebugDisplaySettingsVolume.OpenInRenderingDebugger);
405
406 if (volumeProfile != null)
407 {
408 menu.AddSeparator(string.Empty);
409
410 menu.AddItem(Styles.copyAllSettings, false,
411 () => VolumeComponentCopyPaste.CopySettings(volumeProfile.components));
412
413 if (VolumeComponentCopyPaste.CanPaste(volumeProfile.components))
414 menu.AddItem(Styles.pasteSettings, false, () =>
415 {
416 VolumeComponentCopyPaste.PasteSettings(volumeProfile.components);
417 VolumeManager.instance.OnVolumeProfileChanged(volumeProfile);
418 });
419 else
420 menu.AddDisabledItem(Styles.pasteSettings);
421 }
422
423 menu.DropDown(new Rect(new Vector2(position.x, position.y), Vector2.zero));
424 }
425
426 internal static VolumeComponent CreateNewComponent(Type type)
427 {
428 var volumeComponent = (VolumeComponent) ScriptableObject.CreateInstance(type);
429 volumeComponent.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy;
430 volumeComponent.name = type.Name;
431 return volumeComponent;
432 }
433
434 internal static void ResetComponentsInternal(
435 SerializedObject serializedObject,
436 VolumeProfile asset,
437 VolumeComponent[] components,
438 bool newComponentDefaultOverrideState)
439 {
440 Undo.RecordObjects(components, "Reset All Volume Overrides");
441
442 foreach (var targetComponent in components)
443 {
444 var newComponent = CreateNewComponent(targetComponent.GetType());
445 CopyValuesToComponent(newComponent, targetComponent, false);
446 targetComponent.SetAllOverridesTo(newComponentDefaultOverrideState);
447 }
448
449 serializedObject.ApplyModifiedProperties();
450
451 VolumeManager.instance.OnVolumeProfileChanged(asset);
452
453 // Force save / refresh
454 EditorUtility.SetDirty(asset);
455 AssetDatabase.SaveAssets();
456 }
457
458 internal static void SetComponentEditorsExpanded(List<VolumeComponentEditor> editors, bool expanded)
459 {
460 foreach (var editor in editors)
461 {
462 editor.expanded = expanded;
463 }
464 }
465 }
466}