跪拜 Guibai
← Back to the summary

Click-to-Select Parts on a 3D GLTF Model with Three.js and Vue

Implementation Effect:

  1. Import a high-precision missile-launcher GLTF 3D model.
  2. Click any part of the model with the mouse to individually highlight it in red and make it semi-transparent.
  3. Automatically identify the currently clicked part and display its Chinese name in the upper right corner (missile body / main vehicle body / chassis frame / tire / launch rack).
  4. Clicking a new part automatically cancels the previous highlight and switches the selection effect.

The demo animation is as follows:

dao.gif

Core Principle Explanation:

1. Raycaster Picking

The only universal solution for click-selection on ThreeJS models: raycasting.

Simple understanding: Mouse clicks on the screen → generates a ray emitted from the camera → penetrates the scene → detects which model Mesh it hits → achieves selection.

2. GLTF Model Structure Characteristics

Complex models (missile vehicles, equipment, mechanical models) are composed of multiple Mesh sub-components:

The main vehicle body, tires, launch rack, missile body, and chassis frame are all independent Meshes with their own name attributes, which we can use to map to Chinese part names.

3. Highlight Logic

Complete Code

1. Template Structure:

<template>
  <div class="three-container" ref="content">
    <!-- three rendering canvas mount container -->
    <div class="three" id="three"></div>
    <!-- Upper right corner panel displaying the selected part name -->
    <div class="show-block">
      <div class="show-title">Selected Part:</div>
      <div class="show-content">{{ chooseText }}</div>
    </div>
  </div>
</template>

2. Script Code:

<script>
import * as THREE from 'three'
// gltf model loader, used to import the missile launcher gltf model
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// Orbit controls: mouse drag to rotate, zoom, and pan the model
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

