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);
}
}
}