using System.Collections.Generic; using UnityEngine.Experimental.Rendering.RenderGraphModule; using System.Linq; using System; namespace UnityEngine.Rendering.HighDefinition { /// /// Unity Monobehavior that manages the execution of custom passes. /// It provides /// [ExecuteAlways] [HelpURL(Documentation.baseURL + Documentation.version + Documentation.subURL + "Custom-Pass" + Documentation.endURL)] public class CustomPassVolume : MonoBehaviour { /// /// Whether or not the volume is global. If true, the component will ignore all colliders attached to it /// public bool isGlobal = true; /// /// 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. /// [Tooltip("Sets the Volume priority in the stack. A higher value means higher priority. You can use negative values.")] 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; /// /// 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 // 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(); List m_Colliders = new List(); 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; } } 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 UnityEditor.SceneVisibilityManager.visibilityChanged -= UpdateCustomPassVolumeVisibility; UnityEditor.SceneVisibilityManager.visibilityChanged += UpdateCustomPassVolumeVisibility; #endif } void OnDisable() { UnRegister(this); CleanupPasses(); #if UNITY_EDITOR UnityEditor.SceneVisibilityManager.visibilityChanged -= UpdateCustomPassVolumeVisibility; #endif } #if UNITY_EDITOR void UpdateCustomPassVolumeVisibility() { visible = !UnityEditor.SceneVisibilityManager.instance.IsHidden(gameObject); } #endif bool IsVisible(HDCamera hdCamera) { #if UNITY_EDITOR // Scene visibility if (hdCamera.camera.cameraType == CameraType.SceneView && !visible) return false; #endif // We never execute volume if the layer is not within the culling layers of the camera if ((hdCamera.volumeLayerMask & (1 << gameObject.layer)) == 0) return false; return true; } internal 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 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) { // Ignore volumes that are not in the camera layer mask if ((camera.volumeLayerMask & (1 << volume.gameObject.layer)) == 0) 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) => { 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); // 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 if (cullingParameters.cullingMask != 0 && (cullingParameters.cullingMask & hdCamera.camera.cullingMask) != cullingParameters.cullingMask) result = renderContext.Cull(ref cullingParameters); return result; } internal static void Cleanup() { foreach (var pass in m_ActivePassVolumes) { pass.CleanupPasses(); } } /// /// 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 CustomPass AddPassOfType() where T : CustomPass => AddPassOfType(typeof(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); void OnDrawGizmos() { if (isGlobal || m_Colliders.Count == 0 || !enabled) return; var scale = transform.localScale; var invScale = new Vector3(1f / scale.x, 1f / scale.y, 1f / scale.z); Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, scale); Gizmos.color = CoreRenderPipelinePreferences.volumeGizmoColor; // Draw a separate gizmo for each collider foreach (var collider in m_Colliders) { if (!collider || !collider.enabled) continue; // We'll just use scaling as an approximation for volume skin. It's far from being // correct (and is completely wrong in some cases). Ultimately we'd use a distance // field or at least a tesselate + push modifier on the collider's mesh to get a // better approximation, but the current Gizmo system is a bit limited and because // everything is dynamic in Unity and can be changed at anytime, it's hard to keep // track of changes in an elegant way (which we'd need to implement a nice cache // system for generated volume meshes). switch (collider) { case BoxCollider c: Gizmos.DrawCube(c.center, c.size); if (fadeRadius > 0) { // invert te scale for the fade radius because it's in fixed units Vector3 s = new Vector3( (fadeRadius * 2) / scale.x, (fadeRadius * 2) / scale.y, (fadeRadius * 2) / scale.z ); Gizmos.DrawWireCube(c.center, c.size + s); } break; case SphereCollider c: // For sphere the only scale that is used is the transform.x Matrix4x4 oldMatrix = Gizmos.matrix; Gizmos.matrix = Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one * scale.x); Gizmos.DrawSphere(c.center, c.radius); if (fadeRadius > 0) Gizmos.DrawWireSphere(c.center, c.radius + fadeRadius / scale.x); Gizmos.matrix = oldMatrix; break; case MeshCollider c: // Only convex mesh m_Colliders are allowed if (!c.convex) c.convex = true; // Mesh pivot should be centered or this won't work Gizmos.DrawMesh(c.sharedMesh); // We don't display the Gizmo for fade distance mesh because the distances would be wrong break; default: // Nothing for capsule (DrawCapsule isn't exposed in Gizmo), terrain, wheel and // other m_Colliders... break; } } } #endif } }