Three.js + Vue3 加载GLB模型项目代码详解

0 阅读4分钟

本说明结合 src/App.vue 代码,详细解释如何在 Vue3 项目中用 three.js 加载并显示 glb 模型。


1. 依赖与插件导入

import { onMounted, onUnmounted } from 'vue'
import * as THREE from 'three'
import Stats from 'stats.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'
  • THREE:three.js 主库,包含所有核心3D功能。
  • Stats:性能监控面板,显示FPS等信息。
  • OrbitControls:鼠标/触摸控制相机旋转、缩放、平移。
  • RoomEnvironment:生成物理渲染用的环境贴图。
  • GLTFLoader/DRACOLoader:加载glb/gltf模型及解压Draco压缩网格。

2. 变量声明

let mixer: THREE.AnimationMixer | undefined
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let stats: Stats
const clock = new THREE.Clock()
  • mixer:动画混合器,用于播放模型动画。
  • renderer:WebGL渲染器。
  • controls:轨道控制器。
  • stats:性能监控。
  • clock:three.js动画计时器。

3. Vue生命周期挂载

onMounted(() => { ... })
onUnmounted(() => { ... })
  • onMounted:组件挂载后初始化three.js场景。
  • onUnmounted:组件卸载时清理资源,防止内存泄漏。

4. three.js 场景初始化

4.1 获取容器

const container = document.getElementById('container')
  • 获取用于渲染three.js的DOM节点。

4.2 性能面板

stats = new Stats()
container.appendChild(stats.dom)
  • 添加FPS性能监控面板。

4.3 渲染器

renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(window.innerWidth, window.innerHeight)
container.appendChild(renderer.domElement)
  • 创建WebGL渲染器,抗锯齿,适配高分屏,全屏显示。

4.4 场景与环境

const pmremGenerator = new THREE.PMREMGenerator(renderer)
const scene = new THREE.Scene()
scene.background = new THREE.Color(0xbfe3dd)
scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture
  • 创建场景,设置背景色。
  • 用RoomEnvironment生成物理渲染环境贴图,提升模型质感。

4.5 相机

const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000)
camera.position.set(0, 0, 5)
  • 透视相机,视野40度,近平面0.1,远平面1000,初始z=5。

4.6 轨道控制器

controls = new OrbitControls(camera, renderer.domElement)
controls.target.set(0, 0, 0)
controls.update()
controls.enablePan = false
controls.enableDamping = true
  • 允许鼠标旋转/缩放相机,目标点为原点。
  • 禁止平移,启用阻尼(惯性)。

5. 加载GLB模型

5.1 Draco解码器

const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('/jsm/libs/draco/gltf/')
  • 设置Draco解码器路径,支持压缩网格。

5.2 GLTFLoader加载模型

const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
loader.load('/models/gltf/LittlestTokyo.glb', function (gltf) { ... })
  • 加载glb模型,回调函数处理模型。

5.3 模型居中与相机自适应

const model = gltf.scene
const box = new THREE.Box3().setFromObject(model)
const size = new THREE.Vector3()
box.getSize(size)
const center = new THREE.Vector3()
box.getCenter(center)
model.position.sub(center)
scene.add(model)

const maxDim = Math.max(size.x, size.y, size.z)
const fov = camera.fov * (Math.PI / 180)
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2))
cameraZ *= 2.0 // 稍微拉远一点
camera.position.set(0, 0, cameraZ)
camera.near = cameraZ / 100
camera.far = cameraZ * 100
camera.updateProjectionMatrix()
controls.target.set(0, 0, 0)
controls.update()
  • 计算模型包围盒,自动居中。
  • 根据模型尺寸自动设置相机距离,保证完整显示。

5.4 动画支持

mixer = new THREE.AnimationMixer(model)
if (gltf.animations && gltf.animations.length > 0) {
  mixer.clipAction(gltf.animations[0]).play()
}
  • 如果模型有动画,自动播放。

5.5 启动渲染循环

renderer.setAnimationLoop(animate)
  • 启动three.js渲染循环。

6. 窗口自适应

