using System.Collections.Generic; using UnityEngine.Rendering.RenderGraphModule; using System.Linq; using System; using System.Collections; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; using UnityEditor.SceneManagement; using System.Reflection; #endif namespace UnityEngine.Rendering.HighDefinition { /// /// Unity Monobehavior that manages the execution of custom passes. /// It provides /// [ExecuteAlways] [HDRPHelpURLAttribute("Custom-Pass")] public class CustomPassVolume : MonoBehaviour, IVolume { [SerializeField, FormerlySerializedAs("isGlobal")] private bool m_IsGlobal = true; /// /// Whether or not the volume is global. If true, the component will ignore all colliders attached to it /// public bool isGlobal { get => m_IsGlobal; set => m_IsGlobal = value; } /// /// Distance where the volume start to be rendered, the fadeValue field in C# will be updated to the normalized blend factor for your custom C# passes /// In the fullscreen shader pass and DrawRenderers shaders you can access the _FadeValue /// [Min(0)] public float fadeRadius; /// /// The volume priority, used to determine the execution order when there is multiple volumes with the same injection point. Custom Pass Volume with a higher value is rendered first. /// public float priority; /// /// List of custom passes to execute /// [SerializeReference] public List customPasses = new List(); /// /// Where the custom passes are going to be injected in HDRP /// public CustomPassInjectionPoint injectionPoint = CustomPassInjectionPoint.BeforeTransparent; [SerializeField] internal Camera m_TargetCamera; /// /// Use this field to force the custom pass volume to be executed only for one camera. /// public Camera targetCamera { /// /// Get the target camera of the custom pass. The target camera can be null if the custom pass is in local or global mode. /// get => useTargetCamera ? m_TargetCamera : null; /// /// Sets the target camera of the custom pass volume, this will bypass the volume mode (local or global) and the volume mask of the camera. /// /// The new camera value. A null value will disable target camera mode and fall back to local or global. set { m_TargetCamera = value; useTargetCamera = value != null; } } /// /// Fade value between 0 and 1. it represent how close you camera is from the collider of the custom pass. /// 0 when the camera is outside the volume + fade radius and 1 when it is inside the collider. /// /// The fade value that should be applied to the custom pass effect public float fadeValue { get; private set; } #if UNITY_EDITOR [System.NonSerialized] bool visible = true; #endif [SerializeField] internal bool useTargetCamera; // The current active custom pass volume is simply the smallest overlapping volume with the trigger transform static HashSet m_ActivePassVolumes = new HashSet(); static List m_OverlappingPassVolumes = new List(); static readonly Dictionary> m_GlobalCustomPasses = new(); static readonly List s_EmptyGlobalCustomPassList = new(); internal List m_Colliders = new List(); /// /// The colliders of the volume if is false /// public List colliders => m_Colliders; List m_OverlappingColliders = new List(); static List m_InjectionPoints; static List injectionPoints { get { if (m_InjectionPoints == null) m_InjectionPoints = Enum.GetValues(typeof(CustomPassInjectionPoint)).Cast().ToList(); return m_InjectionPoints; } } // List used to temporarily store the passes to execute at a given injection point, static to avoid GC.Alloc static readonly List m_CurrentInjectionPointList = new(); /// The current injection point used to render passes. Used to determine the injection point of global custom passes. internal static CustomPassInjectionPoint currentGlobalInjectionPoint; /// /// Data structure used to store the global custom pass data needed for evaluation during the frame. /// public readonly struct GlobalCustomPass { /// The priority this custom pass has been added. Define in which order the global custom passes will be executed. public readonly float priority; /// The custom pass instance to be executed. public readonly CustomPass instance; /// /// Utility function to deconstruct a global custom pass into a tuple. /// You can use it directly in a foreach to avoid declaring an intermediate field to store the GlobalCustomPass instance. /// /// /// /// foreach (var (customPass, priority) in GetGlobalCustomPasses(injectionPoint)) /// /// /// The instance of the custom pass stored in this GlobalCustomPass. /// The priority stored in this GlobalCustomPass. public void Deconstruct(out CustomPass instance, out float priority) { instance = this.instance; priority = this.priority; } internal GlobalCustomPass(float priority, CustomPass pass) { this.priority = priority; this.instance = pass; } } // List insertion sort by dichotomie: https://www.jacksondunstan.com/articles/3189 static void InsertSorted(List list, GlobalCustomPass value) { var startIndex = 0; var endIndex = list.Count; while (endIndex > startIndex) { var windowSize = endIndex - startIndex; var middleIndex = startIndex + (windowSize / 2); var compareToResult = list[middleIndex].priority.CompareTo(value.priority); if (compareToResult == 0) { list.Insert(middleIndex, value); return; } else if (compareToResult < 0) startIndex = middleIndex + 1; else endIndex = middleIndex; } list.Insert(startIndex, value); } /// /// Register a custom pass instance at a given injection point. This custom pass will be executed by every camera /// with custom pass enabled in their frame settings. /// /// The injection point where the custom pass will be executed. /// The instance of the custom pass to execute. The same custom pass instance can be registered multiple times. /// Define the execution order of the custom pass. Custom passes are executed from high to low priority. public static void RegisterGlobalCustomPass(CustomPassInjectionPoint injectionPoint, CustomPass customPassInstance, float priority = 0) { if (!m_GlobalCustomPasses.TryGetValue(injectionPoint, out var customPassList)) m_GlobalCustomPasses[injectionPoint] = customPassList = new(); InsertSorted(customPassList, new GlobalCustomPass(priority, customPassInstance)); } /// /// Similar to the RegisterGlobalCustomPass with a protection ensuring that only one custom pass type is existing /// at a certain injection point. The same custom pass type can still be registered to multiple injection point /// as long as there is only one of this type per injection point. /// /// The injection point where the custom pass will be executed. /// The instance of the custom pass to execute. The type of the custom pass will also be used to check if there is already a custom pass of this type registered. /// Define the execution order of the custom pass. Custom passes are executed from high to low priority. /// public static bool RegisterUniqueGlobalCustomPass(CustomPassInjectionPoint injectionPoint, CustomPass customPassInstance, float priority = 0) { var customPassType = customPassInstance.GetType(); if (m_GlobalCustomPasses.TryGetValue(injectionPoint, out var customPassList)) { foreach (var customPass in customPassList) { if (customPass.instance.GetType() == customPassType) return false; } } RegisterGlobalCustomPass(injectionPoint, customPassInstance, priority); return true; } /// /// Remove a custom from the execution lists. /// /// The custom pass instance to remove. If the custom pass instance exists at multiple injection points, they will all be removed. /// True if one or more custom passes were removed. False otherwise. public static bool UnregisterGlobalCustomPass(CustomPass customPassInstance) { bool removed = false; foreach (var injectionPoint in injectionPoints) removed |= UnregisterGlobalCustomPass(injectionPoint, customPassInstance); return removed; } /// /// Unregister all global custom passes registered by any system. /// This function unregister both active and inactive custom passes. /// /// True if at least one custom pass was removed. False otherwise. public static bool UnregisterAllGlobalCustomPasses() { bool removed = false; foreach (var injectionPoint in injectionPoints) removed |= UnregisterAllGlobalCustomPasses(injectionPoint); return removed; } /// /// Unregister all global custom passes at a specific injection point. /// This function unregister both active and inactive custom passes. /// /// The injection point where you want all the custom passes to be removed. /// True if at least one custom pass was removed. False otherwise. public static bool UnregisterAllGlobalCustomPasses(CustomPassInjectionPoint injectionPoint) { bool removed = false; foreach (var (customPass, priority) in GetGlobalCustomPasses(injectionPoint)) removed |= UnregisterGlobalCustomPass(injectionPoint, customPass); return removed; } /// /// Unregister a custom from the injection point execution list. If the same custom pass instance is registered multiple times, they will all be removed. /// /// Selects from which injection point the custom pass instance will be removed /// The custom pass instance to unregister. /// True if one or more custom passes were removed. False otherwise. public static bool UnregisterGlobalCustomPass(CustomPassInjectionPoint injectionPoint, CustomPass customPassInstance) { if (m_GlobalCustomPasses.TryGetValue(injectionPoint, out var customPassList)) return customPassList.RemoveAll(data => data.instance == customPassInstance) > 0; return false; } /// /// Gets all the custom passes registered at a specific injection point. This includes both enabled and disabled custom passes. /// /// The injection point used to filer the resulting custom pass list. /// The list of all the global custom passes in the injection point provided in parameter. If no custom pass were registered for the injection point, an empty list is returned. public static List GetGlobalCustomPasses(CustomPassInjectionPoint injectionPoint) { if (m_GlobalCustomPasses.TryGetValue(injectionPoint, out var customPassList)) return customPassList; return s_EmptyGlobalCustomPassList; } void OnEnable() { // Remove null passes in case of something happens during the deserialization of the passes customPasses.RemoveAll(c => c is null); GetComponents(m_Colliders); Register(this); #if UNITY_EDITOR SceneVisibilityManager.visibilityChanged -= UpdateCustomPassVolumeVisibility; SceneVisibilityManager.visibilityChanged += UpdateCustomPassVolumeVisibility; #endif } void OnDisable() { UnRegister(this); CleanupPasses(); #if UNITY_EDITOR SceneVisibilityManager.visibilityChanged -= UpdateCustomPassVolumeVisibility; #endif } #if UNITY_EDITOR void UpdateCustomPassVolumeVisibility() { visible = !SceneVisibilityManager.instance.IsHidden(gameObject); } #endif bool IsVisible(HDCamera hdCamera) { #if UNITY_EDITOR // Scene visibility if (hdCamera.camera.cameraType == CameraType.SceneView && !visible) return false; // Prefab context mode visibility var stage = PrefabStageUtility.GetCurrentPrefabStage(); if (stage != null) { // Check if the current volume is inside the currently edited prefab bool isVolumeInPrefab = gameObject.scene == stage.scene; if (!isVolumeInPrefab && stage.mode == PrefabStage.Mode.InIsolation) return false; if (!isVolumeInPrefab) { // Prefab context is hidden and the current volume is outside of the prefab so we don't render the effects if (CoreUtils.IsSceneViewPrefabStageContextHidden()) return false; } } #endif if (useTargetCamera) return targetCamera == hdCamera.camera; // We never execute volume if the layer is not within the culling layers of the camera // Special case for the scene view: we can't easily change it's volume later mask, so by default we show all custom passes if (hdCamera.camera.cameraType != CameraType.SceneView && (hdCamera.volumeLayerMask & (1 << gameObject.layer)) == 0) return false; return true; } bool Execute(RenderGraph renderGraph, HDCamera hdCamera, CullingResults cullingResult, CullingResults cameraCullingResult, in CustomPass.RenderTargets targets) { bool executed = false; if (!IsVisible(hdCamera)) return false; foreach (var pass in customPasses) { if (pass != null && pass.WillBeExecuted(hdCamera)) { pass.ExecuteInternal(renderGraph, hdCamera, cullingResult, cameraCullingResult, targets, this); executed = true; } } return executed; } internal static bool ExecuteAllCustomPasses(RenderGraph renderGraph, HDCamera hdCamera, CullingResults cullingResults, CullingResults cameraCullingResults, CustomPassInjectionPoint injectionPoint, in CustomPass.RenderTargets customPassTargets) { bool executed = false; currentGlobalInjectionPoint = injectionPoint; foreach (var (customPass, priority) in GetGlobalCustomPasses(injectionPoint)) { if (customPass == null || !customPass.WillBeExecuted(hdCamera)) continue; executed = true; customPass.ExecuteInternal(renderGraph, hdCamera, cullingResults, cameraCullingResults, customPassTargets, null); } GetActivePassVolumes(injectionPoint, m_CurrentInjectionPointList); foreach (var customPass in m_CurrentInjectionPointList) { if (customPass == null) return false; executed |= customPass.Execute(renderGraph, hdCamera, cullingResults, cameraCullingResults, customPassTargets); } return executed; } internal bool WillExecuteInjectionPoint(HDCamera hdCamera) { bool executed = false; if (!IsVisible(hdCamera)) return false; foreach (var pass in customPasses) { if (pass != null && pass.WillBeExecuted(hdCamera)) executed = true; } return executed; } internal void CleanupPasses() { foreach (var pass in customPasses) pass.CleanupPassInternal(); } static void Register(CustomPassVolume volume) => m_ActivePassVolumes.Add(volume); static void UnRegister(CustomPassVolume volume) => m_ActivePassVolumes.Remove(volume); internal static void Update(HDCamera camera) { var triggerPos = camera.volumeAnchor.position; m_OverlappingPassVolumes.Clear(); // Traverse all volumes foreach (var volume in m_ActivePassVolumes) { if (!volume.IsVisible(camera)) continue; if (volume.useTargetCamera) { if (volume.targetCamera == camera.camera) m_OverlappingPassVolumes.Add(volume); continue; } // Global volumes always have influence if (volume.isGlobal) { volume.fadeValue = 1.0f; m_OverlappingPassVolumes.Add(volume); continue; } // If volume isn't global and has no collider, skip it as it's useless if (volume.m_Colliders.Count == 0) continue; volume.m_OverlappingColliders.Clear(); float sqrFadeRadius = Mathf.Max(float.Epsilon, volume.fadeRadius * volume.fadeRadius); float minSqrDistance = 1e20f; foreach (var collider in volume.m_Colliders) { if (!collider || !collider.enabled) continue; // We don't support concave colliders if (collider is MeshCollider m && !m.convex) continue; var closestPoint = collider.ClosestPoint(triggerPos); var d = (closestPoint - triggerPos).sqrMagnitude; minSqrDistance = Mathf.Min(minSqrDistance, d); // Update the list of overlapping colliders if (d <= sqrFadeRadius) volume.m_OverlappingColliders.Add(collider); } // update the fade value: volume.fadeValue = 1.0f - Mathf.Clamp01(Mathf.Sqrt(minSqrDistance / sqrFadeRadius)); if (volume.m_OverlappingColliders.Count > 0) m_OverlappingPassVolumes.Add(volume); } // Sort the overlapping volumes by priority order (smaller first, then larger and finally globals) m_OverlappingPassVolumes.Sort((v1, v2) => { static float GetVolumeExtent(CustomPassVolume volume) { float extent = 0; foreach (var collider in volume.m_OverlappingColliders) extent += collider.bounds.extents.magnitude; return extent; } // Sort by priority and then by volume extent if (v1.priority == v2.priority) { if (v1.isGlobal && v2.isGlobal) return 0; if (v1.isGlobal) return 1; if (v2.isGlobal) return -1; return GetVolumeExtent(v1).CompareTo(GetVolumeExtent(v2)); } else { return v2.priority.CompareTo(v1.priority); } }); } internal void AggregateCullingParameters(ref ScriptableCullingParameters cullingParameters, HDCamera hdCamera) { foreach (var pass in customPasses) { if (pass != null && pass.enabled) pass.InternalAggregateCullingParameters(ref cullingParameters, hdCamera); } } internal static CullingResults? Cull(ScriptableRenderContext renderContext, HDCamera hdCamera) { CullingResults? result = null; // We need to sort the volumes first to know which one will be executed // TODO: cache the results per camera in the HDRenderPipeline so it's not executed twice per camera Update(hdCamera); // For each injection points, we gather the culling results for hdCamera.camera.TryGetCullingParameters(out var cullingParameters); // By default we don't want the culling to return any objects cullingParameters.cullingMask = 0; cullingParameters.cullingOptions = CullingOptions.None; foreach (var volume in m_OverlappingPassVolumes) volume?.AggregateCullingParameters(ref cullingParameters, hdCamera); // Try to share the culling results from the camera to the custom passes by default bool shareCullingResults = true; // If we don't have anything to cull or the pass is asking for the same culling layers than the camera, we don't have to re-do the culling shareCullingResults &= (cullingParameters.cullingMask & hdCamera.camera.cullingMask) == cullingParameters.cullingMask; shareCullingResults &= cullingParameters.cullingMatrix == hdCamera.camera.cullingMatrix; shareCullingResults &= cullingParameters.isOrthographic == hdCamera.camera.orthographic; if (!shareCullingResults) result = renderContext.Cull(ref cullingParameters); return result; } internal static void Cleanup() { // Cleanup is called when HDRP is destroyed, so we need to cleanup all the passes that were rendered foreach (var pass in m_ActivePassVolumes) pass.CleanupPasses(); foreach (var passList in m_GlobalCustomPasses.Values) { foreach (var pass in passList) if (pass.instance != null) pass.instance.CleanupPassInternal(); } } /// /// Gets the currently active Custom Pass Volume for a given injection point. /// Note this function returns only the first active volume, not the others that will be executed. /// /// The injection point to get the currently active Custom Pass Volume for. /// Returns the Custom Pass Volume instance associated with the injection point. [Obsolete("In order to support multiple custom pass volume per injection points, please use GetActivePassVolumes.")] public static CustomPassVolume GetActivePassVolume(CustomPassInjectionPoint injectionPoint) { var volumes = new List(); GetActivePassVolumes(injectionPoint, volumes); return volumes.FirstOrDefault(); } /// /// Gets the currently active Custom Pass Volume for a given injection point. /// /// The injection point to get the currently active Custom Pass Volume for. /// The list of custom pass volumes to popuplate with the active volumes. public static void GetActivePassVolumes(CustomPassInjectionPoint injectionPoint, List volumes) { volumes.Clear(); foreach (var volume in m_OverlappingPassVolumes) if (volume.injectionPoint == injectionPoint) volumes.Add(volume); } /// /// Add a pass of type passType in the active pass list /// /// The type of the CustomPass to create /// The new custom public T AddPassOfType() where T : CustomPass => AddPassOfType(typeof(T)) as T; /// /// Add a pass of type passType in the active pass list /// /// The type of the CustomPass to create /// The new custom public CustomPass AddPassOfType(Type passType) { if (!typeof(CustomPass).IsAssignableFrom(passType)) { Debug.LogError($"Can't add pass type {passType} to the list because it does not inherit from CustomPass."); return null; } var customPass = Activator.CreateInstance(passType) as CustomPass; customPasses.Add(customPass); return customPass; } #if UNITY_EDITOR // In the editor, we refresh the list of colliders at every frame because it's frequent to add/remove them void Update() => GetComponents(m_Colliders); #endif } }