using UnityEngine.Assertions; using UnityEngine; namespace Rokid.UXR.Interaction { public class CylinderSurface : MonoBehaviour, ISurface, IBounds { public enum NormalFacing { /// /// Raycast hit will register on the outside or inside of the cylinder, /// whichever is hit first. /// Any, /// /// Raycasts will pass through the outside of the cylinder and hit the inside wall. /// In, /// /// Raycast against the outside wall of the cylinder. /// In this mode, raycasts with an origin inside the cylinder will always fail. /// Out, } [SerializeField] private Cylinder _cylinder; [SerializeField] private NormalFacing _facing = NormalFacing.Out; [SerializeField, Tooltip("Height of the cylinder. If zero or negative, height will be infinite.")] private float _height = 1f; public bool IsValid => _cylinder != null && Radius > 0; public float Radius => _cylinder.Radius; public Cylinder Cylinder => _cylinder; public Transform Transform => _cylinder.transform; public Bounds Bounds { get { float maxScale = Mathf.Max(Transform.lossyScale.x, Mathf.Max(Transform.lossyScale.y, Transform.lossyScale.z)); float maxSize = maxScale * (Height + Radius); return new Bounds(Transform.position, new Vector3(maxSize, maxSize, maxSize)); } } public NormalFacing Facing { get => _facing; set => _facing = value; } public float Height { get => _height; set => _height = value; } public bool ClosestSurfacePoint(in Vector3 point, out SurfaceHit hit, float maxDistance) { hit = new SurfaceHit(); if (!IsValid) { return false; } Vector3 localPoint = _cylinder.transform.InverseTransformPoint(point); Vector3 clampedOrigin = localPoint; if (_height > 0) { clampedOrigin.y = Mathf.Clamp(clampedOrigin.y, -_height / 2f, _height / 2f); } Vector3 nearestPointOnCenterAxis = Vector3.Project(clampedOrigin, Vector3.up); Vector3 direction = (clampedOrigin == nearestPointOnCenterAxis) ? Vector3.forward : (clampedOrigin - nearestPointOnCenterAxis).normalized; bool isOutside = (clampedOrigin - nearestPointOnCenterAxis).magnitude > Radius; Vector3 hitPoint = nearestPointOnCenterAxis + direction * Radius; float hitDistance = Vector3.Distance(localPoint, hitPoint); if (maxDistance > 0 && TransformScale(hitDistance) > maxDistance) { return false; } Vector3 normal; switch (_facing) { default: case NormalFacing.Any: normal = isOutside ? direction : -direction; break; case NormalFacing.In: normal = -direction; break; case NormalFacing.Out: normal = direction; break; } hit.Point = _cylinder.transform.TransformPoint(nearestPointOnCenterAxis + direction * Radius); hit.Normal = _cylinder.transform.TransformDirection(normal); hit.Distance = TransformScale(hitDistance); return true; } public bool Raycast(in Ray ray, out SurfaceHit hit, float maxDistance) { // Flatten to 2D and find intersection point with circle in local space, // then convert back into 3D world space hit = new SurfaceHit(); if (!IsValid) { return false; } Ray localRay3D = new Ray(_cylinder.transform.InverseTransformPoint(ray.origin), _cylinder.transform.InverseTransformDirection(ray.direction).normalized); Ray localRay2D = new Ray(CancelY(localRay3D.origin), CancelY(localRay3D.direction).normalized); Vector3 originToCenter2D = -localRay2D.origin; Vector3 projPoint2D = localRay2D.origin + Vector3.Project(originToCenter2D, localRay2D.direction); float magDir2D = Vector3.Magnitude(CancelY(localRay3D.direction)); float magProj = Vector3.Magnitude(projPoint2D); bool isOutside = originToCenter2D.magnitude > Radius; NormalFacing effectiveFacing = _facing == NormalFacing.Any && !isOutside ? NormalFacing.In : _facing; bool hasMissed = magProj > Radius || // Aiming toward but missing Mathf.Approximately(magDir2D, 0f) || // Aiming up or down (isOutside && Vector3.Dot(originToCenter2D, localRay2D.direction) < 0) || // Aiming away (!isOutside && effectiveFacing == NormalFacing.Out); // Origin inside with normals out if (hasMissed) { return false; } float projToWall = Mathf.Sqrt(Mathf.Pow(Radius, 2) - Mathf.Pow(magProj, 2)); float tOuter = Vector3.Distance(localRay2D.origin, projPoint2D - localRay2D.direction * projToWall) / magDir2D; float tInner = Vector3.Distance(localRay2D.origin, projPoint2D + localRay2D.direction * projToWall) / magDir2D; Vector3 localHitpointOuter = localRay3D.GetPoint(tOuter); Vector3 localHitpointInner = localRay3D.GetPoint(tInner); bool hasOuter = (maxDistance <= 0 || TransformScale(tOuter) <= maxDistance) && (_height <= 0 || Mathf.Abs(localHitpointOuter.y) <= _height / 2f); bool hasInner = (maxDistance <= 0 || TransformScale(tInner) <= maxDistance) && (_height <= 0 || Mathf.Abs(localHitpointInner.y) <= _height / 2f); if (effectiveFacing != NormalFacing.In && hasOuter) { hit.Point = _cylinder.transform.TransformPoint(localHitpointOuter); hit.Normal = _cylinder.transform.TransformDirection(CancelY(localHitpointOuter).normalized); hit.Distance = TransformScale(tOuter); } else if (hasInner) { hit.Point = _cylinder.transform.TransformPoint(localHitpointInner); hit.Normal = _cylinder.transform.TransformDirection(CancelY(-localHitpointInner).normalized); hit.Distance = TransformScale(tInner); } else { return false; } return true; } /// /// Local to world transformation of a float, assuming uniform scale. /// private float TransformScale(float val) { return val * _cylinder.transform.lossyScale.x; } /// /// Cancel the Y component of a vector /// private static Vector3 CancelY(in Vector3 vector) { return new Vector3(vector.x, 0, vector.z); } } }