Is there a way to check if a vector is inside of a mesh collider?

I have been spending quite a while on a custom physics system for my game, since it has very, very weird physics. I was using collider.bounds.Contains(), which worked for box colliders that were aligned defaultly, but… silly me spent 7 hours without testing it on a non - box collider.

The physics need to work while INSIDE of a collider, so raycasts are out of the question. While inside that collider I am negating some defined areas of the collider, so whenever I test collision with my custom collision function it will check if it is in that defined area and negate it. That works fine because I just have to check distance to radius since the areas are circles. But I have come across a problem: Whenever I test collision, I create a Vector2 point alongside the player, and test if it is “colliding” with a specific type of object. collider.bounds.Contains() works fine for box colliders, but is there an equivalent for a mesh collider?

This is the final thing to lock in place for the mechanics, so help would be really appreciated!

you could use the mesh collider bounds? Like:

if(collider.bounds.Contains(your vector)) {
     //DoSomething
}

The effect I am getting is it is detecting a box created exactly around the mesh collider. I thought that collider.bounds.Contains maybe instead created a “bounds box” around the collider, instead of being the actual collider itself. Is this not how it works? Maybe I did something else wrong…

EDIT: I tested this in a new project, and I am sure it DOES make a box around it. Even box colliders, if I rotate them, a world aligned bounds will be created around them.

Oh sure, I’ve never actually tried it out so I’ll take your word for it. I know you can get the hit point from ray casting but you said you didn’t want to use ray casts. So you’re probably going to have to write your own collision detection algorithm, As far as I’m aware there isn’t another way to get the vector.

I’ve done it before using this: http://wiki.unity3d.com/index.php/PolyContainsPoint

I managed to write my own version based on that so I could use 3D mesh vertices. But thats a good starting point

1 Like

Thanks! I just found exactly that page before coming to check the post. One thing I didn’t really understand is, how can I take a 2D polygon, and get all of the vertices? Is there a function for it?

As far as I know there is no way of getting a sprites vertices. Only a 3D meshes.

for a broadphase you could check the bounds of the “quad”, then if contained project the vector into local space and get the matching pixel’s alpha for the narrow

I am using 3D meshes, and textured quads for my terrain.

Oh then that’s easy. Look into the Mesh API, you can easily get a meshes vertices.

something like this:

Vector3[]
getVertices (GameObject ObjectWithMeshFilter) {
        MeshFilter mf = ObjectWithMeshFilter.GetComponent<MeshFilter> ();
        return mf.mesh.vertices;
}

Wait… are you doing 2d or 3d?

That link is for a 2d polygon. You’re testing if a 2d point is inside the bounds of a closed polygon.

A 3d mesh is not a 2d closed poly. Instead it’s a chain of multiple 3d polys. Now… you could take that linked method and project it 2d and test that way… but you’d only be determining if the point lays exactly on the surface of a poly and is inside it. And even then… because it uses floats… it’ll fail because you’re off by some 0.00000000000001f units of error caused by the projecting from 3d to 2d.

What it sounds like is that you want to know if a 3d mesh contains a 3d point (represented by a vector3).

Well… there’s a problem with that. What is “INSIDE of a collider”? When a collider is a solid like a Sphere or a Box, that definition is pretty simple. But when it’s a Mesh, it’s not so much. What is “INSIDE” a plane? A plane is a mesh, and it has no insides like a sphere does. Same goes for many 3d mesh surfaces. Like a height map has no “insides”.

You need to define that for a Mesh.

For the other shapes there’s simple algorithms. Just different for each. A Sphere is easy…

public bool PointInSphere(Vector3 pnt, Vector3 sphereCenter, float sphereRadius)
{
    return (sphereCenter - pnt).magnitude < sphereRadius;
}

I actually wrote up a library of geometric shapes to test ‘Contains’ for each. And I have methods to create a Geom struct from a Collider for doing said testing.

Give you an example, this is a Capsule:

using UnityEngine;
using System.Collections.Generic;

namespace com.spacepuppy.Geom
{
    [System.Serializable]
    public struct Capsule : IGeom, IPhysicsGeom, System.Runtime.Serialization.ISerializable
    {

        #region Fields

        private Vector3 _start;
        private Vector3 _end;
        private float _rad;

        #endregion

        #region CONSTRUCTOR

        public Capsule(Vector3 start, Vector3 end, float radius)
        {
            _start = start;
            _end = end;
            _rad = radius;
        }

