Unity: Find a random point within a Collider / Mesh / Bounds

Date: 2021-07-20 | unity | collider | mesh | 3d |

Overview

I've been working on some Monoliths recently (like the one above) that explore autonomous systems through the independent movement of hundreds and thousands of individual objects. To effectively (and efficiently) facilitate this, I wanted a way to spawn my objects inside of a given shape - be it a cube, sphere, triangle, or something more elaborate.

The problem I ran into was: how do I find a random point within an object in Unity 3d?

Goal: Spawn objects inside of a predefined shape

Problem: How to find a random point inside of a shape

In this post, I'll share some approaches I tried, how they work, and some drawbacks of each.

Approach 1: Find a Random Point in a Bounds

The first approach I took was a simpler one - take the rectangular / box-like Bounds and compute a random point in it.

To understand this approach we need to first understand a few terms:

3D Mesh

From Wikipedia: "A collection of vertices, edges, and faces that defines the shape of a polyhedral object".

This is basically the 'thing' you see in 3d environments that gives an object its shape. It's what makes one GameObject look like a sphere vs a cube.

Bounds

From Unity documentation: "An axis-aligned bounding box, or AABB for short, is a box aligned with coordinate axes and fully enclosing some object."

So for an unrotated rectangle, the Bounds will align with the underlying Mesh.

For other Meshes like a sphere, the Bounds will still be rectangular so there's going to be some imprecision here.

Using the Bounds to compute a random point is pretty straight forward:

  • Given a Vector3 (x, y, z), find the min and max of each dimension
  • Compute a random value between min and max

Source code in C#:

public Vector3 GetRandomPointInBounds(Bounds bounds) {
    float minX = bounds.size.x * -0.5f;
    float minY = bounds.size.y * -0.5f;
    float minZ = bounds.size.z * -0.5f;

    return (Vector3)this._gameObject.transform.TransformPoint(
        new Vector3(Random.Range (minX, -minX),
            Random.Range (minY, -minY),
            Random.Range (minZ, -minZ))
    );
}

Note: We use Transform.TransformPoint to convert from the Bounds' Local Space to World Space

Now this approach does have some drawbacks:

  • Only works for rectangular / box-like objects
    • Things like spheres, triangles, etc. will fail

Bounds drawback examples

Bounds drawback examples

Approach 2: Colliders and RayCast

After fixing the simple cube spawning problem I of course got ambitious and decided to move onto more complicated things - like spawning a bunch of cubes in the shape of a triangle! Approach 1 had imprecision problems (i.e. spawning my triangle in the shape of cubes!) so Approach 2 aimed to fix that.

For this approach, we introduce two more concepts:

Collider

Colliders are components specifically built to handle collisions. There are a few kinds but you'll often see Colliders whenever looking for intersects / overlaps with objects in real time.

Primitive Colliders: simple shapes and fast to compute (cubes, spheres, etc)

Other colliders: More complex shapes but slower to compute (can be the same shape as an object mesh)

RayCast

Like a line, but instead of two points is just an origin point, a direction, and a distance.

Can think of this like casting a fishing line or throwing a ball - if their arc was a completely straight line. You set where you cast from, what direction, and how far it will go.

Colliders + RayCast Diagram

Colliders + RayCast Diagram

Using a RayCast + Collider to find a random point within a mesh:

  • Find collision point:
    • RayCast out from the center of the object in a random direction
      • Return if there's a collision point
    • If no collision point, reverse the Ray and try again
      • By default, Colliders don't collide when hit from inside, so this is how we force a collision (if there would've been a collision)
  • Find a random point on line between center and collision point

This method assumes that center of the object is inside of the object. If not, you're gonna get some weird results.

Example class using Colliders and RayCasts in C# to find a random point inside a mesh:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;

public class ColliderRandomPositionProvider : IPositionProvider
{
    private Collider _collider;
    private GameObject _gameObject;

    private float RayLength = 1000.0f;

    public ColliderRandomPositionProvider(GameObject gameObject) {
        var collider = GetColliderFromGameObject(gameObject);

        this._gameObject = gameObject;
        this._collider = collider;
    }

    public Vector3 GetPosition() {

        Vector3? newPoint = null;
        var _ = TryGetRandomPointInCollider(this._collider, out newPoint);

        // Fallback to using bounds
        if(newPoint == null) {
            var meshPositionProvder = new MeshPositionProvider(this._gameObject);
            newPoint = meshPositionProvder.GetPosition();
        }

        return (Vector3)newPoint; 
    }

    // Pulled from https://forum.unity.com/threads/pick-random-point-inside-box-collider.541585/#post-6970754
    private bool TryGetRandomPointInCollider(Collider collider, out Vector3? point) {
        point = null;

        var randomDirection = GetRandomDirectionVector3();
        var random = new System.Random();
        var ray = new Ray(
            this._gameObject.transform.position,
            randomDirection);
        
        if(collider.Raycast(ray, out var hit, Mathf.Infinity)) {
            point = VectorUtilities.LerpByScalar(
                this._gameObject.transform.position,
                hit.point,
                (float)random.NextDouble());
            return true;
        }

        // If we didn't get a hit going outwards, try the opposite direction
        ray.origin = ray.GetPoint(RayLength);
        ray.direction = -ray.direction;

        if(collider.Raycast(ray, out var hitReverse, Mathf.Infinity)) {
            point = VectorUtilities.LerpByScalar(
                this._gameObject.transform.position,
                hitReverse.point,
                (float)random.NextDouble());
            return true;
        }

        return false;
    }

    private Collider GetColliderFromGameObject(GameObject gameObject) {
        var collider = gameObject.GetComponent<Collider>();
        Assert.IsNotNull(collider);

        return collider;
    }

    private Vector3 GetRandomDirectionVector3() {
        return Random.insideUnitSphere.normalized;
    }
}

VectorUtilities - used for finding a point between Point A and Point B:

using UnityEngine;

public static class VectorUtilities {

    /*
        Lerp between a and b. 
        
        LerpScalar scales distance from a on ab line.
    */
    public static Vector3 LerpByScalar(
        Vector3 start,
        Vector3 end,
        float distanceScalar
    ) {
        Vector3 result = distanceScalar * (end - start) + start;
        return result;
    }
}

There are still a few drawbacks with this approach and I haven't handled every edge case:

  • Doesn't handle when center of object outside of object
  • Doesn't handle multiple collisions

But this worked well for me and I'll figure those out when I get there =)

Conclusion

Shout out to all the Unity forum Q&As out there. It took me hours to get this solution working but it would've taken me much longer without them.

You can check out my finished monoliths on IG: @hamy.art

In Collisions and RayCasts,

-HAMY.OUT

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.