理解包围盒
想象一下
-
我们有一个形状非常复杂的物体:比如一个张牙舞爪的雕塑、一个毛绒玩具熊、或者你自己组装的乐高模型。
-
想要把它装进快递盒里寄走:
- 我们不会为它量身定制一个完全贴合每一个凸起和凹陷的盒子(那太费劲、成本太高)。
- 我们会找一个刚好能把这个物体完全装进去的、形状规则的标准纸箱。
- 这个纸箱可能是一个方方正正的矩形纸箱 (对应
Box3
),或者一个圆滚滚的球 (对应Sphere
)。
-
这个标准纸箱,就是包围盒!
包围盒的两种类型
-
轴对齐包围盒:
Box3
(Axis-Aligned Bounding Box - AABB)- 像什么? 一个方方正正的、边平行于世界坐标轴(X, Y, Z轴)的矩形盒子。
- 怎么来的? 计算机找到物体所有顶点在 X, Y, Z 三个方向上的最大值(
max
)和最小值(min
),用这六个值就确定了一个"最小最大"盒子。 - 优点:计算超级快! 因为它的边和坐标轴平行,判断两个AABB是否相交、点是否在盒内等操作非常高效。
- 缺点:不够紧贴物体。 如果物体是旋转的或者细长的(比如一根斜着放的长棍),这个盒子会比实际物体大很多,浪费空间(不够精确)。
- Three.js 中的类:
THREE.Box3
-
包围球:
Sphere
(Bounding Sphere)- 像什么? 一个球体。
- 怎么来的? 计算机先找到物体的中心点(几何中心或边界盒中心),然后找到离这个中心最远的顶点,这段距离就是球的半径。
- 优点:旋转不变性。 无论物体怎么旋转,包围球的大小和位置(中心点)基本不变。判断两个球是否相交(距离小于半径之和)也很快。
- 缺点:可能更不紧贴。 对于形状很不均匀的物体(比如一个十字架、一个L形),球体内部会有很多空白区域,比AABB包裹得更松。
- Three.js 中的类:
THREE.Sphere
我们为什么需要包围盒
- 碰撞检测(初级): 快速判断两个物体是否"有可能"相撞。先检查它们的包围盒(AABB或球)是否相交。如果包围盒都不相交,那物体肯定没撞上,就不用进行更复杂的(计算量大的)精确碰撞检测了。
- 视锥体裁剪: 决定物体是否在相机视野范围内。相机视野也是一个空间区域(一个平截头体)。如果物体的包围盒完全在这个区域外面,那整个物体就不用渲染了,大大节省性能。
- 射线拾取(优化): 当用鼠标点击屏幕时,会发射一条射线。先判断射线是否与物体的包围盒相交。如果不相交,那肯定没点到这个物体,就不用检查射线和物体所有三角面的精确交点了。
- 空间分割算法的基础: 像八叉树(Octree)、BVH(Bounding Volume Hierarchy)这些高级空间管理算法,都需要用包围盒(AABB或球)作为节点来组织场景中的物体。
- 获取物体尺寸和位置: 方便地获取物体在3D空间中的大致尺寸(
Box3
的size
)和位置范围(Box3
的min/max
或Sphere
的center
)。
案例 - 设置模型位于包围盒内的
效果如下

创建一个Box3立方体对象,其参数为立方体的最小和最大坐标
js
const box3 = new THREE.Box3(
new THREE.Vector3(-1, 0, -1),
new THREE.Vector3(1, 1, 1)
);
然后创建一个Mesh
对象,设置Mesh
对象的位置,使其位于Box3
对象的最小坐标处
js
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshNormalMaterial()
);
scene.add(mesh);
mesh.position.set(box3.min.x, 0 , box3.min.z);
案例 - 获取大小
效果如图

通过getSize
方法,可以获取包围盒的宽度,高度,和深度,这里将获取到的值,用来设置Mesh
的缩放
js
const v_min = new THREE.Vector3(0, 0, 0);
const x = 1 / 12 * 4;
const y = 1 / 12 * 2;
const z = 8;
const v_max = new THREE.Vector3(x, y, z);
const box3 = new THREE.Box3(v_min, v_max);
const v_size = new THREE.Vector3();
box3.getSize(v_size);
console.log(v_size);
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1,1,1),
new THREE.MeshNormalMaterial()
);
scene.add(mesh);
mesh.scale.copy(v_size);
案例 - 设置位置
如图

在场景中添加一个立方体, 通过computeBoundingBox
计算几何体的包围盒, 其计算结果保存在geometry.boundingBox
中
js
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshNormalMaterial()
);
mesh.geometry.computeBoundingBox();
const s = new THREE.Vector3();
const box3 = mesh.geometry.boundingBox;
box3.getSize(s);
mesh.position.y = s.y / 2;
scene.add(mesh);
案例 - helper辅助器
three.js 中提供了一个方便观察包围盒的辅助器,通过Box3Helper
创建

