A Production-Ready 3D Car Showroom with Vue and Three.js
Abstract:
A commercial-grade 3D car online showroom built with Vue and Three.js, supporting one-click switching to an interior first-person view, door opening/closing animations, and free mouse drag observation. It includes complete engineering code for scene lighting, model loading, and memory cleanup.
Demo GIF effect as follows:
Foreword:
This article implements a complete, production-ready 3D car display page based on Vue + Three.js, with four core interactive features:
- Mouse orbit controls: left-click to rotate, scroll wheel to zoom the entire car, with smooth damping effect.
- Camera easing transition: one-click to enter/exit the driver's cabin first-person view.
- GLTF native animation playback: click buttons to control door opening/closing animations.
- Complete engineering encapsulation: automatic resource destruction, window resizing, multi-layered showroom light source optimization, no memory leaks.
Overall Functional Effects:
- Basic Scene Environment: Built-in semi-transparent grid floor, layered showroom lighting, light gray fog background. Operation buttons are automatically displayed after the model loads, presenting a minimalist showroom style.
- Orbit Observation Interaction: Uses OrbitControls, disables canvas panning, enables drag damping. The image is smooth and lag-free when the mouse slides, allowing full viewing of the car body, wheels, and interior structure.
- Interior First-Person View Switching: Uses TWEEN animation for smooth camera transitions. Clicking 'Enter Cockpit' automatically switches to the driver's seat perspective. Exiting smoothly returns to the external display position. The two perspectives do not conflict.
- Door Animation Control: The GLTF model has built-in door skeletal animation. AnimationMixer manages animation playback and stopping. Clicking a button toggles the door open/close state, with animation logic and page state bound bidirectionally.
- Automatic Page Resource Release: When the component is destroyed, it fully traverses the scene to release all geometries, materials, renderers, and animation frames, thoroughly solving the common Three.js memory leak problem, suitable for frequent route switching in background management systems.
Complete Code
1. Template Structure:
<template>
<div class="three-container" ref="content">
<!-- Function button area -->
<div class="three-btn-border">
<!-- Enter cockpit camera button -->
<el-button type="primary" class="three-btn" @click="enterCockpit" v-if="isLoad && !isEnterCockpit">Enter Cockpit</el-button>
<!-- Exit cockpit camera button -->
<el-button type="primary" class="three-btn" @click="exitCockpit" v-if="isLoad && isEnterCockpit">Exit Cockpit</el-button>
<!-- Open door animation button -->
<el-button type="primary" class="three-btn" @click="openDoor" v-if="isLoad && !isAnimation">Open Door Animation</el-button>
<!-- Close door animation button -->
<el-button type="primary" class="three-btn" @click="closeDoor" v-if="isLoad && isAnimation">Close Door Animation</el-button>
</div>
<!-- Three.js rendering canvas mount container -->
<div class="three" id="three"></div>
</div>
</template>
2. Script Code:
<script>
// Import Three.js core library
import * as THREE from 'three'
// GLTF model loader
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// Orbit controls, mouse drag to rotate and zoom the scene
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
// Tween animation library, for smooth camera transitions
import TWEEN from '@tweenjs/tween.js'
export default {
// Release resources before component destruction
beforeDestroy() {
this.leaveDestory()
},
data() {
return {
isLoad: false, // Whether the model has finished loading
isEnterCockpit: false, // Whether in the interior first-person view
isAnimation: false, // Whether the door animation is playing
yControls: -0.5, // Y-axis offset of the orbit controls target point
x: -5.73, // Initial camera coordinate X
y: 2.93, // Initial camera coordinate Y
z: 2.86 // Initial camera coordinate Z
}
},
mounted() {
this.$nextTick(() => {
// Initialize the Three scene
this.init()
// Window resize listener
window.onresize = () => {
this.resize()
}
})
},
methods: {
// Method to exit the cockpit, smoothly switching the camera back to the external display view
exitCockpit() {
// Tween animation loop update function
const animate = function () {
if (TWEEN.update()) {
window.myMar3 = requestAnimationFrame(animate)
}
}
// Create camera easing animation
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()
// Start the Tween loop
animate()
this.isEnterCockpit = false
},
// Enter the cockpit, smoothly switching the camera to the interior first-person view
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()
// Mark the interior view state after the animation ends
setTimeout(() => {
this.isEnterCockpit = true
}, 1500)
},
// Component destruction, fully release all Three.js resources to prevent memory leaks
leaveDestory() {
// Mark model loading as terminated
window.carLoad = null
// Clear all rendering animation frames
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
}
// Scene resource destruction
if (window.scene) {
// Traverse and release all mesh geometries and materials
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()
}
}
})
// Clear scene children
if (window.scene.children.length > 0) {
window.scene.remove(window.scene.children[0])
}
// Destroy renderer, camera, controls
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
// Stop door animation
if (window.mixer) {
this.closeDoor()
}
// Clear global model cache
window.gltf = null
window.model = null
window.onresize = null
window.scene.clear()
window.scene = null
}
},
// Canvas size adaptive function
resize() {
let width = this.$refs.content.offsetWidth
let height = this.$refs.content.offsetHeight
// Update camera aspect ratio to correct image stretching
window.camera.aspect = width / height
window.camera.updateProjectionMatrix()
// Synchronize renderer canvas size
window.renderer.setSize(width, height)
},
init() {
let width = this.$refs.content.offsetWidth
let height = this.$refs.content.offsetHeight
// 1. Create scene container
window.scene = new THREE.Scene()
// 2. Create perspective camera
window.camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
// 3. Create WebGL renderer, enable anti-aliasing
window.renderer = new THREE.WebGLRenderer({ antialias: true })
window.renderer.setPixelRatio(window.devicePixelRatio)
// Set canvas background to gray
window.renderer.setClearColor(0xbbbbbb, 1.0)
window.renderer.setSize(width, height)
// Enable shadow rendering
window.renderer.shadowMapEnabled = true
// Initialize camera position
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. Initialize orbit controls
window.controls = new OrbitControls(window.camera, window.renderer.domElement)
window.controls.target.set(0, this.yControls, 0)
window.controls.update()
window.controls.enablePan = false // Disable canvas panning
window.controls.enableDamping = true // Enable drag smooth damping
window.controls.distance = 0
// ========== Lighting System ==========
// Hemisphere ambient light: provides basic ambient diffuse reflection, eliminating completely dark areas
let hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.3)
hemiLight.position.set(0, 500, 0)
window.scene.add(hemiLight)
// Spotlight main light: simulates showroom overhead light, producing car body shadows
let spotLight = new THREE.SpotLight(0xffffff)
spotLight.position.set(-40, 60, -10)
spotLight.castShadow = true // Allow shadow casting
window.scene.add(spotLight)
// 5. GLTF model loader
let loader = new GLTFLoader()
// Global clock, for animation frame delta calculation
window.clock = new THREE.Clock()
window.carLoad = true
// Load the car model
loader.load('static/models/car/scene.gltf', (gltf) => {
if (window.carLoad) {
window.gltf = gltf
window.model = gltf.scene
// Add the car model to the scene
window.scene.add(window.model)
window.renderer.render(window.scene, window.camera)
// Mark model loading complete, display operation buttons
this.isLoad = true
}
})
// 6. Ground grid helper
let grid = new THREE.GridHelper(20, 40, 0xffffff, 0xffffff)
grid.material.opacity = 0.5 // Semi-transparent grid
grid.material.depthWrite = false
grid.material.transparent = true
grid.position.y = -1.1 // Sink the grid to the bottom of the car
window.scene.add(grid)
// 7. Fog effect, slight blurring of distant images
window.scene.fog = new THREE.Fog(0xbbbbbb, 10, 15)
// Start the continuous rendering loop
this.animate()
// Mount the rendering canvas to the page DOM container
document.getElementById('three').appendChild(window.renderer.domElement)
},
// Close door animation, stop the animation mixer
closeDoor() {
window.animationAction.stop()
window.mixer = null
window.animationAction = null
this.isAnimation = false
},
// Play door opening animation
openDoor() {
// Create animation mixer, bind to the car model
window.mixer = new THREE.AnimationMixer(window.model)
// Get the GLTF's built-in door animation and play it
window.animationAction = window.mixer.clipAction(window.gltf.animations[0])
window.animationAction.play()
this.isAnimation = true
},
// Global rendering loop, executed every frame
animate() {
window.myMar = requestAnimationFrame(this.animate)
// Update orbit controls damping effect
window.controls.update()
// Update door animation state
if (window.mixer) {
const delta = window.clock.getDelta()
window.mixer.update(delta)
}
// Render the current scene frame
window.renderer.render(window.scene, window.camera)
}
}
}
</script>
3. CSS Style Code:
.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;
}
Core Feature Breakdown:
1. GLTF Model Loading and Built-in Animation Handling: GLTF is the optimal 3D model format for the web, supporting embedded model animations. This article uses AnimationMixer to read the model's built-in door animation clip, controls playback/stop via button clicks, and binds button visibility state through page boolean variables, resulting in clear interaction logic.
// Initialize animation mixer
window.mixer = new THREE.AnimationMixer(window.model)
// Get the model's built-in animation clip and play it
window.animationAction = window.mixer.clipAction(window.gltf.animations[0])
window.animationAction.play()
Inside the render loop, the animation is updated via clock.getDelta(), ensuring the door animation playback speed is completely consistent across different devices.
2. TWEEN Camera Easing for Interior/Exterior View Switching:
Directly modifying camera coordinates results in a jarring visual jump. The TWEEN library is used for a 1.5-second easing interpolation to achieve a smooth camera transition:
- External display position: distant 45° oblique angle, fully displaying the car's exterior.
- Interior position: driver's first-person perspective, allowing observation of the interior and steering wheel structure. A state identifier is also added, automatically hiding the corresponding button after entering the interior to avoid logic conflicts.
3. Fine-Grained Configuration of OrbitControls:
window.controls.enablePan = false // Disable panning, only allow rotation and zoom
window.controls.enableDamping = true // Enable damping, the image slowly stops after dragging
Disabling the pan function better suits the needs of a car showroom view; enabling damping makes the drag feel smoother.
4. Complete Resource Destruction to Solve Memory Leaks:
Many Three.js demos only do basic rendering and ignore resource release. Repeated route switching can cause page lag and video memory overflow. In the leaveDestory method, this article:
- Traverses the scene to release all Mesh geometries and materials.
- Cancels all requestAnimationFrame animation frames.
- Stops TWEEN camera animations and skeletal animations.
- Destroys renderer, camera, and controller instances, making it suitable for long-term use in background management systems and multi-route switching scenarios.
Model Note: The 3D model used for this demonstration is sourced from the internet, and its copyright belongs to the original author. This article is solely for a Three.js frontend technology learning demonstration, and no modifications have been made to the model itself.
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
[struggle][struggle][struggle][struggle][struggle][struggle]