Vue.js - 集成Three.js构建三维可视化场景教程2(加载GLTF模型)
作者:hangge | 2026-05-15 08:37
前文我演示了如何在 Vue 项目中集成 Three.js,并创建了一个包含立方体、地面和光照的基础 3D 场景,同时实现了 OrbitControls 鼠标交互(点击查看)。在实际工程和数字孪生场景中,我们通常不只是渲染简单的几何体,而是需要加载真实的三维模型(GLTF / GLB 格式)。本文我将接着演示如何在 Vue 中加载 GLTF 模型。





二、加载 GLTF 模型
1,模型准备
我们将需要加载的 GLTF 模型文件放置项目的 public/models 目录下。

2,样例代码
下面代码我使用 Three.js 内置的 GLTFLoader 加载这个 glb 格式的变压器模型。
<template>
<!-- Three.js 渲染容器 -->
<div class="three-container" ref="container"></div>
</template>
<script>
// 核心 Three.js
import * as THREE from 'three'
// 轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// GLTF 模型加载器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
export default {
name: 'ThreeScene',
props: {
enableControls: { type: Boolean, default: true },
width: { type: Number, default: null },
height: { type: Number, default: null }
},
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
rafId: null,
model: null // GLTF 模型引用
}
},
mounted() {
this.$nextTick(() => {
this.initThree()
this.start()
window.addEventListener('resize', this.onWindowResize, { passive: true })
document.addEventListener('visibilitychange', this.onVisibilityChange)
})
},
beforeDestroy() {
this.stop()
window.removeEventListener('resize', this.onWindowResize)
document.removeEventListener('visibilitychange', this.onVisibilityChange)
this.disposeThree()
},
methods: {
/**
* 初始化 Three.js 场景
*/
initThree() {
const container = this.$refs.container
// 1) 场景
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0x111111)
// 2) 相机
const aspect = this.getWidth() / this.getHeight()
this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000)
this.camera.position.set(0, 20, 20)
// 3) 渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
this.renderer.setSize(this.getWidth(), this.getHeight())
this.renderer.outputEncoding = THREE.sRGBEncoding
this.renderer.shadowMap.enabled = true
container.appendChild(this.renderer.domElement)
// 4) 光源
this.scene.add(new THREE.AmbientLight(0xffffff, 1.0)) // 环境光(核心)
const dir = new THREE.DirectionalLight(0xffffff, 2.0) // 主光
dir.position.set(10, 20, 10)
dir.castShadow = true
dir.shadow.mapSize.width = 1024
dir.shadow.mapSize.height = 1024
this.scene.add(dir)
const back = new THREE.DirectionalLight(0xffffff, 0.5) // 侧逆光(提轮廓)
back.position.set(-10, 10, -10)
this.scene.add(back)
// 5) 地面
const groundGeo = new THREE.PlaneGeometry(15, 15)
const groundMat = new THREE.MeshStandardMaterial({ color: 0xcccccc })
const ground = new THREE.Mesh(groundGeo, groundMat)
ground.rotation.x = -Math.PI / 2
ground.position.y = -1.0
ground.receiveShadow = true
this.scene.add(ground)
// 6) 控制器
if (this.enableControls) {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
}
// 7) 加载 GLTF 模型
this.loadGLTFModel('/models/transformer.glb')
},
/**
* GLTF 模型加载方法
* @param {string} url 模型路径
*/
loadGLTFModel(url) {
const loader = new GLTFLoader()
loader.load(
url,
gltf => {
this.model = gltf.scene
this.model.traverse(n => {
if (n.isMesh) {
n.castShadow = true
n.receiveShadow = true
}
})
this.scene.add(this.model)
},
xhr => {
// 可选:加载进度
console.log(`模型加载中: ${(xhr.loaded / xhr.total * 100).toFixed(1)}%`)
},
error => {
console.error('GLTF 模型加载失败:', error)
}
)
},
/**
* 渲染循环(模型保持静止,不旋转)
*/
animate() {
if (this.controls) this.controls.update()
this.renderer.render(this.scene, this.camera)
this.rafId = requestAnimationFrame(this.animate)
},
start() {
if (!this.rafId) this.rafId = requestAnimationFrame(this.animate)
},
stop() {
if (this.rafId) {
cancelAnimationFrame(this.rafId)
this.rafId = null
}
},
getWidth() {
return this.width || this.$refs.container.clientWidth || window.innerWidth
},
getHeight() {
return this.height || this.$refs.container.clientHeight || window.innerHeight
},
onWindowResize() {
if (!this.camera || !this.renderer) return
const w = this.getWidth()
const h = this.getHeight()
this.camera.aspect = w / h
this.camera.updateProjectionMatrix()
this.renderer.setSize(w, h)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
},
onVisibilityChange() {
if (document.hidden) {
this.stop()
} else {
this.start()
}
},
disposeThree() {
this.stop()
if (this.controls) {
this.controls.dispose()
this.controls = null
}
if (this.scene) {
this.scene.traverse(obj => {
if (!obj.isMesh) return
if (obj.geometry) obj.geometry.dispose()
if (obj.material) {
const mat = obj.material
if (Array.isArray(mat)) mat.forEach(m => {
if (m.map) m.map.dispose(); m.dispose()
})
else { if (mat.map) mat.map.dispose(); mat.dispose() }
}
})
}
if (this.renderer) {
this.renderer.forceContextLoss()
this.renderer.domElement && this.renderer.domElement.remove()
this.renderer = null
}
this.scene = null
this.camera = null
this.model = null
}
}
}
</script>
<style scoped>
.three-container {
width: 100%;
height: 100%;
min-height: 400px;
position: relative;
overflow: hidden;
}
</style>
3,运行测试
(1)页面加载后除了会显示地面外,还会加载显示一个变压器 GLTF 模型。