        public Capsule(Vector3 center, Vector3 up, float height, float radius)
        {
            var h = Mathf.Max(0f,(height - (radius * 2.0f)) / 2.0f);
            var change = up.normalized * h;

            _start = center - change;
            _end = center + change;
            _rad = radius;
        }

        #endregion

        #region Properties

        public Vector3 Start
        {
            get { return _start; }
            set { _start = value; }
        }

        public Vector3 End
        {
            get { return _end; }
            set { _end = value; }
        }

        public float Radius
        {
            get { return _rad; }
            set { _rad = value; }
        }

        public float Height
        {
            get
            {
                if (_end == _start)
                    return _rad * 2.0f;
                else
                    return (_end - _start).magnitude + _rad * 2.0f;
            }
            set
            {
                var c = this.Center;
                var up = (_end - _start).normalized;
                var change = up * (value - (_rad * 2.0f));
                _start = c - change;
                _end = c + change;
            }
        }

        public Vector3 Center
        {
            get
            {
                if (_end == _start)
                    return _start;
                else
                    return _start + (_end - _start) * 0.5f;
            }
            set
            {
                var change = (value - this.Center);
                _start += change;
                _end += change;
            }
        }

        public Vector3 Up
        {
            get
            {
                if (_end == _start)
                    return Vector3.up;
                else
                    return (_end - _start).normalized;
            }
        }

        public bool IsSpherical
        {
            get { return _end == _start; }
        }

        #endregion


        #region IGeom Interface

        public AxisInterval Project(Vector3 axis)
        {
            axis.Normalize();
            var c1 = Vector3.Dot(_start, axis);
            var c2 = Vector3.Dot(_end, axis);
            var p1 = c1 - _rad;
            var p2 = c1 + _rad;
            var p3 = c2 - _rad;
            var p4 = c2 + _rad;

            return new AxisInterval(axis, Mathf.Min(p1, p2, p3, p4), Mathf.Max(p1, p2, p3, p4));
        }

        public Bounds GetBounds()
        {
            Vector3 c = this.Center;
            Vector3 sz = new Vector3();
            sz.x = Mathf.Abs(_start.x - c.x) + _rad;
            sz.y = Mathf.Abs(_start.y - c.y) + _rad;
            sz.z = Mathf.Abs(_start.z - c.y) + _rad;
            return new Bounds(c, sz);
        }

        public Sphere GetBoundingSphere()
        {
            return new Sphere(this.Center, (_end - _start).magnitude + _rad);
        }

        public bool Contains(Vector3 pos)
        {
            var sqrRad = _rad * _rad;

            if (this.IsSpherical)
            {
                return Vector3.SqrMagnitude(pos - _start) <= sqrRad;
            }
            else
            {
                if (Vector3.SqrMagnitude(pos - _start) <= sqrRad) return true;
                if (Vector3.SqrMagnitude(pos - _end) <= sqrRad) return true;
            }

            var rail = _end - _start;
            var rod = pos - _start;
            var sqrLen = rod.sqrMagnitude;
            var dot = Vector3.Dot(rod, rail);

            if (dot < 0f || dot > sqrLen)
            {
                return false;
            }
            else
            {
                var disSqr = rod.sqrMagnitude - dot * dot / sqrLen;
                if (disSqr > sqrRad)
                    return false;
                else
                    return true;
            }
        }

        public bool Intersects(IGeom geom)
        {
            //TODO
            throw new System.NotImplementedException();
        }

        public bool Intersects(Bounds bounds)
        {
            //TODO
            throw new System.NotImplementedException();
        }

        #endregion

        #region IPhysicsGeom Interface

        public bool TestOverlap(int layerMask)
        {
            if (_start == _end)
            {
                return Physics.CheckSphere(_start, _rad, layerMask);
            }
            else
            {
                return Physics.CheckCapsule(_start, _end, _rad, layerMask);
            }
        }

        public IEnumerable<Collider> Overlap(int layerMask)
        {
            var hits = new List<Collider>();

            //first overlap start sphere
            hits.AddRange(Physics.OverlapSphere(_start, _rad, layerMask));
            //now overlap the end sphere, don't add duplicates
            foreach (var c in Physics.OverlapSphere(_end, _rad, layerMask))
            {
                if (!hits.Contains(c)) hits.Add(c);
            }
            //lastly cast from start to end, don't add duplicates
            var dir = _end - _start;
            var dist = dir.magnitude;
            foreach (var h in Physics.SphereCastAll(_start, _rad, dir, dist, layerMask))
            {
                if (!hits.Contains(h.collider)) hits.Add(h.collider);
            }

            return hits;
        }

