返回 导航

Vue.js

hangge.com

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.jsdraco_decoder.wasmdraco_wasm_wrapper.js。这个下面的 node_modulesthree 依赖下的相关目录可以找到。

(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)

回到顶部