export default {
  beforeDestroy() {
    // Component destruction lifecycle, execute resource release
    this.leaveDestory()
  },
  mounted() {
    this.$nextTick(() => {
      // Initialize the threejs scene after the DOM is rendered
      this.init()
      // Listen for browser window size changes to adapt the canvas
      window.onresize = () => {
        this.resize()
      }
    })
  },
  data () {
    return {
      // Text displayed in the upper right corner for the selected part
      chooseText: 'No part selected'
    }
  },
  methods: {
    /**
     * Component destruction, release all threejs resources to prevent memory leaks
     * Clear timers, destroy geometries and materials, destroy renderer, controls, scene
     */
    leaveDestory() {
      document.onclick = null
      window.missileMarsLoad = null
      // Stop the animation frame loop
      if (window.myMar) {
        cancelAnimationFrame(window.myMar)
        window.myMar = null
      }
      if (window.scene) {
        // Traverse the scene, release all mesh geometries and materials from video memory
        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])
        }
        // Clear global cached variables
        window.model = null
        window.selectObj = null
        // Destroy the webgl rendering context
        window.renderer.dispose()
        window.renderer.forceContextLoss()
        window.renderer.domElement = null
        window.renderer = null
        window.camera = null
        // Destroy orbit controls
        window.controls.dispose()
        window.controls = null
        window.onresize = null
        window.scene.clear()
        window.scene = null
      }
    },

    /**
     * Window size change, update camera aspect ratio and canvas size for responsive display
     */
    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)
    },

    /**
     * Threejs initialization entry point
     * Create scene, camera, renderer, lights, controls, load the missile launcher model
     */
    init() {
      let width = this.$refs.content.offsetWidth
      let height = this.$refs.content.offsetHeight
      // 1. Create 3D scene container
      window.scene = new THREE.Scene()
      // 2. Create perspective camera, 45-degree field of view, near/far clipping planes 0.1~1000
      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 })
      // Adapt to device pixel ratio for sharp display on high-DPI screens
      window.renderer.setPixelRatio(window.devicePixelRatio)
      // Set canvas background color to match the dark gray background in screenshots
      window.renderer.setClearColor(0x2B2F30, 1.0)
      window.renderer.setSize(width, height)
      // Enable shadows
      window.renderer.shadowMapEnabled = true
      // Set initial camera position
      window.camera.position.x = -0.649176390603234
      window.camera.position.y = -0.013178514581831093
      window.camera.position.z = 1.3688883296014047
      window.camera.lookAt(window.scene.position)

      // 4. Initialize mouse orbit controls
      window.controls = new OrbitControls(window.camera, window.renderer.domElement)
      window.controls.target.set(0.5, 0, 0)
      window.controls.update()
      window.controls.enablePan = true // Allow panning
      window.controls.enableDamping = true // Enable damping for smoother dragging

      // 5. Add ambient hemisphere light for soft environment lighting
      let hemiLight = new THREE.HemisphereLight(0xffffff, 0xffffff, 0.3)
      hemiLight.position.set(0, 500, 0)
      window.scene.add(hemiLight)
      // Spotlight to produce shadow effects
      let spotLight = new THREE.SpotLight(0xffffff)
      spotLight.position.set(-40, 60, -10)
      spotLight.castShadow = true
      window.scene.add(spotLight)

      // 6. Instantiate gltf loader, load the missile launcher model
      let loader = new GLTFLoader()
      window.missileMarsLoad = true
      loader.load('static/models/missileLauncher.gltf', (gltf) => {
        if (window.missileMarsLoad) {
          // Add the parsed model to the 3D scene
          window.model = gltf.scene
          window.scene.add(window.model)
          window.renderer.render(window.scene, window.camera)
          // Bind mouse click event to the canvas to trigger part selection logic
          document.onclick = (e) => {
            this.choose(e)
          }
        }
      })
      // Start the render animation loop
      this.animate()
      // Append the generated canvas DOM to the page div
      document.getElementById('three').appendChild(window.renderer.domElement)
    },

    /**
     * Core function for mouse click picking of model parts (raycasting)
     * Function: Click the model, the selected mesh turns red and semi-transparent, and the Chinese part name is displayed in the upper right corner
     * event Mouse click event object
     */
    choose(event) {
      event.preventDefault()
      // Get mouse screen coordinates
      var Sx = event.clientX;
      var Sy = event.clientY;
      // Convert browser screen coordinates to threejs normalized device coordinates (-1 ~ +1)
      var x = (Sx / window.innerWidth) * 2 - 1;
      var y = -(Sy / window.innerHeight) * 2 + 1;

      // Create a raycaster: emit a ray from the camera position through the mouse click screen point
      var raycaster = new THREE.Raycaster();
      raycaster.setFromCamera(new THREE.Vector2(x, y), window.camera);
      // Perform collision detection between the ray and the missile launcher model, return an array of intersected objects
      var intersects = raycaster.intersectObjects([window.model]);

      // Check if a model object was clicked
      if (intersects.length > 0) {
        // If an object was previously selected, restore the old object's material to its original state
        if (window.selectObj) {
          window.selectObj.object.material.transparent = true;
          window.selectObj.object.material.opacity = 1;
          window.selectObj.object.material.color = new THREE.Color('rgb(255, 255, 255)');
          window.selectObj = null
        }
        // Save the currently clicked intersected object
        window.selectObj = intersects[0]
        // Modify the currently selected part's material: semi-transparent + red highlight
        window.selectObj.object.material.transparent = true;
        window.selectObj.object.material.opacity = 0.7;
        window.selectObj.object.material.color = new THREE.Color('rgb(255, 0, 0)');
        // Get the model mesh's name attribute for part name mapping
        let name = window.selectObj.object.name
        console.log(name)
        // Map the model's internal mesh name to a business Chinese name and assign it for page display
        if (name === 'defaultMaterial_9') {
          this.chooseText = 'Missile Body'
        } else if (name === 'defaultMaterial') {
          this.chooseText = 'Main Vehicle Body'
        } else if (name === 'defaultMaterial_6') {
          this.chooseText = 'Chassis Frame'
        } else if (name === 'defaultMaterial_4' || name === 'defaultMaterial_5') {
          this.chooseText = 'Tire'
        } else if (name === 'defaultMaterial_7') {
          this.chooseText = 'Launch Rack'
        }
      }
    },

    /**
     * requestAnimationFrame animation loop
     * Continuously update controller state and perform scene rendering
     */
    animate() {
      window.myMar = requestAnimationFrame(this.animate)
      window.controls.update()
      window.renderer.render(window.scene, window.camera)
    }
  }
}
</script>

3. CSS Style Code:

.three-container {
    width: 100%;
    height: 100%;
    position: relative;
    overflow: hidden;
}

.three {
    width: 100%;
    height: 100%;
    position: relative;
    z-index: 1;
}

.show-block {
    width: 300px;
    box-sizing: border-box;
    padding: 20px;
    position: absolute;
    right: 20px;
    top: 20px;
    z-index: 2;
    background-color: rgba(31, 31, 31, 0.8);
    color: #FFF;
}

.show-title {
    font-size: 22px;
    line-height: 22px;
    font-weight: bold;
}

.show-content {
    font-size: 20px;
    line-height: 20px;
    margin-top: 20px;
}

Detailed Feature Breakdown:

1. Model Loading

Uses the official GLTFLoader to load external models, supporting the universal gltf/glb formats. Applicable to industrial models, equipment models, and scene models.

2. Raycaster Picking Core Logic

Converts mouse screen coordinates to WebGL normalized device coordinates, emits a ray via Raycaster, performs collision detection with all sub-Meshes of the model, and accurately picks the smallest part.

3. Highlight Switching Mechanism

4. Part Name Mapping

Each sub-Mesh of a complex model has an independent name. By checking the name, the frontend customizes a Chinese definition, achieving a simulation effect of "click to identify the part."

Summary:

This article implements the core capability of fine-grained part picking for ThreeJS 3D models: Model loading → Raycaster picking → Dynamic highlight color change → Part name identification and display → Memory optimization and destruction.

Model Note: The 3D model used for demonstration in this article is sourced from the internet, and its copyright belongs to the original author. This article is only for Three.js frontend technology learning and demonstration, and no modifications have been made to the model itself.