Vue+Three.js实现GLTF模型部件拾取,点击高亮变色+显示部件名称

302 阅读5分钟

实现效果:

  1. 导入高精度导弹车 GLTF 三维模型。
  2. 鼠标点击模型任意部位,单独高亮变红、半透明。
  3. 自动识别当前点击部件,右上角展示中文名称(导弹体/主车体/底盘架/轮胎/发射架)。
  4. 再次点击新部位,自动取消上一次高亮,切换选中效果。

演示动态图效果如下:

dao.gif

核心原理讲解:

1. 射线拾取 Raycaster

ThreeJS 模型点击选中唯一通用方案:射线投射。

简单理解:鼠标点击屏幕 → 生成一条从相机射出的射线 → 穿透场景 → 检测碰到了哪个模型Mesh → 实现选中。

2. GLTF模型结构特点

复杂模型(导弹车、设备、机械模型)都是由多个Mesh子部件组成:

主车体、轮胎、发射架、导弹体、底盘架都是独立Mesh,自带独立 name 属性,我们可以通过 name 映射中文部件名。

3. 高亮逻辑

  • 点击新部件:旧部件材质恢复原色、不透明。
  • 当前点击部件:改为红色、半透明高亮。

完整代码

1. template 结构:

<template>
  <div class="three-container" ref="content">
    <!-- three渲染画布挂载容器 -->
    <div class="three" id="three"></div>
    <!-- 右上角显示选中部件名称面板 -->
    <div class="show-block">
      <div class="show-title">选中部分:</div>
      <div class="show-content">{{ chooseText }}</div>
    </div>
  </div>
</template>

2. script 代码:

<script>
import * as THREE from 'three'
// gltf模型加载器,用来导入导弹车gltf模型
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
// 轨道控制器:鼠标拖拽旋转、缩放、平移模型
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

