用AABBTree加速碰撞检测

AABBTree加速碰撞检测的原理?

减少碰撞检测的执行次数。怎么减少呢?排除那些不可能发生碰撞的形状间的检测。

最简单粗暴的碰撞检测就是两个for循环嵌套

void ForLoopCheckIntersect()
{
    for (int i = 0; i < m_RectList.Count; ++i)
    {
        var shape = m_ShapeList[i];
        for (int j = i + 1; j < m_RectList.Count; ++j)
        {
            var shape_2 = m_ShapeList[j];
            if (shape == shape_2) 
                continue;
            if (!shape.GetBounds().Overlaps(shape_2.GetBounds())) //包围盒粗检测
                continue;
            if (Shape2DHelper.IsShapeIntersect(verts, verts_2)) //细检测
                Debug.Log($"Intersect");
        }
    }
}

但好多形状根本不可能发生碰撞,也再参与检测,明显是执行了很多无效的检测。

 

AABBTree如何排除那些不可能发生的碰撞?

1) 使用2叉树结构。

2) 使用了层次包围盒(BVH)的组织方式,就是形状只保存在叶子节点上,叶子节点的父节点用包围盒包住2个叶子节点,祖父节点再用包围盒包住其子节点,以此类推。

3) 跟二叉查找树有点类似, AABB树的也通过查找规则确定是走left-child还是right-child,每次都可以减少一半查找量。

 

效果

同样的耗时50ms

1) 暴力检测的话可以支持250个OBBRect参与,执行了3112次检测

2) 用AABBTree支持890个OBBRect参与,执行了7834次检测

 

using UnityEngine;

public partial class MyAABBTree
{
    public class Node
    {
        private Node m_Parent;
        private Node m_Left;
        private Node m_Right;

        public Rect m_Bounds; //节点包围盒: 有Shape等于Shape的包围盒大小, 没Shape等于包住两个子节点的包围盒的大小
        public IShape m_Shape; //叶子节点才有Shape

        public Node() { }

        public void RemoveFromParent()
        {
            if (null != m_Parent)
            {
                if (m_Parent.IsLeft(this))
                    m_Parent.Left = null;
                else
                    m_Parent.Right = null;

                m_Parent = null;
            }
        }

        public bool IsLeft(Node n)
        {
            return (null != m_Left && n == m_Left);
        }

        public Node Parent
        {
            get { return m_Parent; }
        }

        public Rect GetBounds() { return m_Bounds; }

        public Node Left
        {
            get { return m_Left; }
            set
            {
                if (null == value)
                {
                    if (null != m_Left)
                    {
                        m_Left.m_Parent = null;
                        m_Left = null;
                    }
                }
                else
                {
                    m_Left = value;
                    value.m_Parent = this;
                }
            }
        }

        public Node Right
        {
            get { return m_Right; }
            set
            {
                if (null == value)
                {
                    if (null != m_Right)
                    {
                        m_Right.m_Parent = null;
                        m_Right = null;
                    }
                }
                else
                {
                    m_Right = value;
                    value.m_Parent = this;
                }
            }
        }

        public bool IsLeaf()
        {
            return (null == m_Left) && (null == m_Right);
        }
    }

}

 

using System.Collections.Generic;
using UnityEngine;

public partial class MyAABBTree
{

    public interface IShape
    {
        Rect GetBounds();
        string GetShapeName();
    }

    private Node m_Root;
    private Dictionary<IShape, Node> m_NodeDict = new Dictionary<IShape, Node>();

    public Node Root
    {
        get { return m_Root; }
    }

    //添加
    public void AddShape(IShape shape)
    {
        var shapeBounds = shape.GetBounds();

        //如果树是空的, 直接创建根节点
        if (null == m_Root)
        {
            m_Root = CreateNode(shape, ref shapeBounds);
            return;
        }

        Node node = m_Root;
        while (!node.IsLeaf())
        {
            var isLeft = IsLeftChild(node.Left.GetBounds(), node.Right.GetBounds(), shapeBounds);
            node = isLeft ? node.Left : node.Right;
        }

        //将原来的变成父节点(包围盒节点), 原来的shape新建left-child, 新的shape新建right-child
        var leftChild = CreateNode(node.m_Shape, ref node.m_Bounds);
        node.Left = leftChild;
        node.m_Shape = null;

        var rightChild = CreateNode(shape, ref shapeBounds);
        node.Right = rightChild;

        UpdateAncestorBounds(node);
    }

    private Node CreateNode(IShape shape, ref Rect bounds)
    {
        var node = new Node();
        m_NodeDict[shape] = node;
        node.m_Shape = shape;
        node.m_Bounds = bounds;
        return node;
    }

    //根据查找规则确定是否是用left-child
    private bool IsLeftChild(Rect left, Rect right, Rect cur)
    {
        var mergeRect1 = MergeRect(ref left, ref cur);
        float area1 = mergeRect1.width * mergeRect1.height + right.width * right.height;

        var mergeRect2 = MergeRect(ref right, ref cur);
        float area2 = mergeRect2.width * mergeRect2.height + left.width * left.height;

        if (area1 < area2)
            return true;
        if (area1 > area2)
            return false;

        //面积相同时: 选中心点距离近的
        var toLeft = cur.center - left.center;
        var toRight = cur.center - right.center;
        return toLeft.sqrMagnitude < toRight.sqrMagnitude;
    }

    public static Rect MergeRect(ref Rect a, ref Rect b)
    {
        float xMin = Mathf.Min(a.xMin, b.xMin);
        float yMin = Mathf.Min(a.yMin, b.yMin);
        float xMax = Mathf.Max(a.xMax, b.xMax);
        float yMax = Mathf.Max(a.yMax, b.yMax);
        var result = new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
        return result;
    }

    //更新所有祖先节点的包围盒
    private void UpdateAncestorBounds(Node node)
    {
        while (null != node)
        {
            var leftBounds = node.Left.GetBounds();
            var rightBounds = node.Right.GetBounds();
            node.m_Bounds = MergeRect(ref leftBounds, ref rightBounds);
            node = node.Parent;
        }
    }

    //删除
    public bool RemoveShape(IShape shape)
    {
        if (!m_NodeDict.TryGetValue(shape, out var node))
            return false;
        m_NodeDict.Remove(shape);

        if (m_Root == node)
        {
            m_Root = null;
            return true;
        }

        var parent = node.Parent;
        var sibling = parent.IsLeft(node) ? parent.Right : parent.Left;
        if (parent == m_Root)
        {
            m_Root = sibling;
        }
        else //删掉一个Shape节点后, 用兄弟节点替换掉父节点
        {
            parent.Left = null;
            parent.Right = null;
            parent.m_Shape = sibling.m_Shape;
            parent.m_Bounds = sibling.m_Bounds;

            UpdateAncestorBounds(parent.Parent);
        }

        return true;
    }

    //更新
    public bool UpdateShape(IShape shape)
    {
        if (!m_NodeDict.TryGetValue(shape, out var node))
            return false;

        if (RectContains(node.m_Bounds, shape.GetBounds())) //还在原来的包围盒范围内
            return false;

        RemoveShape(shape);
        AddShape(shape);
        return true;
    }

    private static bool RectContains(Rect a, Rect b)
    {
        return !(b.xMin < a.xMin || b.xMax > a.xMax || b.yMin < a.yMin || b.yMax > a.yMax);
    }

    //查找: 返回与指定区域相交的所有形状
    public List<IShape> Query(Rect bounds)
    {
        if (null == m_Root)
            return null;

        var resultList = new List<IShape>();
        var stack = new Stack<Node>();
        stack.Push(m_Root);
        do
        {
            var node = stack.Pop();
            if (!node.m_Bounds.Overlaps(bounds))
                continue;

            if (node.IsLeaf())
            {
                resultList.Add(node.m_Shape);
                continue;
            }

            stack.Push(node.Left);
            stack.Push(node.Right);
        } while (stack.Count > 0);

        return resultList;
    }

}

 

还需要优化的地方:

1) 不是平衡树,频繁添加、删除后可能退化成链表,可以考虑改成用红黑树来优化。

 

测试代码

using UnityEngine;

public class OBBRect : MonoBehaviour, MyAABBTree.IShape
{

    public float m_Width = 0.2f;
    public float m_Height = 0.1f;

    private Vector2[] m_Verts = new Vector2[4];

    public static Rect GetBounds(Vector2[] verts)
    {
        float xMin = float.MaxValue;
        float yMin = float.MaxValue;

        float xMax = float.MinValue;
        float yMax = float.MinValue;

        for (int i = 0; i < 4; ++i)
        {
            var vert = verts[i];
            xMin = Mathf.Min(xMin, vert.x);
            yMin = Mathf.Min(yMin, vert.y);

            xMax = Mathf.Max(xMax, vert.x);
            yMax = Mathf.Max(yMax, vert.y);
        }

        return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
    }

    public Rect GetBounds()
    {
        var verts = GetVerts();
        return GetBounds(verts);
    }

    public Vector2[] GetVerts()
    {
        var localMin = new Vector2(-m_Width * 0.5f, -m_Height * 0.5f);
        var localMax = new Vector2(m_Width * 0.5f, m_Height * 0.5f);
        var localLeftTop = new Vector2(localMin.x, localMax.y);
        var localRightBottom = new Vector2(localMax.x, localMin.y);

        m_Verts[0] = this.transform.TransformPoint(localMin); //min
        m_Verts[1] = this.transform.TransformPoint(localLeftTop); //left-top
        m_Verts[2] = this.transform.TransformPoint(localMax); //max
        m_Verts[3] = this.transform.TransformPoint(localRightBottom); //right-bottom

        return m_Verts;
    }

    public string GetShapeName()
    {
        return "Rect";
    }

#if UNITY_EDITOR
    public Color m_GizmosColor = Color.white;

    private void OnDrawGizmos()
    {
        if (m_Width <= 0 || m_Height <= 0)
            return;

        var center = transform.position;

        Gizmos.color = m_GizmosColor;

        var verts = GetVerts();
        for (int i = 0; i < 4; ++i)
        {
            Gizmos.DrawLine(verts[i], verts[(i+1)%4]);
        }

        Gizmos.color = Color.white;
    }
#endif

}

 

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

public class MyAABBTreeTest : MonoBehaviour
{
    public Transform m_ShapesRoot;
    public GameObject m_RectTemplate;

    [Range(0, 9)]
    public int m_ApiType = 1;

    public bool m_Benchmark = false; //测试多少个shape参与碰撞检测时, 执行耗时超过50ms
    [Range(0, 999)]
    public int m_TestShapeCount = 1;

    public int m_CostTime = 0;
    public int m_CheckCount = 0;

    private List<OBBRect> m_RectList = new List<OBBRect>();

    private MyAABBTree m_Tree;

    void Start()
    {
        m_Tree = new MyAABBTree();
        for (int i = 0; i < m_ShapesRoot.childCount; ++i)
        {
            var shape = m_ShapesRoot.GetChild(i).GetComponent<OBBRect>();
            if (null == shape) continue;
            m_RectList.Add(shape);
            m_Tree.AddShape(shape);
        }
    }

    void Update()
    {
        for (int i = 0; i < 10; ++i)
        {
            if (m_RectList.Count >= m_TestShapeCount) break;
            CreateRect();
        }

        switch (m_ApiType)
        {
        case 1:
            ForLoopCheckIntersect();
            break;

        case 2:
            AABBTreeCheckIntersect();
            break;

        }

        CheckTimeCost();
    }

    void ForLoopCheckIntersect()
    {
        for (int i = 0; i < m_RectList.Count; ++i)
        {
            var shape = m_RectList[i];
            shape.m_GizmosColor = Color.white;
        }

        var t = DateTime.Now;
        m_CheckCount = 0;
        for (int i = 0; i < m_RectList.Count; ++i)
        {
            var shape = m_RectList[i];

            for (int j = i + 1; j < m_RectList.Count; ++j)
            {
                var shape_2 = m_RectList[j];
                if (null == shape_2 || shape == shape_2) continue;
                m_CheckCount++;

                var verts = shape.GetVerts();
                var verts_2 = shape_2.GetVerts();
                if (!OBBRect.GetBounds(verts).Overlaps(OBBRect.GetBounds(verts_2))) //包围盒粗检测
                    continue;

                if (Shape2DHelper.IsPolygonIntersect(verts, verts_2))
                {
                    shape.m_GizmosColor = Color.red;
                    shape_2.m_GizmosColor = Color.red;
                }
            }
        }
        m_CostTime = (int)(DateTime.Now - t).TotalMilliseconds;
    }

    void AABBTreeCheckIntersect()
    {
        for (int i = 0; i < m_RectList.Count; ++i)
        {
            var shape = m_RectList[i];
            shape.m_GizmosColor = Color.white;
            m_Tree.UpdateShape(shape);
        }

        var t = DateTime.Now;
        m_CheckCount = 0;
        for (int i = 0; i < m_RectList.Count; ++i)
        {
            var shape = m_RectList[i];

            var intersectList = m_Tree.Query(shape.GetBounds());
            if (null != intersectList)
            {
                for (int j = 0; j < intersectList.Count; ++j)
                {
                    var shape_2 = intersectList[j] as OBBRect;
                    if (null == shape_2 || shape == shape_2) continue;
                    m_CheckCount++;

                    if (Shape2DHelper.IsPolygonIntersect(shape.GetVerts(), shape_2.GetVerts()))
                    {
                        shape.m_GizmosColor = Color.red;
                        shape_2.m_GizmosColor = Color.red;
                    }
                }
            }
        }
        m_CostTime = (int)(DateTime.Now - t).TotalMilliseconds;
    }

