Last active
August 1, 2025 12:02
-
-
Save nilpunch/27548a9227417839d2d085f7482a4343 to your computer and use it in GitHub Desktop.
Unity physics utility to find closest point on any type of collider, including concave (non-convex) colliders.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System.Runtime.InteropServices; | |
| using UnityEngine; | |
| using Object = UnityEngine.Object; | |
| public static class PhysicsUtils | |
| { | |
| private static readonly Collider[] _colliders = new Collider[512]; | |
| private static SphereCollider _probeSphere; | |
| private static SphereCollider ProbeSphere | |
| { | |
| get | |
| { | |
| if (_probeSphere == null) | |
| { | |
| var go = new GameObject(nameof(ProbeSphere)); | |
| go.hideFlags = HideFlags.HideInInspector | HideFlags.HideInHierarchy; | |
| Object.DontDestroyOnLoad(go); | |
| _probeSphere = go.AddComponent<SphereCollider>(); | |
| _probeSphere.enabled = false; | |
| } | |
| return _probeSphere; | |
| } | |
| } | |
| public static bool ClosestPoint(Vector3 position, float checkRadius, out RaycastHit hit, int layerMask = ~0, float epsilon = 1e-5f) | |
| { | |
| hit = default; | |
| var overlaps = Physics.OverlapSphereNonAlloc(position, checkRadius + 0.001f, _colliders, layerMask, QueryTriggerInteraction.Ignore); | |
| if (overlaps == 0) | |
| { | |
| return false; | |
| } | |
| var minDistance = float.MaxValue; | |
| var closestNormal = Vector3.zero; | |
| Collider closestCollider = null; | |
| ProbeSphere.enabled = true; | |
| for (var i = 0; i < overlaps; i++) | |
| { | |
| var collider = _colliders[i]; | |
| if (ClosestPointOnCollider(checkRadius, position, collider, | |
| collider.transform.position, collider.transform.rotation, | |
| out var normal, out var distance, epsilon)) | |
| { | |
| if (distance < minDistance) | |
| { | |
| minDistance = distance; | |
| closestNormal = normal; | |
| closestCollider = collider; | |
| } | |
| } | |
| } | |
| ProbeSphere.enabled = false; | |
| if (minDistance.Equals(float.MaxValue)) | |
| { | |
| return false; | |
| } | |
| hit.distance = minDistance; | |
| hit.point = position - closestNormal * minDistance; | |
| hit.normal = closestNormal; | |
| SetHitCollider(ref hit, closestCollider); | |
| return true; | |
| } | |
| private static bool ClosestPointOnCollider( | |
| float overestimate, | |
| Vector3 position, | |
| Collider collider, | |
| Vector3 colliderPosition, | |
| Quaternion colliderRotation, | |
| out Vector3 normal, | |
| out float distance, | |
| float epsilon = 1e-5f) | |
| { | |
| if (collider is not MeshCollider meshCollider || meshCollider.convex) | |
| { | |
| var direction = position - collider.ClosestPoint(position); | |
| distance = direction.magnitude; | |
| normal = direction / distance; | |
| return distance <= radius; | |
| } | |
| var probeSphere = ProbeSphere; | |
| probeSphere.radius = radius; | |
| if (!Physics.ComputePenetration( | |
| probeSphere, position, Quaternion.identity, | |
| collider, colliderPosition, colliderRotation, | |
| out normal, out var depth)) | |
| { | |
| distance = default; | |
| return false; | |
| } | |
| distance = radius - depth; | |
| radius *= 0.5f; | |
| var step = radius * 0.5f; | |
| while (step > epsilon) | |
| { | |
| probeSphere.radius = radius; | |
| if (Physics.ComputePenetration( | |
| probeSphere, position, Quaternion.identity, | |
| collider, colliderPosition, colliderRotation, | |
| out var currentNormal, out depth)) | |
| { | |
| distance = radius - depth; | |
| normal = currentNormal; | |
| radius -= step; | |
| } | |
| else | |
| { | |
| radius += step; | |
| } | |
| step *= 0.5f; | |
| } | |
| return true; | |
| } | |
| private static readonly int _sizeOfHit = Marshal.SizeOf<RaycastHit>(); | |
| private static readonly int _colliderFieldOffset = (int)Marshal.OffsetOf<RaycastHit>("m_Collider"); | |
| private static void SetHitCollider(ref RaycastHit hit, Collider collider) | |
| { | |
| var colliderId = collider.GetInstanceID(); | |
| var bytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref hit, _sizeOfHit)); | |
| MemoryMarshal.Write(bytes.Slice(_colliderFieldOffset, sizeof(int)), ref colliderId); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment