3. vue2项目中使用three.js开发三维IT机房

lxf2023-03-11 10:11:01

三维IT机房可以将机房数据可视化,让企业更好的监控和管理IT 机柜

  • 前端页面对IT 机房进行三维展示
  • 当鼠标划入IT 机柜的时候,提示当前机柜的详细信息
  • 一键显示机房中过热的机柜

1. 准备一份IT机房模型

3. vue2项目中使用three.js开发三维IT机房

1-1-建模思路

  • 简化模型,能用贴图表现的细节,就用贴图。这样可提高渲染速度。
  • 将光效融入贴图中,即模型贴图后便具备光效和体感。这样在three 中就无需打灯,即可提高开发速度,亦可提高渲染效率。

1-2-建模软件

可以3d建模的软件有很多,3dsMax、ZRender、C4D 都可以。如使用3dsMax 建模和导出gltf。

1-3-模型文件

GLTF 模型文件包含了整个场景的数据,比如几何体、材质、动画、相机等。

GLTF 模型在使用起来,要比传统的obj 模型方便很多。

在导出GLTF模型后,一般会包含以下文件:

  • gltf 模型文件
  • bin文件
  • 贴图文件

3. vue2项目中使用three.js开发三维IT机房

1-4-规范模型的结构和命名

  • 在建模软件中,同一类型的模型文件可以放入一个数组里,数组可以多层嵌套。
  • 当前的机房模型比较简单,没有使用数组,所有的Mesh对象都是平展开的。

为了便于访问和区分模型,需要对模型进行规范命名,如机房中的IT机柜都是按照cabinet-001、cabinet-002、cabinet-003 命名的。

假设IT机柜的名称都是唯一的,那我们便可以基于这个名称从后端获取相应机柜的详细信息。

3. vue2项目中使用three.js开发三维IT机房

2、Vue项目 加载GLTF格式模型

2.1 安装three 相关的依赖

npm install three @types/three --save --registry=https://registry.npm.taobao.org

3. vue2项目中使用three.js开发三维IT机房

2.2 Vue项目 构建本地3D模型

3. vue2项目中使用three.js开发三维IT机房

<template>
  <div id="container"></div>
</template>

<script>
import * as Three from 'three'
let scene = null,
camera=null,
renderer=null,
mesh=null
export default {
  data () {
    return {
        
    };
  },
  methods:{
    init(){
      let container = document.getElementById('container');
      camera = new Three.PerspectiveCamera(70, container.clientWidth/container.clientHeight, 0.01, 10);
      camera.position.z = 1
      scene = new Three.Scene()
      let geometry = new Three.BoxGeometry(0.2, 0.2, 0.2);
      let material = new Three.MeshNormalMaterial();

      mesh = new Three.Mesh(geometry, material);
      scene.add(mesh);

      renderer = new Three.WebGLRenderer({antialias:true});
      renderer.setSize(container.clientWidth,container.clientHeight);
      container.appendChild(renderer.domElement);
    },
    animate(){
      requestAnimationFrame(this.animate);
      console.log(this.animate,'132')
      mesh.rotation.x += 0.01;
      mesh.rotation.y += 0.02;
      renderer.render(scene,camera); 
    }
  },
  mounted(){
      this.init()
      this.animate()
  }

}

</script>
<style scoped>
#container{
    width: 100vw;
    height: 100vh;
}
</style>

2.3 Vue项目导入外部GLTF格式模型——GLTFLoader()

2.3.1 在public文件夹放置外部模型

GLTF格式模型文件下载:pan.baidu.com/s/16KAD-E-4…

在public文件夹里建立一个models 文件夹,将之前的模型文件放进去

3. vue2项目中使用three.js开发三维IT机房

2.3.2 GLTF 模型的导入、渲染和相机变换

<template>
  <div>
    <div id="container"></div>
  </div>
</template>

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