    protected void CheckTimeCost()
    {
        if (m_CostTime >= 100) //防止卡住
        {
            for (int i = 1; i <= 10; ++i)
            {
                var childIndex = m_ShapesRoot.childCount - i;
                if (childIndex < 0) break;
                m_ShapesRoot.GetChild(childIndex).gameObject.SetActive(false);
            }
        }

        if (m_Benchmark)
        {
            if (m_CostTime >= 50)
            {
                Debug.Log($"type:{m_ApiType}, shapeCount:{m_RectList.Count}, totalMs:{m_CostTime}, checkCount:{m_CheckCount}");
                if (m_ApiType >= 2)
                {
                    m_ApiType = 1;
                    m_Benchmark = false;
                }
                else
                    m_ApiType++;
            }
            else
            {
                if (m_RectList.Count >= m_TestShapeCount)
                {
                    m_TestShapeCount++;
                }
            }
        }
    }

    private void CreateRect()
    {
        var go = GameObject.Instantiate(m_RectTemplate, m_ShapesRoot, false);
        go.name = $"Rect ({m_RectList.Count})";
        var trans = go.transform;
        trans.position = new Vector2(UnityEngine.Random.Range(-5, 5), UnityEngine.Random.Range(-5, 5));
        trans.localEulerAngles = new Vector3(0, 0, UnityEngine.Random.Range(-30, 30));

        var shape = go.GetComponent<OBBRect>();
        shape.m_Width = UnityEngine.Random.Range(0.1f, 0.5f);
        shape.m_Height = shape.m_Width * UnityEngine.Random.Range(0.6f, 0.8f);
        m_RectList.Add(shape);
        m_Tree.AddShape(shape);
    }

#if UNITY_EDITOR
    public Color[] m_NodeDepthColors = new Color[] { new Color(0, 1, 0, 0.5f) };
    public Color m_LeafColor = new Color(0, 0, 1, 0.5f);

    private Stack<MyAABBTree.Node> m_DrawStack = new Stack<MyAABBTree.Node>();
    private void OnDrawGizmos()
    {
        switch (m_ApiType)
        {
        case 1:
            DrawGizmos_ForLoop();
            break;

        case 2:
            DrawGizmos_AABBTree();
            break;
        }
        
    }

    private void DrawGizmos_AABBTree()
    {
        if (null != m_Tree && null != m_Tree.Root)
        {
            Gizmos.color = Color.green;

            int nodeNum = 1;
            var node = m_Tree.Root;
            m_DrawStack.Push(node);
            DrawNodeBounds(node, nodeNum);
            do
            {
                node = m_DrawStack.Pop();

                var left = node.Left;
                if (null != left)
                {
                    nodeNum++;
                    DrawNodeBounds(left, nodeNum);
                    m_DrawStack.Push(left);
                }

                var right = node.Right;
                if (null != right)
                {
                    nodeNum++;
                    DrawNodeBounds(right, nodeNum);
                    m_DrawStack.Push(right);
                }
            } while (m_DrawStack.Count > 0);

            Gizmos.color = Color.white;
        }
    }

    private void DrawNodeBounds(MyAABBTree.Node node, int nodeNum)
    {
        if (node.IsLeaf())
            Gizmos.color = m_LeafColor;
        else
        {
            int depth = (int)Mathf.Log(nodeNum, 2);
            Gizmos.color = m_NodeDepthColors[depth % m_NodeDepthColors.Length];
        }

        var bounds = node.m_Bounds;
        var min = bounds.min;
        var max = bounds.max;
        var leftTop = new Vector2(min.x, max.y);
        var rightBottom = new Vector2(max.x, min.y);
        Gizmos.DrawLine(min, leftTop);
        Gizmos.DrawLine(leftTop, max);
        Gizmos.DrawLine(max, rightBottom);
        Gizmos.DrawLine(rightBottom, min);
    }

    private void DrawGizmos_ForLoop()
    {
        Gizmos.color = m_LeafColor;
        for (int i = 0; i < m_RectList.Count; ++i)
        {
            var rect = m_RectList[i];
            var bounds = rect.GetBounds();
            var min = bounds.min;
            var max = bounds.max;
            var leftTop = new Vector2(min.x, max.y);
            var rightBottom = new Vector2(max.x, min.y);

            Gizmos.DrawLine(min, leftTop);
            Gizmos.DrawLine(leftTop, max);
            Gizmos.DrawLine(max, rightBottom);
            Gizmos.DrawLine(rightBottom, min);
        }
        Gizmos.color = Color.white;
    }

#endif

}

 

参考

物理引擎学习08-AABB树_游蓝海的博客-CSDN博客

 

posted @ 2023-11-28 00:04  yanghui01  阅读(127)  评论(0编辑  收藏  举报