引言

Three.js 提供的 Box3(三维边界框)类通过计算模型的空间包围盒,我们能够实现:

  • 智能镜头聚焦:自动计算最佳观察位置和视角
  • 自适应缩放:根据模型尺寸动态调整显示比例
  • 安全导航:在镜头移动时进行简单碰撞检测,避免穿模

让我们从 Box3 的基础特性开始,逐步熟悉其在不同场景中的使用,涵盖从包围盒计算到最终镜头调校的全流程实现。

一、基础概念

  1. 坐标系系统和辅助工具
  • Three.js采用右手坐标系,这是计算机图形学的标准坐标系系统:
    伸出右手:拇指→X轴(右),食指→Y轴(上),中指→Z轴(朝向用户)
    旋转正方向:四指弯曲方向为旋转正方向(逆时针)
    记忆口诀:“右手握拳,拇指朝右,中指指你”

  • 世界坐标与局部坐标

维度世界坐标(World)局部坐标(Local)
参照系整个场景的原点(0,0,0)相对于父对象的位置
特点全局统一标准层级继承关系
获取方法object.positionobject.localToWorld()
典型应用物理计算/光照计算模型部件组装
  • 坐标转换原理
    当使用Box3.setFromObject()时,Three.js会自动处理:
    遍历对象所有顶点(局部坐标)
    应用对象及其所有父级的变换矩阵
    转换为世界坐标计算包围盒
属性/方法作用Box3相关注意
object.position局部坐标位置需结合父级矩阵计算世界坐标
object.matrixWorld完整世界变换矩阵setFromObject会自动应用
vector.applyMatrix4()坐标系统转换Box3计算前需确保坐标统一
object.updateMatrix()更新局部变换矩阵修改参数后必须调用
// 手动转换坐标示例
const box = new THREE.Box3();
const worldPosition = new THREE.Vector3();

object.traverse(child => {
  if (child.isMesh) {
    child.geometry.attributes.position.array.forEach((val, i) => {
      if (i % 3 === 0) { // 每3个值是一个顶点(x,y,z)
        worldPosition.set(
          child.geometry.attributes.position.array[i],
          child.geometry.attributes.position.array[i+1],
          child.geometry.attributes.position.array[i+2]
        );
        worldPosition.applyMatrix4(child.matrixWorld);
        box.expandByPoint(worldPosition);
      }
    });
  }
});
  1. Box3 核心概念

最小点(min)和最大点(max)的定义

包围盒的空间表示方法,如下表:

方法输入类型是否递归是否重置原有包围盒典型应用场景性能考虑
expandByObjectObject3D否(扩展原有盒子)渐进式构建组合包围盒中等(需遍历子对象)
setFromObjectObject3D是(完全重置)计算单个/组合对象的精确包围盒中等(需遍历子对象)
setFromArray数值数组是(完全重置)从原始顶点数据快速计算

二、案例

场景一:模型自动聚焦

Three.js 的 OrbitControls 提供了内置方法 fitToBox(box),可直接将相机视角调整以适配指定包围盒。

function autoFitModel(model, camera, controls) {
  const box = new THREE.Box3().setFromObject(model);
  if (box.isEmpty()) return;

  // 使用官方 fitToBox
  controls.fitToBox(box, true, {
    duration: 800,
    paddingLeft: 0.1,
    paddingRight: 0.1,
    paddingTop: 0.1,
    paddingBottom: 0.1
  });
}

场景二:自适应缩放比例计算

利用 Box3的min和max 计算模型的包围盒,然后根据视图区域大小自动调整模型缩放比例,使其完美适配显示区域。