export default {
  data() {
    return {
      mesh: null,
      camera: null,
      scene: null,
      renderer: null,
      controls: null
    }
  },
  mounted() {
    this.init()
  },
  methods: {
    // 初始化
    init() {
      this.createScene() // 创建场景
      this.loadGLTF() // 加载GLTF模型
      // this.createLight() // 创建光源
      this.createCamera() // 创建相机
      this.createRender() // 创建渲染器
      this.createControls() // 创建控件对象
      this.render() // 渲染
    },
    // 创建场景
    createScene() {
      this.scene = new THREE.Scene()
    },
    // 加载GLTF模型
    loadGLTF() {
      const loader = new GLTFLoader()
      loader.load(`./models/machineRoom.gltf`, ({ scene: { children } }) => {
        console.log(...children);
        this.scene.add(...children)
      })
    },

    // 创建光源
    createLight() {
      // 环境光
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.1) // 创建环境光
      this.scene.add(ambientLight) // 将环境光添加到场景

      const spotLight = new THREE.SpotLight(0xffffff) // 创建聚光灯
      spotLight.position.set(150, 150, 150)
      spotLight.castShadow = true
      this.scene.add(spotLight)
    },
    // 创建相机
    createCamera() {
      const element = document.getElementById('container')
      const width = element.clientWidth // 窗口宽度
      const height = element.clientHeight // 窗口高度
      const k = width / height // 窗口宽高比
      // PerspectiveCamera( fov, aspect, near, far )
      this.camera = new THREE.PerspectiveCamera(45, k, 0.1, 1000)
      this.camera.position.set(0, 10, 15) // 设置相机位置

      this.camera.lookAt(0, 0, 0) // 设置相机方向
      this.scene.add(this.camera)
    },
    // 创建渲染器
    createRender() {
      const element = document.getElementById('container')
      this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
      this.renderer.setSize(element.clientWidth, element.clientHeight) // 设置渲染区域尺寸
      // this.renderer.shadowMap.enabled = true // 显示阴影
      // this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
      this.renderer.setClearColor(0x3f3f3f, 1) // 设置背景颜色
      element.appendChild(this.renderer.domElement)
    },

    render() {
      // if (this.mesh) {
      //   this.mesh.rotation.z += 0.006
      // }
      this.renderer.render(this.scene, this.camera)
      requestAnimationFrame(this.render)
    },
    // 创建控件对象
    createControls() {
      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
    }
  }
}
</script>
<style>
#container {
  position: absolute;
  width: 100vw;
  height: 100vh;
}
</style>

3. vue2项目中使用three.js开发三维IT机房

和之前的3dsmax 图片对比一下,就会发现:贴图颜色变深了

3. 调整外部3D模型数据

3.1 分析一下模型数据

 // 加载GLTF模型
    loadGLTF() {
      const loader = new GLTFLoader()
      loader.load(`./models/machineRoom.gltf`, ({ scene: { children } }) => {
        console.log(...children);
        this.scene.add(...children)
      })
    },

分析一下children里的Mesh 对象,可以发现:所有Mesh对象的material材质都是MeshStandardMaterial 类型。

3. vue2项目中使用three.js开发三维IT机房

再分析一下material 中的map 贴图,可以发现其map贴图为 Texture 对象,其具备以下重要信息:

  • name 是贴图图片的名称。
  • flipY为false,即不对图像的y轴做翻转。
  • image图像源是ImageBitmap 类型。
  • wrapS 纹理横向重复,即THREE.RepeatWrapping。
  • wrapT 纹理纵向重复,即THREE.RepeatWrapping。

注:THREE.RepeatWrapping=1000

3. vue2项目中使用three.js开发三维IT机房

在此,我们要知道以下threejs 知识:

  • MeshStandardMaterial 材质会感光,我们不需要打光,需要将其材质换成MeshBasicMaterial
  • ImageBitmap 的图像类型是渲染效果变黑的关键原因,因此需要将其换成Image() 对象。

接下来给模型换一个材质和图像源。

3.2 修改材质和图像源

  1. 添加maps属性,用来存储纹理对象,以避免贴图的重复加载
// ES6的Map数据结构
this.maps = new Map()
  1. 在加载GLTF 的时候,用changeMat()方法修改Mesh 对象的材质
// 加载GLTF模型
    loadGLTF() {
      const loader = new GLTFLoader()
      loader.load(`./models/machineRoom.gltf`, ({ scene: { children } }) => {
        console.log(...children);
  
        children.forEach((obj) => {
          const { map, color} = obj.material 
          this.changeMat(obj, map, color)
        })
        
        this.scene.add(...children)
      })
    },
  1. 添加一个修改材质的方法changeMat()
changeMat(obj, map, color) {
  if (map) {
    obj.material = new THREE.MeshBasicMaterial({
      map: this.crtTexture(map.name)
    })
  } else {
    obj.material = new THREE.MeshBasicMaterial({color})
  }
},

changeMat() 方法的参数:

  • obj:需要修改材质的Mesh 对象
  • map:GLTF 模型里的贴图对象
  • color:GLTF 模型的颜色

其中的if 逻辑是:若Mesh模型有贴图,就为其换一个材质和贴图;否则,就换一个材质,并继承原GLTF 模型的颜色。 3. vue2项目中使用three.js开发三维IT机房

  1. 添加建立纹理对象的方法crtTexture()
crtTexture(imgName) {
      let curTexture = this.maps.get(imgName)
      if (!curTexture) {
        curTexture=new THREE.TextureLoader().load('./models/'+imgName)
        curTexture.flipY = false
        curTexture.wrapS = 1000
        curTexture.wrapT = 1000
        this.maps.set(
          imgName,
          curTexture
        )
      }
      return curTexture
    },

