本文介绍threejs引入glb格式模型展示
1.鼠标事件交互
2.局部放大效果
3.端口状态渲染
4.点击鼠标改变端口状态

<template>
  <!-- 三维画布 -->
  <div style="width:100%;height:100%;position:relative;">
    <div id="three_div" ref="draw" class="draw" />
    <div v-if="loadSuccess" class="loadingBox">
      <div class="progress">
        <div class="progress-bar progress-bar-danger progress-bar-striped active" :style="{width:progress}">
          <div class="progress-value">{{ progress }}</div>
        </div>
      </div>
    </div>
  </div>
</template>

js代码

<script>
  import * as THREE from 'three' // 三维
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js' // 控制器
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' // 控制器
  import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' // 控制器
  export default {
    props: {
      faceList: {
        type: Array,
        default: () => []
      },
      capacity: {
        type: [Number, String],
        default: 0
      }
    },
    data() {
      return {
        // 声明渲染器
        renderer: '',
        // 声明相机
        camera: '',
        // 声明场景
        scene: '',
        // 声明几何体
        geometry: '',
        // 声明材质
        material: '',
        // 声明网格
        mesh: '',
        // 声明相机控制器
        controls: '',
        // 画布大小
        clientWidth: '',
        clientHeight: '',
        // 模型组
        faceGroup: [],
        // 记住选中的端口
        selectedPort: null,
        progress: 0,
        loadSuccess: true
      }
    },
    computed: {
      portStatus() {
        let img = new THREE.TextureLoader().load('/res/glb/glbstatus/white.jpg')
        if (this.capacity === 96 || this.capacity === 24) {
          img = new THREE.TextureLoader().load('/res/glb/glbstatus/blue48.jpg')
        }
        return {
          IDLE: {
            img: img,
            name: this.$t('PORT_STATUS_IDLE'),
            typeName: 'IDLE'
          },
          PROCESS: {
            img: new THREE.TextureLoader().load('/res/glb/glbstatus/process.jpg'),
            name: this.$t('SERVICE_AVAILABLE'),
            typeName: 'PROCESS'
          },
          MAIN: {
            img: new THREE.TextureLoader().load('/res/glb/glbstatus/occupy.jpg'),
            name: this.$t('SERVICE_UNAVAILABLE'),
            typeName: 'MAIN'
          },
          BACKUP: {
            img: new THREE.TextureLoader().load('/res/glb/glbstatus/backup.jpg'),
            name: this.$t('PORT_STATUS_BACKUP'),
            typeName: 'BACKUP'
          },
          DAMAGE: {
            img: new THREE.TextureLoader().load('/res/glb/glbstatus/damage.jpg'),
            name: this.$t('PORT_STATUS_DAMAGE'),
            typeName: 'DAMAGE'
          },
          OCCUPY: {
            img: new THREE.TextureLoader().load('/res/glb/glbstatus/damage1.jpg'),
            name: this.$t('LINK_FAILURE'),
            typeName: 'OCCUPY'
          },
          CONN: {
            img: new THREE.TextureLoader().load('/res/glb/glbstatus/occupy.jpg'),
            name: this.$t('LINK_FAILURE'),
            typeName: 'CONN'
          }
        }
      }
    },
    watch: {
      faceList(val) {
        console.log(val)
      }
    },
    mounted() {
      this.init()
    },
    beforeDestroy() {
      window.removeEventListener('resize', this.changeSize)
      window.removeEventListener('click', this.portClick)
      window.removeEventListener('wheel', this.handleMouseWheel)
      this.removeObj(this.scene)
      this.renderer && this.renderer.dispose()
      this.renderer.forceContextLoss()
      this.renderer.domElement = null
      this.renderer.content = null
      this.renderer = null
      cancelAnimationFrame(this._animate)
      THREE.Cache.clear()
    },
    methods: {
      init() {
        // 初始化渲染器
        this.initRenderer()
        // 初始化场景
        this.initScene()
        // 初始化相机
        this.initCamera()
        // 引入模型
        this.initgltfLoader()
        // 初始化光源
        this.initLight()
        // 初始化动画
        this.animate()
        // 添加事件
        this.addmeth()
      },
      // 初始化渲染器
      initRenderer() {
        // 实例化渲染器
        this.renderer = new THREE.WebGLRenderer({
          antialias: true, // 是否开启抗锯齿
          alpha: true // 是否可以将背景色设置为透明
        })
        // 设置渲染区域尺寸
        this.renderer.setSize(
          this.$refs.draw.offsetWidth,
          this.$refs.draw.offsetHeight
        )
        // 告诉渲染器需要阴影效果
        this.renderer.shadowMap.enabled = true
        this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
        // 设置背景色
        this.renderer.setClearColor(0x000000, 0) // 设置背景颜色
        this.$refs.draw.appendChild(this.renderer.domElement)
      },
      // 初始化场景
      initScene() {
        // 实例化场景
        this.scene = new THREE.Scene()
        // 红线是X轴,绿线是Y轴,蓝线是Z轴
        var axesHelper = new THREE.AxesHelper(0)
        this.scene.add(axesHelper)
      },
      // 初始化相机
      initCamera() {
        this.clientWidth = this.$refs.draw.clientWidth
        this.clientHeight = this.$refs.draw.clientHeight
        const k = this.clientWidth / this.clientHeight // 窗口宽高比
        // 参数:PerspectiveCamera
        // fov — 垂直视野角度(从底部到顶部,以度为单位。 默认值为50。)
        // aspect — 长宽比(一般为渲染器、画布长宽比,默认为1)
        // near — 近距离(默认值为0.1)
        // far — 远距离(默认为2000,必须大于近距离的值。)
        this.camera = new THREE.PerspectiveCamera(45, k, 0.1, 1000)
        this.camera.position.set(30, 30, 100)
        // 创建相机控制器
        this.controls = new OrbitControls(this.camera, this.renderer.domElement)
        this.controls.enableZoom = false
      },
      // 引入外部模型 gltf
      initgltfLoader() {
        const gltfLoader = new GLTFLoader()
        const dracoLoader = new DRACOLoader()
        dracoLoader.setDecoderPath('/res/glb/draco/gltf/')// 这个是加载draco算法,这样才能解析压缩后的gltf模型格式.
        gltfLoader.setDRACOLoader(dracoLoader)
        // 引入默认纹理
        gltfLoader.load('/res/glb/device3d/AFS' + this.capacity + '.glb', (gltf) => {
          const model = gltf.scene
          console.log(model)
          if (this.capacity === 24) {
            model.scale.set(0.3, 0.3, 0.3) // 缩放
            model.position.set(0, -5, 0)
          } else if (this.capacity === 96) {
            model.scale.set(0.7, 0.7, 0.7) // 缩放
            model.position.set(0, -30, 0)
          } else {
            model.scale.set(0.7, 0.7, 0.7) // 缩放
            model.position.set(0, -20, 0)
          }

          const portGroup = model.children && model.children[0].children.find(item => item.name === 'PORT_GROUP')
          if (!portGroup) return
          const portList = portGroup.children
          portList.forEach(item => {
            item.nameIndex = item.name.replace(/[^\d]/g, '')
          })
          portList.sort((a, b) => { return a.nameIndex - b.nameIndex })

          this.faceGroup[0] = portList.filter(item => item.name.includes('Port_A'))
          this.faceGroup[1] = portList.filter(item => item.name.includes('Port_B'))

          this.faceList.forEach((item, index) => {
            item.portList.forEach((port, k) => {
              this.faceGroup[index][k].portData = port
              this.faceGroup[index][k].portId = port.portInfo.portId
            })
          })
          setTimeout(() => {
            // 给端口重新赋纹理
            this.faceGroup.forEach(item => {
              item.forEach(port => {
                const texture = this.portStatus[port.portData.portInfo.status]
                port.children.forEach(mesh => {
                  mesh.material = new THREE.MeshPhongMaterial({ color: 0xFFFFFF })
                  mesh.material.map = texture.img
                  mesh.textureName = texture.typeName
                })
              })
            })
          }, 0)
          this.scene.add(model)
        }, (xhr) => {
          const percentage = Math.floor(xhr.loaded / xhr.total * 100)
          this.progress = percentage + '%'
          if (percentage >= 99) {
            setTimeout(() => {
              this.loadSuccess = false
            }, 2000)
          }
        })
      },
      // 添加光源
      initLight() {
        // 全局环境光
        const ambientLight = new THREE.AmbientLight('#ffffff', 0.1)
        this.scene.add(ambientLight)
        // 点光源
        const pointLight = new THREE.PointLight('#ffffff', 1)
        pointLight.position.set(100, 500, 500)
        this.camera.add(pointLight)

        this.scene.add(this.camera)
      },
      addmeth() {
        // 监听窗口尺寸变化
        window.addEventListener('resize', this.changeSize, false)
        window.addEventListener('click', this.portClick, false)
        window.addEventListener('wheel', this.handleMouseWheel, false)
      },
      handleMouseWheel(event) {
        // 设置相机缩放比数值越大缩放越明显
        const factor = 2
        // 从鼠标位置转化为webgl屏幕坐标位置
        const glScreenX = (event.clientX / this.controls.domElement.width) * 2 - 1
        const glScreenY = -(event.clientY / this.controls.domElement.height) * 2 + 1
        const vector = new THREE.Vector3(glScreenX, glScreenY, 0)
        // 从屏幕向量转为3d空间向量
        vector.unproject(this.controls.object)
        // 相机偏移量
        vector.sub(this.controls.object.position).setLength(factor)
        if (event.deltaY < 0) {
          this.controls.object.position.add(vector)
          this.controls.target.add(vector)
        } else {
          this.controls.object.position.sub(vector)
          this.controls.target.sub(vector)
        }
        this.controls.update()
      },
      // 运行动画
      animate() {
        this._animate = requestAnimationFrame(this.animate.bind(this)) // 循环调用函数
        // 刷新相机控制器
        this.controls.update()
        this.renderer.render(this.scene, this.camera)
      },
      // 点击端口
      portClick(event) {
        // 保持原事件
        event.preventDefault()
        this.getIntersects(event.layerX, event.layerY)
      },
      getIntersects(layerX, layerY) {
        // 建立射线
        const raycaster = new THREE.Raycaster()
        // // 建立一个空物体
        const mouseVector = new THREE.Vector3()
        const x = (layerX / this.clientWidth) * 2 - 1
        const y = -(layerY / this.clientHeight) * 2 + 1
        mouseVector.set(x, y, 1)
        raycaster.setFromCamera(mouseVector, this.camera)
        raycaster.params.Line.threshold = 0.01
        const intersections = raycaster.intersectObjects(this.scene.children, true)
        let selectedObject = null // 被选中的模型

        if (intersections.length > 0) {
          for (var i = 0; i < intersections.length; i++) {
            // 遍历线相交模型
            if (intersections[i].object instanceof THREE.Mesh) {
              // 取第一个(距离最近)的相交Mesh类型模型
              // 如果要排除地面等参照模型,也可在此处添加判断条件
              selectedObject = intersections[i].object
              break
            }
          }
        }
        if (selectedObject) {
          const selected = new THREE.TextureLoader().load('/res/glb/glbstatus/selected.jpg')
          if (selectedObject.parent.name.includes('Port_')) {
            if (this.selectedPort && this.selectedPort.parent.portData.connPortId !== selectedObject.parent.portData.connPortId) {
              this.faceGroup.forEach(item => {
                item.forEach(port => {
                  const texture = this.portStatus[port.portData.portInfo.status]
                  if (this.selectedPort.parent.portData.connPortId === port.portData.connPortId ||
                    this.selectedPort.parent.portData.connPortId === port.portData.portInfo.portId) {
                    port.children.forEach(mesh => {
                      mesh.material.map = texture.img
                      mesh.textureName = texture.typeName
                    })
                  }
                })
              })
            }
            selectedObject.parent.children.forEach(item => {
              if (item.textureName !== 'SELECTED') {
                this.curTexture = item.textureName
                item.material.map = selected
                item.textureName = 'SELECTED'
                this.selectedPort = selectedObject
                this.$emit('portClick', selectedObject.parent.portData)
              } else {
                item.material.map = this.portStatus[this.curTexture].img
                item.textureName = this.curTexture
                this.$emit('portClick', null)
              }
            })
            this.faceGroup.forEach(item => {
              const obj = item.find(port => port.portId === selectedObject.parent.portData.connPortId)
              if (obj) {
                obj.children.forEach(mesh => {
                  if (mesh.textureName !== 'SELECTED') {
                    mesh.material.map = selected
                    mesh.textureName = 'SELECTED'
                  } else {
                    mesh.material.map = this.portStatus[this.curTexture].img
                    mesh.textureName = this.curTexture
                  }
                })
              }
            })
          }
          // console.log(selectedObject)
        }
      },
      // 监听尺寸变化
      changeSize() {
        // 重置渲染器输出画布canvas尺寸
        this.renderer.setSize(
          this.$refs.draw.offsetWidth,
          this.$refs.draw.offsetHeight
        )
        this.clientWidth = this.$refs.draw.clientWidth
        this.clientHeight = this.$refs.draw.clientHeight
        const k = this.clientWidth / this.clientHeight // 窗口宽高比
        // 重置相机投影的相关参数
        this.camera.aspect = k
        // 如果相机的一些属性发生了变化,
        // 需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
        this.camera.updateProjectionMatrix()
      },
      removeObj(obj) {
        let arr = obj.children.filter(x => x)
        arr.forEach(item => {
          if (item.children.length) {
            this.removeObj(item)
          } else {
            item.clear()
          }
        })
        obj.clear()
        arr = null
      }
    }
  }
