using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine.Assertions;
namespace UnityEngine.Rendering.PostProcessing
{
///
/// This manager tracks all volumes in the scene and does all the interpolation work. It is
/// automatically created as soon as Post-processing is active in a scene.
///
public sealed class PostProcessManager
{
static PostProcessManager s_Instance;
///
/// The current singleton instance of .
///
public static PostProcessManager instance
{
get
{
if (s_Instance == null)
s_Instance = new PostProcessManager();
return s_Instance;
}
}
const int k_MaxLayerCount = 32; // Max amount of layers available in Unity
readonly Dictionary> m_SortedVolumes;
readonly List m_Volumes;
readonly Dictionary m_SortNeeded;
readonly List m_BaseSettings;
readonly List m_TempColliders;
///
/// This dictionary maps all available to their
/// corresponding . It can be used to list all loaded
/// builtin and custom effects.
///
public readonly Dictionary settingsTypes;
PostProcessManager()
{
m_SortedVolumes = new Dictionary>();
m_Volumes = new List();
m_SortNeeded = new Dictionary();
m_BaseSettings = new List();
m_TempColliders = new List(5);
settingsTypes = new Dictionary();
ReloadBaseTypes();
}
#if UNITY_EDITOR
// Called every time Unity recompile scripts in the editor. We need this to keep track of
// any new custom effect the user might add to the project
[UnityEditor.Callbacks.DidReloadScripts]
static void OnEditorReload()
{
instance.ReloadBaseTypes();
}
#endif
void CleanBaseTypes()
{
settingsTypes.Clear();
foreach (var settings in m_BaseSettings)
RuntimeUtilities.Destroy(settings);
m_BaseSettings.Clear();
}
// This will be called only once at runtime and everytime script reload kicks-in in the
// editor as we need to keep track of any compatible post-processing effects in the project
void ReloadBaseTypes()
{
CleanBaseTypes();
// Rebuild the base type map
var types = RuntimeUtilities.GetAllTypesDerivedFrom()
.Where(
t => t.IsDefined(typeof(PostProcessAttribute), false)
&& !t.IsAbstract
);
foreach (var type in types)
{
settingsTypes.Add(type, type.GetAttribute());
// Create an instance for each effect type, these will be used for the lowest
// priority global volume as we need a default state when exiting volume ranges
var inst = (PostProcessEffectSettings)ScriptableObject.CreateInstance(type);
inst.SetAllOverridesTo(true, false);
m_BaseSettings.Add(inst);
}
}
///
/// Gets a list of all volumes currently affecting the given layer. Results aren't sorted
/// and the list isn't cleared.
///
/// The layer to look for
/// A list to store the volumes found
/// Should we skip disabled volumes?
/// Should we skip 0-weight volumes?
public void GetActiveVolumes(PostProcessLayer layer, List results, bool skipDisabled = true, bool skipZeroWeight = true)
{
// If no trigger is set, only global volumes will have influence
int mask = layer.volumeLayer.value;
var volumeTrigger = layer.volumeTrigger;
bool onlyGlobal = volumeTrigger == null;
var triggerPos = onlyGlobal ? Vector3.zero : volumeTrigger.position;
// Sort the cached volume list(s) for the given layer mask if needed and return it
var volumes = GrabVolumes(mask);
// Traverse all volumes
foreach (var volume in volumes)
{
// Skip disabled volumes and volumes without any data or weight
if ((skipDisabled && !volume.enabled) || volume.profileRef == null || (skipZeroWeight && volume.weight <= 0f))
continue;
// Global volume always have influence
if (volume.isGlobal)
{
results.Add(volume);
continue;
}
if (onlyGlobal)
continue;
// If volume isn't global and has no collider, skip it as it's useless
var colliders = m_TempColliders;
volume.GetComponents(colliders);
if (colliders.Count == 0)
continue;
// Find closest distance to volume, 0 means it's inside it
float closestDistanceSqr = float.PositiveInfinity;
foreach (var collider in colliders)
{
if (!collider.enabled)
continue;
var closestPoint = collider.ClosestPoint(triggerPos); // 5.6-only API
var d = ((closestPoint - triggerPos) / 2f).sqrMagnitude;
if (d < closestDistanceSqr)
closestDistanceSqr = d;
}
colliders.Clear();
float blendDistSqr = volume.blendDistance * volume.blendDistance;
// Check for influence
if (closestDistanceSqr <= blendDistSqr)
results.Add(volume);
}
}
///
/// Gets the highest priority volume affecting a given layer.
///
/// The layer to look for
/// The highest priority volume affecting the layer
public PostProcessVolume GetHighestPriorityVolume(PostProcessLayer layer)
{
if (layer == null)
throw new ArgumentNullException("layer");
return GetHighestPriorityVolume(layer.volumeLayer);
}
///
/// Gets the highest priority volume affecting in a given
/// .
///
/// The layer mask to look for
/// The highest priority volume affecting the layer mask
///
public PostProcessVolume GetHighestPriorityVolume(LayerMask mask)
{
float highestPriority = float.NegativeInfinity;
PostProcessVolume output = null;
List volumes;
if (m_SortedVolumes.TryGetValue(mask, out volumes))
{
foreach (var volume in volumes)
{
if (volume.priority > highestPriority)
{
highestPriority = volume.priority;
output = volume;
}
}
}
return output;
}
///
/// Helper method to spawn a new volume in the scene.
///
/// The unity layer to put the volume in
/// The priority to set this volume to
/// A list of effects to put in this volume
///
public PostProcessVolume QuickVolume(int layer, float priority, params PostProcessEffectSettings[] settings)
{
var gameObject = new GameObject()
{
name = "Quick Volume",
layer = layer,
hideFlags = HideFlags.HideAndDontSave
};
var volume = gameObject.AddComponent();
volume.priority = priority;
volume.isGlobal = true;
var profile = volume.profile;
foreach (var s in settings)
{
Assert.IsNotNull(s, "Trying to create a volume with null effects");
profile.AddSettings(s);
}
return volume;
}
internal void SetLayerDirty(int layer)
{
Assert.IsTrue(layer >= 0 && layer <= k_MaxLayerCount, "Invalid layer bit");
foreach (var kvp in m_SortedVolumes)
{
var mask = kvp.Key;
if ((mask & (1 << layer)) != 0)
m_SortNeeded[mask] = true;
}
}
internal void UpdateVolumeLayer(PostProcessVolume volume, int prevLayer, int newLayer)
{
Assert.IsTrue(prevLayer >= 0 && prevLayer <= k_MaxLayerCount, "Invalid layer bit");
Unregister(volume, prevLayer);
Unregister(volume, newLayer);
Register(volume, newLayer);
}
void Register(PostProcessVolume volume, int layer)
{
m_Volumes.Add(volume);
// Look for existing cached layer masks and add it there if needed
foreach (var kvp in m_SortedVolumes)
{
var mask = kvp.Key;
if ((mask & (1 << layer)) != 0)
kvp.Value.Add(volume);
}
SetLayerDirty(layer);
}
internal void Register(PostProcessVolume volume)
{
int layer = volume.gameObject.layer;
Register(volume, layer);
}
void Unregister(PostProcessVolume volume, int layer)
{
m_Volumes.Remove(volume);
foreach (var kvp in m_SortedVolumes)
{
var mask = kvp.Key;
// Skip layer masks this volume doesn't belong to
if ((mask & (1 << layer)) == 0)
continue;
kvp.Value.Remove(volume);
}
}
internal void Unregister(PostProcessVolume volume)
{
Unregister(volume, volume.previousLayer);
Unregister(volume, volume.gameObject.layer);
}
// Faster version of OverrideSettings to force replace values in the global state
void ReplaceData(PostProcessLayer postProcessLayer)
{
foreach (var settings in m_BaseSettings)
{
var target = postProcessLayer.GetBundle(settings.GetType()).settings;
int count = settings.parameters.Count;
for (int i = 0; i < count; i++)
target.parameters[i].SetValue(settings.parameters[i]);
}
}
internal void UpdateSettings(PostProcessLayer postProcessLayer, Camera camera)
{
// Reset to base state
ReplaceData(postProcessLayer);
// If no trigger is set, only global volumes will have influence
int mask = postProcessLayer.volumeLayer.value;
var volumeTrigger = postProcessLayer.volumeTrigger;
bool onlyGlobal = volumeTrigger == null;
var triggerPos = onlyGlobal ? Vector3.zero : volumeTrigger.position;
// Sort the cached volume list(s) for the given layer mask if needed and return it
var volumes = GrabVolumes(mask);
// Traverse all volumes
foreach (var volume in volumes)
{
#if UNITY_EDITOR
// Skip volumes that aren't in the scene currently displayed in the scene view
if (!IsVolumeRenderedByCamera(volume, camera))
continue;
#endif
// Skip disabled volumes and volumes without any data or weight
if (!volume.enabled || volume.profileRef == null || volume.weight <= 0f)
continue;
var settings = volume.profileRef.settings;
// Global volume always have influence
if (volume.isGlobal)
{
postProcessLayer.OverrideSettings(settings, Mathf.Clamp01(volume.weight));
continue;
}
if (onlyGlobal)
continue;
// If volume isn't global and has no collider, skip it as it's useless
var colliders = m_TempColliders;
volume.GetComponents(colliders);
if (colliders.Count == 0)
continue;
// Find closest distance to volume, 0 means it's inside it
float closestDistanceSqr = float.PositiveInfinity;
foreach (var collider in colliders)
{
if (!collider.enabled)
continue;
var closestPoint = collider.ClosestPoint(triggerPos); // 5.6-only API
var d = ((closestPoint - triggerPos) / 2f).sqrMagnitude;
if (d < closestDistanceSqr)
closestDistanceSqr = d;
}
colliders.Clear();
float blendDistSqr = volume.blendDistance * volume.blendDistance;
// Volume has no influence, ignore it
// Note: Volume doesn't do anything when `closestDistanceSqr = blendDistSqr` but
// we can't use a >= comparison as blendDistSqr could be set to 0 in which
// case volume would have total influence
if (closestDistanceSqr > blendDistSqr)
continue;
// Volume has influence
float interpFactor = 1f;
if (blendDistSqr > 0f)
interpFactor = 1f - (closestDistanceSqr / blendDistSqr);
// No need to clamp01 the interpolation factor as it'll always be in [0;1[ range
postProcessLayer.OverrideSettings(settings, interpFactor * Mathf.Clamp01(volume.weight));
}
}
List GrabVolumes(LayerMask mask)
{
List list;
if (!m_SortedVolumes.TryGetValue(mask, out list))
{
// New layer mask detected, create a new list and cache all the volumes that belong
// to this mask in it
list = new List();
foreach (var volume in m_Volumes)
{
if ((mask & (1 << volume.gameObject.layer)) == 0)
continue;
list.Add(volume);
m_SortNeeded[mask] = true;
}
m_SortedVolumes.Add(mask, list);
}
// Check sorting state
bool sortNeeded;
if (m_SortNeeded.TryGetValue(mask, out sortNeeded) && sortNeeded)
{
m_SortNeeded[mask] = false;
SortByPriority(list);
}
return list;
}
// Custom insertion sort. First sort will be slower but after that it'll be faster than
// using List.Sort() which is also unstable by nature.
// Sort order is ascending.
static void SortByPriority(List volumes)
{
Assert.IsNotNull(volumes, "Trying to sort volumes of non-initialized layer");
for (int i = 1; i < volumes.Count; i++)
{
var temp = volumes[i];
int j = i - 1;
while (j >= 0 && volumes[j].priority > temp.priority)
{
volumes[j + 1] = volumes[j];
j--;
}
volumes[j + 1] = temp;
}
}
static bool IsVolumeRenderedByCamera(PostProcessVolume volume, Camera camera)
{
#if UNITY_2018_3_OR_NEWER && UNITY_EDITOR
return UnityEditor.SceneManagement.StageUtility.IsGameObjectRenderedByCamera(volume.gameObject, camera);
#else
return true;
#endif
}
}
}