        public bool Cast(Vector3 direction, out RaycastHit hitinfo, float distance, int layerMask)
        {
            if (_start == _end)
            {
                return Physics.SphereCast(_start, _rad, direction, out hitinfo, distance, layerMask);
            }
            else
            {
                return Physics.CapsuleCast(_start, _end, _rad, direction, out hitinfo, distance, layerMask);
            }
        }

        public IEnumerable<RaycastHit> CastAll(Vector3 direction, float distance, int layerMask)
        {
            if (_start == _end)
            {
                return Physics.SphereCastAll(_start, _rad, direction, distance, layerMask);
            }
            else
            {
                return Physics.CapsuleCastAll(_start, _end, _rad, direction, distance, layerMask);
            }
        }

        #endregion


        #region ISerializable Interface

        private Capsule(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
        {
            _start = new Vector3(info.GetSingle("start.x"), info.GetSingle("start.y"), info.GetSingle("start.z"));
            _end = new Vector3(info.GetSingle("end.x"), info.GetSingle("end.y"), info.GetSingle("end.z"));
            _rad = info.GetSingle("radius");
        }

        void System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
        {
            info.AddValue("start.x", _start.x);
            info.AddValue("start.y", _start.y);
            info.AddValue("start.z", _start.z);
            info.AddValue("end.x", _end.x);
            info.AddValue("end.y", _end.y);
            info.AddValue("end.z", _end.z);
            info.AddValue("radius", _rad);
        }

        #endregion


        #region Static Interface

        public static Capsule FromCollider(CharacterController cap)
        {
            var cent = cap.transform.position + cap.center;
            var hsc = Mathf.Max(cap.transform.lossyScale.x, cap.transform.lossyScale.y);
            var vsc = cap.transform.lossyScale.y;

            return new Capsule(cent, Vector3.up, cap.height * vsc, cap.radius * hsc);
        }

        public static Capsule FromCollider(CapsuleCollider cap)
        {
            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);
        }

        public static Capsule FromCollider(SphereCollider sph)
        {
            var cent = sph.transform.TransformPoint(sph.center);
            return new Capsule(cent, cent, sph.radius);
        }

        #endregion

    }
}

A portion of the geom classes are actually available on a google code project of mine. I only released a small sub-section of the full framework, so it only contains three solids.

Sphere -

AABox -

Capsule -

Again though… Meshes, that’s a whole other question. As it depends on what the Mesh is.

There are algorithms that exist for generic meshes. One is the “Separating Axis Theorem”. But this only works on convex meshes. This is why Unity actually has a tick box to perform collision detection on a mesh with convex mesh only.

You’ll actually notice that the IGeom interface in that framework has a method called “Project”.

That Project method is actually used for implementing the “Separating Axis Theorem”. It’s just that the released stuff I have doesn’t include convex mesh testing.

2 Likes

here’s a quick n dirty one:
pseudo code

public bool isPointInVol( obj, pos) //checks if supplied position is inside supplied mesh object
(
    var = obj.mesh;
  var nVerts=tMesh.numverts;
  bool isInVol = true ;

    for v = 1 to nVerts while isInVol
    if asin (dot (getNormal tMesh v) (normalize(((getVert tMesh v)*obj.transform) - pos))) <= 0.0
      isInVol = false ;
   
  tMesh = nVerts = vPos = undefined ;

    return isInVol ;
)

Actually, it’s not that complex as long as your mesh fits a few criteria:

  • It’s a sealed volume
  • It’s convex (or is split up into convex sections which you check individually)
  • No edge is shared by more than two faces

While that sounds awfully specific, what it boils down to is “it has to be a mesh where the concepts of inside and outside actually make sense”. (If a mesh doesn’t fit any one of those rules there’s no clear “inside”.)

Assuming those things are true, all you need to do is check for each face that the desired vector is on the “inside” side, and that’s math you can look up in any number of game dev or math resources.

Ummm… yeah.

That’s pretty much exactly what I was saying. My point of talking about a plane or height map was that they’re examples of things that aren’t solid geometry (sealed volumes). That’s why later in the post I specifically said mentioned convex hulls in regards to the “Separating Axis Theorem”.

Yeah, I thought that the half of the thread I hadn’t got to might have got to that stuff, but I only had a moment before I had to duck out and thought it better to mash out something potentially helpful than to not.

Better to have it said twice than not to have it said at all. :wink: