2 using System
.Collections
.Generic
;
4 using UnityEngine
.Assertions
;
6 namespace UnityEngine
.Rendering
.PostProcessing
9 /// This manager tracks all volumes in the scene and does all the interpolation work. It is
10 /// automatically created as soon as Post-processing is active in a scene.
12 public sealed class PostProcessManager
14 static PostProcessManager s_Instance
;
17 /// The current singleton instance of <see cref="PostProcessManager"/>.
19 public static PostProcessManager instance
23 if (s_Instance
== null)
24 s_Instance
= new PostProcessManager();
30 const int k_MaxLayerCount
= 32; // Max amount of layers available in Unity
31 readonly Dictionary
<int, List
<PostProcessVolume
>> m_SortedVolumes
;
32 readonly List
<PostProcessVolume
> m_Volumes
;
33 readonly Dictionary
<int, bool> m_SortNeeded
;
34 readonly List
<PostProcessEffectSettings
> m_BaseSettings
;
35 readonly List
<Collider
> m_TempColliders
;
38 /// This dictionary maps all <see cref="PostProcessEffectSettings"/> available to their
39 /// corresponding <see cref="PostProcessAttribute"/>. It can be used to list all loaded
40 /// builtin and custom effects.
42 public readonly Dictionary
<Type
, PostProcessAttribute
> settingsTypes
;
46 m_SortedVolumes
= new Dictionary
<int, List
<PostProcessVolume
>>();
47 m_Volumes
= new List
<PostProcessVolume
>();
48 m_SortNeeded
= new Dictionary
<int, bool>();
49 m_BaseSettings
= new List
<PostProcessEffectSettings
>();
50 m_TempColliders
= new List
<Collider
>(5);
52 settingsTypes
= new Dictionary
<Type
, PostProcessAttribute
>();
57 // Called every time Unity recompile scripts in the editor. We need this to keep track of
58 // any new custom effect the user might add to the project
59 [UnityEditor
.Callbacks
.DidReloadScripts
]
60 static void OnEditorReload()
62 instance
.ReloadBaseTypes();
68 settingsTypes
.Clear();
70 foreach (var settings
in m_BaseSettings
)
71 RuntimeUtilities
.Destroy(settings
);
73 m_BaseSettings
.Clear();
76 // This will be called only once at runtime and everytime script reload kicks-in in the
77 // editor as we need to keep track of any compatible post-processing effects in the project
78 void ReloadBaseTypes()
82 // Rebuild the base type map
83 var types
= RuntimeUtilities
.GetAllAssemblyTypes()
85 t
=> t
.IsSubclassOf(typeof(PostProcessEffectSettings
))
86 && t
.IsDefined(typeof(PostProcessAttribute
), false)
90 foreach (var type
in types
)
92 settingsTypes
.Add(type
, type
.GetAttribute
<PostProcessAttribute
>());
94 // Create an instance for each effect type, these will be used for the lowest
95 // priority global volume as we need a default state when exiting volume ranges
96 var inst
= (PostProcessEffectSettings
)ScriptableObject
.CreateInstance(type
);
97 inst
.SetAllOverridesTo(true, false);
98 m_BaseSettings
.Add(inst
);
103 /// Gets a list of all volumes currently affecting the given layer. Results aren't sorted
104 /// and the list isn't cleared.
106 /// <param name="layer">The layer to look for</param>
107 /// <param name="results">A list to store the volumes found</param>
108 /// <param name="skipDisabled">Should we skip disabled volumes?</param>
109 /// <param name="skipZeroWeight">Should we skip 0-weight volumes?</param>
110 public void GetActiveVolumes(PostProcessLayer layer
, List
<PostProcessVolume
> results
, bool skipDisabled
= true, bool skipZeroWeight
= true)
112 // If no trigger is set, only global volumes will have influence
113 int mask
= layer
.volumeLayer
.value;
114 var volumeTrigger
= layer
.volumeTrigger
;
115 bool onlyGlobal
= volumeTrigger
== null;
116 var triggerPos
= onlyGlobal
? Vector3
.zero
: volumeTrigger
.position
;
118 // Sort the cached volume list(s) for the given layer mask if needed and return it
119 var volumes
= GrabVolumes(mask
);
121 // Traverse all volumes
122 foreach (var volume
in volumes
)
124 // Skip disabled volumes and volumes without any data or weight
125 if ((skipDisabled
&& !volume
.enabled
) || volume
.profileRef
== null || (skipZeroWeight
&& volume
.weight
<= 0f
))
128 // Global volume always have influence
138 // If volume isn't global and has no collider, skip it as it's useless
139 var colliders
= m_TempColliders
;
140 volume
.GetComponents(colliders
);
141 if (colliders
.Count
== 0)
144 // Find closest distance to volume, 0 means it's inside it
145 float closestDistanceSqr
= float.PositiveInfinity
;
147 foreach (var collider
in colliders
)
149 if (!collider
.enabled
)
152 var closestPoint
= collider
.ClosestPoint(triggerPos
); // 5.6-only API
153 var d
= ((closestPoint
- triggerPos
) / 2f
).sqrMagnitude
;
155 if (d
< closestDistanceSqr
)
156 closestDistanceSqr
= d
;
160 float blendDistSqr
= volume
.blendDistance
* volume
.blendDistance
;
162 // Check for influence
163 if (closestDistanceSqr
<= blendDistSqr
)
169 /// Gets the highest priority volume affecting a given layer.
171 /// <param name="layer">The layer to look for</param>
172 /// <returns>The highest priority volume affecting the layer</returns>
173 public PostProcessVolume
GetHighestPriorityVolume(PostProcessLayer layer
)
176 throw new ArgumentNullException("layer");
178 return GetHighestPriorityVolume(layer
.volumeLayer
);
182 /// Gets the highest priority volume affecting <see cref="PostProcessLayer"/> in a given
183 /// <see cref="LayerMask"/>.
185 /// <param name="mask">The layer mask to look for</param>
186 /// <returns>The highest priority volume affecting the layer mask</returns>
187 /// <seealso cref="PostProcessLayer.volumeLayer"/>
188 public PostProcessVolume
GetHighestPriorityVolume(LayerMask mask
)
190 float highestPriority
= float.NegativeInfinity
;
191 PostProcessVolume output
= null;
193 List
<PostProcessVolume
> volumes
;
194 if (m_SortedVolumes
.TryGetValue(mask
, out volumes
))
196 foreach (var volume
in volumes
)
198 if (volume
.priority
> highestPriority
)
200 highestPriority
= volume
.priority
;
210 /// Helper method to spawn a new volume in the scene.
212 /// <param name="layer">The unity layer to put the volume in</param>
213 /// <param name="priority">The priority to set this volume to</param>
214 /// <param name="settings">A list of effects to put in this volume</param>
215 /// <returns></returns>
216 public PostProcessVolume
QuickVolume(int layer
, float priority
, params PostProcessEffectSettings
[] settings
)
218 var gameObject
= new GameObject()
220 name
= "Quick Volume",
222 hideFlags
= HideFlags
.HideAndDontSave
225 var volume
= gameObject
.AddComponent
<PostProcessVolume
>();
226 volume
.priority
= priority
;
227 volume
.isGlobal
= true;
228 var profile
= volume
.profile
;
230 foreach (var s
in settings
)
232 Assert
.IsNotNull(s
, "Trying to create a volume with null effects");
233 profile
.AddSettings(s
);
239 internal void SetLayerDirty(int layer
)
241 Assert
.IsTrue(layer
>= 0 && layer
<= k_MaxLayerCount
, "Invalid layer bit");
243 foreach (var kvp
in m_SortedVolumes
)
247 if ((mask
& (1 << layer
)) != 0)
248 m_SortNeeded
[mask
] = true;
252 internal void UpdateVolumeLayer(PostProcessVolume volume
, int prevLayer
, int newLayer
)
254 Assert
.IsTrue(prevLayer
>= 0 && prevLayer
<= k_MaxLayerCount
, "Invalid layer bit");
255 Unregister(volume
, prevLayer
);
256 Register(volume
, newLayer
);
259 void Register(PostProcessVolume volume
, int layer
)
261 m_Volumes
.Add(volume
);
263 // Look for existing cached layer masks and add it there if needed
264 foreach (var kvp
in m_SortedVolumes
)
268 if ((mask
& (1 << layer
)) != 0)
269 kvp
.Value
.Add(volume
);
272 SetLayerDirty(layer
);
275 internal void Register(PostProcessVolume volume
)
277 int layer
= volume
.gameObject
.layer
;
278 Register(volume
, layer
);
281 void Unregister(PostProcessVolume volume
, int layer
)
283 m_Volumes
.Remove(volume
);
285 foreach (var kvp
in m_SortedVolumes
)
289 // Skip layer masks this volume doesn't belong to
290 if ((mask
& (1 << layer
)) == 0)
293 kvp
.Value
.Remove(volume
);
297 internal void Unregister(PostProcessVolume volume
)
299 int layer
= volume
.gameObject
.layer
;
300 Unregister(volume
, layer
);
303 // Faster version of OverrideSettings to force replace values in the global state
304 void ReplaceData(PostProcessLayer postProcessLayer
)
306 foreach (var settings
in m_BaseSettings
)
308 var target
= postProcessLayer
.GetBundle(settings
.GetType()).settings
;
309 int count
= settings
.parameters
.Count
;
311 for (int i
= 0; i
< count
; i
++)
312 target
.parameters
[i
].SetValue(settings
.parameters
[i
]);
316 internal void UpdateSettings(PostProcessLayer postProcessLayer
, Camera camera
)
318 // Reset to base state
319 ReplaceData(postProcessLayer
);
321 // If no trigger is set, only global volumes will have influence
322 int mask
= postProcessLayer
.volumeLayer
.value;
323 var volumeTrigger
= postProcessLayer
.volumeTrigger
;
324 bool onlyGlobal
= volumeTrigger
== null;
325 var triggerPos
= onlyGlobal
? Vector3
.zero
: volumeTrigger
.position
;
327 // Sort the cached volume list(s) for the given layer mask if needed and return it
328 var volumes
= GrabVolumes(mask
);
330 // Traverse all volumes
331 foreach (var volume
in volumes
)
334 // Skip volumes that aren't in the scene currently displayed in the scene view
335 if (!IsVolumeRenderedByCamera(volume
, camera
))
339 // Skip disabled volumes and volumes without any data or weight
340 if (!volume
.enabled
|| volume
.profileRef
== null || volume
.weight
<= 0f
)
343 var settings
= volume
.profileRef
.settings
;
345 // Global volume always have influence
348 postProcessLayer
.OverrideSettings(settings
, Mathf
.Clamp01(volume
.weight
));
355 // If volume isn't global and has no collider, skip it as it's useless
356 var colliders
= m_TempColliders
;
357 volume
.GetComponents(colliders
);
358 if (colliders
.Count
== 0)
361 // Find closest distance to volume, 0 means it's inside it
362 float closestDistanceSqr
= float.PositiveInfinity
;
364 foreach (var collider
in colliders
)
366 if (!collider
.enabled
)
369 var closestPoint
= collider
.ClosestPoint(triggerPos
); // 5.6-only API
370 var d
= ((closestPoint
- triggerPos
) / 2f
).sqrMagnitude
;
372 if (d
< closestDistanceSqr
)
373 closestDistanceSqr
= d
;
377 float blendDistSqr
= volume
.blendDistance
* volume
.blendDistance
;
379 // Volume has no influence, ignore it
380 // Note: Volume doesn't do anything when `closestDistanceSqr = blendDistSqr` but
381 // we can't use a >= comparison as blendDistSqr could be set to 0 in which
382 // case volume would have total influence
383 if (closestDistanceSqr
> blendDistSqr
)
386 // Volume has influence
387 float interpFactor
= 1f
;
389 if (blendDistSqr
> 0f
)
390 interpFactor
= 1f
- (closestDistanceSqr
/ blendDistSqr
);
392 // No need to clamp01 the interpolation factor as it'll always be in [0;1[ range
393 postProcessLayer
.OverrideSettings(settings
, interpFactor
* Mathf
.Clamp01(volume
.weight
));
397 List
<PostProcessVolume
> GrabVolumes(LayerMask mask
)
399 List
<PostProcessVolume
> list
;
401 if (!m_SortedVolumes
.TryGetValue(mask
, out list
))
403 // New layer mask detected, create a new list and cache all the volumes that belong
404 // to this mask in it
405 list
= new List
<PostProcessVolume
>();
407 foreach (var volume
in m_Volumes
)
409 if ((mask
& (1 << volume
.gameObject
.layer
)) == 0)
413 m_SortNeeded
[mask
] = true;
416 m_SortedVolumes
.Add(mask
, list
);
419 // Check sorting state
421 if (m_SortNeeded
.TryGetValue(mask
, out sortNeeded
) && sortNeeded
)
423 m_SortNeeded
[mask
] = false;
424 SortByPriority(list
);
430 // Custom insertion sort. First sort will be slower but after that it'll be faster than
431 // using List<T>.Sort() which is also unstable by nature.
432 // Sort order is ascending.
433 static void SortByPriority(List
<PostProcessVolume
> volumes
)
435 Assert
.IsNotNull(volumes
, "Trying to sort volumes of non-initialized layer");
437 for (int i
= 1; i
< volumes
.Count
; i
++)
439 var temp
= volumes
[i
];
442 while (j
>= 0 && volumes
[j
].priority
> temp
.priority
)
444 volumes
[j
+ 1] = volumes
[j
];
448 volumes
[j
+ 1] = temp
;
452 static bool IsVolumeRenderedByCamera(PostProcessVolume volume
, Camera camera
)
454 #if UNITY_2018_3_OR_NEWER && UNITY_EDITOR
455 return UnityEditor
.SceneManagement
.StageUtility
.IsGameObjectRenderedByCamera(volume
.gameObject
, camera
);