2021-09-09 20:42:29 -04:00

413 lines
17 KiB
C#

using System.Collections.Generic;
using UnityEngine.Experimental.Rendering.RenderGraphModule;
using System.Linq;
using System;
namespace UnityEngine.Rendering.HighDefinition
{
/// <summary>
/// Unity Monobehavior that manages the execution of custom passes.
/// It provides
/// </summary>
[ExecuteAlways]
[HelpURL(Documentation.baseURL + Documentation.version + Documentation.subURL + "Custom-Pass" + Documentation.endURL)]
public class CustomPassVolume : MonoBehaviour
{
/// <summary>
/// Whether or not the volume is global. If true, the component will ignore all colliders attached to it
/// </summary>
public bool isGlobal = true;
/// <summary>
/// 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
/// </summary>
[Min(0)]
public float fadeRadius;
/// <summary>
/// The volume priority, used to determine the execution order when there is multiple volumes with the same injection point.
/// </summary>
[Tooltip("Sets the Volume priority in the stack. A higher value means higher priority. You can use negative values.")]
public float priority;
/// <summary>
/// List of custom passes to execute
/// </summary>
[SerializeReference]
public List<CustomPass> customPasses = new List<CustomPass>();
/// <summary>
/// Where the custom passes are going to be injected in HDRP
/// </summary>
public CustomPassInjectionPoint injectionPoint = CustomPassInjectionPoint.BeforeTransparent;
/// <summary>
/// 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.
/// </summary>
/// <value>The fade value that should be applied to the custom pass effect</value>
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<CustomPassVolume> m_ActivePassVolumes = new HashSet<CustomPassVolume>();
static List<CustomPassVolume> m_OverlappingPassVolumes = new List<CustomPassVolume>();
List<Collider> m_Colliders = new List<Collider>();
List<Collider> m_OverlappingColliders = new List<Collider>();
static List<CustomPassInjectionPoint> m_InjectionPoints;
static List<CustomPassInjectionPoint> injectionPoints
{
get
{
if (m_InjectionPoints == null)
m_InjectionPoints = Enum.GetValues(typeof(CustomPassInjectionPoint)).Cast<CustomPassInjectionPoint>().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();
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="injectionPoint">The injection point to get the currently active Custom Pass Volume for.</param>
/// <returns>Returns the Custom Pass Volume instance associated with the injection point.</returns>
[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<CustomPassVolume>();
GetActivePassVolumes(injectionPoint, volumes);
return volumes.FirstOrDefault();
}
/// <summary>
/// Gets the currently active Custom Pass Volume for a given injection point.
/// </summary>
/// <param name="injectionPoint">The injection point to get the currently active Custom Pass Volume for.</param>
/// <param name="volumes">The list of custom pass volumes to popuplate with the active volumes.</param>
public static void GetActivePassVolumes(CustomPassInjectionPoint injectionPoint, List<CustomPassVolume> volumes)
{
volumes.Clear();
foreach (var volume in m_OverlappingPassVolumes)
if (volume.injectionPoint == injectionPoint)
volumes.Add(volume);
}
/// <summary>
/// Add a pass of type passType in the active pass list
/// </summary>
/// <typeparam name="T">The type of the CustomPass to create</typeparam>
/// <returns>The new custom</returns>
public CustomPass AddPassOfType<T>() where T : CustomPass => AddPassOfType(typeof(T));
/// <summary>
/// Add a pass of type passType in the active pass list
/// </summary>
/// <param name="passType">The type of the CustomPass to create</param>
/// <returns>The new custom</returns>
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
}
}