[Wiki/HOWTO] Oriented Bounding Box in Unity ... finally ? eek !

Found a solution if anyone is interested:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using MathGeoLib;

public class Intersection : MonoBehaviour
{
    public GameObject m_MyObject, m_NewObject;
    Collider m_Collider, m_Collider2;
    private List<Vector3> inboundsVertices = new List<Vector3>();
    private bool drawVertGizmos = false;

    [SerializeField]
    private OrientedBoundingBox obb;

    void Start()
    {
        //Check that the first GameObject exists in the Inspector and fetch the Collider
        if (m_MyObject != null)
            m_Collider = m_MyObject.GetComponent<Collider>();

        //Check that the second GameObject exists in the Inspector and fetch the Collider
        if (m_NewObject != null)
            m_Collider2 = m_NewObject.GetComponent<Collider>();
    }

    void Update()
    {
        float xAngle = m_NewObject.transform.eulerAngles.x;
        float yAngle = m_NewObject.transform.eulerAngles.y;
        float zAngle = m_NewObject.transform.eulerAngles.z;
        Quaternion rotation = Quaternion.Euler(xAngle, yAngle, zAngle);
        Matrix4x4 m = Matrix4x4.Rotate(rotation);

        Vector3 xRotation = new Vector3(m.m00, m.m10, m.m20);
        Vector3 yRotation = new Vector3(m.m01, m.m11, m.m21);
        Vector3 zRotation = new Vector3(m.m02, m.m12, m.m22);

        obb = new OrientedBoundingBox(m_Collider2.bounds.center, m_NewObject.transform.localScale / 2, xRotation, yRotation, zRotation);
        //If the first GameObject's Bounds enters the second GameObject's Bounds, output the message
        if (m_Collider.bounds.Intersects(m_Collider2.bounds))
        {
            CheckVertices(m_MyObject, obb);
            drawVertGizmos = true;
        }
    }

    void OnDrawGizmos()
    {
        if (drawVertGizmos)
        {
            DrawVertexGizmos();
            //DrawBoundingBox();
        }
    }

    void DrawBoundingBox()
    {
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireCube(m_NewObject.transform.position, m_Collider2.bounds.size);
    }

    void DrawVertexGizmos()
    {
        foreach (Vector3 vertex in inboundsVertices)
        {
            Gizmos.DrawSphere(vertex, 0.04f);
        }
    }

    void CheckVertices(GameObject obj, OrientedBoundingBox bounds)
    {
        inboundsVertices.Clear();
        if (obj == null)
            return;
        MeshFilter mf = obj.GetComponent<MeshFilter>();
        if (mf == null)
            return;
        Vector3[] verticesToCheck = obj.GetComponent<MeshFilter>().mesh.vertices;
        foreach (Vector3 vertex in verticesToCheck)
        {
            Vector3 pos = obj.transform.TransformPoint(vertex);
            if (bounds.Contains(pos))
            {
                inboundsVertices.Add(pos);
            }
        }
    }
}

Here is the result:

Does not work on Mac M1 (2020.3.1f1) because of DllNotFoundException

The error message sounds:
DllNotFoundException: MathGeoLib.Exports MathGeoLib.OrientedBoundingBox.Enclose (UnityEngine.Vector3 point) (at Assets/virtualPlayground/Boxes/Dim-Boxes/MathGeoLib.Exports.v0.1/MathGeoLib/OrientedBoundingBox.cs:209)

Is there a fix?

My guess is some of these lines at the beginning of the OrientedBoundingBox.cs need an update:

using System;
using System.Runtime.InteropServices;
using JetBrains.Annotations;

#if UNITY || UNITY_EDITOR
using Plane = UnityEngine.Plane;
using Vector3 = UnityEngine.Vector3;
#endif

#pragma warning disable IDE1006 // naming rules blah blah blah

// ReSharper disable once CheckNamespace
namespace MathGeoLib
{
    [PublicAPI]
    [Serializable]
    [StructLayout(LayoutKind.Sequential)]
    public sealed class OrientedBoundingBox
    {
        #region Native

        private static class NativeMethods
        {
#if UNITY || UNITY_EDITOR
            private const string DllName = "MathGeoLib.Exports";
#else
            private const string DllName = "MathGeoLib.Exports.dll";
#endif