crtTexture() 会根据贴图名称建立Texture 纹理对象。

  • TextureLoader().load() 可以根据贴图路径,加载贴图,返回一个Texture 对象。
  • curTexture 的flipY、wrapS、wrapT是对原始GLTF 贴图的相应属性的继承。

3. vue2项目中使用three.js开发三维IT机房

这样就和3dsmax中的模型效果一致了。

3. vue2项目中使用three.js开发三维IT机房

4. 模型选择——添加鼠标事件

对IT 机柜做选择,让选中的机柜高亮

  1. 添加两个属性、三个鼠标事件
//机柜集合
cabinets: [],
//鼠标划入的机柜
curCabinet: '',
//鼠标划入机柜事件,参数为机柜对象
onMouseOverCabinet (cabinet) {console.log(cabinet);  },
//鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
onMouseMoveCabinet(x,y) {console.log(x,y); },
//鼠标划出机柜的事件
onMouseOutCabinet () { }
  1. 为maps 添加一个机柜的高亮贴图。之后鼠标划入机柜时,会将其贴图更换为高亮贴图。
mounted() {
    this.maps = new Map()
    this.crtTexture("cabinet-hover.jpg")
    // this.init()
},
  1. 在加载GLTF 模型时,若模型名称包含'cabinet',便将其存入cabinets
// 加载GLTF模型
    loadGLTF() {
      const loader = new GLTFLoader()
      loader.load(`./models/machineRoom.gltf`, ({ scene: { children } }) => {
        console.log(...children);
        
        children.forEach((obj) => {
          const { map, color} = obj.material 
          this.changeMat(obj, map, color)
          if (obj.name.includes('cabinet')) {
            this.cabinets.push(obj)
          }
        })
        
        this.scene.add(...children)
      })
    },
  1. 建立一个射线投射器,一个二维点,以避免在鼠标选择时机柜时重复实例化;

    添加选择模型的方法selectCabinet(x,y),其参数为鼠标的坐标位

selectCabinet(x, y) {
      const {cabinets,renderer,camera,maps,curCabinet}=this
      const { width, height } = renderer.domElement
      //射线投射器,可基于鼠标点和相机,在世界坐标系内建立一条射线,用于选中模型
      const raycaster = new THREE.Raycaster()
      //鼠标在裁剪空间中的点位
      const pointer = new THREE.Vector2()

      // 鼠标的canvas坐标转裁剪坐标
      pointer.set(
        (x / width) * 2 - 1,
        -(y / height) * 2 + 1,
      )
      // 基于鼠标点的裁剪坐标位和相机设置射线投射器
      raycaster.setFromCamera(
        pointer, camera
      )
      // 选择机柜
      const intersect = raycaster.intersectObjects(cabinets)[0]
      let intersectObj=intersect ? intersect.object : null
      // 若之前已有机柜被选择,且不等于当前所选择的机柜,取消之前选择的机柜的高亮
      if (curCabinet && curCabinet!== intersectObj) {
        const material =curCabinet.material
        material.setValues({
          map: maps.get('cabinet.jpg')
        })
      }
      /* 
        若当前所选对象不为空:
          触发鼠标在机柜上移动的事件。
          若当前所选对象不等于上一次所选对象:
            更新curCabinet。
            将模型高亮。
            触发鼠标划入机柜事件。
        否则若上一次所选对象存在:
          置空curCabinet。
          触发鼠标划出机柜事件。
      */
      if (intersectObj) {
        this.onMouseMoveCabinet(x,y)
        if (intersectObj !== curCabinet) {
          this.curCabinet= intersectObj
          const material = intersectObj.material
          material.setValues({
            map: maps.get('cabinet-hover.jpg')
          })
          this.onMouseOverCabinet(intersectObj)
        }
      } else if(curCabinet) {
        this.curCabinet = null
        this.onMouseOutCabinet()
      }
    },
  1. 为容器添加鼠标移动事件
<template>
  <div>
    <div id="container" @mousemove="mouseMove"></div>
  </div>
</template>
// 鼠标移动事件
mouseMove({clientX,clientY}) {
  this.selectCabinet(clientX, clientY)
},

4.1 机柜选中效果

3. vue2项目中使用three.js开发三维IT机房

<template>
  <div>
    <div id="container" @mousemove="mouseMove"></div>
  </div>
</template>

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

