三维IT机房可以将机房数据可视化,让企业更好的监控和管理IT 机柜
-
在前端页面对IT 机房进行三维展示
-
当鼠标划入IT 机柜的时候,提示当前机柜的详细信息
-
一键显示机房中过热的机柜
1. 准备一份IT机房模型
1-1-建模思路
-
简化模型,能用贴图表现的细节,就用贴图。这样可提高渲染速度。
-
将光效融入贴图中,即模型贴图后便具备光效和体感。这样在three 中就无需打灯,即可提高开发速度,亦可提高渲染效率。
1-2-建模软件
可以3d建模的软件有很多,3dsMax、ZRender、C4D 都可以。如使用3dsMax
建模和导出gltf。
1-3-模型文件
GLTF 模型文件包含了整个场景的数据,比如几何体、材质、动画、相机等。
GLTF 模型在使用起来,要比传统的obj 模型方便很多。
在导出GLTF模型后,一般会包含以下文件:
-
gltf 模型文件
-
bin文件
-
贴图文件
1-4-规范模型的结构和命名
-
在建模软件中,同一类型的模型文件可以放入一个数组里,数组可以多层嵌套。
-
当前的机房模型比较简单,没有使用数组,所有的Mesh对象都是平展开的。
为了便于访问和区分模型,需要对模型进行规范命名,如机房中的IT机柜都是按照cabinet-001、cabinet-002、cabinet-003 命名的。
假设IT机柜的名称都是唯一的,那我们便可以基于这个名称从后端获取相应机柜的详细信息。
2、Vue项目 加载GLTF格式模型
2.1 安装three 相关的依赖
bash
npm install three @types/three --save --registry=https://registry.npm.taobao.org
2.2 Vue项目 构建本地3D模型
xml
<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 文件夹,将之前的模型文件放进去
2.3.2 GLTF 模型的导入、渲染和相机变换
xml
<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>
和之前的3dsmax 图片对比一下,就会发现:贴图颜色变深了
3. 调整外部3D模型数据
3.1 分析一下模型数据
javascript
// 加载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
类型。
再分析一下material 中的map 贴图,可以发现其map贴图为 Texture 对象,其具备以下重要信息:
-
name 是贴图图片的名称。
-
flipY为false,即不对图像的y轴做翻转。
-
image图像源是ImageBitmap 类型。
-
wrapS 纹理横向重复,即THREE.RepeatWrapping。
-
wrapT 纹理纵向重复,即THREE.RepeatWrapping。
注:THREE.RepeatWrapping=1000
在此,我们要知道以下threejs 知识:
-
MeshStandardMaterial 材质会感光,我们不需要打光,需要将其材质换成MeshBasicMaterial。
-
ImageBitmap 的图像类型是渲染效果变黑的关键原因,因此需要将其换成Image() 对象。
接下来给模型换一个材质和图像源。
3.2 修改材质和图像源
-
添加maps属性,用来存储纹理对象,以避免贴图的重复加载
javascript
// ES6的Map数据结构
this.maps = new Map()
-
在加载GLTF 的时候,用changeMat()方法修改Mesh 对象的材质
javascript
// 加载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)
})
},
-
添加一个修改材质的方法
changeMat()
arduino
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 模型的颜色。
-
添加建立纹理对象的方法crtTexture()
ini
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 贴图的相应属性的继承。
这样就和3dsmax中的模型效果一致了。
4. 模型选择——添加鼠标事件
对IT 机柜做选择,让选中的机柜高亮
-
添加两个属性、三个鼠标事件
csharp
//机柜集合
cabinets: [],
//鼠标划入的机柜
curCabinet: '',
scss
//鼠标划入机柜事件,参数为机柜对象
onm ouseOverCabinet (cabinet) {console.log(cabinet); },
//鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
onm ouseMoveCabinet(x,y) {console.log(x,y); },
//鼠标划出机柜的事件
onm ouseOutCabinet () { }
-
为maps 添加一个机柜的高亮贴图。之后鼠标划入机柜时,会将其贴图更换为高亮贴图。
javascript
mounted() {
this.maps = new Map()
this.crtTexture("cabinet-hover.jpg")
// this.init()
},
-
在加载GLTF 模型时,若模型名称包含'cabinet',便将其存入cabinets
javascript
// 加载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)
})
},
-
建立一个射线投射器,一个二维点,以避免在鼠标选择时机柜时重复实例化;
添加选择模型的方法
selectCabinet(x,y)
,其参数为鼠标的坐标位
kotlin
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()
}
},
-
为容器添加鼠标移动事件
xml
<template>
<div>
<div id="container" @mousemove="mouseMove"></div>
</div>
</template>
javascript
// 鼠标移动事件
mouseMove({clientX,clientY}) {
this.selectCabinet(clientX, clientY)
},
4.1 机柜选中效果
kotlin
<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)
},
//鼠标划入机柜事件,参数为机柜对象
onm ouseOverCabinet (cabinet) {console.log(cabinet); },
//鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
onm ouseMoveCabinet(x,y) {console.log(x,y); },
//鼠标划出机柜的事件
onm ouseOutCabinet () { }
}
}
</script>
<style>
#container {
position: absolute;
width: 100%;
height: 100%;
}
</style>
5-信息提示
在鼠标划入IT机柜的时候,提示机柜的详细信息。
其最简单的做法就是用HTML 建立一个信息面板,当鼠标在IT机柜上移动的时候,就让其随鼠标移动。
5.1.建立信息提示板
less
state: {
planePos: {
//信息面板的位置
left: 0,
top:0
},
//信息面板的可见性
planeDisplay: 'none',
//机柜信息
curCabinet: {
//名称
name:'Loading……',
//温度
temperature: 0,
//容量
capacity: 0,
//服务器数量
count:0
}
}
xml
<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>
设置面板样式
css
#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.设置信息面板的可见性和位置
根据绑定在机柜对象上的鼠标事件,设置信息面板的可见性和位置
javascript
//鼠标划入机柜事件,参数为机柜对象
onm ouseOverCabinet (cabinet) {
console.log(cabinet);
this.state.planeDisplay = 'block'
},
//鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
onm ouseMoveCabinet(x,y) {
console.log(x,y);
this.state.planePos.left = x + 'px'
this.state.planePos.top = y + 'px'
},
//鼠标划出机柜的事件
onm ouseOutCabinet () {
this.state.planeDisplay = 'none'
}
5.3 信息提示的页面效果
xml
<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)
},
//鼠标划入机柜事件,参数为机柜对象
onm ouseOverCabinet (cabinet) {
console.log(cabinet);
this.state.planeDisplay = 'block'
},
//鼠标在机柜上移动的事件,参数为鼠标在canvas画布上的坐标位
onm ouseMoveCabinet(x,y) {
console.log(x,y);
this.state.planePos.left = x + 'px'
this.state.planePos.top = y + 'px'
},
//鼠标划出机柜的事件
onm ouseOutCabinet () {
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机柜名获取机柜信息
7. 获取并展示机柜信息面板的内容
当鼠标划入IT机柜时,根据机柜名请求机柜数据,更新相应的state
javascript
getCabinateByName(name) {
let path = 'http://127.0.0.1:4523/m1/2003080-0-default/name/'
return fetch(path + name).then((res) => res.json());
}
kotlin
//鼠标划入机柜事件,参数为机柜对象
onm ouseOverCabinet (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 最终效果
xml
<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)
},
//鼠标划入机柜事件,参数为机柜对象
onm ouseOverCabinet (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画布上的坐标位
onm ouseMoveCabinet(x,y) {
// console.log(x,y);
this.state.planePos.left = x + 'px'
this.state.planePos.top = y + 'px'
},
//鼠标划出机柜的事件
onm ouseOutCabinet () {
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>