            [DllImport(DllName)]
.............

It uses a native library, which the OP compiled for Windows. You need to compile that for Mac.

Thank you.
The problem is I am using it in AssetStore asset and I am using Windows myself.
This error has been reported by asset user, using this asset on Mac.
I guess I can’t recompile that for mac while do it being on Windows?

Maybe someone is also using this library on Mac and could share the mac plugin with me?

Thanks for sharing this, here’s an editor window script which uses the plugin to generate a cube using the properties of the bounding box, maybe someone will find this useful.

using UnityEngine;
using UnityEditor;
using MathGeoLib;

public class BoundingBoxCubeGenerator : EditorWindow
{
    private GameObject referenceObject;
    private GameObject generatedCube;

    [MenuItem("Tools/Bounding Box Cube Generator")]
    public static void ShowWindow()
    {
        GetWindow<BoundingBoxCubeGenerator>("Bounding Box Cube Generator");
    }

    void OnGUI()
    {
        GUILayout.Label("Generate Cube from Oriented Bounding Box", EditorStyles.boldLabel);
        referenceObject = (GameObject)EditorGUILayout.ObjectField("Reference Object", referenceObject, typeof(GameObject), true);

        if (GUILayout.Button("Generate Cube"))
        {
            GenerateOrientedBoundingBoxCube();
        }

        if (generatedCube != null)
        {
            EditorGUILayout.LabelField("Generated Cube:", generatedCube.name);
        }
    }

    private void GenerateOrientedBoundingBoxCube()
    {
        if (referenceObject != null)
        {
            MeshFilter meshFilter = referenceObject.GetComponent<MeshFilter>();
            if (meshFilter != null)
            {
                Vector3[] vertices = meshFilter.mesh.vertices;
                OrientedBoundingBox obb = OrientedBoundingBox.OptimalEnclosing(vertices);

                // Transform vertices to world space
                for (int i = 0; i < vertices.Length; i++)
                {
                    vertices[i] = referenceObject.transform.TransformPoint(vertices[i]);
                }

                // Recalculate OBB for transformed vertices
                obb = OrientedBoundingBox.OptimalEnclosing(vertices);

                if (generatedCube != null)
                {
                    DestroyImmediate(generatedCube);
                }

                generatedCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
                generatedCube.transform.position = obb.Center;
                generatedCube.transform.localScale = obb.Extent * 2; // Scale
                generatedCube.transform.rotation = Quaternion.LookRotation(obb.Axis1, obb.Axis2);
            }
            else
            {
                EditorUtility.DisplayDialog("Error", "The reference object does not have a MeshFilter component.", "OK");
            }
        }
        else
        {
            EditorUtility.DisplayDialog("Error", "Reference object is not set.", "OK");
        }
    }
}

I’m kinda missing the point of this, tbh. Can’t you just use mesh.bounds (which is expressed in the object’s local space) and transform it with the object’s rotation? I’ve done this in the past countless times to get oriented bbs and they work just fine for me.

Oh, you mean getting the minimum encasing oriented bounding box, right? What I’ve used to do this is find the mesh covariance matrix, then use this to extract the best rotation, it’s fairly fast (specially when compared to alternatives like singular value decomposition)

1 Like

I noticed that this script uses the wrong rotation on line 59; it should instead be:

generatedCube.transform.rotation = Quaternion.LookRotation(obb.Axis3, obb.Axis2);

Here is the complete correct script:

using UnityEngine;
using UnityEditor;
using MathGeoLib;

public class BoundingBoxCubeGenerator : EditorWindow
{
    private GameObject referenceObject;
    private GameObject generatedCube;

    [MenuItem("Tools/Bounding Box Cube Generator")]
    public static void ShowWindow()
    {
        GetWindow<BoundingBoxCubeGenerator>("Bounding Box Cube Generator");
    }

    void OnGUI()
    {
        GUILayout.Label("Generate Cube from Oriented Bounding Box", EditorStyles.boldLabel);
        referenceObject = (GameObject)EditorGUILayout.ObjectField("Reference Object", referenceObject, typeof(GameObject), true);

        if (GUILayout.Button("Generate Cube"))
        {
            GenerateOrientedBoundingBoxCube();
        }

        if (generatedCube != null)
        {
            EditorGUILayout.LabelField("Generated Cube:", generatedCube.name);
        }
    }