export default {
  data() {
    return {
      mesh: null,
      camera: null,
      scene: null,
      renderer: null,
      controls: null,
      maps: null,
      //机柜集合
      cabinets: [],
      //鼠标划入的机柜
      curCabinet: '',
      // raycaster: '',
      // pointer: '',
    }
  },
  mounted() {
    this.maps = new Map()
    this.crtTexture("cabinet-hover.jpg")
    this.init()
  },
  methods: {
    // 初始化
    init() {
      this.createScene() // 创建场景
      this.loadGLTF() // 加载GLTF模型
      // this.createLight() // 创建光源
      this.createCamera() // 创建相机
      this.createRender() // 创建渲染器
      this.createControls() // 创建控件对象
      this.render() // 渲染
      
    },
    // 创建场景
    createScene() {
      this.scene = new THREE.Scene()
    },
    // 加载GLTF模型
    loadGLTF() {
      const loader = new GLTFLoader()
      loader.load(`./models/machineRoom.gltf`, ({ scene: { children } }) => {
        console.log(...children);
        
        children.forEach((obj) => {
          const { map, color} = obj.material 
          this.changeMat(obj, map, color)
          if (obj.name.includes('cabinet')) {
            this.cabinets.push(obj)
          }
        })
        
        this.scene.add(...children)
      })
    },

    // 创建光源
    createLight() {
      // 环境光
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.1) // 创建环境光
      this.scene.add(ambientLight) // 将环境光添加到场景

      const spotLight = new THREE.SpotLight(0xffffff) // 创建聚光灯
      spotLight.position.set(150, 150, 150)
      spotLight.castShadow = true
      this.scene.add(spotLight)
    },
    // 创建相机
    createCamera() {
      const element = document.getElementById('container')
      const width = element.clientWidth // 窗口宽度
      const height = element.clientHeight // 窗口高度
      const k = width / height // 窗口宽高比
      // PerspectiveCamera( fov, aspect, near, far )
      this.camera = new THREE.PerspectiveCamera(45, k, 0.1, 1000)
      this.camera.position.set(0, 10, 15) // 设置相机位置

      this.camera.lookAt(0, 0, 0) // 设置相机方向
      this.scene.add(this.camera)
    },
    // 创建渲染器
    createRender() {
      const element = document.getElementById('container')
      this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
      this.renderer.setSize(element.clientWidth, element.clientHeight) // 设置渲染区域尺寸
      // this.renderer.shadowMap.enabled = true // 显示阴影
      // this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
      this.renderer.setClearColor(0x3f3f3f, 1) // 设置背景颜色
      element.appendChild(this.renderer.domElement)
    },

    render() {
      // if (this.mesh) {
      //   this.mesh.rotation.z += 0.006
      // }
      this.renderer.render(this.scene, this.camera)
      requestAnimationFrame(this.render)
    },
    // 创建控件对象
    createControls() {
      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
    },
    changeMat(obj, map, color) {
      if (map) {
        obj.material = new THREE.MeshBasicMaterial({
          map: this.crtTexture(map.name)
        })
      } else {
        obj.material = new THREE.MeshBasicMaterial({color})
      }
    },
    crtTexture(imgName) {
      let curTexture = this.maps.get(imgName)
      if (!curTexture) {
        curTexture=new THREE.TextureLoader().load('./models/'+imgName)
        curTexture.flipY = false
        curTexture.wrapS = 1000
        curTexture.wrapT = 1000
        this.maps.set(
          imgName,
          curTexture
        )
      }
      return curTexture
    },
    selectCabinet(x, y) {
      const {cabinets,renderer,camera,maps,curCabinet}=this
      const { width, height } = renderer.domElement
      //射线投射器,可基于鼠标点和相机,在世界坐标系内建立一条射线,用于选中模型
      const raycaster = new THREE.Raycaster()
      //鼠标在裁剪空间中的点位
      const pointer = new THREE.Vector2()

      // 鼠标的canvas坐标转裁剪坐标
      pointer.set(
        (x / width) * 2 - 1,
        -(y / height) * 2 + 1,
      )
      // 基于鼠标点的裁剪坐标位和相机设置射线投射器
      raycaster.setFromCamera(
        pointer, camera
      )
      // 选择机柜
      const intersect = raycaster.intersectObjects(cabinets)[0]
      let intersectObj=intersect ? intersect.object : null
      // 若之前已有机柜被选择,且不等于当前所选择的机柜,取消之前选择的机柜的高亮
      if (curCabinet && curCabinet!== intersectObj) {
        const material =curCabinet.material
        material.setValues({
          map: maps.get('cabinet.jpg')
        })
      }
      /* 
        若当前所选对象不为空:
          触发鼠标在机柜上移动的事件。
          若当前所选对象不等于上一次所选对象:
            更新curCabinet。
            将模型高亮。
            触发鼠标划入机柜事件。
        否则若上一次所选对象存在:
          置空curCabinet。
          触发鼠标划出机柜事件。
      */
      if (intersectObj) {
        this.onMouseMoveCabinet(x,y)
        if (intersectObj !== curCabinet) {
          this.curCabinet= intersectObj
          const material = intersectObj.material
          material.setValues({
            map: maps.get('cabinet-hover.jpg')
          })
          this.onMouseOverCabinet(intersectObj)
        }
      } else if(curCabinet) {
        this.curCabinet = null
        this.onMouseOutCabinet()
      }
    },
    // 鼠标移动事件
    mouseMove({clientX,clientY}) {
      this.selectCabinet(clientX, clientY)
    },
    //鼠标划入机柜事件,参数为机柜对象
    onMouseOverCabinet (cabinet) {console.log(cabinet);  },
    //鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
    onMouseMoveCabinet(x,y) {console.log(x,y); },
    //鼠标划出机柜的事件
    onMouseOutCabinet () { }
  }
}
</script>
<style>
#container {
  position: absolute;
  width: 100%;
  height: 100%;
}
</style>

