一、Bounds(包围盒)概述

1.什么是包围盒?

包围盒算法是一种求解离散点集最优包围空间的方法。
基本思想是用体积稍大且特性简单的几何体(称为包围盒)来近似地代替复杂的几何对象。
最常见的包围盒算法有AABB包围盒(Axis-aligned bounding box),包围球(Sphere),方向包围盒OBB(Oriented bounding box),固定方向凸包FDH(Fixed directions hulls或k-DOP)。

2.包围盒的类型

2.1 AABB包围盒(Axis-aligned bounding box)

AABB是应用最早的包围盒。它被定义为包含该对象,且边平行于坐标轴的最小六面体。故描述一个AABB,仅需六个标量。AABB构造比较简单,存储空间小,但紧密性差,尤其对不规则几何形体,冗余空间很大,当对象旋转时,无法对其进行相应的旋转。处理对象是刚性并且是凸的,不适合包含软体变形的复杂的虚拟环境情况。AABB也是比较简单的一类包围盒。但对于沿斜对角方向放置的瘦长形对象,其紧密性较差。由于AABB相交测试的简单性及较好的紧密性,因此得到了广泛的应用,还可以用于软体对象的碰撞检测。

2.2 包围球(Sphere)

包围球被定义为包含该对象的最小的球体。确定包围球,首先需分别计算组成对象的基本几何元素集合中所有元素的顶点的x,y,z坐标的均值以确定包围球的球心,再由球心与三个最大值坐标所确定的点间的距离确定半径r。包围球的碰撞检测主要是比较两球间半径和与球心距离的大小。

2.3 OBB方向包围盒(Oriented bounding box)

OBB是较为常用的包围盒类型。它是包含该对象且相对于坐标轴方向任意的最小的长方体。OBB最大特点是它的方向的任意性,这使得它可以根据被包围对象的形状特点尽可能紧密的包围对象,但同时也使得它的相交测试变得复杂。OBB包围盒比AABB包围盒和包围球更加紧密地逼近物体,能比较显著地减少包围体的个数,从而避免了大量包围体之间的相交检测。但OBB之间的相交检测比AABB或包围球体之间的相交检测更费时。

2.4 FDH固定方向凸包(Fixed directions hulls或k-DOP)

FDH(k-DOP)是一种特殊的凸包,继承了AABB简单性的特点,但其要具备良好的空间紧密度,必须使用足够多的固定方向。被定义为包含该对象且它的所有面的法向量都取自一个固定的方向(k个向量)集合的凸包。FDH比其他包围体更紧密地包围原物体,创建的层次树也就有更少的节点,求交检测时就会减少更多的冗余计算,但相互间的求交运算较为复杂。

2.5 包围盒选择

任何实时三维交互式程序,如果没有碰撞检测,都是没有价值,甚至无法使用的。游戏中最常用的碰撞检测技术莫过于包围盒(bounding volume)碰撞检测。对于以60pfs运行的游戏来说,处理每一帧数据的时间只有0.0167s左右,对于不同的游戏,碰撞检测大概需要占10~30%的时间,也就是说,所有碰撞检测必须在0.002~0.005s内完成,非常巨大的挑战。
因此,任何包围盒都应该满足以下特性:

  1. 快速的碰撞检测;
  2. 能紧密覆盖所包围的对象;
  3. 包围盒应该非常容易计算;
  4. 能方便的旋转和变换坐标;
  5. 低内存占用。

最常见的包围盒有:Sphere,AABB,OBB等,外加一个比较特殊的frustum。Sphere能很好的满足1,3,4,5条,但通常包含了太多无用的空间,容易导致错误的碰撞结果。AABB应该是sphere与obb之间的解决方案,同时兼顾了效率和空间覆盖范围。OBB是三者中精度最高的,但检测代价也是最高的。
最终使用哪一种包围盒,是一个非常痛苦的过程,我们需要在效率和精度之间做出权衡取舍。前几天刚好完成了基本的碰撞检测函数,以下是我的一些测试数据,在一定程度上可以作为参考。纯C#代码实现,没有任何GPU加速,单线程在Q6600上运行。

  • AABB包围盒(Axis-aligned bounding box):100万次测试,1000次碰撞,耗时0.014s。
  • 包围球(Sphere):100万次测试,大约有16000次碰撞,耗时0.016s。
  • OBB方向包围盒(Oriented bounding box):使用传统的separate axis算法,100万次测试,30万次碰撞,耗时0.160s左右。对于没有碰撞的情况,几乎在前6条轴的检测中,就能结束检测,也就是说大约50万次(50%)测试都在检测第七条轴之前结束。
  • Vertical-agliened OBB:普通OBB的特殊版本,只能绕Y轴旋转。100w次测试,同样30万次碰撞,耗时0.08s,几乎比普通OBB快了一倍。
  • Frustum-AABB:使用<< Optimized View Frustum Culling Algorithms for Bounding Boxes >>中的算法,100w次测试,6万次碰撞,耗时0.096s。目前我计算n-vertex和p-vertex的方法是瓶颈,大约0.016s的时间花在计算这两个点上。 相比XNA中的BoundingFrustum.Intersects,同样的测试需要0.5s左右。