    private void GenerateOrientedBoundingBoxCube()
    {
        if (referenceObject != null)
        {
            MeshFilter meshFilter = referenceObject.GetComponent<MeshFilter>();
            if (meshFilter != null)
            {
                Vector3[] vertices = meshFilter.mesh.vertices;
                OrientedBoundingBox obb = OrientedBoundingBox.OptimalEnclosing(vertices);

                // Transform vertices to world space
                for (int i = 0; i < vertices.Length; i++)
                {
                    vertices[i] = referenceObject.transform.TransformPoint(vertices[i]);
                }

                // Recalculate OBB for transformed vertices
                obb = OrientedBoundingBox.OptimalEnclosing(vertices);

                if (generatedCube != null)
                {
                    DestroyImmediate(generatedCube);
                }

                generatedCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
                generatedCube.transform.position = obb.Center;
                generatedCube.transform.localScale = obb.Extent * 2; // Scale
                generatedCube.transform.rotation = Quaternion.LookRotation(obb.Axis3, obb.Axis2);
            }
            else
            {
                EditorUtility.DisplayDialog("Error", "The reference object does not have a MeshFilter component.", "OK");
            }
        }
        else
        {
            EditorUtility.DisplayDialog("Error", "Reference object is not set.", "OK");
        }
    }
}

Hi

Does this give a different result than the collider resulting from adding a box collider to the object ?

The box should have the same size I guess, but I haven’t checked.

1 Like

Here is also a small script for drawing a gizmo bounding box around the object.

public class BoundingGizmo : MonoBehaviour
{
    public bool show_bounds;
    private void OnDrawGizmosSelected()
    {
        if (!show_bounds) return;
        MeshRenderer mesh_renderer = GetComponent<MeshRenderer>();

        MeshFilter meshFilter = mesh_renderer.GetComponent<MeshFilter>();
        if (!meshFilter) return;

        Mesh mesh = meshFilter.mesh;
        if (!mesh) return;

        Vector3[] vertices = mesh.vertices;
        if (vertices.Length <= 0) return;

        for (var i = 0; i < vertices.Length; i++)
        {
            vertices[i] = transform.TransformPoint(vertices[i]);
        }
        OrientedBoundingBox obb = OrientedBoundingBox.OptimalEnclosing(vertices);

        Quaternion rotation = Quaternion.LookRotation(obb.Axis3, obb.Axis2);
        Matrix4x4 trans_mat = new Matrix4x4();
        trans_mat.SetTRS(obb.Center, rotation, new Vector3(1, 1, 1));

        Gizmos.matrix = trans_mat;
        Gizmos.DrawWireCube(Vector3.zero, obb.Extent * 2);
    }
}

Here’s my OBB in 2D if anyone’s interested

using System;
using UnityEngine;

// https://forum.unity.com/threads/get-getworldcorners-when-rect-transform-is-rotated.1607061/
public readonly struct OrientedRect {

  readonly Vector2 _c, _size, _polar;
  readonly float _rot;

  public Vector2 center => _c;
  public Vector2 size => _size;
  public float rotation => _rot;

  /// <summary> Constructs a new oriented rectangle. </summary>
  /// <param name="size"> Components must be non-negative. </param>
  /// <param name="rotation"> In radians. </param>
  public OrientedRect(Vector2 center, Vector2 size, float rotation = 0f) {
    if(size.x < 0f || size.y < 0f) throw new ArgumentException("Invalid size.", nameof(size));
    (_c, _size, _rot) = (center, size, rotation);
    _polar = polar(_rot);
  }

  /// <summary> Constructs a new oriented rectangle from existing Rect. </summary>
  /// <param name="rotation"> In radians. </param>
  public OrientedRect(Rect rect, float rotation = 0f) : this(rect.center, rect.size, rotation) {}

  public Rect GetRect() => new Rect(center - .5f * size, size);

  public bool Overlaps(OrientedRect other) => new OverlapSolver(this, other).OverlapDetected();

  public Vector2 GetVertex(int index) => index >= 0? applyRotation(cmul(unitVert(index), size), _polar) + center
                                                   : throw new ArgumentException("Index must be >= 0", nameof(index));

  class OverlapSolver {

    OrientedRect _a, _b;

    public OverlapSolver(OrientedRect a, OrientedRect b)
      => (_a, _b) = (a, b);

    OrientedRect pick(int index) => index == 0? _a : _b;

    // worst case: overlap detected = 4*2*4 = 32 point projections (2*4 vertices against 2*2 normal directions)
    // this is because the algorithm assumes there is an overlap until proven otherwise
    public bool OverlapDetected() {
      for(int i = 0; i < 4; i++) { // 4 = 2 rects * 2 normals
        var ri = i >> 1; // 0, 0, 1, 1
        var ni = i & 1;  // 0, 1, 0, 1

        // get edge points
        var p1 = pick(ri).GetVertex(ni);
        var p2 = pick(ri).GetVertex(ni+1);

        var edgeNormal = perp(p2 - p1).normalized;
        if(!intervalsOverlap(edgeNormal)) return false;
      }

      return true;
    }

    bool intervalsOverlap(Vector2 ld) { // ld: projection line direction
      extentsOf(0, ref ld, out var min1, out var max1);
      extentsOf(1, ref ld, out var min2, out var max2);
      return inRange(min2, min1, max1) || inRange(min1, min2, max2);

      static bool inRange(float v, float min, float max)
        => min <= v && v <= max;

      void extentsOf(int ri, ref Vector2 ld, out float min, out float max) {
        min = float.PositiveInfinity;
        max = float.NegativeInfinity;
        for(int vi = 0; vi < 4; vi++) { // 4 = four rect vertices
          var p = pick(ri).GetVertex(vi);
          var d = dot(p, ld);
          if(d < min) min = d;
          if(d > max) max = d;
        }
      }

    }

  }

  static Vector2 unitVert(int i) => new(.5f * spsq(i), .5f * spsq(i+1));
  static int spsq(int i) => (i & 3) < 2? -1 : 1; // signed pair sequence: -1,-1,+1,+1,-1,-1... (i >= 0)
  static float dot(Vector2 a, Vector2 b) => a.x * b.x + a.y * b.y;
  static Vector2 cmul(Vector2 a, Vector2 b) => new(a.x * b.x, a.y * b.y);
  static Vector2 polar(float rad) => new(MathF.Cos(rad), MathF.Sin(rad));
  static Vector2 perp(Vector2 v) => new(-v.y, v.x);
  static Vector2 applyRotation(Vector2 v, Vector2 trig)
    => new(v.x * trig.x - v.y * trig.y, v.x * trig.y + v.y * trig.x);

}

Visualized test

using System;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

[ExecuteInEditMode]
public class RectanglesTest : MonoBehaviour {

