473 lines
17 KiB
C#
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
|
|||
|
}
|
|||
|
}
|