5-信息提示

在鼠标划入IT机柜的时候,提示机柜的详细信息。

其最简单的做法就是用HTML 建立一个信息面板,当鼠标在IT机柜上移动的时候,就让其随鼠标移动。

5.1.建立信息提示板

state: {
    planePos: {
      //信息面板的位置
      left: 0,
      top:0
    },
    //信息面板的可见性
    planeDisplay: 'none',
    //机柜信息
    curCabinet: {
      //名称
      name:'Loading……',
      //温度
      temperature: 0,
      //容量
      capacity: 0,
      //服务器数量
      count:0
    }
  }
<template>
  <div>
    <div id="container" @mousemove="mouseMove">
      <div
        id='plane'
        :style="{left: state.planePos.left,top:state.planePos.top,display: state.planeDisplay}"
      >
        <p>机柜名称:{{ state.curCabinet.name }}</p>
        <p>机柜温度:{{ state.curCabinet.temperature }}°</p>
        <p>使用情况:{{ state.curCabinet.count}} / {{ state.curCabinet.capacity}}</p>
      </div>
    </div>
  </div>
</template>

设置面板样式

#plane{
  position: absolute;
  top: 0;
  left: 0;
  background-color: rgba(0,0,0,0.5);
  color: #fff;
  padding: 0 18px;
  transform: translate(12px,-100%);
  display: none;
}

5.2.设置信息面板的可见性和位置

根据绑定在机柜对象上的鼠标事件,设置信息面板的可见性和位置

//鼠标划入机柜事件,参数为机柜对象
onMouseOverCabinet (cabinet) {
  console.log(cabinet);
  this.state.planeDisplay = 'block'
},
//鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
onMouseMoveCabinet(x,y) {
  console.log(x,y);
  this.state.planePos.left = x + 'px'
  this.state.planePos.top = y + 'px'
},
//鼠标划出机柜的事件
onMouseOutCabinet () { 
  this.state.planeDisplay = 'none'
}

5.3 信息提示的页面效果

3. vue2项目中使用three.js开发三维IT机房

<template>
  <div>
    <div id="container" @mousemove="mouseMove">
      <div
        id='plane'
        :style="{left: state.planePos.left,top:state.planePos.top,display: state.planeDisplay}"
      >
        <p>机柜名称:{{ state.curCabinet.name }}</p>
        <p>机柜温度:{{ state.curCabinet.temperature }}°</p>
        <p>使用情况:{{ state.curCabinet.count}} / {{ state.curCabinet.capacity}}</p>
      </div>
    </div>
  </div>
</template>

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