window.addEventListener('resize', onWindowResize)
function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
}
  • 浏览器窗口变化时,自动调整相机和渲染器。

7. 渲染循环与动画

function animate() {
  const delta = clock.getDelta()
  if (mixer) mixer.update(delta)
  controls.update()
  stats.update()
  renderer.render(scene, camera)
}
  • 每帧更新时间、动画、控制器、性能面板,并渲染场景。

8. 资源清理

onUnmounted(() => {
  window.removeEventListener('resize', () => {})
  if (renderer) renderer.dispose()
})
  • 组件卸载时移除事件监听,释放WebGL资源。

9. 样式设置

html, body, #container {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  overflow: hidden;
}
  • 保证渲染区域全屏,无滚动条。

总结

本项目实现了three.js在Vue3中的标准集成方式,支持GLB模型加载、动画、交互、性能监控和自适应。

<template>
  <div id="container" style="width: 100vw; height: 100vh;"></div>
</template>

<script setup lang="ts">
  import { onMounted, onUnmounted } from 'vue'
  import * as THREE from 'three'
  import Stats from 'stats.js'
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
  import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js'
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'

  let mixer: THREE.AnimationMixer | undefined
  let renderer: THREE.WebGLRenderer
  let controls: OrbitControls
  let stats: Stats
  const clock = new THREE.Clock()

  onMounted(() => {
    const container = document.getElementById('container')
    if (!container) return

    stats = new Stats()
    container.appendChild(stats.dom)

    renderer = new THREE.WebGLRenderer({ antialias: true })
    renderer.setPixelRatio(window.devicePixelRatio)
    renderer.setSize(window.innerWidth, window.innerHeight)
    container.appendChild(renderer.domElement)

    const pmremGenerator = new THREE.PMREMGenerator(renderer)
    const scene = new THREE.Scene()
    scene.background = new THREE.Color(0xbfe3dd)
    scene.environment = pmremGenerator.fromScene(new RoomEnvironment(), 0.04).texture

    // 先初始化相机在原点
    const camera = new THREE.PerspectiveCamera(40, window.innerWidth / window.innerHeight, 0.1, 1000)
    camera.position.set(0, 0, 5)

    controls = new OrbitControls(camera, renderer.domElement)
    controls.target.set(0, 0, 0)
    controls.update()
    controls.enablePan = false
    controls.enableDamping = true

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

    const loader = new GLTFLoader()
    loader.setDRACOLoader(dracoLoader)
    loader.load('/models/gltf/LittlestTokyo.glb', function (gltf) {
      const model = gltf.scene
      // 计算包围盒
      const box = new THREE.Box3().setFromObject(model)
      const size = new THREE.Vector3()
      box.getSize(size)
      const center = new THREE.Vector3()
      box.getCenter(center)
      // 居中
      model.position.sub(center)
      scene.add(model)

      // 自动设置相机距离
      const maxDim = Math.max(size.x, size.y, size.z)
      const fov = camera.fov * (Math.PI / 180)
      let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2))
      cameraZ *= 2.0 // 稍微拉远一点
      camera.position.set(0, 0, cameraZ)
      camera.near = cameraZ / 100
      camera.far = cameraZ * 100
      camera.updateProjectionMatrix()
      controls.target.set(0, 0, 0)
      controls.update()

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

      renderer.setAnimationLoop(animate)
    }, undefined, function (e) {
      console.error(e)
    })

    window.addEventListener('resize', onWindowResize)

    function onWindowResize() {
      camera.aspect = window.innerWidth / window.innerHeight
      camera.updateProjectionMatrix()
      renderer.setSize(window.innerWidth, window.innerHeight)
        }

  function animate() {
    const delta = clock.getDelta()
    if (mixer) mixer.update(delta)
    controls.update()
    stats.update()
    renderer.render(scene, camera)
  }
})

onUnmounted(() => {
  window.removeEventListener('resize', () => {})
  if (renderer) renderer.dispose()
})
</script>

<style>
html, body, #container {
  width: 100vw;
  height: 100vh;
  margin: 0;
  padding: 0;
  overflow: hidden;
}
</style>