DynamicOcclusionRaycasting.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. #if DEBUG
  2. //#define DEBUG_SHOW_RAYCAST_LINES
  3. #endif
  4. using UnityEngine;
  5. using UnityEngine.Serialization;
  6. namespace VLB
  7. {
  8. [ExecuteInEditMode]
  9. [HelpURL(Consts.HelpUrlDynamicOcclusionRaycasting)]
  10. public class DynamicOcclusionRaycasting : DynamicOcclusionAbstractBase
  11. {
  12. /// <summary>
  13. /// Should it interact with 2D or 3D occluders?
  14. /// </summary>
  15. public Dimensions dimensions = Consts.DynOcclusionRaycastingDimensionsDefault;
  16. /// <summary>
  17. /// The beam can only be occluded by objects located on the layers matching this mask.
  18. /// It's very important to set it as restrictive as possible (checking only the layers which are necessary)
  19. /// to perform a more efficient process in order to increase the performance.
  20. /// </summary>
  21. public LayerMask layerMask = Consts.DynOcclusionLayerMaskDefault;
  22. /// <summary>
  23. /// Should this beam be occluded by triggers or not?
  24. /// </summary>
  25. public bool considerTriggers = Consts.DynOcclusionRaycastingConsiderTriggersDefault;
  26. /// <summary>
  27. /// Minimum 'area' of the collider to become an occluder.
  28. /// Colliders smaller than this value will not block the beam.
  29. /// </summary>
  30. public float minOccluderArea = Consts.DynOcclusionRaycastingMinOccluderAreaDefault;
  31. /// <summary>
  32. /// Approximated percentage of the beam to collide with the surface in order to be considered as occluder
  33. /// </summary>
  34. public float minSurfaceRatio = Consts.DynOcclusionRaycastingMinSurfaceRatioDefault;
  35. /// <summary>
  36. /// Max angle (in degrees) between the beam and the surface in order to be considered as occluder
  37. /// </summary>
  38. public float maxSurfaceDot = Consts.DynOcclusionRaycastingMaxSurfaceDotDefault;
  39. /// <summary>
  40. /// Alignment of the computed clipping plane:
  41. /// </summary>
  42. public PlaneAlignment planeAlignment = Consts.DynOcclusionRaycastingPlaneAlignmentDefault;
  43. /// <summary>
  44. /// Translate the plane. We recommend to set a small positive offset in order to handle non-flat surface better.
  45. /// </summary>
  46. public float planeOffset = Consts.DynOcclusionRaycastingPlaneOffsetDefault;
  47. /// <summary>
  48. /// Fade out the beam before the computed clipping plane in order to soften the transition.
  49. /// </summary>
  50. [FormerlySerializedAs("fadeDistanceToPlane")]
  51. public float fadeDistanceToSurface = Consts.DynOcclusionFadeDistanceToSurfaceDefault;
  52. [System.Obsolete("Use 'fadeDistanceToSurface' instead")]
  53. public float fadeDistanceToPlane { get { return fadeDistanceToSurface; } set { fadeDistanceToSurface = value; } }
  54. public bool IsColliderHiddenByDynamicOccluder(Collider collider)
  55. {
  56. Debug.Assert(collider, "You should pass a valid Collider to VLB.DynamicOcclusion.IsColliderHiddenByDynamicOccluder");
  57. if (!planeEquationWS.IsValid())
  58. return false;
  59. var isInside = GeometryUtility.TestPlanesAABB(new Plane[] { planeEquationWS }, collider.bounds);
  60. return !isInside;
  61. }
  62. public class HitResult
  63. {
  64. public HitResult(RaycastHit hit3D)
  65. {
  66. point = hit3D.point;
  67. normal = hit3D.normal;
  68. distance = hit3D.distance;
  69. collider3D = hit3D.collider;
  70. collider2D = null;
  71. }
  72. public HitResult(RaycastHit2D hit2D)
  73. {
  74. point = hit2D.point;
  75. normal = hit2D.normal;
  76. distance = hit2D.distance;
  77. collider2D = hit2D.collider;
  78. collider3D = null;
  79. }
  80. public HitResult()
  81. {
  82. point = Vector3.zero;
  83. normal = Vector3.zero;
  84. distance = 0;
  85. collider2D = null;
  86. collider3D = null;
  87. }
  88. public Vector3 point;
  89. public Vector3 normal;
  90. public float distance;
  91. Collider2D collider2D;
  92. Collider collider3D;
  93. public bool hasCollider { get { return collider2D || collider3D; } }
  94. public string name
  95. {
  96. get
  97. {
  98. if (collider3D) return collider3D.name;
  99. else if (collider2D) return collider2D.name;
  100. else return "null collider";
  101. }
  102. }
  103. public Bounds bounds
  104. {
  105. get
  106. {
  107. if (collider3D) return collider3D.bounds;
  108. else if (collider2D) return collider2D.bounds;
  109. else return new Bounds();
  110. }
  111. }
  112. }
  113. /// <summary>
  114. /// Get information about the current occluder hit by the beam.
  115. /// Can be null if the beam is not occluded.
  116. /// </summary>
  117. public HitResult currentHit { get; private set; }
  118. protected override string GetShaderKeyword() { return "VLB_OCCLUSION_CLIPPING_PLANE"; }
  119. protected override MaterialManager.DynamicOcclusion GetDynamicOcclusionMode() { return MaterialManager.DynamicOcclusion.ClippingPlane; }
  120. float m_RangeMultiplier = 1f;
  121. public Plane planeEquationWS { get; private set; }
  122. #if UNITY_EDITOR
  123. public struct EditorDebugData
  124. {
  125. public int lastFrameUpdate;
  126. }
  127. public EditorDebugData editorDebugData;
  128. public static bool editorShowDebugPlane = true;
  129. public static bool editorRaycastAtEachFrame = true;
  130. private static bool editorPrefsLoaded = false;
  131. public static void EditorLoadPrefs()
  132. {
  133. if (!editorPrefsLoaded)
  134. {
  135. editorShowDebugPlane = UnityEditor.EditorPrefs.GetBool("VLB_DYNOCCLUSION_SHOWDEBUGPLANE", true);
  136. editorRaycastAtEachFrame = UnityEditor.EditorPrefs.GetBool("VLB_DYNOCCLUSION_RAYCASTINGEDITOR", true);
  137. editorPrefsLoaded = true;
  138. }
  139. }
  140. #endif
  141. protected override void OnValidateProperties()
  142. {
  143. base.OnValidateProperties();
  144. minOccluderArea = Mathf.Max(minOccluderArea, 0f);
  145. fadeDistanceToSurface = Mathf.Max(fadeDistanceToSurface, 0f);
  146. }
  147. protected override void OnEnablePostValidate()
  148. {
  149. currentHit = null;
  150. #if UNITY_EDITOR
  151. EditorLoadPrefs();
  152. editorDebugData.lastFrameUpdate = 0;
  153. #endif
  154. }
  155. protected override void OnDisable()
  156. {
  157. base.OnDisable();
  158. SetHit(null);
  159. }
  160. void Start()
  161. {
  162. if (Application.isPlaying)
  163. {
  164. var triggerZone = GetComponent<TriggerZone>();
  165. if (triggerZone)
  166. {
  167. m_RangeMultiplier = Mathf.Max(1f, triggerZone.rangeMultiplier);
  168. }
  169. }
  170. }
  171. Vector3 GetRandomVectorAround(Vector3 direction, float angleDiff)
  172. {
  173. var halfAngle = angleDiff * 0.5f;
  174. return Quaternion.Euler(Random.Range(-halfAngle, halfAngle), Random.Range(-halfAngle, halfAngle), Random.Range(-halfAngle, halfAngle)) * direction;
  175. }
  176. QueryTriggerInteraction queryTriggerInteraction { get { return considerTriggers ? QueryTriggerInteraction.Collide : QueryTriggerInteraction.Ignore; } }
  177. float raycastMaxDistance { get { return m_Master.raycastDistance * m_RangeMultiplier * m_Master.lossyScale.z; } }
  178. HitResult GetBestHit(Vector3 rayPos, Vector3 rayDir)
  179. {
  180. return dimensions == Dimensions.Dim2D ? GetBestHit2D(rayPos, rayDir) : GetBestHit3D(rayPos, rayDir);
  181. }
  182. HitResult GetBestHit3D(Vector3 rayPos, Vector3 rayDir)
  183. {
  184. var hits = Physics.RaycastAll(rayPos, rayDir, raycastMaxDistance, layerMask.value, queryTriggerInteraction);
  185. int bestHit = -1;
  186. float bestLength = float.MaxValue;
  187. for (int i = 0; i < hits.Length; ++i)
  188. {
  189. if (hits[i].collider.gameObject != m_Master.gameObject) // skip collider from TriggerZone
  190. {
  191. if (hits[i].collider.bounds.GetMaxArea2D() >= minOccluderArea)
  192. {
  193. if (hits[i].distance < bestLength)
  194. {
  195. bestLength = hits[i].distance;
  196. bestHit = i;
  197. }
  198. }
  199. }
  200. }
  201. #if DEBUG_SHOW_RAYCAST_LINES
  202. Debug.DrawLine(rayPos, rayPos + rayDir * raycastMaxDistance, bestHit != -1 ? Color.green : Color.red);
  203. #endif
  204. if (bestHit != -1)
  205. return new HitResult(hits[bestHit]);
  206. else
  207. return new HitResult();
  208. }
  209. HitResult GetBestHit2D(Vector3 rayPos, Vector3 rayDir)
  210. {
  211. var hits = Physics2D.RaycastAll(new Vector2(rayPos.x, rayPos.y), new Vector2(rayDir.x, rayDir.y), raycastMaxDistance, layerMask.value);
  212. int bestHit = -1;
  213. float bestLength = float.MaxValue;
  214. for (int i = 0; i < hits.Length; ++i)
  215. {
  216. if (!considerTriggers && hits[i].collider.isTrigger) // do not query triggers if considerTriggers is disabled
  217. continue;
  218. if (hits[i].collider.gameObject != m_Master.gameObject) // skip collider from TriggerZone
  219. {
  220. if (hits[i].collider.bounds.GetMaxArea2D() >= minOccluderArea)
  221. {
  222. if (hits[i].distance < bestLength)
  223. {
  224. bestLength = hits[i].distance;
  225. bestHit = i;
  226. }
  227. }
  228. }
  229. }
  230. #if DEBUG_SHOW_RAYCAST_LINES
  231. Debug.DrawLine(rayPos, rayPos + rayDir * raycastMaxDistance, bestHit != -1 ? Color.green : Color.red);
  232. #endif
  233. if (bestHit != -1)
  234. return new HitResult(hits[bestHit]);
  235. else
  236. return new HitResult();
  237. }
  238. enum Direction {
  239. Up,
  240. Down,
  241. Left,
  242. Right,
  243. Max2D = Down,
  244. Max3D = Right,
  245. };
  246. uint m_PrevNonSubHitDirectionId = 0;
  247. uint GetDirectionCount() { return dimensions == Dimensions.Dim2D ? ((uint)Direction.Max2D + 1) : ((uint)Direction.Max3D + 1); }
  248. Vector3 GetDirection(uint dirInt)
  249. {
  250. dirInt = dirInt % GetDirectionCount();
  251. switch (dirInt)
  252. {
  253. case (uint)Direction.Up: return m_Master.raycastGlobalUp;
  254. case (uint)Direction.Right: return m_Master.raycastGlobalRight;
  255. case (uint)Direction.Down: return -m_Master.raycastGlobalUp;
  256. case (uint)Direction.Left: return -m_Master.raycastGlobalRight;
  257. }
  258. return Vector3.zero;
  259. }
  260. bool IsHitValid(HitResult hit, Vector3 forwardVec)
  261. {
  262. if (hit.hasCollider)
  263. {
  264. float dot = Vector3.Dot(hit.normal, -forwardVec);
  265. return dot >= maxSurfaceDot;
  266. }
  267. return false;
  268. }
  269. protected override bool OnProcessOcclusion(ProcessOcclusionSource source)
  270. {
  271. #if UNITY_EDITOR
  272. editorDebugData.lastFrameUpdate = Time.frameCount;
  273. #endif
  274. var raycastGlobalForward = m_Master.raycastGlobalForward;
  275. var bestHit = GetBestHit(transform.position, raycastGlobalForward);
  276. if (IsHitValid(bestHit, raycastGlobalForward))
  277. {
  278. if (minSurfaceRatio > 0.5f)
  279. {
  280. var raycastDistance = m_Master.raycastDistance;
  281. for (uint i = 0; i < GetDirectionCount(); i++)
  282. {
  283. var dir3 = GetDirection(i + m_PrevNonSubHitDirectionId) * (minSurfaceRatio * 2 - 1);
  284. dir3.Scale(transform.localScale);
  285. var startPt = transform.position + dir3 * m_Master.coneRadiusStart;
  286. var newPt = transform.position + dir3 * m_Master.coneRadiusEnd + raycastGlobalForward * raycastDistance;
  287. var bestHitSub = GetBestHit(startPt, (newPt - startPt).normalized);
  288. if (IsHitValid(bestHitSub, raycastGlobalForward))
  289. {
  290. if (bestHitSub.distance > bestHit.distance)
  291. {
  292. bestHit = bestHitSub;
  293. }
  294. }
  295. else
  296. {
  297. m_PrevNonSubHitDirectionId = i;
  298. bestHit = null;
  299. break;
  300. }
  301. }
  302. }
  303. }
  304. else
  305. {
  306. bestHit = null;
  307. }
  308. SetHit(bestHit);
  309. return bestHit != null;
  310. }
  311. void SetHit(HitResult hit)
  312. {
  313. if (hit == null)
  314. {
  315. SetClippingPlaneOff();
  316. }
  317. else
  318. {
  319. switch (planeAlignment)
  320. {
  321. case PlaneAlignment.Beam:
  322. SetClippingPlane(new Plane(-m_Master.raycastGlobalForward, hit.point));
  323. break;
  324. case PlaneAlignment.Surface:
  325. default:
  326. SetClippingPlane(new Plane(hit.normal, hit.point));
  327. break;
  328. }
  329. }
  330. currentHit = hit;
  331. }
  332. protected override void OnModifyMaterialCallback(MaterialModifier.Interface owner)
  333. {
  334. Debug.Assert(owner != null);
  335. var planeWS = planeEquationWS;
  336. owner.SetMaterialProp(ShaderProperties.DynamicOcclusionClippingPlaneWS, new Vector4(planeWS.normal.x, planeWS.normal.y, planeWS.normal.z, planeWS.distance));
  337. owner.SetMaterialProp(ShaderProperties.DynamicOcclusionClippingPlaneProps, fadeDistanceToSurface);
  338. }
  339. void SetClippingPlane(Plane planeWS)
  340. {
  341. planeWS = planeWS.TranslateCustom(planeWS.normal * planeOffset);
  342. SetPlaneWS(planeWS);
  343. m_Master._INTERNAL_SetDynamicOcclusionCallback(GetShaderKeyword(), OnModifyMaterialCallback);
  344. }
  345. void SetClippingPlaneOff()
  346. {
  347. SetPlaneWS(new Plane());
  348. m_Master._INTERNAL_SetDynamicOcclusionCallback(GetShaderKeyword(), null);
  349. }
  350. void SetPlaneWS(Plane planeWS)
  351. {
  352. planeEquationWS = planeWS;
  353. #if UNITY_EDITOR
  354. m_DebugPlaneLocal = planeWS;
  355. if (m_DebugPlaneLocal.IsValid())
  356. {
  357. float dist;
  358. if (m_DebugPlaneLocal.Raycast(new Ray(transform.position, m_Master.raycastGlobalForward), out dist))
  359. m_DebugPlaneLocal.distance = dist; // compute local distance
  360. }
  361. #endif
  362. }
  363. #if UNITY_EDITOR
  364. void LateUpdate()
  365. {
  366. if (!Application.isPlaying)
  367. {
  368. // In Editor, process raycasts at each frame update
  369. if (!editorRaycastAtEachFrame)
  370. SetHit(null);
  371. else
  372. ProcessOcclusion(ProcessOcclusionSource.EditorUpdate);
  373. }
  374. }
  375. Plane m_DebugPlaneLocal;
  376. void OnDrawGizmos()
  377. {
  378. if (!editorShowDebugPlane)
  379. return;
  380. if (m_DebugPlaneLocal.IsValid())
  381. {
  382. var planePos = transform.position + m_DebugPlaneLocal.distance * m_Master.raycastGlobalForward;
  383. float planeSize = Mathf.Lerp(m_Master.coneRadiusStart, m_Master.coneRadiusEnd, Mathf.InverseLerp(0f, m_Master.raycastDistance, m_DebugPlaneLocal.distance));
  384. Utils.GizmosDrawPlane(
  385. m_DebugPlaneLocal.normal,
  386. planePos,
  387. m_Master.color.Opaque(),
  388. Matrix4x4.identity,
  389. planeSize,
  390. planeSize * 0.5f);
  391. UnityEditor.Handles.color = m_Master.color.Opaque();
  392. UnityEditor.Handles.DrawWireDisc(planePos,
  393. m_DebugPlaneLocal.normal,
  394. planeSize * (minSurfaceRatio * 2 - 1));
  395. }
  396. }
  397. #endif
  398. }
  399. }