export default {
  data() {
    return {
      mesh: null,
      camera: null,
      scene: null,
      renderer: null,
      controls: null,
      maps: null,
      //机柜集合
      cabinets: [],
      //鼠标划入的机柜
      curCabinet: '',
      state: {
        planePos: {
          //信息面板的位置
          left: 0,
          top:0
        },
        //信息面板的可见性
        planeDisplay: 'none',
        //机柜信息
        curCabinet: {
          //名称
          name:'Loading……',
          //温度
          temperature: 0,
          //容量
          capacity: 0,
          //服务器数量
          count:0
        }
      }
    }
  },
  mounted() {
    this.maps = new Map()
    this.crtTexture("cabinet-hover.jpg")
    this.init()
  },
  methods: {
    // 初始化
    init() {
      this.createScene() // 创建场景
      this.loadGLTF() // 加载GLTF模型
      // this.createLight() // 创建光源
      this.createCamera() // 创建相机
      this.createRender() // 创建渲染器
      this.createControls() // 创建控件对象
      this.render() // 渲染
      
    },
    // 创建场景
    createScene() {
      this.scene = new THREE.Scene()
    },
    // 加载GLTF模型
    loadGLTF() {
      const loader = new GLTFLoader()
      loader.load(`./models/machineRoom.gltf`, ({ scene: { children } }) => {
        console.log(...children);
        
        children.forEach((obj) => {
          const { map, color} = obj.material 
          this.changeMat(obj, map, color)
          if (obj.name.includes('cabinet')) {
            this.cabinets.push(obj)
          }
        })
        
        this.scene.add(...children)
      })
    },

    // 创建光源
    createLight() {
      // 环境光
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.1) // 创建环境光
      this.scene.add(ambientLight) // 将环境光添加到场景

      const spotLight = new THREE.SpotLight(0xffffff) // 创建聚光灯
      spotLight.position.set(150, 150, 150)
      spotLight.castShadow = true
      this.scene.add(spotLight)
    },
    // 创建相机
    createCamera() {
      const element = document.getElementById('container')
      const width = element.clientWidth // 窗口宽度
      const height = element.clientHeight // 窗口高度
      const k = width / height // 窗口宽高比
      // PerspectiveCamera( fov, aspect, near, far )
      this.camera = new THREE.PerspectiveCamera(45, k, 0.1, 1000)
      this.camera.position.set(0, 10, 15) // 设置相机位置

      this.camera.lookAt(0, 0, 0) // 设置相机方向
      this.scene.add(this.camera)
    },
    // 创建渲染器
    createRender() {
      const element = document.getElementById('container')
      this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
      this.renderer.setSize(element.clientWidth, element.clientHeight) // 设置渲染区域尺寸
      // this.renderer.shadowMap.enabled = true // 显示阴影
      // this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
      this.renderer.setClearColor(0x3f3f3f, 1) // 设置背景颜色
      element.appendChild(this.renderer.domElement)
    },

    render() {
      // if (this.mesh) {
      //   this.mesh.rotation.z += 0.006
      // }
      this.renderer.render(this.scene, this.camera)
      requestAnimationFrame(this.render)
    },
    // 创建控件对象
    createControls() {
      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
    },
    changeMat(obj, map, color) {
      if (map) {
        obj.material = new THREE.MeshBasicMaterial({
          map: this.crtTexture(map.name)
        })
      } else {
        obj.material = new THREE.MeshBasicMaterial({color})
      }
    },
    crtTexture(imgName) {
      let curTexture = this.maps.get(imgName)
      if (!curTexture) {
        curTexture=new THREE.TextureLoader().load('./models/'+imgName)
        curTexture.flipY = false
        curTexture.wrapS = 1000
        curTexture.wrapT = 1000
        this.maps.set(
          imgName,
          curTexture
        )
      }
      return curTexture
    },
    selectCabinet(x, y) {
      const {cabinets,renderer,camera,maps,curCabinet}=this
      const { width, height } = renderer.domElement
      //射线投射器,可基于鼠标点和相机,在世界坐标系内建立一条射线,用于选中模型
      const raycaster = new THREE.Raycaster()
      //鼠标在裁剪空间中的点位
      const pointer = new THREE.Vector2()

      // 鼠标的canvas坐标转裁剪坐标
      pointer.set(
        (x / width) * 2 - 1,
        -(y / height) * 2 + 1,
      )
      // 基于鼠标点的裁剪坐标位和相机设置射线投射器
      raycaster.setFromCamera(
        pointer, camera
      )
      // 选择机柜
      const intersect = raycaster.intersectObjects(cabinets)[0]
      let intersectObj=intersect ? intersect.object : null
      // 若之前已有机柜被选择,且不等于当前所选择的机柜,取消之前选择的机柜的高亮
      if (curCabinet && curCabinet!== intersectObj) {
        const material =curCabinet.material
        material.setValues({
          map: maps.get('cabinet.jpg')
        })
      }
      /* 
        若当前所选对象不为空:
          触发鼠标在机柜上移动的事件。
          若当前所选对象不等于上一次所选对象:
            更新curCabinet。
            将模型高亮。
            触发鼠标划入机柜事件。
        否则若上一次所选对象存在:
          置空curCabinet。
          触发鼠标划出机柜事件。
      */
      if (intersectObj) {
        this.onMouseMoveCabinet(x,y)
        if (intersectObj !== curCabinet) {
          this.curCabinet= intersectObj
          const material = intersectObj.material
          material.setValues({
            map: maps.get('cabinet-hover.jpg')
          })
          this.onMouseOverCabinet(intersectObj)
        }
      } else if(curCabinet) {
        this.curCabinet = null
        this.onMouseOutCabinet()
      }
    },
    // 鼠标移动事件
    mouseMove({clientX,clientY}) {
      this.selectCabinet(clientX, clientY)
    },
    //鼠标划入机柜事件,参数为机柜对象
    onMouseOverCabinet (cabinet) {
      console.log(cabinet);
      this.state.planeDisplay = 'block'
    },
    //鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
    onMouseMoveCabinet(x,y) {
      console.log(x,y);
      this.state.planePos.left = x + 'px'
      this.state.planePos.top = y + 'px'
    },
    //鼠标划出机柜的事件
    onMouseOutCabinet () { 
      this.state.planeDisplay = 'none'
    }
  }
}
</script>
<style>
#container {
  position: absolute;
  width: 100%;
  height: 100%;
}
#plane{
  position: absolute;
  top: 0;
  left: 0;
  background-color: rgba(0,0,0,0.5);
  color: #fff;
  padding: 0 18px;
  transform: translate(12px,-100%);
  display: none;
}
</style>

