Keep Sphere Collider within Capsule Collider

Hi all,

I’m trying simply drag a sphere around, whilst keeping it bound within a capsule collider.

If I use the bounds of the capsule to check if the point I want to move the sphere is it will work only if the capsule isn’t rotated (I gather it tests the bounding box around it and therefore it means I can move the sphere outside of the capsule).

Using the following code to move the sphere, and test if it’s within the capsule I get the following results:

    [SerializeField]
    Collider m_CapsuleCollider;

    void Update ()
    {
        if(Input.GetMouseButton(0))
        {
            Vector3 pos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, (transform.position - Camera.main.transform.position).z));

            if(IsPointWithinBounds(pos))
            {
                transform.position = pos;
            }
        }
    }

    bool IsPointWithinBounds(Vector3 point)
    {
        return m_CapsuleCollider.bounds.Contains(point);
    }

As you can see here is the sphere hitting the bounding box of the capsule (when it is not rotated).

Using the same code, when the capsule is rotated and those the bounding box’s edges move outside of the capsule, the sphere is allowed to move outside of it.

My aim is to be able to drag the sphere around the inside of the capsule collider, without any of the sphere moving outside of it.

I’ve included the example project, so you can see it all working.

Any help would be most appreciated.

2628514–184723–CapsuleColliderDemo.zip (28.6 KB)

Don’t use bounds, that’s going to give you a axis aligned rectoid of the capsule.

Instead you want to compare the position of the sphere relative to the capsule, and make sure it doesn’t exit the capsule.

A capsule can be seen as really just 2 spheres tethered together. The data the defines a sphere is very often just the points at which these 2 spheres are positioned, and the radius of said spheres.

public struct Capsule
{
    public Vector3 PosA;
    public Vector3 PosB;
    public float Radius;
}

And of course we know a sphere is just one point and its radius.

public struct Sphere
{
    public Vector3 Pos;
    public float Radius;
}

Now imagine if you had a sphere inside a bigger sphere… how you would know it had left the bounds of the larger sphere is if the distance between the 2 spheres was larger than the difference of their radii:

public bool SphereEncapsulatesOther(Sphere small, Sphere large)
{
    return Vector3.Distance(large.Pos, small.Pos) < large.Radius - small.Radius;
}

Well a capsule will be similar… it’s encapsulated by the capsule if it’s inside either of the spheres. So we just have to check if EITHER sphere encapsulates… BUT there’s an exception, if the sphere exists in between the 2 spheres, it’s also considered encapsulated.

Really, you could simplify this though… cause really, the capsule is like a series of spheres all along a line from PosA to PosB. So really you could just test the difference of the spheres position from the nearest point on the line segment from PosA to PosB. If that distance is less than the difference in radii, than you’re inside the capsule, if it’s larger you’re outside the capsule.

You can get this information via a dot product.

public bool CapsuleEncapsulatesSphere(Capsule cap, Sphere sphere)
{
    var rail = cap.PosB - cap.PosA;
    var len = rail.magnitude;
    //the capsule is spherical if len is 0
    if(len < 0.001f) return Vector3.Distance(cap.PosA, sphere.Pos) < cap.Radius - sphere.Radius;

    var diff = cap.Radius - sphere.Radius;
    diff *= diff; //we'll be working in square distances for efficiency
    var rod = sphere.Pos - cap.PosA;
    var sqrLen = rod.sqrMagnitude;
    var dot = Vector3.Dot(rod, rail);

    if(dot < -diff || dot > sqrLen + diff)
    {
        //we fall well outside of the range of the segment from A to B
        return false;
    }
    else
    {
        var disSqr = rod.sqrMagnitude - dot * dot / sqrLen;
        return disSqr < diff;
    }
}

Pretty much something like that.

I based this off my code at:

Thanks for the answer. I’m not trying to translate that method to use a CapsuleCollider and SphereCollider, but without much luck.

I’ve also discovered Unity - Scripting API: Physics.CheckCapsule but not sure if that will accomplish what I’m looking for.

I wonder why Unity’s physics seem doesn’t stop the sphere collider from leaving the capsule collider, I guess this is because I’m overriding the transform position.

I’ve included the updated demo project, with my hair ripping progress so far! :smile:

2629328–184804–CapsuleColliderDemoV2.zip (8.7 KB)

Physics.CheckCapsule checks if some capsule defined by the input overlaps anything (not a capsule collider necessarily, you could just toss in raw data). And by overlap, that means partially as well as completely. So if the sphere was sticking half in and half out of the capsule that would still return true.

