摘要:

基于 Vue 结合 Three.js 实现商用级 3D 汽车在线展厅,支持一键切换车内第一视角、车门开关动画、鼠标自由拖拽观察,完整包含场景光照、模型加载、内存销毁全套工程化代码。
演示动态图效果如下:
前言:
本文基于 Vue + Three.js 完整实现一套可直接落地的 3D 汽车展示页面,核心包含四大交互功能:
整体功能效果:
完整代码
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 秒缓动插值,实现顺滑镜头过渡:
3. OrbitControls 轨道控制器精细化配置:
复制代码window.controls.enablePan = false // 禁用平移,只允许旋转缩放
window.controls.enableDamping = true // 开启阻尼,拖拽后画面缓慢停止
关闭平移功能,更贴合汽车展厅查看需求;开启阻尼后拖拽手感更顺滑
4. 完整资源销毁,解决内存泄漏:
很多 Three.js Demo 只做基础渲染,忽略资源释放,路由反复切换页面会造成页面卡顿、显存溢出。本文在leaveDestory方法中:
模型说明: 本文演示所用的 3D 模型来源于网络,版权归原作者所有。本文仅做 Three.js 前端技术学习演示,并未对模型本身做任何修改。