js
const min = new THREE.Vector3(-0.5, -0.75, -1.125);
const max = new THREE.Vector3(0.125, 0.25, 0.5);
const box3 = new THREE.Box3(min, max);
const box3Helper = new THREE.Box3Helper(box3, 'deepskyblue');
scene.add(box3Helper);
案例 - 从对象中设置Box3
效果如下

需要了解的API
setFromObject
其作用是快速计算出这个物体在当前状态下所占用的最小矩形空间 (轴对齐包围盒,AABB),并保存到 Box3
实例中, 它可以作用在任何Object3D
上,包括 Mesh
(网格)、Group
(群组)、Camera
(相机)等。如果是 Group
,会递归计算其所有子物体合并后的总包围盒。
js
const box3 = new THREE.Box3();
const box3Helper = new THREE.Box3Helper(box3, 'deepskyblue');
scene.add(box3Helper);
const group = new THREE.Group();
let i=0;
const len = 3;
while(i<len){
const mesh = new THREE.Mesh( new THREE.BoxGeometry(1, 1, 1), new THREE.MeshNormalMaterial() );
mesh.position.x = -2.5 + 5 * Math.random();
mesh.position.y = -2.5 + 5 * Math.random();
mesh.position.z = -2.5 + 5 * Math.random();
group.add(mesh);
i++;
}
scene.add(group);
box3.setFromObject(group);
案例 - intersects相交
效果图图

从图中可以看出,与包围盒相交的立方体为橙黄,反之为粉红,这里要接触到一个新的API
需要了解的API
intersectsBox 用于 快速检测两个 3D 物体是否可能碰撞 的数学方法,通过判断它们的"包围盒"是否重叠来实现高性能碰撞初筛。
包围盒相交需同时满足以下三个条件:
坐标轴 | 重叠条件(公式) | 现实比喻 |
---|---|---|
X轴 | box1.min.x ≤ box2.max.x 且 box1.max.x ≥ box2.min.x |
两辆车在车道上横向重叠 |
Y轴 | box1.min.y ≤ box2.max.y 且 box1.max.y ≥ box2.min.y |
无人机与鸟高度层重叠 |
Z轴 | box1.min.z ≤ box2.max.z 且 box1.max.z ≥ box2.min.z |
两艘船前后位置重叠 |
js
const box3 = new THREE.Box3(
new THREE.Vector3(-2, -2, -2),
new THREE.Vector3(2, 2, 2)
);
const box3Helper = new THREE.Box3Helper(box3, 'deepskyblue');
scene.add(box3Helper);
const group = new THREE.Group();
let i=0;
const len = 40;
while(i<len){
const material = new THREE.MeshBasicMaterial({
color: 'deeppink',
transparent: true,
opacity: 0.5
})
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(0.5, 0.5, 0.5),
material
);
mesh.position.x = -4 + 8 * Math.random();
mesh.position.y = -4 + 8 * Math.random();
mesh.position.z = -4 + 8 * Math.random();
const mesh_b3 = new THREE.Box3();
mesh_b3.setFromObject(mesh); // 从对象中设置Box3
if(mesh_b3.intersectsBox(box3)) { // 判断是否相交
mesh.material.color = new THREE.Color('orange');
}
group.add(mesh);
i++;
}
scene.add(group);
案例 - 随机边界
在包围盒内生成随机位置立方体,看效果

js
const setAxis = function(mesh, box3, axis, per){
const meshSize = new THREE.Vector3();
mesh.geometry.computeBoundingBox(); // 计算立方体边界盒的大小
mesh.geometry.boundingBox.getSize(meshSize);
const boxSize = new THREE.Vector3();
box3.getSize(boxSize);
mesh.position[axis] = box3.min[axis] + meshSize[axis] / 2 + ( boxSize[axis] - meshSize[axis] ) * per;
};
const rnd = function () {
return THREE.MathUtils.seededRandom();
}
const min = new THREE.Vector3(-1.0, -1.0, -1.0);
const max = new THREE.Vector3(1.0, 1.0, 1.0);
const box3 = new THREE.Box3(min, max);
const box3Helper = new THREE.Box3Helper(box3, 'deepskyblue');
scene.add(box3Helper);
let i = 0;
const len = 60;
while(i < len){
const w = 0.5 + 0.75 * rnd(), // 立方体宽
h = 0.5 + 0.75 * rnd(),
d = 0.5 + 0.75 * rnd();
const mesh = new THREE.Mesh(
new THREE.BoxGeometry(w, h, d),
new THREE.MeshNormalMaterial({
transparent: true,
opacity: 0.25
})
);
setAxis(mesh, box3, 'x', rnd());
setAxis(mesh, box3, 'y', rnd());
setAxis(mesh, box3, 'z', rnd());
scene.add(mesh);
i++;
};