6. 使用Apifox 添加获取机柜信息的接口

参照Apifox自带的示例项目,自己添加一个接口,根据IT机柜名获取机柜信息

3. vue2项目中使用three.js开发三维IT机房

7. 获取并展示机柜信息面板的内容

当鼠标划入IT机柜时,根据机柜名请求机柜数据,更新相应的state

getCabinateByName(name) {
   let path = 'http://127.0.0.1:4523/m1/2003080-0-default/name/'
   return fetch(path + name).then((res) => res.json());
}
//鼠标划入机柜事件,参数为机柜对象
onMouseOverCabinet (cabinet) {
  console.log(cabinet.name);
  this.state.planeDisplay = 'block'
  //基于cabinet.name 获取机柜数据
  this.getCabinateByName(cabinet.name).then(({ data }) => {
    this.state.curCabinet = { ...data, name: cabinet.name }

  });
},

7.1 最终效果

3. vue2项目中使用three.js开发三维IT机房

<template>
  <div>
    <div id="container" @mousemove="mouseMove">
      <div
        id='plane'
        :style="{left: state.planePos.left,top:state.planePos.top,display: state.planeDisplay}"
      >
        <p>机柜名称:{{ state.curCabinet.name }}</p>
        <p>机柜温度:{{ state.curCabinet.temperature }}°</p>
        <p>使用情况:{{ state.curCabinet.count}} / {{ state.curCabinet.capacity}}</p>
      </div>
    </div>
  </div>
</template>

<script>
import * as THREE from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'