function fixview2() {
  const target = this.model
  // 获取容器宽高
  const containerWidth = this.container.clientWidth
  const containerHeight = this.container.clientHeight
  const aspect = containerWidth / containerHeight

  // 定义包围盒的最大高度,宽度根据宽高比自动调整
  const maxHeight = 5
  const maxWidth = maxHeight * aspect
  const maxDepth = maxHeight // 可选,也可以设成其他值

  // 创建包围盒,围绕原点对称分布
  const box = new THREE.Box3()
  box.min.set(-maxWidth / 2, -maxHeight / 2, -maxDepth / 2)
  box.max.set(maxWidth / 2, maxHeight / 2, maxDepth / 2)
  const center = new THREE.Vector3()
  box.getCenter(center)
  const boxx = box.max.x - box.min.x  // 计算包围盒在 x 轴上的长度
  const boxy = box.max.y - box.min.y
  const boxz = box.max.z - box.min.z
  const targetBox = new THREE.Box3().setFromObject(target)
  const targetx = targetBox.max.x - targetBox.min.x // 计算目标模型包围盒在 x 轴上的长度
  const targety = targetBox.max.y - targetBox.min.y
  const targetz = targetBox.max.y - targetBox.min.y
  const scaleRatio =
    boxx > targetx
      ? Math.min(boxx / targetx, boxy / targety, boxz / targetz)
      : Math.min(targetx / boxx, targety / boxy, targetz / boxz) // 计算缩放比例
  target.scale.set(scaleRatio, scaleRatio, scaleRatio) // 设置模型缩放比例
  target.position.x = 0
  target.position.y = -maxHeight / 2
  target.position.z = 0

  // const helper = new THREE.Box3Helper(box, 0xff0000) // 红色线框
  // this.scene.add(helper)
}

场景三:镜头移动碰撞检测

快速空间范围检测轴对齐包围盒(AABB):Box3表示的是与坐标轴对齐的立方体区域,计算效率极高(平均复杂度O(1))

碰撞初筛:在精确碰撞检测前,先用Box3快速排除明显不会碰撞的对象,通过 box1.intersectsBox(box2) 方法快速判断两个包围盒是否重叠

class CameraCollisionSystem {
    constructor(scene) {
        this.colliders = []; // 存储带碰撞体的对象
    }

    addCollider(object) {
        const box = new THREE.Box3().setFromObject(object);
        this.colliders.push({ object, box });
    }

    checkCollision(cameraPosition, radius = 0.5) {
        // 创建相机近似包围盒(球形→AABB转换)
        const cameraBox = new THREE.Box3(
            new THREE.Vector3(
                cameraPosition.x - radius,
                cameraPosition.y - radius,
                cameraPosition.z - radius
            ),
            new THREE.Vector3(
                cameraPosition.x + radius,
                cameraPosition.y + radius,
                cameraPosition.z + radius
            )
        );

        // 检测与场景物体的碰撞
        return this.colliders.some(item => {
            item.box.copy(new THREE.Box3().setFromObject(item.object)); // 更新碰撞体
            return item.box.intersectsBox(cameraBox);
        });
    }
}

与OrbitControls集成

const controls = new OrbitControls(camera, renderer.domElement);
const collisionSystem = new CameraCollisionSystem(scene);

// 添加碰撞体
scene.traverse(obj => {
    if (obj.isMesh && obj.userData.isCollider) {
        collisionSystem.addCollider(obj);
    }
});

// 在controls更新前检测
controls.addEventListener('change', () => {
    if (collisionSystem.checkCollision(camera.position)) {
        // 回退到安全位置
        camera.position.copy(lastSafePosition);
        controls.target.copy(lastSafeTarget);
    } else {
        lastSafePosition.copy(camera.position);
        lastSafeTarget.copy(controls.target);
    }
});

合理运用Box3,开发者可以在保证性能的前提下实现可靠的镜头碰撞检测。对于更复杂的需求,建议结合射线检测(Raycaster)或物理引擎(如Cannon.js)进行扩展,空间分割(Broad Phase)使用 八叉树进行碰撞检测(另一篇文章)

"内存溢出"问题原因分析

当使用 Box3.setFromObject() 方法处理复杂场景时,可能会遇到浏览器迭代最大限制的问题,这通常表现为性能下降或直接报错。下面我将详细解释原因并提供多种解决方案。

  1. 递归遍历的深度问题
    setFromObject() 会递归遍历对象及其所有子对象,当场景图非常深时(如:嵌套组>100层),可能触发浏览器的最大调用堆栈限制。

  2. 大规模场景的顶点数量
    对于包含大量几何体的对象(如:顶点数>100万),计算所有顶点的最小/最大值会导致长时间阻塞。

  3. 动态更新频率过高
    在动画循环中每帧调用 setFromObject() 处理复杂对象,会导致性能瓶颈。

不同场景下的方法选择

场景特征推荐方案理由
超深层级对象非递归避免调用堆栈溢出
超多顶点对象Web Worker防止主线程阻塞
动态变化对象简化几何平衡精度与性能
静态复杂场景分批处理减少计算压力

参考

  1. PerspectiveCamera基本设置
  2. 轨道控制器(OrbitControls)的初始化
  3. Box3 API 文档
  4. threejs中文教程
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