Vue+Three.js打造交互式3D汽车展示展厅(车内视角 / 车门动画 / 轨道控制器)

作者:袖梨 2026-07-17

摘要

Vue+Three.js搭建交互式3D汽车展示展厅(车内视角 / 车门动画 / 轨道控制器)

基于 Vue 结合 Three.js 实现商用级 3D 汽车在线展厅,支持一键切换车内第一视角、车门开关动画、鼠标自由拖拽观察,完整包含场景光照、模型加载、内存销毁全套工程化代码。

演示动态图效果如下:

前言

本文基于 Vue + Three.js 完整实现一套可直接落地的 3D 汽车展示页面,核心包含四大交互功能:

  1. 鼠标轨道控制器:左键旋转、滚轮缩放整车,带平滑阻尼效果。
  2. 镜头缓动切换:一键进入 / 退出车内驾驶室第一视角。
  3. GLTF 原生动画播放:点击按钮控制车门打开 / 关闭动画。
  4. 完整工程化封装:资源自动销毁、窗口自适应、多层展厅光源优化,无内存泄漏。

整体功能效果

  1. 基础场景环境:内置半透明网格地面、分层展厅光源、浅灰色雾化背景,模型加载完成后自动展示操作按钮,整体是简约展厅风格。
  2. 轨道观察交互:采用 OrbitControls 控制器,禁止画布平移,开启拖拽阻尼,鼠标滑动时画面顺滑不卡顿,可全方位查看车身外观、轮毂、内饰结构。
  3. 车内第一视角切换:通过 TWEEN 动画实现镜头平滑过渡,点击「进入驾驶室」自动切换至车内主驾驶视角,退出后缓动回到外部展示机位,两种视角互不冲突。
  4. 车门动画控制:GLTF 模型内置车门骨骼动画,通过 AnimationMixer 管理动画播放、停止,点击按钮切换车门开合状态,动画逻辑与页面状态双向绑定。
  5. 页面资源自动释放:组件销毁时完整遍历场景释放所有几何体、材质、渲染器、动画帧,彻底解决 Three.js 常见内存泄漏问题,适配后台路由频繁切换场景。

完整代码

1. template 结构:

 复制代码<template>
  <div class="three-container" ref="content">
		
    <!-- 功能按钮区域 -->
    <div class="three-btn-border">
			
        <!-- 进入驾驶室镜头按钮 -->
        <el-button type="primary" class="three-btn" @click="enterCockpit" v-if="isLoad && !isEnterCockpit">进入驾驶室</el-button>
				
        <!-- 退出驾驶室镜头按钮 -->	
        <el-button type="primary" class="three-btn" @click="exitCockpit" v-if="isLoad && isEnterCockpit">退出驾驶室</el-button>
			
        <!-- 打开车门动画按钮 -->
        <el-button type="primary" class="three-btn" @click="openDoor" v-if="isLoad && !isAnimation">打开车门动画</el-button>
			
        <!-- 关闭车门动画按钮 -->
        <el-button type="primary" class="three-btn" @click="closeDoor" v-if="isLoad && isAnimation">关闭车门动画</el-button>
			
    </div>
		
    <!-- Three.js渲染画布挂载容器 -->
    <div class="three" id="three"></div>
		
  </div>
</template>

2. script 代码:

 复制代码<script>