export default {
  data() {
    return {
      mesh: null,
      camera: null,
      scene: null,
      renderer: null,
      controls: null,
      maps: null,
      //机柜集合
      cabinets: [],
      //鼠标划入的机柜
      curCabinet: '',
      state: {
        planePos: {
          //信息面板的位置
          left: 0,
          top:0
        },
        //信息面板的可见性
        planeDisplay: 'none',
        //机柜信息
        curCabinet: {
          //名称
          name:'Loading……',
          //温度
          temperature: 0,
          //容量
          capacity: 0,
          //服务器数量
          count:0
        }
      }
    }
  },
  mounted() {
    this.maps = new Map()
    this.crtTexture("cabinet-hover.jpg")
    this.init()
  },
  methods: {
    // 初始化
    init() {
      this.createScene() // 创建场景
      this.loadGLTF() // 加载GLTF模型
      // this.createLight() // 创建光源
      this.createCamera() // 创建相机
      this.createRender() // 创建渲染器
      this.createControls() // 创建控件对象
      this.render() // 渲染
      
    },
    // 创建场景
    createScene() {
      this.scene = new THREE.Scene()
    },
    // 加载GLTF模型
    loadGLTF() {
      const loader = new GLTFLoader()
      loader.load(`./models/machineRoom.gltf`, ({ scene: { children } }) => {
        console.log(...children);
        
        children.forEach((obj) => {
          const { map, color} = obj.material 
          this.changeMat(obj, map, color)
          if (obj.name.includes('cabinet')) {
            this.cabinets.push(obj)
          }
        })
        
        this.scene.add(...children)
      })
    },

    // 创建光源
    createLight() {
      // 环境光
      const ambientLight = new THREE.AmbientLight(0xffffff, 0.1) // 创建环境光
      this.scene.add(ambientLight) // 将环境光添加到场景

      const spotLight = new THREE.SpotLight(0xffffff) // 创建聚光灯
      spotLight.position.set(150, 150, 150)
      spotLight.castShadow = true
      this.scene.add(spotLight)
    },
    // 创建相机
    createCamera() {
      const element = document.getElementById('container')
      const width = element.clientWidth // 窗口宽度
      const height = element.clientHeight // 窗口高度
      const k = width / height // 窗口宽高比
      // PerspectiveCamera( fov, aspect, near, far )
      this.camera = new THREE.PerspectiveCamera(45, k, 0.1, 1000)
      this.camera.position.set(0, 10, 15) // 设置相机位置

      this.camera.lookAt(0, 0, 0) // 设置相机方向
      this.scene.add(this.camera)
    },
    // 创建渲染器
    createRender() {
      const element = document.getElementById('container')
      this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
      this.renderer.setSize(element.clientWidth, element.clientHeight) // 设置渲染区域尺寸
      // this.renderer.shadowMap.enabled = true // 显示阴影
      // this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
      this.renderer.setClearColor(0x3f3f3f, 1) // 设置背景颜色
      element.appendChild(this.renderer.domElement)
    },

    render() {
      // if (this.mesh) {
      //   this.mesh.rotation.z += 0.006
      // }
      this.renderer.render(this.scene, this.camera)
      requestAnimationFrame(this.render)
    },
    // 创建控件对象
    createControls() {
      this.controls = new OrbitControls(this.camera, this.renderer.domElement)
    },
    changeMat(obj, map, color) {
      if (map) {
        obj.material = new THREE.MeshBasicMaterial({
          map: this.crtTexture(map.name)
        })
      } else {
        obj.material = new THREE.MeshBasicMaterial({color})
      }
    },
    crtTexture(imgName) {
      let curTexture = this.maps.get(imgName)
      if (!curTexture) {
        curTexture=new THREE.TextureLoader().load('./models/'+imgName)
        curTexture.flipY = false
        curTexture.wrapS = 1000
        curTexture.wrapT = 1000
        this.maps.set(
          imgName,
          curTexture
        )
      }
      return curTexture
    },
    selectCabinet(x, y) {
      const {cabinets,renderer,camera,maps,curCabinet}=this
      const { width, height } = renderer.domElement
      //射线投射器,可基于鼠标点和相机,在世界坐标系内建立一条射线,用于选中模型
      const raycaster = new THREE.Raycaster()
      //鼠标在裁剪空间中的点位
      const pointer = new THREE.Vector2()

      // 鼠标的canvas坐标转裁剪坐标
      pointer.set(
        (x / width) * 2 - 1,
        -(y / height) * 2 + 1,
      )
      // 基于鼠标点的裁剪坐标位和相机设置射线投射器
      raycaster.setFromCamera(
        pointer, camera
      )
      // 选择机柜
      const intersect = raycaster.intersectObjects(cabinets)[0]
      let intersectObj=intersect ? intersect.object : null
      // 若之前已有机柜被选择,且不等于当前所选择的机柜,取消之前选择的机柜的高亮
      if (curCabinet && curCabinet!== intersectObj) {
        const material =curCabinet.material
        material.setValues({
          map: maps.get('cabinet.jpg')
        })
      }
      /* 
        若当前所选对象不为空:
          触发鼠标在机柜上移动的事件。
          若当前所选对象不等于上一次所选对象:
            更新curCabinet。
            将模型高亮。
            触发鼠标划入机柜事件。
        否则若上一次所选对象存在:
          置空curCabinet。
          触发鼠标划出机柜事件。
      */
      if (intersectObj) {
        this.onMouseMoveCabinet(x,y)
        if (intersectObj !== curCabinet) {
          this.curCabinet= intersectObj
          const material = intersectObj.material
          material.setValues({
            map: maps.get('cabinet-hover.jpg')
          })
          this.onMouseOverCabinet(intersectObj)
        }
      } else if(curCabinet) {
        this.curCabinet = null
        this.onMouseOutCabinet()
      }
    },
    // 鼠标移动事件
    mouseMove({clientX,clientY}) {
      this.selectCabinet(clientX, clientY)
    },
    //鼠标划入机柜事件,参数为机柜对象
    onMouseOverCabinet (cabinet) {
      console.log(cabinet.name);
      this.state.planeDisplay = 'block'
      //基于cabinet.name 获取机柜数据
      this.getCabinateByName(cabinet.name).then(({ data }) => {
        this.state.curCabinet = { ...data, name: cabinet.name }

      });
    },
    //鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
    onMouseMoveCabinet(x,y) {
      // console.log(x,y);
      this.state.planePos.left = x + 'px'
      this.state.planePos.top = y + 'px'
    },
    //鼠标划出机柜的事件
    onMouseOutCabinet () { 
      this.state.planeDisplay = 'none'
    },
    getCabinateByName(name) {
       let path = 'http://127.0.0.1:4523/m1/2003080-0-default/name/'
       return fetch(path + name).then((res) => res.json());
    }
  }
}
</script>
<style>
#container {
  position: absolute;
  width: 100%;
  height: 100%;
}
#plane{
  position: absolute;
  top: 0;
  left: 0;
  background-color: rgba(0,0,0,0.5);
  color: #fff;
  padding: 0 18px;
  transform: translate(12px,-100%);
  display: none;
  text-align: left;
}
</style>