Skip to content

多个物体的包围盒

这是一个可以直接在 VitePress 页面中运行的 Three.js 示例。上方显示运行效果,下方展示对应源码。

运行效果

等待模型加载

原理

单个物体可以用 new THREE.Box3().setFromObject(object) 得到世界空间包围盒。多个物体的整体包围盒可以先创建一个空的 Box3,再遍历每个物体,把每个物体自己的包围盒通过 combinedBox.union(objectBox) 合并进去。

这个示例分别加载 ironman_mk44.glbporsche_911.glb。黄色和蓝色线框表示两个模型各自的包围盒,红色线框表示合并后的整体包围盒。GUI 中可以隐藏单独包围盒、隐藏整体包围盒、切换模型显示,以及移动两个模型观察整体包围盒如何变化。

源码

js
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'

const canvas = document.querySelector('canvas')
const demo = canvas.parentElement

const scene = new THREE.Scene()
scene.background = new THREE.Color('#111827')

const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 2.5, 6)

const renderer = new THREE.WebGLRenderer({
	canvas,
	antialias: true
})

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
renderer.outputColorSpace = THREE.SRGBColorSpace
renderer.setSize(window.innerWidth, window.innerHeight)

const ambientLight = new THREE.AmbientLight('#ffffff', 0.9)
scene.add(ambientLight)

const directionalLight = new THREE.DirectionalLight('#ffffff', 2)
directionalLight.position.set(4, 6, 5)
scene.add(directionalLight)

const gridHelper = new THREE.GridHelper(12, 12)
scene.add(gridHelper)

const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true

const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')

const gltfLoader = new GLTFLoader()
gltfLoader.setDRACOLoader(dracoLoader)

const modelConfigs = [
	{
		name: 'Iron Man MK44',
		path: '/models/ironman_mk44.glb',
		position: new THREE.Vector3(-2.2, 0, 0),
		boxColor: '#facc15'
	},
	{
		name: 'Porsche 911',
		path: '/models/porsche_911.glb',
		position: new THREE.Vector3(2.2, 0, 0),
		boxColor: '#38bdf8'
	}
]

const objectItems = []
const combinedBox = new THREE.Box3()

let combinedBoxHelper = null

const loadModel = (config) => new Promise((resolve, reject) => {
	gltfLoader.load(
		config.path,
		(gltf) => {
			const holder = new THREE.Group()

			holder.name = config.name
			holder.position.copy(config.position)
			holder.add(gltf.scene)
			scene.add(holder)

			const box = new THREE.Box3().setFromObject(holder)
			const boxHelper = new THREE.Box3Helper(box, config.boxColor)
			scene.add(boxHelper)

			resolve({
				...config,
				holder,
				box,
				boxHelper
			})
		},
		undefined,
		reject
	)
})

const updateBoundingBoxes = () => {
	combinedBox.makeEmpty()

	objectItems.forEach((item) => {
		item.box.setFromObject(item.holder)
		item.boxHelper.box.copy(item.box)
		combinedBox.union(item.box)
	})

	combinedBoxHelper.box.copy(combinedBox)
}

Promise.all(modelConfigs.map(loadModel)).then((items) => {
	objectItems.push(...items)
	combinedBoxHelper = new THREE.Box3Helper(combinedBox, '#ef4444')
	scene.add(combinedBoxHelper)
	updateBoundingBoxes()
})

function animate() {
	requestAnimationFrame(animate)

	controls.update()
	renderer.render(scene, camera)
}

animate()