多模型嵌合

全局

模型配置(位置 & 旋转)

1
2
3
4
5
6
const modelConfigs = [
{ position: [-1.85, 0, -2.5], rotation: 0 }, // 左上角
{ position: [-2.5, 0, 1.85], rotation: Math.PI / 2 }, // 左下角
{ position: [1.85, 0, 2.5], rotation: Math.PI }, // 右下角
{ position: [2.5, 0, -1.85], rotation: -Math.PI / 2 }, // 右上角
]

模型计数

所有模型加载完后启用动画循环

1
2
let modelsLoaded = 0 // 已加载模型计数
const totalModels = modelConfigs.length // 总模型数量

对齐函数

将模型底部对齐到 Y=0

1
2
3
4
5
6
7
function alignModelToGround(model) {
const box = new THREE.Box3().setFromObject(model)
const bottomY = box.min.y
// 将模型上移,使底部对齐到Y=0
model.position.y -= bottomY
return box
}

init

重置相机位置

相机视角调到模型上方(好观测调整模型位置)

1
camera.position.set(0, 30, 0)

修改 loader 逻辑

forEach 遍历加载模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
// 加载4个模型实例
modelConfigs.forEach((config, index) => {
loader.load(
MODEL_PATH,
function (gltf) {
const model = gltf.scene
// 应用缩放
model.scale.set(0.01, 0.01, 0.01)
// 将模型底部对齐到Y=0
alignModelToGround(model)
// 应用位置和旋转
model.position.x = config.position[0]
model.position.z = config.position[2]
model.rotation.y = config.rotation

scene.add(model)

// 只为第一个模型创建动画混合器(避免重复动画)
if (index === 0) {
mixer = new THREE.AnimationMixer(model)
if (gltf.animations.length)
mixer.clipAction(gltf.animations[0]).play()
}
modelsLoaded++
// 所有模型加载完成后开始动画循环
if (modelsLoaded === totalModels) {
renderer.setAnimationLoop(animate)
}
},
undefined,
function (e) {
console.error(`模型 ${index + 1} 加载出错:`, e)
}
)
})

网格(可视化y轴)

1
2
3
// 添加网格地面以更好地观察拼合效果
const gridHelper = new THREE.GridHelper(10, 10, 0x888888, 0x444444)
scene.add(gridHelper)

可视化数据 (选)

HTML

1
2
3
4
5
<div class="data">
<p>模型原始尺寸: {{ originalSize }}</p>
<p>模型缩放后尺寸: {{ scaledSize }}</p>
<p>模型中心坐标: {{ centerPosition }}</p>
</div>

CSS

1
2
3
4
5
6
7
8
9
10
11
.data {
position: absolute;
bottom: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px;
border-radius: 5px;
font-family: monospace;
z-index: 100;
}

全局响应式

1
2
3
const originalSize = ref('加载中...')
const scaledSize = ref('加载中...')
const centerPosition = ref('加载中...')

loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function (gltf) {
const model = gltf.scene

// 先计算原始尺寸(应用缩放前)
const originalBox = new THREE.Box3().setFromObject(model)
const originalSizeVec = new THREE.Vector3()
originalBox.getSize(originalSizeVec)

model.scale.set(0.01, 0.01, 0.01)
alignModelToGround(model)

model.position.x = config.position[0]
model.position.z = config.position[2]
model.rotation.y = config.rotation

// 计算缩放后的尺寸和中心坐标
const scaledBox = new THREE.Box3().setFromObject(model)
const scaledSizeVec = new THREE.Vector3()
const center = new THREE.Vector3()
scaledBox.getSize(scaledSizeVec)
scaledBox.getCenter(center)

// 更新显示数据(只在第一个模型时更新)
if (index === 0) {
originalSize.value = `${originalSizeVec.x.toFixed(
2
)} * ${originalSizeVec.y.toFixed(2)} * ${originalSizeVec.z.toFixed(
2
)}`
scaledSize.value = `${scaledSizeVec.x.toFixed(
2
)} * ${scaledSizeVec.y.toFixed(2)} * ${scaledSizeVec.z.toFixed(2)}`
centerPosition.value = `${center.x.toFixed(2)}, ${center.y.toFixed(
2
)}, ${center.z.toFixed(2)}`
}

scene.add(model)

if (index === 0) {
mixer = new THREE.AnimationMixer(model)
if (gltf.animations.length)
mixer.clipAction(gltf.animations[0]).play()
}
modelsLoaded++

if (modelsLoaded === totalModels) {
renderer.setAnimationLoop(animate)
}
},