(以上均为对随机数据的测试,因此不同包围盒之间的实际碰撞次数并没有可比性,也不代表不同类型间的精度)
显然,AABB是性价比最高的,OBB虽然有较高精度,但相对其计算代价来说,并不划算,可以考虑用多个AABB来近似OBB,或者使用代价相对较低的Vertical-agliened OBB。Sphere看起来简单,但计算涉及到开方(虽然Math.Sqrt会直接编译为fsqrt指令),因此仍然没有AABB快(只需要6条逻辑比较指令)

二、Unity中的Bounds

1.Bounds结构体

unity api对Bounds的解释:

An axis-aligned bounding box, or AABB for short, is a box aligned with coordinate axes and fully enclosing some object. Because the box is never rotated with respect to the axes, it can be defined by just its center and extents, or alternatively by min and max points.

翻译

  • 轴对齐边框(简称AABB)是与坐标轴对齐并完全包围某个对象的框。 因为方框不会相对于坐标轴旋转,所以可以通过它的中心和范围来定义它,或者通过最小和最大点来定义它。

Unity用Bounds这个结构体struct来描述AABB包围盒,获取一个物体AABB包围盒的API有三种:RenderColliderMesh

  • Render:GetComponent<Renderer>().bounds—世界坐标
  • Collider:GetComponent<Collider>().bounds—世界坐标
  • Mesh:GetComponent<MeshFilter>().bounds—本地坐标

    把Mesh.bounds本地坐标换算成世界坐标bounds:

    var centerPoint = transform.TransformPoint(bounds.center);
    Bounds newBounds = new Bounds(centerPoint, bounds.size);
    

注意:不管是2D还是3D碰撞以及精灵和都是有bounds属性的。
总结:幸亏有了这个结构体,在实际开发中适当的使用包围盒会省去很多麻烦,做为结构体他和Vector3一样是不允许为空的。在一些属性中他是只读的。

1.1 Public Attribute(公告属性)
  • center:边界盒的中心(世界坐标);
  • extents:边界框的范围,总是size的一半;
  • max:(世界坐标)边界盒的最大点,这个值总是等于center+extents;
  • min:(世界坐标)边界盒的最小点,这个值总是等于center-extents;
  • size:边界盒的总大小。

我们不能直接修改Bounds结构体里头的centersize属性都不能直接设置。
我们通过画线的方式分别看一下各个参数在预制体上的位置。

Bounds bounds = this.GetComponent<Collider>().bounds;
Debug.DrawLine(bounds.center, bounds.center + bounds.extents, Color.red);


我们可以看到这个向量的长度是从中心点到右上角的长度。
由于extentssize的一半,所以我们这样画size

//得到左下角的位置
Vector3 p1 = bounds.center - bounds.extents;
Debug.DrawLine(p1, p1 + bounds.size, Color.green);


我们可以看到size的长度刚好是从左下角到右上角的长度。
然后我们分别画出最小值和最大值和中心点的连线。

Debug.DrawLine(bounds.center,  bounds.min, Color.gray);
Debug.DrawLine(bounds.center, bounds.max, Color.cyan);


由此可以看出最小值在左下角,最大值在右上角。

1.2 Public Functions(公告函数)
  • Encapsulate:重新计算最大最小点;
  • Contains:可判断点是否包含在边界框内(世界坐标)如我们需判断你是否点击了某个精灵则可以用Contains()
  • SetMinMax:设置边界盒的最小最大值;
  • SqrDistance:点和该边界盒之间的最小平方距离;
  • IntersectRay:射线与改边界盒相交吗?
  • Intersects:与另一个边界相交吗?比如我们需要判断两个精灵是否有重叠在一起则就可以使用Intersects()

三、旋转对Bounds的影响

我们对上面的小方块旋转:

旋转之后我们发现这个最小值最大值不再是小方块的左下角右上角
换言之他并不是和自身的坐标轴对齐
于是,我尝试着画出这个小方块的Bounds:

//后左下角
Vector3 backBottomLeft = bounds.min;
///后右下角
Vector3 backBottomRight = backBottomLeft + new Vector3(bounds.size.x, 0, 0);
///前左下角
Vector3 forwardBottomLeft = backBottomLeft + new Vector3(0, 0, bounds.size.z);
///前右下角
Vector3 forwardBottomRight = backBottomLeft + new Vector3(bounds.size.x, 0, bounds.size.z);
///后右上角
Vector3 backTopRight = backBottomLeft + new Vector3(bounds.size.x, bounds.size.y, 0);
///前左上角
Vector3 forwardTopLeft = backBottomLeft + new Vector3(0, bounds.size.y, bounds.size.z);
///后左上角
Vector3 backTopLeft = backBottomLeft + new Vector3(0, bounds.size.y, 0);
///前右上角
Vector3 forwardTopRight = bounds.max;

Debug.DrawLine(bounds.min, backBottomRight, Color.red);
Debug.DrawLine(backBottomRight, forwardBottomRight, Color.red);
Debug.DrawLine(forwardBottomRight, forwardBottomLeft, Color.red);
Debug.DrawLine(forwardBottomLeft, bounds.min, Color.red);

Debug.DrawLine(bounds.min, backTopLeft, Color.red);
Debug.DrawLine(backBottomRight, backTopRight, Color.red);
Debug.DrawLine(forwardBottomRight, bounds.max, Color.red);
Debug.DrawLine(forwardBottomLeft, forwardTopLeft, Color.red);

Debug.DrawLine(backTopRight, backTopLeft, Color.red);
Debug.DrawLine(backTopLeft, forwardTopLeft, Color.red);
Debug.DrawLine(forwardTopLeft, bounds.max, Color.red);
Debug.DrawLine(bounds.max, backTopRight, Color.red);

运行结果如下:
当小方块完全不做旋转时,本地坐标轴和世界坐标轴重合

旋转45度之后

注:红框是我们画出的小方块的Bounds,
我们发现小方块的Bounds没有随着小方块旋转,但是它仍然完全包裹着小方块
即是:它是与世界坐标轴对齐,完全包围的对象是它自身的预制体

四、Bounds和碰撞器Collider的区别

  • 碰撞器Collider的方框始终跟着模型旋转移动,缩放跟着模型的,只要模型不缩放它也不缩放。
    属于obb包围盒:他是有向的;检测精度较好
  • Bounds跟随模型移动,而不会跟模型着旋转,而是随着模型旋转而缩放变大变小,始终包裹模型。
    属于aabb包围盒:他是无向的;检测精度较差

五、相关方法

1.多物体Bounds(Encapsulate)

计算多物体Bounds,则要遍历所有子物体,然后调用Encapsulate方法来计算。

Bounds bounds;
Renderer[] renderers = model.GetComponentsInChildren<Renderer>();
for (int i = 0; i < renderers.Length; i++)
{
    bounds.Encapsulate(renderers[i].bounds);
}

2.计算包围盒的八个顶点

center = bounds.center;
ext = bounds.extents;
 
float deltaX = Mathf.Abs(ext.x);
float deltaY = Mathf.Abs(ext.y);
float deltaZ = Mathf.Abs(ext.z);
 
#region 获取AABB包围盒顶点
points = new Vector3[8];
points[0] = center + new Vector3(-deltaX, deltaY, -deltaZ);        // 上前左(相对于中心点)
points[1] = center + new Vector3(deltaX, deltaY, -deltaZ);         // 上前右
points[2] = center + new Vector3(deltaX, deltaY, deltaZ);          // 上后右
points[3] = center + new Vector3(-deltaX, deltaY, deltaZ);         // 上后左
 
points[4] = center + new Vector3(-deltaX, -deltaY, -deltaZ);       // 下前左
points[5] = center + new Vector3(deltaX, -deltaY, -deltaZ);        // 下前右
points[6] = center + new Vector3(deltaX, -deltaY, deltaZ);         // 下后右
points[7] = center + new Vector3(-deltaX, -deltaY, deltaZ);        // 下后左
#endregion

3.绘制bounds方框

