Click-to-Select Parts on a 3D GLTF Model with Three.js and Vue
Implementation Effect:
- Import a high-precision missile-launcher GLTF 3D model.
- Click any part of the model with the mouse to individually highlight it in red and make it semi-transparent.
- 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).
- Clicking a new part automatically cancels the previous highlight and switches the selection effect.
The demo animation is as follows:
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
- Clicking a new part: The old part's material restores its original color and opacity.
- Currently clicked part: Changed to red, semi-transparent highlight.
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
- On each new click, prioritizes restoring the color and opacity of the previously selected part.
- The current part is changed to a red semi-transparent highlight, making the visual hierarchy very distinct.
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.