using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using UnityEngine.Assertions; #if UNITY_EDITOR using UnityEditor; #endif namespace UnityEngine.Rendering { using UnityObject = UnityEngine.Object; /// /// A global manager that tracks all the Volumes in the currently loaded Scenes and does all the /// interpolation work. /// public sealed class VolumeManager { static readonly Lazy s_Instance = new Lazy(() => new VolumeManager()); /// /// The current singleton instance of . /// public static VolumeManager instance => s_Instance.Value; /// /// A reference to the main . /// /// public VolumeStack stack { get; set; } /// /// The current list of all available types that derive from . /// [Obsolete("Please use baseComponentTypeArray instead.")] public IEnumerable baseComponentTypes { get => baseComponentTypeArray; private set => baseComponentTypeArray = value.ToArray(); } static readonly Dictionary> s_SupportedVolumeComponentsForRenderPipeline = new(); internal static List<(string, Type)> GetSupportedVolumeComponents(Type currentPipelineType) { if (s_SupportedVolumeComponentsForRenderPipeline.TryGetValue(currentPipelineType, out var supportedVolumeComponents)) return supportedVolumeComponents; supportedVolumeComponents = FilterVolumeComponentTypes( VolumeManager.instance.baseComponentTypeArray, currentPipelineType); s_SupportedVolumeComponentsForRenderPipeline[currentPipelineType] = supportedVolumeComponents; return supportedVolumeComponents; } static List<(string, Type)> FilterVolumeComponentTypes(Type[] types, Type currentPipelineType) { var volumes = new List<(string, Type)>(); foreach (var t in types) { string path = string.Empty; var attrs = t.GetCustomAttributes(false); bool skipComponent = false; // Look for the attributes of this volume component and decide how is added and if it needs to be skipped foreach (var attr in attrs) { switch (attr) { case VolumeComponentMenu attrMenu: { path = attrMenu.menu; if (attrMenu is VolumeComponentMenuForRenderPipeline supportedOn) skipComponent |= !supportedOn.pipelineTypes.Contains(currentPipelineType); break; } case HideInInspector attrHide: case ObsoleteAttribute attrDeprecated: skipComponent = true; break; } } if (skipComponent) continue; // If no attribute or in case something went wrong when grabbing it, fallback to a // beautified class name if (string.IsNullOrEmpty(path)) { #if UNITY_EDITOR path = ObjectNames.NicifyVariableName(t.Name); #else path = t.Name; #endif } volumes.Add((path, t)); } return volumes .OrderBy(i => i.Item1) .ToList(); } /// /// The current list of all available types that derive from . /// public Type[] baseComponentTypeArray { get; private set; } // Max amount of layers available in Unity const int k_MaxLayerCount = 32; // Cached lists of all volumes (sorted by priority) by layer mask readonly Dictionary> m_SortedVolumes; // Holds all the registered volumes readonly List m_Volumes; // Keep track of sorting states for layer masks readonly Dictionary m_SortNeeded; // Internal list of default state for each component type - this is used to reset component // states on update instead of having to implement a Reset method on all components (which // would be error-prone) readonly List m_ComponentsDefaultState; internal VolumeComponent GetDefaultVolumeComponent(Type volumeComponentType) { foreach (VolumeComponent component in m_ComponentsDefaultState) { if (component.GetType() == volumeComponentType) return component; } return null; } // Recycled list used for volume traversal readonly List m_TempColliders; // The default stack the volume manager uses. // We cache this as users able to change the stack through code and // we want to be able to switch to the default one through the ResetMainStack() function. VolumeStack m_DefaultStack = null; VolumeManager() { m_SortedVolumes = new Dictionary>(); m_Volumes = new List(); m_SortNeeded = new Dictionary(); m_TempColliders = new List(8); m_ComponentsDefaultState = new List(); ReloadBaseTypes(); m_DefaultStack = CreateStack(); stack = m_DefaultStack; } /// /// Creates and returns a new to use when you need to store /// the result of the Volume blending pass in a separate stack. /// /// /// /// public VolumeStack CreateStack() { var stack = new VolumeStack(); stack.Reload(m_ComponentsDefaultState); return stack; } /// /// Resets the main stack to be the default one. /// Call this function if you've assigned the main stack to something other than the default one. /// public void ResetMainStack() { stack = m_DefaultStack; } /// /// Destroy a Volume Stack /// /// Volume Stack that needs to be destroyed. public void DestroyStack(VolumeStack stack) { stack.Dispose(); } // 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 component in the project void ReloadBaseTypes() { m_ComponentsDefaultState.Clear(); // Grab all the component types we can find baseComponentTypeArray = CoreUtils.GetAllTypesDerivedFrom() .Where(t => !t.IsAbstract).ToArray(); var flags = System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic; // Keep an instance of each type to be used in a virtual lowest priority global volume // so that we have a default state to fallback to when exiting volumes foreach (var type in baseComponentTypeArray) { type.GetMethod("Init", flags)?.Invoke(null, null); var inst = (VolumeComponent)ScriptableObject.CreateInstance(type); m_ComponentsDefaultState.Add(inst); } } /// /// Registers a new Volume in the manager. Unity does this automatically when a new Volume is /// enabled, or its layer changes, but you can use this function to force-register a Volume /// that is currently disabled. /// /// The volume to register. /// The LayerMask that this volume is in. /// public void Register(Volume 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) { // We add the volume to sorted lists only if the layer match and if it doesn't contain the volume already. if ((kvp.Key & (1 << layer)) != 0 && !kvp.Value.Contains(volume)) kvp.Value.Add(volume); } SetLayerDirty(layer); } /// /// Unregisters a Volume from the manager. Unity does this automatically when a Volume is /// disabled or goes out of scope, but you can use this function to force-unregister a Volume /// that you added manually while it was disabled. /// /// The Volume to unregister. /// The LayerMask that this Volume is in. /// public void Unregister(Volume volume, int layer) { m_Volumes.Remove(volume); foreach (var kvp in m_SortedVolumes) { // Skip layer masks this volume doesn't belong to if ((kvp.Key & (1 << layer)) == 0) continue; kvp.Value.Remove(volume); } } /// /// Checks if a is active in a given LayerMask. /// /// A type derived from /// The LayerMask to check against /// true if the component is active in the LayerMask, false /// otherwise. public bool IsComponentActiveInMask(LayerMask layerMask) where T : VolumeComponent { int mask = layerMask.value; foreach (var kvp in m_SortedVolumes) { if (kvp.Key != mask) continue; foreach (var volume in kvp.Value) { if (!volume.enabled || volume.profileRef == null) continue; if (volume.profileRef.TryGet(out T component) && component.active) return true; } } return false; } 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(Volume volume, int prevLayer, int newLayer) { Assert.IsTrue(prevLayer >= 0 && prevLayer <= k_MaxLayerCount, "Invalid layer bit"); Unregister(volume, prevLayer); Register(volume, newLayer); } // Go through all listed components and lerp overridden values in the global state void OverrideData(VolumeStack stack, List components, float interpFactor) { var numComponents = components.Count; for (int i = 0; i < numComponents; i++) { var component = components[i]; if (!component.active) continue; var state = stack.GetComponent(component.GetType()); component.Override(state, interpFactor); } } // Faster version of OverrideData to force replace values in the global state internal void ReplaceData(VolumeStack stack) { var resetParameters = stack.defaultParameters; var resetParametersCount = resetParameters.Length; for (int i = 0; i < resetParametersCount; i++) { var resetParam = resetParameters[i]; var targetParam = resetParam.parameter; targetParam.overrideState = false; targetParam.SetValue(resetParam.defaultValue); } } /// /// Checks the state of the base type library. This is only used in the editor to handle /// entering and exiting of play mode and domain reload. /// [Conditional("UNITY_EDITOR")] public void CheckBaseTypes() { // Editor specific hack to work around serialization doing funky things when exiting if (m_ComponentsDefaultState == null || (m_ComponentsDefaultState.Count > 0 && m_ComponentsDefaultState[0] == null)) ReloadBaseTypes(); } /// /// Checks the state of a given stack. This is only used in the editor to handle entering /// and exiting of play mode and domain reload. /// /// The stack to check. [Conditional("UNITY_EDITOR")] public void CheckStack(VolumeStack stack) { // The editor doesn't reload the domain when exiting play mode but still kills every // object created while in play mode, like stacks' component states var components = stack.components; if (components == null) { stack.Reload(m_ComponentsDefaultState); return; } foreach (var kvp in components) { if (kvp.Key == null || kvp.Value == null) { stack.Reload(m_ComponentsDefaultState); return; } } } // Returns true if must execute Update() in full, and false if we can early exit. bool CheckUpdateRequired(VolumeStack stack) { if (m_Volumes.Count == 0) { if (stack.requiresReset) { // Update the stack one more time in case there was a volume that just ceased to exist. This ensures // the stack will return to default values correctly. stack.requiresReset = false; return true; } // There were no volumes last frame either, and stack has been returned to defaults, so no update is // needed and we can early exit from Update(). return false; } stack.requiresReset = true; // Stack must be reset every frame whenever there are volumes present return true; } /// /// Updates the global state of the Volume manager. Unity usually calls this once per Camera /// in the Update loop before rendering happens. /// /// A reference Transform to consider for positional Volume blending /// /// The LayerMask that the Volume manager uses to filter Volumes that it should consider /// for blending. public void Update(Transform trigger, LayerMask layerMask) { Update(stack, trigger, layerMask); } /// /// Updates the Volume manager and stores the result in a custom . /// /// The stack to store the blending result into. /// A reference Transform to consider for positional Volume blending. /// /// The LayerMask that Unity uses to filter Volumes that it should consider /// for blending. /// public void Update(VolumeStack stack, Transform trigger, LayerMask layerMask) { Assert.IsNotNull(stack); CheckBaseTypes(); CheckStack(stack); if (!CheckUpdateRequired(stack)) return; // Start by resetting the global state to default values ReplaceData(stack); bool onlyGlobal = trigger == null; var triggerPos = onlyGlobal ? Vector3.zero : trigger.position; // Sort the cached volume list(s) for the given layer mask if needed and return it var volumes = GrabVolumes(layerMask); Camera camera = null; // Behavior should be fine even if camera is null if (!onlyGlobal) trigger.TryGetComponent(out camera); // Traverse all volumes int numVolumes = volumes.Count; for (int i = 0; i < numVolumes; i++) { Volume volume = volumes[i]; if (volume == null) continue; #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; // Global volumes always have influence if (volume.isGlobal) { OverrideData(stack, volume.profileRef.components, 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; int numColliders = colliders.Count; for (int c = 0; c < numColliders; c++) { var collider = colliders[c]; if (!collider.enabled) continue; var closestPoint = collider.ClosestPoint(triggerPos); var d = (closestPoint - triggerPos).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 OverrideData(stack, volume.profileRef.components, interpFactor * Mathf.Clamp01(volume.weight)); } } /// /// Get all volumes on a given layer mask sorted by influence. /// /// The LayerMask that Unity uses to filter Volumes that it should consider. /// An array of volume. public Volume[] GetVolumes(LayerMask layerMask) { var volumes = GrabVolumes(layerMask); volumes.RemoveAll(v => v == null); return volumes.ToArray(); } 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(); var numVolumes = m_Volumes.Count; for (int i = 0; i < numVolumes; i++) { var volume = m_Volumes[i]; 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; } // Stable insertion sort. Faster than List.Sort() for our needs. 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; // Sort order is ascending while (j >= 0 && volumes[j].priority > temp.priority) { volumes[j + 1] = volumes[j]; j--; } volumes[j + 1] = temp; } } static bool IsVolumeRenderedByCamera(Volume volume, Camera camera) { #if UNITY_2018_3_OR_NEWER && UNITY_EDITOR // GameObject for default global volume may not belong to any scene, following check prevents it from being culled if (!volume.gameObject.scene.IsValid()) return true; // IsGameObjectRenderedByCamera does not behave correctly when camera is null so we have to catch it here. return camera == null ? true : UnityEditor.SceneManagement.StageUtility.IsGameObjectRenderedByCamera(volume.gameObject, camera); #else return true; #endif } } /// /// A scope in which a Camera filters a Volume. /// [Obsolete("VolumeIsolationScope is deprecated, it does not have any effect anymore.")] public struct VolumeIsolationScope : IDisposable { /// /// Constructs a scope in which a Camera filters a Volume. /// /// Unused parameter. public VolumeIsolationScope(bool unused) { } /// /// Stops the Camera from filtering a Volume. /// void IDisposable.Dispose() { } } }