/// <summary> 绘制Bounds方框 </summary>
/// <param name="bounds"></param>
/// <param name="color"></param>
/// <param name="offsetSize"></param>
/// <param name="duration"></param>
public static void DrawBoundBoxLine(Bounds bounds, Color color = default(Color), float offsetSize = 1f, float duration = 0.1f)
{
    //先计算出包围盒8个点
    Vector3[] points = new Vector3[8];
    var width_x = bounds.size.x * offsetSize;
    var hight_y = bounds.size.y * offsetSize;
    var length_z = bounds.size.z * offsetSize;

    var LeftBottomPoint = bounds.min;
    var rightUpPoint = bounds.max;
    var centerPoint = bounds.center;
    var topPoint = new Vector3(centerPoint.x, centerPoint.y + hight_y / 2, centerPoint.z);
    var bottomPoint = new Vector3(centerPoint.x, centerPoint.y - hight_y * 0.5f, centerPoint.z);

    points[0] = LeftBottomPoint + Vector3.right * width_x;
    points[1] = LeftBottomPoint + Vector3.up * hight_y;
    points[2] = LeftBottomPoint + Vector3.forward * length_z;

    points[3] = rightUpPoint - Vector3.right * width_x;
    points[4] = rightUpPoint - Vector3.up * hight_y;
    points[5] = rightUpPoint - Vector3.forward * length_z;

    points[6] = LeftBottomPoint;
    points[7] = rightUpPoint;

    Debug.DrawLine(LeftBottomPoint, points[0], color, duration);
    Debug.DrawLine(LeftBottomPoint, points[1], color, duration);
    Debug.DrawLine(LeftBottomPoint, points[2], color, duration);

    Debug.DrawLine(rightUpPoint, points[3], color, duration);
    Debug.DrawLine(rightUpPoint, points[4], color, duration);
    Debug.DrawLine(rightUpPoint, points[5], color, duration);

    Debug.DrawLine(points[1], points[3], color, duration);
    Debug.DrawLine(points[2], points[4], color, duration);
    Debug.DrawLine(points[0], points[5], color, duration);

    Debug.DrawLine(points[2], points[3], color, duration);
    Debug.DrawLine(points[0], points[4], color, duration);
    Debug.DrawLine(points[1], points[5], color, duration);
}

4.绘制碰撞器方框

4.1方法一
/// <summary> 绘制boxCollider的绿色方框 </summary>
/// <param name="color"></param>
void DrawGizmosOnRunTime(Color color)
{
    var boxCollider = GetComponent<BoxCollider>();
    Gizmos.color = color;
    Matrix4x4 rotationMatrix = Matrix4x4.TRS(boxCollider.transform.position, boxCollider.transform.rotation, boxCollider.transform.lossyScale);
    Gizmos.matrix = rotationMatrix;
    Gizmos.DrawWireCube(boxCollider.center, boxCollider.size);
}
void OnDrawGizmos()
{
    DrawGizmosOnRunTime(Color.red);
}
4.2方法二
/// <summary> 绘制boxCollider的绿色方框 </summary>
/// <param name="boxCollider"></param>
/// <param name="color"></param>
/// <param name="offsetSize"></param>
public static void DrawOnGameViewRuntime(BoxCollider boxCollider, Color color = default(Color), float offsetSize = 1f)
{  
    float width = 0.1f;
    Vector3 rightDir = boxCollider.transform.right.normalized;
    Vector3 forwardDir = boxCollider.transform.forward.normalized;
    Vector3 upDir = boxCollider.transform.up.normalized;
    Vector3 center = boxCollider.transform.position + boxCollider.center;
    Vector3 size = boxCollider.size * offsetSize;
    size.x *= boxCollider.transform.lossyScale.x;
    size.y *= boxCollider.transform.lossyScale.y;
    size.z *= boxCollider.transform.lossyScale.z;

    Debug.DrawLine(center + upDir * size.y / 2f + rightDir * size.x / 2f + forwardDir * size.z / 2f, center + upDir * size.y / 2f - rightDir * size.x / 2f + forwardDir * size.z / 2f, color);
    Debug.DrawLine(center - upDir * size.y / 2f + rightDir * size.x / 2f + forwardDir * size.z / 2f, center - upDir * size.y / 2f - rightDir * size.x / 2f + forwardDir * size.z / 2f, color);
    Debug.DrawLine(center + upDir * size.y / 2f + rightDir * size.x / 2f + forwardDir * size.z / 2f, center - upDir * size.y / 2f + rightDir * size.x / 2f + forwardDir * size.z / 2f, color);
    Debug.DrawLine(center + upDir * size.y / 2f - rightDir * size.x / 2f + forwardDir * size.z / 2f, center - upDir * size.y / 2f - rightDir * size.x / 2f + forwardDir * size.z / 2f, color);
    Debug.DrawLine(center + upDir * size.y / 2f + rightDir * size.x / 2f - forwardDir * size.z / 2f, center + upDir * size.y / 2f - rightDir * size.x / 2f - forwardDir * size.z / 2f, color);
    Debug.DrawLine(center - upDir * size.y / 2f + rightDir * size.x / 2f - forwardDir * size.z / 2f, center - upDir * size.y / 2f - rightDir * size.x / 2f - forwardDir * size.z / 2f, color);
    Debug.DrawLine(center + upDir * size.y / 2f + rightDir * size.x / 2f - forwardDir * size.z / 2f, center - upDir * size.y / 2f + rightDir * size.x / 2f - forwardDir * size.z / 2f, color);
    Debug.DrawLine(center + upDir * size.y / 2f - rightDir * size.x / 2f - forwardDir * size.z / 2f, center - upDir * size.y / 2f - rightDir * size.x / 2f - forwardDir * size.z / 2f, color);
    Debug.DrawLine(center + upDir * size.y / 2f + rightDir * size.x / 2f + forwardDir * size.z / 2f, center + upDir * size.y / 2f + rightDir * size.x / 2f - forwardDir * size.z / 2f, color);
    Debug.DrawLine(center - upDir * size.y / 2f + rightDir * size.x / 2f + forwardDir * size.z / 2f, center - upDir * size.y / 2f + rightDir * size.x / 2f - forwardDir * size.z / 2f, color);
    Debug.DrawLine(center + upDir * size.y / 2f - rightDir * size.x / 2f + forwardDir * size.z / 2f, center + upDir * size.y / 2f - rightDir * size.x / 2f - forwardDir * size.z / 2f, color);
    Debug.DrawLine(center - upDir * size.y / 2f - rightDir * size.x / 2f + forwardDir * size.z / 2f, center - upDir * size.y / 2f - rightDir * size.x / 2f - forwardDir * size.z / 2f, color);
}