// 引入Three.js核心库
import * as THREE from 'three'
// GLTF模型加载器
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// 轨道控制器,鼠标拖拽旋转、缩放场景
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// Tween动画库,实现镜头平滑过渡
import TWEEN from '@tweenjs/tween.js'export default {
  // 组件销毁前释放资源
  beforeDestroy() {
    this.leaveDestory()
  },
  data() {
    return {
      isLoad: false,  // 模型是否加载完成
      isEnterCockpit: false,  // 是否进入车内第一视角
      isAnimation: false,  // 车门动画是否播放中
      yControls: -0.5,  // 轨道控制器瞄准点Y轴偏移
      x: -5.73,  // 初始相机坐标X
      y: 2.93,  // 初始相机坐标Y
      z: 2.86  // 初始相机坐标Z
    }
  },
  mounted() {
    this.$nextTick(() => {
        // 初始化Three场景
        this.init()
        // 窗口变化大小监听
        window.onresize = () => {
            this.resize()
        }
    })
  },
  methods: {
    // 退出驾驶室的方法,镜头平滑切回外部展示视角
    exitCockpit() {
      // Tween动画循环更新函数
      const animate = function () {
        if (TWEEN.update()) {
          window.myMar3 = requestAnimationFrame(animate)
        }
      }
			
      // 创建镜头缓动动画
      window.myMarTween3 = new TWEEN.Tween({ x: window.camera.position.x, y: window.camera.position.y, z: window.camera.position.z, yControls: window.controls.target.y })
        .to({ x: this.x, y: this.y, z: this.z, yControls: this.yControls }, 1500)
        .onUpdate((val) => {
          if (window.myMarTween3) {
            window.camera.position.x = val.x
            window.camera.position.y = val.y
            window.camera.position.z = val.z
            window.controls.target.y = val.yControls
          }
        })
        .start()
				
      // 启动Tween循环
      animate()
			
      this.isEnterCockpit = false
    },
    // 进入驾驶室,镜头平滑切换至车内第一视角
    enterCockpit() {
      const animate = function () {
        if (TWEEN.update()) {
          window.myMar2 = requestAnimationFrame(animate)
        }
      }
      window.myMarTween2 = new TWEEN.Tween({ x: window.camera.position.x, y: window.camera.position.y, z: window.camera.position.z, yControls: window.controls.target.y })
        .to({ x: 0.07, y: 0.44, z: -1.29, yControls: 0 }, 1500)
        .onUpdate((val) => {
          if (window.myMarTween2) {
            window.camera.position.x = val.x
            window.camera.position.y = val.y
            window.camera.position.z = val.z
            window.controls.target.y = val.yControls
          }
        })
        .start()
      animate()
			
      // 动画结束后标记车内视角状态
      setTimeout(() => {
        this.isEnterCockpit = true
      }, 1500)
    },
    // 组件销毁,完整释放Three.js所有资源,防止内存泄漏
    leaveDestory() {
      // 标记模型加载终止
      window.carLoad = null
      // 清除所有渲染动画帧
      if (window.myMar) {
        cancelAnimationFrame(window.myMar)
        window.myMar = null
      }
      if (window.myMar2) {
        cancelAnimationFrame(window.myMar2)
        window.myMar2 = null
      }
      if (window.myMarTween2) {
        window.myMarTween2.stop()
        window.myMarTween2 = null
      }
      if (window.myMar3) {
        cancelAnimationFrame(window.myMar3)
        window.myMar3 = null
      }
      if (window.myMarTween3) {
        window.myMarTween3.stop()
        window.myMarTween3 = null
      }
      // 场景资源销毁
      if (window.scene) {
        // 遍历释放所有网格几何体、材质
        window.scene.traverse(function (v) {
            if (v.type === 'Mesh') {
                if (v.geometry && v.geometry.dispose) {
                    v.geometry.dispose()
                }
                if (v.material && v.material.dispose) {
                    v.material.dispose()
                }
            }
        })
        // 清空场景子元素
        if (window.scene.children.length > 0) {
          window.scene.remove(window.scene.children[0])
        }
        // 销毁渲染器、相机、控制器
        window.renderer.dispose()
        window.renderer.forceContextLoss()
        window.renderer.domElement = null
        window.renderer = null
        window.camera = null
        window.controls.dispose()
        window.controls = null
        window.clock = null
        // 停止车门动画
        if (window.mixer) {
          this.closeDoor()
        }
        // 清空全局模型缓存
        window.gltf = null
        window.model = null
        window.onresize = null
        window.scene.clear()
        window.scene = null
      }
    },
    // 画布尺寸自适应函数
    resize() {
        let width = this.$refs.content.offsetWidth
        let height = this.$refs.content.offsetHeight
        // 更新相机宽高比,修正画面拉伸
        window.camera.aspect = width / height
        window.camera.updateProjectionMatrix()
        // 渲染器同步画布尺寸
        window.renderer.setSize(width, height)
    },
    init() {
        let width = this.$refs.content.offsetWidth
        let height = this.$refs.content.offsetHeight
        // 1. 创建场景容器
        window.scene = new THREE.Scene()
        // 2. 创建透视相机
        window.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
        // 3. 创建WebGL渲染器,开启抗锯齿
        window.renderer = new THREE.WebGLRenderer({ antialias: true })
        window.renderer.setPixelRatio(window.devicePixelRatio)
        // 设置画布背景灰色
        window.renderer.setClearColor(0xbbbbbb, 1.0)
        window.renderer.setSize(width, height)
        // 开启阴影渲染
        window.renderer.shadowMapEnabled = true
        // 初始化相机位置
        window.camera.position.x = this.x
        window.camera.position.y = this.y
        window.camera.position.z = this.z
        window.camera.lookAt(window.scene.position)
        // 4. 初始化轨道控制器
        window.controls = new OrbitControls(window.camera, window.renderer.domElement)
        window.controls.target.set(0, this.yControls, 0)
        window.controls.update()
        window.controls.enablePan = false  // 禁止平移画布
        window.controls.enableDamping = true  // 开启拖拽平滑阻尼
        window.controls.distance = 0
			
        // ========== 光照系统 ==========
        // 半球环境光:提供基础环境漫反射,消除完全死黑区域
        let hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.3)
        hemiLight.position.set(0, 500, 0)
        window.scene.add(hemiLight)
        // 聚光主光源:模拟展厅顶光,产生车身投影
        let spotLight = new THREE.SpotLight(0xffffff)
        spotLight.position.set(-40, 60, -10)
        spotLight.castShadow = true  // 允许投射阴影
        window.scene.add(spotLight)
			
        // 5. GLTF模型加载器
        let loader = new GLTFLoader()
        // 全局时钟,用于动画帧差值计算
        window.clock = new THREE.Clock()
        window.carLoad = true
        // 加载汽车模型
        loader.load('static/models/car/scene.gltf', (gltf) => {
            if (window.carLoad) {
                window.gltf = gltf
                window.model = gltf.scene
	        // 将车辆模型加入场景
                window.scene.add(window.model)
                window.renderer.render(window.scene, window.camera)
	        // 标记模型加载完成,显示操作按钮
                this.isLoad = true
             }
         })
			
         // 6. 地面网格辅助线
         let grid = new THREE.GridHelper(20, 40, 0xffffff, 0xffffff)
         grid.material.opacity = 0.5  // 半透明网格
         grid.material.depthWrite = false
         grid.material.transparent = true
         grid.position.y = -1.1  // 网格下沉至车辆底部
         window.scene.add(grid)
			
         // 7. 雾化效果,远处画面轻微虚化
         window.scene.fog = new THREE.Fog(0xbbbbbb, 10, 15)
			
         // 启动持续渲染循环
         this.animate()
			
         // 将渲染画布挂载到页面DOM容器
         document.getElementById('three').appendChild(window.renderer.domElement)
    },
    // 关闭车门动画,停止动画混合器
    closeDoor() {
        window.animationAction.stop()
        window.mixer = null
        window.animationAction = null
        this.isAnimation = false
    },
    // 播放车门打开动画
    openDoor() {
        // 创建动画混合器,绑定车辆模型
        window.mixer = new THREE.AnimationMixer(window.model)
	     // 获取GLTF自带车门动画并播放
        window.animationAction = window.mixer.clipAction(window.gltf.animations[0])
        window.animationAction.play()
        this.isAnimation = true
    },
    // 全局渲染循环,每一帧执行
    animate() {
        window.myMar = requestAnimationFrame(this.animate)
	     // 更新轨道控制器阻尼效果
        window.controls.update()
        // 更新车门动画状态
        if (window.mixer) {
            const delta = window.clock.getDelta()
            window.mixer.update(delta)
        }
        // 渲染当前场景画面
        window.renderer.render(window.scene, window.camera)
    }
  }
}
</script>

