1. Three.js 加载器简介
Three.js 提供了多种加载器,用于加载不同格式的 3D 模型、纹理和其他资源。在本文中使用的是 和 : GLTFLoader DRACOLoader
GLTFLoader:- 用于加载 GLTF/GLB 格式的 3D 模型。
- GLTF 是一种轻量级的 3D 文件格式,支持几何体、材质、动画、场景等数据。
- 返回的对象包含模型的场景 (
gltf.scene)、动画 (gltf.animations) 等信息。
DRACOLoader:- 用于解码 Draco 压缩的 GLTF 模型。
- Draco 是一种高效的 3D 数据压缩技术,可以显著减少文件大小。
- 需要通过
setDecoderPath()设置解码器路径。
2. 实现步骤
以下是代码中的主要实现步骤:
(1) 初始化加载器
const gltfLoader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
gltfLoader.setDRACOLoader(dracoLoader);
- 创建 实例。
GLTFLoader - 创建 实例,并设置解码器路径。
DRACOLoader - 将 关联到 。
DRACOLoaderGLTFLoader
(2) 加载模型
gltfLoader.load(
modelPath,
(gltf) => {
// 加载成功回调
},
(xhr) => {
// 加载进度回调
},
(error) => {
// 加载失败回调
}
);
- 调用
load()方法加载模型文件。 - 提供加载成功、进度更新和错误处理的回调函数。
(3) 模型加载完成后的操作
const model = gltf.scene;
model.scale.set(0.05, 0.05, 0.05);
scene.add(model);
gltf.animations.forEach((clip) => {
const mixer = new THREE.AnimationMixer(model);
const action = mixer.clipAction(clip);
action.play();
mixers.push(mixer);
});
- 获取模型对象 (
gltf.scene) 并调整其比例。 - 遍历
gltf.animations,为每个动画创建 和 ,并播放动画。AnimationMixerAnimationAction - 将模型添加到场景中。
(4) 渲染循环
function animate() {
requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
mixers.forEach((mixer) => mixer.update(deltaTime));
renderer.render(scene, camera);
}
animate();
- 在渲染循环中调用
mixer.update(deltaTime)更新动画。 - 使用
requestAnimationFrame实现连续渲染。
3. 加载成功后获取模型结构
加载完成后,可以通过 gltf.scene 获取模型的结构。gltf.scene 是一个 THREE.Group 对象,包含了模型的所有子对象(如网格、灯光等)。以下是如何遍历和操作模型结构的方法:
(1) 打印模型结构
console.log(gltf.scene);
- 打印整个模型对象,查看其层次结构。
(2) 遍历模型子对象
gltf.scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
console.log('Found mesh:', child.name);
console.log('Material:', child.material);
console.log('Geometry:', child.geometry);
}
});
- 使用
traverse()方法递归遍历模型的子对象。 - 检查每个子对象是否是
THREE.Mesh,并获取其名称、材质和几何体。
(3) 修改模型组件
gltf.scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.color.set(0xff0000); // 修改材质颜色
child.visible = false; // 隐藏对象
}
});
- 遍历模型并修改特定组件的属性(如颜色、可见性等)。
4. 如何控制组件
Three.js 中的模型组件(如网格、灯光等)可以通过访问其属性进行控制。以下是一些常见的操作:
(1) 控制模型位置、旋转和缩放
model.position.set(0, 1, 0); // 设置位置
model.rotation.set(0, Math.PI / 4, 0); // 设置旋转
model.scale.set(2, 2, 2); // 设置缩放
(2) 显示/隐藏模型或组件
model.visible = false; // 隐藏整个模型
gltf.scene.traverse((child) => {
if (child instanceof THREE.Mesh && child.name === 'ComponentName') {
child.visible = false; // 隐藏特定组件
}
});
(3) 修改材质属性
gltf.scene.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material.transparent = true; // 启用透明度
child.material.opacity = 0.5; // 设置透明度
}
});
5. 如何控制动画
Three.js 使用 和 来控制动画。以下是常见的动画控制方法: AnimationMixer AnimationAction
(1) 播放动画
const mixer = new THREE.AnimationMixer(model);
const action = mixer.clipAction(clip);
action.play();
(2) 暂停/恢复动画
action.paused = true; // 暂停动画
action.paused = false; // 恢复动画
(3) 改变动画速度
action.timeScale = 2; // 加快动画速度
action.timeScale = 0.5; // 减慢动画速度
(4) 跳转到特定时间点
action.time = 2; // 跳转到第 2 秒
(5) 循环模式
action.setLoop(THREE.LoopOnce, 1); // 只播放一次
action.clampWhenFinished = true; // 停留在最后一帧
6. 总结
通过上述内容,您可以了解如何使用 Three.js 的加载器加载模型、获取模型结构、控制组件以及管理动画。以下是关键点总结:
- 加载器: 用于加载 GLTF 模型, 用于解码 Draco 压缩。
GLTFLoader``DRACOLoader - 模型结构:通过
gltf.scene获取模型对象,并使用traverse()遍历子对象。 - 组件控制:通过访问模型或组件的属性(如位置、旋转、材质等)进行控制。
- 动画控制:使用 和 播放、暂停、调整速度或跳转动画
AnimationMixer``AnimationAction
7. ** 源码**
<template>
<el-container style="width: 100%; height: 100%">
<!-- 头部标题 -->
<el-header class="header"> GLBLoader</el-header>
<!-- 主要内容区域,包含 Three.js 画布 -->
<el-main class="canvas-container">
<canvas ref="canvas" class="canvas"></canvas>
</el-main>
</el-container>
</template>
<script lang="ts">
export default {
name: 'GLBLoader', // 使用多词名称
}
</script>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { type GLTF, GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
import { MathUtils } from 'three'
// Three.js 相关变量声明
// 场景
let scene: THREE.Scene
// 透视相机
let camera: THREE.PerspectiveCamera
// WebGL渲染器
let renderer: THREE.WebGLRenderer
// 动画帧ID
let animationFrameId: number
let controls: InstanceType<typeof OrbitControls>
let gltfLoader: InstanceType<typeof GLTFLoader>
const mixers: THREE.AnimationMixer[] = [];
// 渲染循环
const clock = new THREE.Clock();
// 画布引用
const canvas = ref<HTMLCanvasElement>()
// 组件挂载时初始化场景并开始动画
onMounted(() => {
initScene()
animate()
// 添加窗口大小改变事件监听
window.addEventListener('resize', onWindowResize)
})
// 初始化场景
function initScene() {
if (!canvas.value) return
makeRenderer(canvas.value.clientWidth, canvas.value.clientHeight)
makeScene()
// 加载GLB模型
gltfLoader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/draco/')
gltfLoader.setDRACOLoader(dracoLoader)
gltfLoader.load(
new URL('@/assets/models/gltf/LittlestTokyo.glb', import.meta.url).href,
(gltf: GLTF): void => {
console.log('模型加载完成')
console.log(gltf)
// 获取模型对象
const model = gltf.scene
model.scale.set(0.05, 0.05, 0.05)
gltf.animations.forEach((clip) => {
// 创建动画混合器
const mixer = new THREE.AnimationMixer(model)
// 创建动画剪辑
const action = mixer.clipAction(clip)
// 播放动画剪辑
action.play()
// 将动画混合器存储到数组中
mixers.push(mixer);
})
// 添加模型到场景
scene.add(model)
},
(xhr: ProgressEvent) => {
console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
},
(error: Error) => {
console.error('An error occurred while loading the model.', error)
},
)
makeCamera(canvas.value.clientWidth, canvas.value.clientHeight)
orbitControls()
}
function makeScene() {
// 创建场景
scene = new THREE.Scene()
//添加光源
const light = new THREE.DirectionalLight(0xffffff, 10)
//添加环境光源
const ambientLight = new THREE.AmbientLight(0xffffff, 1)
scene.add(light)
scene.add(ambientLight)
}
function makeRenderer(width: number, height: number) {
// 创建WebGL渲染器
renderer = new THREE.WebGLRenderer({ canvas: canvas.value, antialias: true })
renderer.setSize(width, height)
// 设置设备像素比,确保在高分辨率屏幕上清晰显示
renderer.setPixelRatio(window.devicePixelRatio)
}
function makeCamera(width: number, height: number) {
// 创建透视相机
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
// 设置相机位置
camera.position.set(0, 10, 20)
camera.lookAt(0, 0, 0)
}
function orbitControls() {
controls = new OrbitControls(camera, canvas.value)
// 限制摄像机的极角范围(单位是弧度)
controls.minPolarAngle = 0 // 最小角度,0 表示水平视角
controls.maxPolarAngle = MathUtils.degToRad(85) // 最大角度,例如 85 度
// 可选:限制摄像机的缩放范围
controls.minDistance = 5 // 最小距离
controls.maxDistance = 100 // 最大距离
}
// 动画循环函数
function animate() {
animationFrameId = requestAnimationFrame(animate)
controls.update()
// 计算时间差
const deltaTime = clock.getDelta();
// 更新所有动画混合器
mixers.forEach((mixer) => mixer.update(deltaTime));
// 渲染场景
renderer.render(scene, camera)
}
// 清理函数
function cleanup() {
// 取消动画帧`
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
// 清理渲染器
if (renderer) {
// 移除所有事件监听器
renderer.domElement.removeEventListener('resize', onWindowResize)
// 释放渲染器资源
renderer.dispose()
// 清空渲染器
renderer.forceContextLoss()
// 移除画布
renderer.domElement.remove()
}
// 清理场景
if (scene) {
// 遍历场景中的所有对象
scene.traverse((object) => {
if (object instanceof THREE.Mesh) {
// 释放几何体资源
object.geometry.dispose()
// 释放材质资源
if (Array.isArray(object.material)) {
object.material.forEach((material) => material.dispose())
} else {
object.material.dispose()
}
}
})
// 清空场景
scene.clear()
}
// 清理相机
if (camera) {
camera.clear()
}
}
// 窗口大小改变时的处理函数
function onWindowResize() {
if (!canvas.value) return
const width = canvas.value.clientWidth
const height = canvas.value.clientHeight
console.log('onWindowResize', width, height)
// 更新相机
camera.aspect = width / height
camera.updateProjectionMatrix()
// 更新渲染器
renderer.setSize(width, height)
// 确保渲染器的像素比与设备匹配
renderer.setPixelRatio(window.devicePixelRatio)
}
// 组件卸载时清理资源
onUnmounted(() => {
cleanup()
// 移除窗口大小改变事件监听
window.removeEventListener('resize', onWindowResize)
})
</script>
<style scoped>
/* 头部样式 */
.header {
text-align: center;
font-size: 20px;
font-weight: bold;
color: #333;
}
/* 画布容器样式 */
.canvas-container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* 画布样式 // 保持画布比例*/
.canvas {
width: 100%;
height: 100%;
}
</style>