5.求两个包围盒之间的距离

// Distance between two ClosestPointOnBounds
// this is needed in cases where entites are really big. in those cases,
// we can't just move to entity.transform.position, because it will be
// unreachable. instead we have to go the closest point on the boundary.
//
// Vector3.Distance(a.transform.position, b.transform.position):
//    _____        _____
//   |     |      |     |
//   |  x==|======|==x  |
//   |_____|      |_____|
//
//
// Utils.ClosestDistance(a.collider, b.collider):
//    _____        _____
//   |     |      |     |
//   |     |x====x|     |
//   |_____|      |_____|
//
public static float ClosestDistance(Collider a, Collider b)
{
    return Vector3.Distance(a.ClosestPointOnBounds(b.transform.position),
                            b.ClosestPointOnBounds(a.transform.position));
}

6.计算所有包围盒的中心点

计算出多个Bounds的中心点。

[MenuItem ("MyMenu/Do Test")]
static void Test () 
{
	Transform parent = Selection.activeGameObject.transform;
	Vector3 postion = parent.position;
	Quaternion rotation = parent.rotation;
	Vector3 scale = parent.localScale;
	parent.position = Vector3.zero;
	parent.rotation = Quaternion.Euler(Vector3.zero);
	parent.localScale = Vector3.one;

	Vector3 center = Vector3.zero;
	Renderer[] renders = parent.GetComponentsInChildren<Renderer>();
	foreach (Renderer child in renders)
	{
		center += child.bounds.center;   
	}
	center /= parent.GetComponentsInChildren<Transform>().Length; 
	Bounds bounds = new Bounds(center,Vector3.zero);
	foreach (Renderer child in renders)
	{
		bounds.Encapsulate(child.bounds);   
	}

	parent.position = postion;
	parent.rotation = rotation;
	parent.localScale = scale;

	foreach(Transform t in parent)
	{
		t.position = t.position -  bounds.center;
	}
	parent.transform.position = bounds.center + parent.position;
}

参考连接

  1. https://blog.csdn.net/SmillCool/article/details/126708371
  2. https://blog.csdn.net/weixin_42977419/article/details/100045078
  3. https://blog.csdn.net/cscscsliqi/article/details/80405059
  4. https://blog.csdn.net/Windgs_YF/article/details/87884884
  5. https://blog.csdn.net/u013628121/article/details/128117992
  6. https://blog.csdn.net/sinat_25415095/article/details/104588989
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