3. css样式代码:

 复制代码.content {
    width: 100%;
    height: 100vh;
    overflow: hidden;
}.three-btn-border {
    position: absolute;
    right: 10px;
    bottom: 10px;
    z-index: 2;
    display: flex;
    justify-content: start;
    align-items: center;
}.three-btn {
    cursor: pointer;
    margin-left: 10px;
}.three {
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 1;
}

核心功能拆解讲解

1. GLTF 模型加载与模型内置动画处理:GLTF 是 Web 端最优三维模型格式,支持嵌入模型内置的动画,本文通过AnimationMixer读取模型自带车门动画片段,点击按钮控制播放 / 停止,通过页面布尔变量绑定按钮显隐状态,交互逻辑清晰。

 复制代码// 初始化动画混合器
window.mixer = new THREE.AnimationMixer(window.model)
// 获取模型内置动画片段并播放
window.animationAction = window.mixer.clipAction(window.gltf.animations[0])
window.animationAction.play()

渲染循环内通过clock.getDelta()更新动画,保证不同设备上车门动画播放速度完全一致。

2. TWEEN 镜头缓动切换车内 / 车外视角

直接修改相机坐标会画面生硬跳跃,使用 TWEEN 库做 1.5 秒缓动插值,实现顺滑镜头过渡:

  • 外部展示机位:远距离斜侧 45°,完整展示整车外观
  • 车内机位:主驾驶第一视角,可观察内饰、方向盘结构 同时增加状态标识,进入车内后自动隐藏对应按钮,避免逻辑冲突。

3. OrbitControls 轨道控制器精细化配置

 复制代码window.controls.enablePan = false // 禁用平移,只允许旋转缩放
window.controls.enableDamping = true // 开启阻尼,拖拽后画面缓慢停止

关闭平移功能,更贴合汽车展厅查看需求;开启阻尼后拖拽手感更顺滑

4. 完整资源销毁,解决内存泄漏

很多 Three.js Demo 只做基础渲染,忽略资源释放,路由反复切换页面会造成页面卡顿、显存溢出。本文在leaveDestory方法中:

  • 遍历场景释放所有 Mesh 几何体、材质。
  • 取消全部 requestAnimationFrame 动画帧。
  • 停止 TWEEN 镜头动画、骨骼动画。
  • 销毁渲染器、相机、控制器实例,适合后台管理系统、多路由切换场景长期使用。

模型说明: 本文演示所用的 3D 模型来源于网络,版权归原作者所有。本文仅做 Three.js 前端技术学习演示,并未对模型本身做任何修改。

相关文章

精彩推荐