  [SerializeField] Vector2 _center1;
  [SerializeField] Vector2 _size1;
  [SerializeField] [Range(0f, 360f)] float _rotation1;
  [SerializeField] Vector2 _center2;
  [SerializeField] Vector2 _size2;
  [SerializeField] [Range(0f, 360f)] float _rotation2;
  [SerializeField] [Min(0.2f)] float _gizmoScale = 2f;

#if UNITY_EDITOR

  void OnDrawGizmos() {
    var or1 = new OrientedRect(_center1, _size1, _rotation1 * Mathf.Deg2Rad);
    var or2 = new OrientedRect(_center2, _size2, _rotation2 * Mathf.Deg2Rad);

    var overlap = or1.Overlaps(or2);

    drawOrientedRect(or1, overlap? Color.red : Color.cyan);
    drawOrientedRect(or2, overlap? Color.red : Color.magenta);
    drawPoint(or1.GetVertex(0), .08f, Color.white);
  }

  void OnValidate() {
    _size1 = lbound(_size1, 0f);
    _size2 = lbound(_size2, 0f);
  }

  void drawOrientedRect(OrientedRect or, Color? color = null, float thickness = 1f) {
    if(color.HasValue) Handles.color = color.Value;

    var last = Vector2.zero;

    for(int i = 0; i <= 4; i++) {
      var cur = or.GetVertex(i);
      if(i > 0) drawSeg(last, cur, null, thickness * _gizmoScale);
      last = cur;
    }
  }

  void drawSeg(Vector2 a, Vector2 b, Color? color = null, float thickness = 1f) {
    if(color.HasValue) Handles.color = color.Value;
    Handles.DrawLine(v3(a), v3(b), thickness * _gizmoScale);
  }

  void drawPoint(Vector2 p, float radius, Color? color = null) {
    if(color.HasValue) Handles.color = color.Value;
    Handles.DrawSolidDisc(v3(p), Vector3.back, radius * _gizmoScale);
  }

  static Vector3 v3(Vector2 v) => new Vector3(v.x, v.y, 0f);
  static Vector2 lbound(Vector2 v, float limit) => new Vector2(lbound(v.x, limit), lbound(v.y, limit));
  static float lbound(float n, float limit) => MathF.Max(n, limit);

#endif

}

Disclaimer: I don’t claim this to be the best implementation, feel free to modify it if you’d like to avoid garbage etc.

1 Like