CylinderSurface.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. using UnityEngine.Assertions;
  2. using UnityEngine;
  3. namespace Rokid.UXR.Interaction {
  4. public class CylinderSurface : MonoBehaviour, ISurface, IBounds
  5. {
  6. public enum NormalFacing
  7. {
  8. /// <summary>
  9. /// Raycast hit will register on the outside or inside of the cylinder,
  10. /// whichever is hit first.
  11. /// </summary>
  12. Any,
  13. /// <summary>
  14. /// Raycasts will pass through the outside of the cylinder and hit the inside wall.
  15. /// </summary>
  16. In,
  17. /// <summary>
  18. /// Raycast against the outside wall of the cylinder.
  19. /// In this mode, raycasts with an origin inside the cylinder will always fail.
  20. /// </summary>
  21. Out,
  22. }
  23. [SerializeField]
  24. private Cylinder _cylinder;
  25. [SerializeField]
  26. private NormalFacing _facing = NormalFacing.Out;
  27. [SerializeField, Tooltip("Height of the cylinder. If zero or negative, height will be infinite.")]
  28. private float _height = 1f;
  29. public bool IsValid => _cylinder != null && Radius > 0;
  30. public float Radius => _cylinder.Radius;
  31. public Cylinder Cylinder => _cylinder;
  32. public Transform Transform => _cylinder.transform;
  33. public Bounds Bounds
  34. {
  35. get
  36. {
  37. float maxScale = Mathf.Max(Transform.lossyScale.x,
  38. Mathf.Max(Transform.lossyScale.y, Transform.lossyScale.z));
  39. float maxSize = maxScale * (Height + Radius);
  40. return new Bounds(Transform.position,
  41. new Vector3(maxSize, maxSize, maxSize));
  42. }
  43. }
  44. public NormalFacing Facing
  45. {
  46. get => _facing;
  47. set => _facing = value;
  48. }
  49. public float Height
  50. {
  51. get => _height;
  52. set => _height = value;
  53. }
  54. public bool ClosestSurfacePoint(in Vector3 point, out SurfaceHit hit, float maxDistance)
  55. {
  56. hit = new SurfaceHit();
  57. if (!IsValid)
  58. {
  59. return false;
  60. }
  61. Vector3 localPoint = _cylinder.transform.InverseTransformPoint(point);
  62. Vector3 clampedOrigin = localPoint;
  63. if (_height > 0)
  64. {
  65. clampedOrigin.y = Mathf.Clamp(clampedOrigin.y, -_height / 2f, _height / 2f);
  66. }
  67. Vector3 nearestPointOnCenterAxis = Vector3.Project(clampedOrigin, Vector3.up);
  68. Vector3 direction = (clampedOrigin == nearestPointOnCenterAxis) ?
  69. Vector3.forward : (clampedOrigin - nearestPointOnCenterAxis).normalized;
  70. bool isOutside = (clampedOrigin - nearestPointOnCenterAxis).magnitude > Radius;
  71. Vector3 hitPoint = nearestPointOnCenterAxis + direction * Radius;
  72. float hitDistance = Vector3.Distance(localPoint, hitPoint);
  73. if (maxDistance > 0 && TransformScale(hitDistance) > maxDistance)
  74. {
  75. return false;
  76. }
  77. Vector3 normal;
  78. switch (_facing)
  79. {
  80. default:
  81. case NormalFacing.Any:
  82. normal = isOutside ? direction : -direction;
  83. break;
  84. case NormalFacing.In:
  85. normal = -direction;
  86. break;
  87. case NormalFacing.Out:
  88. normal = direction;
  89. break;
  90. }
  91. hit.Point = _cylinder.transform.TransformPoint(nearestPointOnCenterAxis + direction * Radius);
  92. hit.Normal = _cylinder.transform.TransformDirection(normal);
  93. hit.Distance = TransformScale(hitDistance);
  94. return true;
  95. }
  96. public bool Raycast(in Ray ray, out SurfaceHit hit, float maxDistance)
  97. {
  98. // Flatten to 2D and find intersection point with circle in local space,
  99. // then convert back into 3D world space
  100. hit = new SurfaceHit();
  101. if (!IsValid)
  102. {
  103. return false;
  104. }
  105. Ray localRay3D = new Ray(_cylinder.transform.InverseTransformPoint(ray.origin),
  106. _cylinder.transform.InverseTransformDirection(ray.direction).normalized);
  107. Ray localRay2D = new Ray(CancelY(localRay3D.origin), CancelY(localRay3D.direction).normalized);
  108. Vector3 originToCenter2D = -localRay2D.origin;
  109. Vector3 projPoint2D = localRay2D.origin + Vector3.Project(originToCenter2D, localRay2D.direction);
  110. float magDir2D = Vector3.Magnitude(CancelY(localRay3D.direction));
  111. float magProj = Vector3.Magnitude(projPoint2D);
  112. bool isOutside = originToCenter2D.magnitude > Radius;
  113. NormalFacing effectiveFacing = _facing == NormalFacing.Any && !isOutside ? NormalFacing.In : _facing;
  114. bool hasMissed = magProj > Radius || // Aiming toward but missing
  115. Mathf.Approximately(magDir2D, 0f) || // Aiming up or down
  116. (isOutside && Vector3.Dot(originToCenter2D, localRay2D.direction) < 0) || // Aiming away
  117. (!isOutside && effectiveFacing == NormalFacing.Out); // Origin inside with normals out
  118. if (hasMissed)
  119. {
  120. return false;
  121. }
  122. float projToWall = Mathf.Sqrt(Mathf.Pow(Radius, 2) - Mathf.Pow(magProj, 2));
  123. float tOuter = Vector3.Distance(localRay2D.origin, projPoint2D - localRay2D.direction * projToWall) / magDir2D;
  124. float tInner = Vector3.Distance(localRay2D.origin, projPoint2D + localRay2D.direction * projToWall) / magDir2D;
  125. Vector3 localHitpointOuter = localRay3D.GetPoint(tOuter);
  126. Vector3 localHitpointInner = localRay3D.GetPoint(tInner);
  127. bool hasOuter = (maxDistance <= 0 || TransformScale(tOuter) <= maxDistance) &&
  128. (_height <= 0 || Mathf.Abs(localHitpointOuter.y) <= _height / 2f);
  129. bool hasInner = (maxDistance <= 0 || TransformScale(tInner) <= maxDistance) &&
  130. (_height <= 0 || Mathf.Abs(localHitpointInner.y) <= _height / 2f);
  131. if (effectiveFacing != NormalFacing.In && hasOuter)
  132. {
  133. hit.Point = _cylinder.transform.TransformPoint(localHitpointOuter);
  134. hit.Normal = _cylinder.transform.TransformDirection(CancelY(localHitpointOuter).normalized);
  135. hit.Distance = TransformScale(tOuter);
  136. }
  137. else if (hasInner)
  138. {
  139. hit.Point = _cylinder.transform.TransformPoint(localHitpointInner);
  140. hit.Normal = _cylinder.transform.TransformDirection(CancelY(-localHitpointInner).normalized);
  141. hit.Distance = TransformScale(tInner);
  142. }
  143. else
  144. {
  145. return false;
  146. }
  147. return true;
  148. }
  149. /// <summary>
  150. /// Local to world transformation of a float, assuming uniform scale.
  151. /// </summary>
  152. private float TransformScale(float val)
  153. {
  154. return val * _cylinder.transform.lossyScale.x;
  155. }
  156. /// <summary>
  157. /// Cancel the Y component of a vector
  158. /// </summary>
  159. private static Vector3 CancelY(in Vector3 vector)
  160. {
  161. return new Vector3(vector.x, 0, vector.z);
  162. }
  163. }
  164. }