</script>

css代码

<style lang="scss">
#three_div{
 width:100%;
 height: 100%;
}

//进度条
.loadingBox{
  position: absolute;
  top:0;
  left:0;
  right: 0;
  bottom: 0;
  background: #020721;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1;
}
.progress {
    height: 17px;
    background: #262626;
    padding: 3px;
    overflow: visible;
    border-radius: 20px;
    border-top: 1px solid #000;
    border-bottom: 1px solid #7992a8;
    margin-top: 50px;
    width: 300px;

    .progress-bar {
        border-radius: 20px;
        position: relative;
        animation: animate-positive 2s;
        float: left;
        width: 0;
        height: 100%;
        font-size: 12px;
        line-height: 20px;
        color: #fff;
        text-align: center;
        background-color: #2962f9;
        -webkit-transition: width .6s ease;
        -o-transition: width .6s ease;
        transition: width .6s ease;

    }

    .active {
        animation: reverse stripes 0.40s linear infinite, animate-positive 2s;
    }

    .progress-value {
        display: none;
        padding: 3px 7px;
        font-size: 13px;
        color: #fff;
        border-radius: 4px;
        background: #191919;
        border: 1px solid #000;
        position: absolute;
        top: -40px;
        right: -10px;
    }

    .progress-value:after {
        content: "";
        border-top: 10px solid #191919;
        border-left: 10px solid transparent;
        border-right: 10px solid transparent;
        position: absolute;
        bottom: -6px;
        left: 26%;
    }
}

.progress-bar-striped {
    background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
    background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
    background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
    -webkit-background-size: 40px 40px;
    background-size: 40px 40px;
}

@-webkit-keyframes stripes {
    from {
        background-position: 40px 0
    }

    to {
        background-position: 0 0
    }
}

@-o-keyframes stripes {
    from {
        background-position: 40px 0
    }

    to {
        background-position: 0 0
    }
}

@keyframes stripes {
    from {
        background-position: 40px 0
    }

    to {
        background-position: 0 0
    }
}

@-webkit-keyframes animate-positive {
    0% {
        width: 0;
    }
}

@keyframes animate-positive {
    0% {
        width: 0;
    }
}

</style>