(2)按住鼠标左键拖动可以将视图绕模型旋转,使用滚轮可以缩放视图,按住右键拖动则可平移视角。

附:解决 No DRACOLoader instance provided.错误
1,问题描述
有时我们会发现一些 glb 格式的模型无法加载显示,控制台报如下错误:
Error: THREE.GLTFLoader: No DRACOLoader instance provided.
at new GLTFDracoMeshCompressionExtension (GLTFLoader.js:1996:1)
at GLTFLoader.parse (GLTFLoader.js:509:1)
at Object.eval [as onLoad] (GLTFLoader.js:311:1)
at eval (three.core.js:44528:1)
2,问题原因
这个错误是因为我们使用了 DRACO 压缩的 GLTF 模型,但没有正确设置 DRACOLoader。
提示:Draco 压缩是 GLTF 文件常用的一种顶点压缩技术,用于减小模型体积。
3,解决办法
(1)首先我们要获得 DRACO 解码器文件,具体包括 draco_decoder.js、draco_decoder.wasm、draco_wasm_wrapper.js。这个下面的 node_modules 中 three 依赖下的相关目录可以找到。

(2)然后将这三个文件放到 public/draco/ 目录下:

(3)最后在代码中引入 DRACOLoader 并绑定给 GLTFLoader 即可。注意,这里需要指定 draco 解码器文件位置。
提示:即使我们设置了 DRACOLoader,对加载非 DRACO 压缩的普通 GLTF 模型也是没有影响的。
<template>
<!-- Three.js 渲染容器 -->
<div class="three-container" ref="container"></div>
</template>
<script>
// 核心 Three.js
import * as THREE from 'three'
// 轨道控制器
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// GLTF 模型加载器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
// 压缩纹理加载器
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
export default {
name: 'ThreeScene',
props: {
enableControls: { type: Boolean, default: true },
width: { type: Number, default: null },
height: { type: Number, default: null }
},
data() {
return {
scene: null,
camera: null,
renderer: null,
controls: null,
rafId: null,
model: null // GLTF 模型引用
}
},
mounted() {
this.$nextTick(() => {
this.initThree()
this.start()
window.addEventListener('resize', this.onWindowResize, { passive: true })
document.addEventListener('visibilitychange', this.onVisibilityChange)
})
},
beforeDestroy() {
this.stop()
window.removeEventListener('resize', this.onWindowResize)
document.removeEventListener('visibilitychange', this.onVisibilityChange)
this.disposeThree()
},
methods: {
/**
* 初始化 Three.js 场景
*/
initThree() {
const container = this.$refs.container
// 1) 场景
this.scene = new THREE.Scene()
this.scene.background = new THREE.Color(0x111111)
// 2) 相机
const aspect = this.getWidth() / this.getHeight()
this.camera = new THREE.PerspectiveCamera(60, aspect, 0.1, 1000)
this.camera.position.set(0, 20, 20)
// 3) 渲染器
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
this.renderer.setSize(this.getWidth(), this.getHeight())
this.renderer.outputEncoding = THREE.sRGBEncoding
this.renderer.shadowMap.enabled = true
container.appendChild(this.renderer.domElement)
// 4) 光源
this.scene.add(new THREE.AmbientLight(0xffffff, 1.0)) // 环境光(核心)
const dir = new THREE.DirectionalLight(0xffffff, 2.0) // 主光
dir.position.set(10, 20, 10)
dir.castShadow = true
dir.shadow.mapSize.width = 1024
dir.shadow.mapSize.height = 1024
this.scene.add(dir)
const back = new THREE.DirectionalLight(0xffffff, 0.5) // 侧逆光(提轮廓)
back.position.set(-10, 10, -10)
this.scene.add(back)
// 5) 地面
const groundGeo = new THREE.PlaneGeometry(15, 15)
const groundMat = new THREE.MeshStandardMaterial({ color: 0xcccccc })
const ground = new THREE.Mesh(groundGeo, groundMat)
ground.rotation.x = -Math.PI / 2
ground.position.y = -1.0
ground.receiveShadow = true
this.scene.add(ground)
// 6) 控制器
if (this.enableControls) {
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.enableDamping = true
}
// 7) 加载 GLTF 模型
this.loadGLTFModel('/models/car_type_0.glb')
},
/**
* GLTF 模型加载方法
* @param {string} url 模型路径
*/
loadGLTFModel(url) {
const loader = new GLTFLoader()
// 创建 DRACOLoader
const dracoLoader = new DRACOLoader()
// 指定 draco 解码器文件位置(可放在 public/draco/ 目录)
dracoLoader.setDecoderPath('/draco/')
// 绑定给 GLTFLoader
loader.setDRACOLoader(dracoLoader)
loader.load(
url,
gltf => {
this.model = gltf.scene
this.model.traverse(n => {
if (n.isMesh) {
n.castShadow = true
n.receiveShadow = true
}
})
this.scene.add(this.model)
},
xhr => {
// 可选:加载进度
console.log(`模型加载中: ${(xhr.loaded / xhr.total * 100).toFixed(1)}%`)
},
error => {
console.error('GLTF 模型加载失败:', error)
}
)
},
/**
* 渲染循环(模型保持静止,不旋转)
*/
animate() {
if (this.controls) this.controls.update()
this.renderer.render(this.scene, this.camera)
this.rafId = requestAnimationFrame(this.animate)
},
start() {
if (!this.rafId) this.rafId = requestAnimationFrame(this.animate)
},
stop() {
if (this.rafId) {
cancelAnimationFrame(this.rafId)
this.rafId = null
}
},
getWidth() {
return this.width || this.$refs.container.clientWidth || window.innerWidth
},
getHeight() {
return this.height || this.$refs.container.clientHeight || window.innerHeight
},
onWindowResize() {
if (!this.camera || !this.renderer) return
const w = this.getWidth()
const h = this.getHeight()
this.camera.aspect = w / h
this.camera.updateProjectionMatrix()
this.renderer.setSize(w, h)
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
},
onVisibilityChange() {
if (document.hidden) {
this.stop()
} else {
this.start()
}
},
disposeThree() {
this.stop()
if (this.controls) {
this.controls.dispose()
this.controls = null
}
if (this.scene) {
this.scene.traverse(obj => {
if (!obj.isMesh) return
if (obj.geometry) obj.geometry.dispose()
if (obj.material) {
const mat = obj.material
if (Array.isArray(mat)) mat.forEach(m => {
if (m.map) m.map.dispose(); m.dispose()
})
else { if (mat.map) mat.map.dispose(); mat.dispose() }
}
})
}
if (this.renderer) {
this.renderer.forceContextLoss()
this.renderer.domElement && this.renderer.domElement.remove()
this.renderer = null
}
this.scene = null
this.camera = null
this.model = null
}
}
}
</script>
<style scoped>
.three-container {
width: 100%;
height: 100%;
min-height: 400px;
position: relative;
overflow: hidden;
}
</style>
全部评论(0)