跪拜 Guibai
← Back to the summary

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:

car.gif

Foreword:

This article implements a complete, production-ready 3D car display page based on Vue + Three.js, with four core interactive features:

  1. Mouse orbit controls: left-click to rotate, scroll wheel to zoom the entire car, with smooth damping effect.
  2. Camera easing transition: one-click to enter/exit the driver's cabin first-person view.
  3. GLTF native animation playback: click buttons to control door opening/closing animations.
  4. Complete engineering encapsulation: automatic resource destruction, window resizing, multi-layered showroom light source optimization, no memory leaks.

Overall Functional Effects:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

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:

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.

Comments

Top 1 from juejin.cn, machine-translated. The original thread is authoritative.

oqoxud

[struggle][struggle][struggle][struggle][struggle][struggle]