Car/Assets/VolumetricLightBeam/Scripts/SD/DynamicOcclusionRaycasting.cs

473 lines
17 KiB
C#

#if DEBUG
//#define DEBUG_SHOW_RAYCAST_LINES
#endif
using UnityEngine;
using UnityEngine.Serialization;
namespace VLB
{
[ExecuteInEditMode]
[HelpURL(Consts.Help.SD.UrlDynamicOcclusionRaycasting)]
public class DynamicOcclusionRaycasting : DynamicOcclusionAbstractBase
{
public new const string ClassName = "DynamicOcclusionRaycasting";
/// <summary>
/// Should it interact with 2D or 3D occluders?
/// </summary>
public Dimensions dimensions = Consts.DynOcclusion.RaycastingDimensionsDefault;
/// <summary>
/// The beam can only be occluded by objects located on the layers matching this mask.
/// It's very important to set it as restrictive as possible (checking only the layers which are necessary)
/// to perform a more efficient process in order to increase the performance.
/// </summary>
public LayerMask layerMask = Consts.DynOcclusion.LayerMaskDefault;
/// <summary>
/// Should this beam be occluded by triggers or not?
/// </summary>
public bool considerTriggers = Consts.DynOcclusion.RaycastingConsiderTriggersDefault;
/// <summary>
/// Minimum 'area' of the collider to become an occluder.
/// Colliders smaller than this value will not block the beam.
/// </summary>
public float minOccluderArea = Consts.DynOcclusion.RaycastingMinOccluderAreaDefault;
/// <summary>
/// Approximated percentage of the beam to collide with the surface in order to be considered as occluder
/// </summary>
public float minSurfaceRatio = Consts.DynOcclusion.RaycastingMinSurfaceRatioDefault;
/// <summary>
/// Max angle (in degrees) between the beam and the surface in order to be considered as occluder
/// </summary>
public float maxSurfaceDot = Consts.DynOcclusion.RaycastingMaxSurfaceDotDefault;
/// <summary>
/// Alignment of the computed clipping plane:
/// </summary>
public PlaneAlignment planeAlignment = Consts.DynOcclusion.RaycastingPlaneAlignmentDefault;
/// <summary>
/// Translate the plane. We recommend to set a small positive offset in order to handle non-flat surface better.
/// </summary>
public float planeOffset = Consts.DynOcclusion.RaycastingPlaneOffsetDefault;
/// <summary>
/// Fade out the beam before the computed clipping plane in order to soften the transition.
/// </summary>
[FormerlySerializedAs("fadeDistanceToPlane")]
public float fadeDistanceToSurface = Consts.DynOcclusion.RaycastingFadeDistanceToSurfaceDefault;
[System.Obsolete("Use 'fadeDistanceToSurface' instead")]
public float fadeDistanceToPlane { get { return fadeDistanceToSurface; } set { fadeDistanceToSurface = value; } }
public bool IsColliderHiddenByDynamicOccluder(Collider collider)
{
Debug.Assert(collider, "You should pass a valid Collider to VLB.DynamicOcclusion.IsColliderHiddenByDynamicOccluder");
if (!planeEquationWS.IsValid())
return false;
var isInside = GeometryUtility.TestPlanesAABB(new Plane[] { planeEquationWS }, collider.bounds);
return !isInside;
}
public struct HitResult
{
public HitResult(ref RaycastHit hit3D)
{
point = hit3D.point;
normal = hit3D.normal;
distance = hit3D.distance;
collider3D = hit3D.collider;
collider2D = null;
}
public HitResult(ref RaycastHit2D hit2D)
{
point = hit2D.point;
normal = hit2D.normal;
distance = hit2D.distance;
collider2D = hit2D.collider;
collider3D = null;
}
public Vector3 point;
public Vector3 normal;
public float distance;
Collider2D collider2D;
Collider collider3D;
public bool hasCollider { get { return collider2D || collider3D; } }
public string name
{
get
{
if (collider3D) return collider3D.name;
else if (collider2D) return collider2D.name;
else return "null collider";
}
}
public Bounds bounds
{
get
{
if (collider3D) return collider3D.bounds;
else if (collider2D) return collider2D.bounds;
else return new Bounds();
}
}
public void SetNull() { collider2D = null; collider3D = null; }
}
/// <summary>
/// Get information about the current occluder hit by the beam.
/// Can be null if the beam is not occluded.
/// </summary>
HitResult m_CurrentHit;
protected override string GetShaderKeyword() { return ShaderKeywords.SD.OcclusionClippingPlane; }
protected override MaterialManager.SD.DynamicOcclusion GetDynamicOcclusionMode() { return MaterialManager.SD.DynamicOcclusion.ClippingPlane; }
float m_RangeMultiplier = 1f;
public Plane planeEquationWS { get; private set; }
#if UNITY_EDITOR
public HitResult editorCurrentHitResult { get { return m_CurrentHit; } }
public struct EditorDebugData
{
public int lastFrameUpdate;
}
public EditorDebugData editorDebugData;
public static bool editorShowDebugPlane = true;
public static bool editorRaycastAtEachFrame = true;
private static bool editorPrefsLoaded = false;
public static void EditorLoadPrefs()
{
if (!editorPrefsLoaded)
{
editorShowDebugPlane = UnityEditor.EditorPrefs.GetBool(EditorPrefsStrings.DynOcclusion.PrefShowDebugPlane, true);
editorRaycastAtEachFrame = UnityEditor.EditorPrefs.GetBool(EditorPrefsStrings.DynOcclusion.PrefRaycastingEditor, true);
editorPrefsLoaded = true;
}
}
#endif
protected override void OnValidateProperties()
{
base.OnValidateProperties();
minOccluderArea = Mathf.Max(minOccluderArea, 0f);
fadeDistanceToSurface = Mathf.Max(fadeDistanceToSurface, 0f);
}
protected override void OnEnablePostValidate()
{
m_CurrentHit.SetNull();
#if UNITY_EDITOR
EditorLoadPrefs();
editorDebugData.lastFrameUpdate = 0;
#endif
}
protected override void OnDisable()
{
base.OnDisable();
SetHitNull();
}
void Start()
{
if (Application.isPlaying)
{
var triggerZone = GetComponent<TriggerZone>();
if (triggerZone)
{
m_RangeMultiplier = Mathf.Max(1f, triggerZone.rangeMultiplier);
}
}
}
Vector3 GetRandomVectorAround(Vector3 direction, float angleDiff)
{
var halfAngle = angleDiff * 0.5f;
return Quaternion.Euler(Random.Range(-halfAngle, halfAngle), Random.Range(-halfAngle, halfAngle), Random.Range(-halfAngle, halfAngle)) * direction;
}
QueryTriggerInteraction queryTriggerInteraction { get { return considerTriggers ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore; } }
float raycastMaxDistance { get { return m_Master.raycastDistance * m_RangeMultiplier * m_Master.GetLossyScale().z; } }
HitResult GetBestHit(Vector3 rayPos, Vector3 rayDir)
{
return dimensions == Dimensions.Dim2D ? GetBestHit2D(rayPos, rayDir) : GetBestHit3D(rayPos, rayDir);
}
HitResult GetBestHit3D(Vector3 rayPos, Vector3 rayDir)
{
var hits = Physics.RaycastAll(rayPos, rayDir, raycastMaxDistance, layerMask.value, queryTriggerInteraction);
int bestHit = -1;
float bestLength = float.MaxValue;
for (int i = 0; i < hits.Length; ++i)
{
if (hits[i].collider.gameObject != m_Master.gameObject) // skip collider from TriggerZone
{
if (hits[i].collider.bounds.GetMaxArea2D() >= minOccluderArea)
{
if (hits[i].distance < bestLength)
{
bestLength = hits[i].distance;
bestHit = i;
}
}
}
}
#if DEBUG_SHOW_RAYCAST_LINES
Debug.DrawLine(rayPos, rayPos + rayDir * raycastMaxDistance, bestHit != -1 ? Color.green : Color.red);
#endif
if (bestHit != -1)
return new HitResult(ref hits[bestHit]);
else
return new HitResult();
}
HitResult GetBestHit2D(Vector3 rayPos, Vector3 rayDir)
{
var hits = Physics2D.RaycastAll(new Vector2(rayPos.x, rayPos.y), new Vector2(rayDir.x, rayDir.y), raycastMaxDistance, layerMask.value);
int bestHit = -1;
float bestLength = float.MaxValue;
for (int i = 0; i < hits.Length; ++i)
{
if (!considerTriggers && hits[i].collider.isTrigger) // do not query triggers if considerTriggers is disabled
continue;
if (hits[i].collider.gameObject != m_Master.gameObject) // skip collider from TriggerZone
{
if (hits[i].collider.bounds.GetMaxArea2D() >= minOccluderArea)
{
if (hits[i].distance < bestLength)
{
bestLength = hits[i].distance;
bestHit = i;
}
}
}
}
#if DEBUG_SHOW_RAYCAST_LINES
Debug.DrawLine(rayPos, rayPos + rayDir * raycastMaxDistance, bestHit != -1 ? Color.green : Color.red);
#endif
if (bestHit != -1)
return new HitResult(ref hits[bestHit]);
else
return new HitResult();
}
enum Direction {
Up,
Down,
Left,
Right,
Max2D = Down,
Max3D = Right,
};
uint m_PrevNonSubHitDirectionId = 0;
uint GetDirectionCount() { return dimensions == Dimensions.Dim2D ? ((uint)Direction.Max2D + 1) : ((uint)Direction.Max3D + 1); }
Vector3 GetDirection(uint dirInt)
{
dirInt = dirInt % GetDirectionCount();
switch (dirInt)
{
case (uint)Direction.Up: return m_Master.raycastGlobalUp;
case (uint)Direction.Right: return m_Master.raycastGlobalRight;
case (uint)Direction.Down: return -m_Master.raycastGlobalUp;
case (uint)Direction.Left: return -m_Master.raycastGlobalRight;
}
return Vector3.zero;
}
bool IsHitValid(ref HitResult hit, Vector3 forwardVec)
{
if (hit.hasCollider)
{
float dot = Vector3.Dot(hit.normal, -forwardVec);
return dot >= maxSurfaceDot;
}
return false;
}
protected override bool OnProcessOcclusion(ProcessOcclusionSource source)
{
#if UNITY_EDITOR
editorDebugData.lastFrameUpdate = Time.frameCount;
#endif
var raycastGlobalForward = m_Master.raycastGlobalForward;
var bestHit = GetBestHit(transform.position, raycastGlobalForward);
if (IsHitValid(ref bestHit, raycastGlobalForward))
{
if (minSurfaceRatio > 0.5f)
{
var raycastDistance = m_Master.raycastDistance;
for (uint i = 0; i < GetDirectionCount(); i++)
{
var dir3 = GetDirection(i + m_PrevNonSubHitDirectionId) * (minSurfaceRatio * 2 - 1);
dir3.Scale(transform.localScale);
var startPt = transform.position + dir3 * m_Master.coneRadiusStart;
var newPt = transform.position + dir3 * m_Master.coneRadiusEnd + raycastGlobalForward * raycastDistance;
var bestHitSub = GetBestHit(startPt, (newPt - startPt).normalized);
if (IsHitValid(ref bestHitSub, raycastGlobalForward))
{
if (bestHitSub.distance > bestHit.distance)
{
bestHit = bestHitSub;
}
}
else
{
m_PrevNonSubHitDirectionId = i;
bestHit.SetNull();
break;
}
}
}
}
else
{
bestHit.SetNull();
}
SetHit(ref bestHit);
return bestHit.hasCollider;
}
void SetHit(ref HitResult hit)
{
if (!hit.hasCollider)
{
SetHitNull();
}
else
{
switch (planeAlignment)
{
case PlaneAlignment.Beam:
SetClippingPlane(new Plane(-m_Master.raycastGlobalForward, hit.point));
break;
case PlaneAlignment.Surface:
default:
SetClippingPlane(new Plane(hit.normal, hit.point));
break;
}
m_CurrentHit = hit;
}
}
void SetHitNull()
{
SetClippingPlaneOff();
m_CurrentHit.SetNull();
}
protected override void OnModifyMaterialCallback(MaterialModifier.Interface owner)
{
Debug.Assert(owner != null);
var planeWS = planeEquationWS;
owner.SetMaterialProp(ShaderProperties.SD.DynamicOcclusionClippingPlaneWS, new Vector4(planeWS.normal.x, planeWS.normal.y, planeWS.normal.z, planeWS.distance));
owner.SetMaterialProp(ShaderProperties.SD.DynamicOcclusionClippingPlaneProps, fadeDistanceToSurface);
}
void SetClippingPlane(Plane planeWS)
{
planeWS = planeWS.TranslateCustom(planeWS.normal * planeOffset);
SetPlaneWS(planeWS);
Debug.Assert(m_MaterialModifierCallbackCached != null);
m_Master._INTERNAL_SetDynamicOcclusionCallback(GetShaderKeyword(), m_MaterialModifierCallbackCached);
}
void SetClippingPlaneOff()
{
SetPlaneWS(new Plane());
m_Master._INTERNAL_SetDynamicOcclusionCallback(GetShaderKeyword(), null);
}
void SetPlaneWS(Plane planeWS)
{
planeEquationWS = planeWS;
#if UNITY_EDITOR
m_DebugPlaneLocal = planeWS;
if (m_DebugPlaneLocal.IsValid())
{
float dist;
if (m_DebugPlaneLocal.Raycast(new Ray(transform.position, m_Master.raycastGlobalForward), out dist))
m_DebugPlaneLocal.distance = dist; // compute local distance
}
#endif
}
#if UNITY_EDITOR
void LateUpdate()
{
if (!Application.isPlaying)
{
// In Editor, process raycasts at each frame update
if (!editorRaycastAtEachFrame)
SetHitNull();
else
ProcessOcclusion(ProcessOcclusionSource.EditorUpdate);
}
}
Plane m_DebugPlaneLocal;
void OnDrawGizmos()
{
if (!editorShowDebugPlane)
return;
if (m_DebugPlaneLocal.IsValid())
{
var planePos = transform.position + m_DebugPlaneLocal.distance * m_Master.raycastGlobalForward;
float planeDistNormalized = Mathf.Clamp01(Mathf.InverseLerp(0f, m_Master.raycastDistance, m_DebugPlaneLocal.distance));
float planeSize = Mathf.Lerp(m_Master.coneRadiusStart, m_Master.coneRadiusEnd, planeDistNormalized);
var color = m_Master.ComputeColorAtDepth(planeDistNormalized).ComputeComplementaryColor(false);
Utils.GizmosDrawPlane(
m_DebugPlaneLocal.normal,
planePos,
color,
Matrix4x4.identity,
planeSize,
planeSize * 0.5f);
UnityEditor.Handles.color = color;
UnityEditor.Handles.DrawWireDisc(planePos,
m_DebugPlaneLocal.normal,
planeSize * (minSurfaceRatio * 2 - 1));
}
}
#endif
}
}