As for why unity Physics doesn’t bother with not allowing the sphere to stay in the capsule is because it’s not the way the physics engine is designed (PhysX, which is by Nvidia, not Unity). In PhysX (and well a lot of contemporary physics engines), bodies are considered solid. So nothing should be inside the other. For it to be inside a capsule, you’re really saying your body is solid every EXCEPT for the capsule… like the mold of a capsule, rather than the capsule itself.

If you’re having problems getting the extents and radius from a CapsuleCollider (unity’s capsule collider is stored as position, height, and radius…) you can see my ‘FromCollider’ method in my CapsuleCollider struct I linked:

        public static Capsule FromCollider(CapsuleCollider cap, bool local = false)
        {
            if(local)
            {
                float r = cap.radius;
                float h = Mathf.Max(r, cap.height);
                Vector3 ax;
                switch (cap.direction)
                {
                    case 0:
                        ax = Vector3.right;
                        break;
                    case 1:
                        ax = Vector3.up;
                        break;
                    case 2:
                        ax = Vector3.right;
                        break;
                    default:
                        ax = Vector3.up;
                        break;
                }
                return new Capsule(cap.center, ax, h, r);
            }
            else
            {
                Vector3 axis;
                float hsc;
                float vsc;
                switch (cap.direction)
                {
                    case 0:
                        axis = cap.transform.right;
                        hsc = Mathf.Max(cap.transform.lossyScale.x, cap.transform.lossyScale.y);
                        vsc = cap.transform.lossyScale.x;
                        break;
                    case 1:
                        axis = cap.transform.up;
                        hsc = Mathf.Max(cap.transform.lossyScale.x, cap.transform.lossyScale.y);
                        vsc = cap.transform.lossyScale.y;
                        break;
                    case 2:
                        axis = cap.transform.forward;
                        hsc = Mathf.Max(cap.transform.lossyScale.z, cap.transform.lossyScale.y);
                        vsc = cap.transform.lossyScale.z;
                        break;
                    default:
                        return new Capsule();
                }

                var cent = cap.center;

                cent = cap.transform.TransformPoint(cent);
                return new Capsule(cent, axis, cap.height * vsc, cap.radius * hsc);
            }
        }

And here’s the one for SphereCollider:

        public static Sphere FromCollider(SphereCollider s, bool local = false)
        {
            if (s == null) return new Sphere();

            if(local)
            {
                return new Sphere(s.center, s.radius);
            }
            else
            {
                var sc = s.transform.lossyScale;
                var cent = s.transform.TransformPoint(s.center);
                var msc = Mathf.Max(sc.x, sc.y, sc.z);
                return new Sphere(cent, s.radius * msc);
            }
        }

I’ve imported your framework and sub’d in the FromCollider methods and used the Capsule and Sphere structs, but I’m still stuck. I’ve swapped Capsule.PosA and Capsule.PosB, for Capsule.Start and Capsule.End in the CapsuleEncapsulatesSphere method, but can’t seem to get it working. Any idea where I’m going wrong? Thanks for all your help so far btw!

using UnityEngine;
using System.Collections;
using com.spacepuppy.Geom;

public class MoveSphere : MonoBehaviour {

    [SerializeField]
    CapsuleCollider m_CapsuleCollider;

    [SerializeField]
    SphereCollider m_sphereCollider;

    void Update ()
    {
        if(Input.GetMouseButton(0))
        {
            Vector3 pos = Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, (transform.position - Camera.main.transform.position).z));
            
            if(CapsuleEncapsulatesSphere(Capsule.FromCollider(m_CapsuleCollider, false), Sphere.FromCollider(m_sphereCollider, false)))
            {
                transform.position = pos;
            }
        }
    }

    public bool CapsuleEncapsulatesSphere(Capsule cap, Sphere sphere)
    {
        var rail = cap.End - cap.Start;
        var len = rail.magnitude;
        //the capsule is spherical if len is 0
        if(len < 0.001f) return Vector3.Distance(cap.Start, sphere.Center) < cap.Radius - sphere.Radius;

        var diff = cap.Radius - sphere.Radius;
        diff *= diff; //we'll be working in square distances for efficiency
        var rod = sphere.Center - cap.End;
        var sqrLen = rod.sqrMagnitude;
        var dot = Vector3.Dot(rod, rail);

        if(dot < -diff || dot > sqrLen + diff)
        {
            //we fall well outside of the range of the segment from A to B
            return false;
        }
        else
        {
            var disSqr = rod.sqrMagnitude - dot * dot / sqrLen;
            return disSqr < diff;
        }
    }
}