Three.js-硬要自学系列36之专项学习包围盒

理解包围盒

想象一下

  1. 我们有一个形状非常复杂的物体:比如一个张牙舞爪的雕塑、一个毛绒玩具熊、或者你自己组装的乐高模型。

  2. 想要把它装进快递盒里寄走

    • 我们不会为它量身定制一个完全贴合每一个凸起和凹陷的盒子(那太费劲、成本太高)。
    • 我们会找一个刚好能把这个物体完全装进去的、形状规则的标准纸箱
    • 这个纸箱可能是一个方方正正的矩形纸箱 (对应 Box3),或者一个圆滚滚的球 (对应 Sphere)。
  3. 这个标准纸箱,就是包围盒!

包围盒的两种类型

  • 轴对齐包围盒: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空间中的大致尺寸(Box3size)和位置范围(Box3min/maxSpherecenter)。

案例 - 设置模型位于包围盒内的

效果如下

创建一个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.xbox1.max.x ≥ box2.min.x 两辆车在车道上横向重叠
Y轴 box1.min.y ≤ box2.max.ybox1.max.y ≥ box2.min.y 无人机与鸟高度层重叠
Z轴 box1.min.z ≤ box2.max.zbox1.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++;
};
相关推荐
OEC小胖胖4 小时前
去中心化身份:2025年Web3身份验证系统开发实践
前端·web3·去中心化·区块链
vvilkim5 小时前
Electron 进程间通信(IPC)深度优化指南
前端·javascript·electron
ai小鬼头7 小时前
百度秒搭发布:无代码编程如何让普通人轻松打造AI应用?
前端·后端·github
漂流瓶jz7 小时前
清除浮动/避开margin折叠:前端CSS中BFC的特点与限制
前端·css·面试
前端 贾公子7 小时前
在移动端使用 Tailwind CSS (uniapp)
前端·uni-app
散步去海边7 小时前
Cursor 进阶使用教程
前端·ai编程·cursor
清幽竹客7 小时前
vue-30(理解 Nuxt.js 目录结构)
前端·javascript·vue.js
weiweiweb8887 小时前
cesium加载Draco几何压缩数据
前端·javascript·vue.js
幼儿园技术家7 小时前
微信小店与微信小程序简单集成指南
前端
我不吃饼干9 天前
鸽了六年的某大厂面试题:你会手写一个模板引擎吗?
前端·javascript·面试