这两天看了这两篇文章(原文链接见文末),很好奇他是怎么绘制包围盒出来的,看了博主的源码,发现有点问题不能用,自己做了修改,文末会给出整个项目的链接。原理其实不难,具体看源码,有不懂的地方可以参考后面的解释。
先来看看效果。
项目中还添加了给物体包围盒添加同样大小碰撞器的编辑器脚本,如图。
Bounds(包围盒)概述与AABB包围盒应用
Untiy网格编程篇(一)包围盒效果
源码工程
顺带推荐下自己的博客。
--------------------------------------------------------------------------------分割线--------------------------------------------------------------------------------
接着上周末梳理下程序逻辑。
Unity用Bounds这个结构体来描述AABB包围盒,获取一个物体AABB包围盒的核心API是Rederer组件的bounds属性。
// 获取一个物体的AABB包围盒大小
Bounds bounds = gameObejct.GetComponentsInChildren<Renderer>().bounds;
AABB包围盒实质是个长方体,如下图的红色线部分。Bounds用center和extens属性来描述这个长方体。
// 包围盒的中心
Vector3 center = bounds.center;
// 包围盒的extens
Vector3 ext = bounds.extens;
// 还有个size属性,其等于extens的两倍 size = 2 * ext
vector3 size = bounds.size;
如果一个物体包含多个子物体,则要遍历所有子物体,然后调用Encapsulate方法来计算Bounds。
Bounds bounds;
Renderer[] renderers = model.GetComponentsInChildren<Renderer>();
for (int i = 0; i < renderers.Length; i++)
{
bounds.Encapsulate(renderers[i].bounds);
}
我们已经知道extens,那么包围盒的中心到各个平面的距离就已知了,然后计算八个顶点的坐标就是很简单的事情。这个一看图和代码就明白。
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
// 点point是否在这个包围盒内部
public bool Contains(Vector3 point);
// bounds会自动扩充大小(改变center和extens),来包含这个point
public void Encapsulate(Vector3 point);
// bounds会自动扩充大小(改变center和extens),把原本的bounds和传入的bounds都包含进来
public void Encapsulate(Bounds bounds);
// 这条射线是否与这个包围盒相交
public bool IntersectRay(Ray ray);
这里给大家出个思考问题,如果不用Bounds的现成方法,但我们知道包围盒的八个顶点,那么该如何判断一点是否在这个长方体内部?或者大家可以先考虑如果已知长方形的四个顶点的坐标,如何判断一个点是否在长方形内部。
(我在这儿给大家提供两种思路,一个是计算该点到各个面的距离然后比对是否超过了长方体的长宽高;二是计算点积,一个点在长方体内部的话,其与各个顶点的连线与长方体各条边的夹角都是小于90°的,这种情况所有点积都是大于0的,若点在外部,则肯定有大于90°的,则点积小于0)
包围盒效果实质是在8个顶点处创建如下图的一个几何体,这几何体由三个相同大小的长方体组成。
所以问题转换为如何根据一个顶点坐标去构建长方体。画了张图,帮助大家理解(找了半天,没找到合适的画图软件,无奈只能手画了,大家简单看看就好)。
注意看坐标轴,这里ABCD-EFGH构成的长方体的方向(从O点出发,指向ABCD平面)是朝向X轴的负方向的。
所以对应ABCD-EFGH的坐标点计算如下。对应的方法是GetCubeVertexByPoint()。
float halfWidth = m_LineWidth * 0.5f; // OO1 OO2 OO3等
float deltaLength = m_LineLength - halfWidth; // DO`
// case DIRECTION.X_NEGATIVE: // 指向X轴负方向
vertex[0] = initPos + new Vector3(-deltaLength, halfWidth, -halfWidth); // 点A
vertex[1] = initPos + new Vector3(-deltaLength, halfWidth, halfWidth); // 点B
vertex[2] = initPos + new Vector3(-deltaLength, -halfWidth, halfWidth); // 点C
vertex[3] = initPos + new Vector3(-deltaLength, -halfWidth, -halfWidth); // 点D
vertex[4] = initPos + new Vector3(halfWidth, halfWidth, -halfWidth); // 点E
vertex[5] = initPos + new Vector3(halfWidth, halfWidth, halfWidth); // 点F
vertex[6] = initPos + new Vector3(halfWidth, -halfWidth, halfWidth); // 点G
vertex[7] = initPos + new Vector3(halfWidth, -halfWidth, -halfWidth); // 点H
单个长方体的顶点求出来了,然后求出几何体的所有顶点,并添加至集合。对应的方法是AddVertex()。
vertex.AddRange(GetCubeVertexByPoint(boundsPoint[5], DIRECTION.Z_POSITIVE));
vertex.AddRange(GetCubeVertexByPoint(boundsPoint[5], DIRECTION.Y_POSITIVE));
vertex.AddRange(GetCubeVertexByPoint(boundsPoint[5], DIRECTION.X_NEGATIVE));
8个几何体的顶点全都求到之后,就需要根据顶点构建网格。
一共有多少个网格呢?一个长方体有6个面,一个面2个网格,共12个网格。则一个几何体会有36个网格,完整的包围框则有36*8个网格。
构建网格时需要注意的是,Unity中,默认顶点要逆时针排列才是可见的。顺时针是不可见的。
如ABCD这个面,它需要两个网格,而且顶点必须是逆时针排列。代码里面是选择的102和032的顺序。(当然你也可以把BD当作对角线,然后两个网格设置为031、132。)
triangles[baseTrianglesIndex + 0] = vertexIndex + 1;
triangles[baseTrianglesIndex + 1] = vertexIndex + 0;
triangles[baseTrianglesIndex + 2] = vertexIndex + 2;
triangles[baseTrianglesIndex + 3] = vertexIndex + 0;
triangles[baseTrianglesIndex + 4] = vertexIndex + 3;
triangles[baseTrianglesIndex + 5] = vertexIndex + 2;
其他面也是同理。具体见AddTriangles()方法。
最后生成BoundsEffect物体。
步骤是先实例化一个Mesh,然后设置Mesh的vertices和triangles。
然后创建一个空物体,添加MeshFilter和MeshRenderer组件,并设置MeshFilter的mesh属性为刚创建的Mesh。
Mesh effectMesh = new Mesh();
effectMesh.name = "Effect-Mesh";
effectMesh.vertices = vertex.ToArray();
effectMesh.triangles = triangles;
GameObject effectObj = new GameObject(name + "-Effect");
effectObj.AddComponent<MeshFilter>().mesh = effectMesh;
MeshRenderer effectMeshRenderer = effectObj.AddComponent<MeshRenderer>();
effectMeshRenderer.material = m_EffectMaterial;