export default {
  beforeDestroy() {
    // 组件销毁生命周期,执行资源释放
    this.leaveDestory()
  },
  mounted() {
    this.$nextTick(() => {
      // dom渲染完成后初始化threejs场景
      this.init()
      // 浏览器窗口大小变化监听,适配画布
      window.onresize = () => {
        this.resize()
      }
    })
  },
  data () {
    return {
      // 右上角展示选中部件的文字
      chooseText: '未选中任何部分'
    }
  },
  methods: {
    /**
     * 组件销毁,释放threejs全部资源,防止内存泄漏
     * 清除定时器、销毁几何体材质、销毁渲染器、控制器、场景
     */
    leaveDestory() {
      document.onclick = null
      window.missileMarsLoad = null
      // 停止动画帧循环
      if (window.myMar) {
        cancelAnimationFrame(window.myMar)
        window.myMar = 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.model = null
        window.selectObj = null
        // 销毁webgl渲染上下文
        window.renderer.dispose()
        window.renderer.forceContextLoss()
        window.renderer.domElement = null
        window.renderer = null
        window.camera = null
        // 销毁轨道控制器
        window.controls.dispose()
        window.controls = 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)
    },

    /**
     * threejs初始化入口
     * 创建场景、相机、渲染器、灯光、控制器、加载导弹车模型
     */
    init() {
      let width = this.$refs.content.offsetWidth
      let height = this.$refs.content.offsetHeight
      // 1.创建3D场景容器
      window.scene = new THREE.Scene()
      // 2.创建透视相机,45度视场角,近远裁剪面0.1~1000
      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(0x2B2F30, 1.0)
      window.renderer.setSize(width, height)
      // 开启阴影
      window.renderer.shadowMapEnabled = true
      // 设置相机初始位置
      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.初始化鼠标轨道控制器
      window.controls = new OrbitControls(window.camera, window.renderer.domElement)
      window.controls.target.set(0.5, 0, 0)
      window.controls.update()
      window.controls.enablePan = true // 允许平移
      window.controls.enableDamping = true // 开启阻尼拖拽,滑动更顺滑

      // 5.添加环境半球光,柔和环境照明
      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)

      // 6.实例化gltf加载器,加载导弹发射车模型
      let loader = new GLTFLoader()
      window.missileMarsLoad = true
      loader.load('static/models/missileLauncher.gltf', (gltf) => {
        if (window.missileMarsLoad) {
          // 把解析完成的模型加入3D场景
          window.model = gltf.scene
          window.scene.add(window.model)
          window.renderer.render(window.scene, window.camera)
          // 给画布绑定鼠标点击事件,触发部件选中逻辑
          document.onclick = (e) => {
            this.choose(e)
          }
        }
      })
      // 启动渲染动画循环
      this.animate()
      // 将渲染生成的canvas dom挂载到页面div
      document.getElementById('three').appendChild(window.renderer.domElement)
    },

    /**
     * 鼠标点击拾取模型部件核心函数(射线检测)
     * 功能:点击模型,选中mesh变红半透明高亮,右上角显示部件中文名称
     * event 鼠标点击事件对象
     */
    choose(event) {
      event.preventDefault()
      // 获取鼠标屏幕坐标
      var Sx = event.clientX;
      var Sy = event.clientY;
      // 将浏览器屏幕坐标 转为 threejs标准化设备坐标(-1 ~ +1)
      var x = (Sx / window.innerWidth) * 2 - 1;
      var y = -(Sy / window.innerHeight) * 2 + 1;

      // 创建射线投射器:从相机位置,穿过鼠标点击的屏幕点发射射线
      var raycaster = new THREE.Raycaster();
      raycaster.setFromCamera(new THREE.Vector2(x, y), window.camera);
      // 射线与导弹车模型做碰撞检测,返回相交到的物体数组
      var intersects = raycaster.intersectObjects([window.model]);
			
      // 判断是否点击到模型物体
      if (intersects.length > 0) {
        // 如果上一次已经选中物体,把旧物体材质恢复原始状态
        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
        }
        // 保存当前点击相交的物体
        window.selectObj = intersects[0]
        // 修改当前选中部件材质:半透明+红色高亮
        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)');
        // 获取模型mesh的name属性,做部件名称映射
        let name = window.selectObj.object.name
        console.log(name)
        // 根据模型内部mesh名字,映射为业务中文名称,赋值给页面显示
        if (name === 'defaultMaterial_9') {
          this.chooseText = '导弹体'
        } else if (name === 'defaultMaterial') {
          this.chooseText = '主车体'
        } else if (name === 'defaultMaterial_6') {
          this.chooseText = '底盘架'
        } else if (name === 'defaultMaterial_4' || name === 'defaultMaterial_5') {
          this.chooseText = '轮胎'
        } else if (name === 'defaultMaterial_7') {
          this.chooseText = '发射架'
        }
      }
    },

    /**
     * requestAnimationFrame动画循环
     * 持续更新控制器状态、执行场景渲染
     */
    animate() {
      window.myMar = requestAnimationFrame(this.animate)
      window.controls.update()
      window.renderer.render(window.scene, window.camera)
    }
  }
}
</script>

3. css样式代码:

.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;
}

功能详细拆解:

1. 模型加载

使用官方 GLTFLoader 加载外部模型,支持 gltf / glb 通用格式,工业模型、设备模型、场景模型全部通用。

2. 射线拾取核心逻辑

将鼠标屏幕坐标转为 WebGL 标准设备坐标,通过 Raycaster 发射射线,与模型所有子 Mesh 做碰撞检测,精准拾取最小部件。

3. 高亮切换机制

  • 每次新点击,优先恢复上一次选中部件的颜色、透明度。
  • 当前部件改为 红色半透明高亮,视觉层级非常明显。

4. 部件名称映射

复杂模型每个子Mesh都有独立name,通过判断name,前端自定义中文释义,实现“点击即识别部件”的仿真效果。

总结:

本文实现了 ThreeJS 三维模型精细化部件拾取核心能力: 模型加载 → 射线拾取 → 动态高亮变色 → 部件名称识别展示 → 内存优化销毁。

模型说明:   本文演示所用的 3D 模型来源于网络,版权归原作者所有。本文仅做 Three.js 前端技术学习演示,并未对模型本身做任何修改。