三维数据中心可视化系统
这是一个基于 Three.js 和 Vue 3 开发的三维数据中心可视化系统,通过 3D 界面,直观展示数据中心的实时状态和运行情况。
核心特点
- 使用 Three.js 技术构建逼真的三维场景,呈现数据中心的真实物理环境
- 集成 ECharts 图表库,通过折线图和饼图等方式展示设备运行数据
- 支持设备选择、高亮显示,快速定位目标设备
- 鼠标悬停显示详细信息
技术栈
- 前端框架:采用 Vue 3 框架,结合 TypeScript
- 3D 引擎:基于 Three.js 实现高性能 3D 渲染
- 动画效果:使用 GSAP 实现流畅的动画过渡
- 数据可视化:集成 ECharts 提供专业的数据图表
- 构建工具:使用 Vite 实现快速的开发和构建体验
核心代码分析
1. 三维场景管理 (Viewer 模块)
Viewer 模块是整个 3D 系统的核心,负责创建和管理 Three.js 场景:
export default class Viewer {
// 存储Canvas元素的ID
public id: string;
// Three.js的场景对象,所有3D内容都添加到这里
public scene!: Scene;
// 透视相机,提供3D视角
public camera!: PerspectiveCamera;
// WebGL渲染器,负责将3D场景渲染到屏幕
public renderer!: WebGLRenderer;
// 轨道控制器,让用户可以旋转和缩放场景
public controls!: OrbitControls;
// ...其他属性
constructor(id: string) {
// 保存Canvas元素ID
this.id = id;
// 初始化所有组件
this.initViewer();
}
private initViewer() {
// 创建事件总线,用于组件间通信
this.emitter = mitt();
// 初始化渲染器(显示3D画面的工具)
this.initRenderer();
// 创建3D场景(放置3D物体的空间)
this.initScene();
// 添加光源(没有光就看不见物体)
this.initLight();
// 设置摄像机(决定我们看场景的视角)
this.initCamera();
// 添加控制器(让用户可以旋转和缩放场景)
this.initControl();
// 创建天空盒作为背景
this.initSkybox();
// 动画循环函数 - 实现场景的连续更新
const animate = () => {
// 如果场景已销毁则停止动画
if (this.isDestroy) return;
// 请求下一帧动画(类似电影的帧)
requestAnimationFrame(animate);
// 更新DOM元素尺寸
this.updateDom();
// 渲染当前画面
this.readerDom();
// 执行所有注册的动画效果
this.animateEventList.forEach((event) => {
if (event.fun && event.content) {
event.fun(event.content);
}
});
};
// 启动动画循环(开始不断刷新画面)
animate();
}
}
这段代码负责搭建虚拟的 3D 舞台:
- 创建一个空间(场景)
- 放置一个摄像机(观众的视角)
- 添加灯光照明
- 绘制天空背景
- 设置交互控制(让用户可以旋转和缩放视角)
- 建立动画循环(不断刷新画面,实现流畅动态效果)
2. 模型加载与处理 (ModelLoader 模块)
ModelLoader 负责将 3D 模型文件加载到场景中:
export default class ModelLoder {
// 引用Viewer实例,用于访问场景
protected viewer: Viewer;
// GLTF模型加载器,用于加载.glb/.gltf格式的3D模型
private gltfLoader: GLTFLoader;
// DRACO解码器,用于解压缩模型以提高加载性能
private dracoLoader: DRACOLoader;
constructor(viewer: Viewer, dracolPath: string = `${publicPath}/draco/`) {
// 保存Viewer引用
this.viewer = viewer;
// 创建GLTF加载器
this.gltfLoader = new GLTFLoader();
// 创建DRACO解码器(用于减小模型文件体积,加快加载速度)
this.dracoLoader = new DRACOLoader();
// 设置解码器路径,告诉程序去哪里找解压工具
this.dracoLoader.setDecoderPath(dracolPath);
// 将解码器附加到GLTF加载器
this.gltfLoader.setDRACOLoader(this.dracoLoader);
}
// 将3D模型加载到场景中
public loadModelToScene(url: string, callback: LoadModelCallbackFn<BaseModel>) {
// 构建完整的模型URL路径
const publicUrl = `${publicPath}${url}`;
// 加载模型并添加到场景
this.loadModel(publicUrl, (model) => {
// 将模型添加到场景中
this.viewer.scene.add(model.object);
// 模型加载完成后执行回调函数
callback && callback(model);
});
}
}
这个模块负责:
- 从文件中读取 3D 模型数据(比如机房、服务器机柜等)
- 解压缩复杂的模型数据(使用 DRACOLoader 提高加载效率)
- 将模型添加到 3D 场景中
- 完成后通知其他部分进行后续处理
3. 基础模型处理 (BaseModel 模块)
BaseModel 提供了对加载模型的各种操作方法:
export default class BaseModel {
// 引用Viewer实例
protected viewer: Viewer;
// 保存GLTF模型数据
public gltf: GLTF;
// 模型的根对象
public object: THREE.Group;
// 保存模型原始材质,用于后续恢复
public originMaterials: Material[] = [];
// ...其他属性和方法
// 设置模型缩放比例(调整大小)
public setScalc(x: number, y?: number, z?: number) {
// 设置模型的缩放比例,如果y和z未提供,则使用x值
this.object.scale.set(x, y || x, z || x);
}
// 开启模型阴影效果(让画面更真实)
public openCastShadow(names = []) {
// 遍历模型的所有子对象
this.gltf.scene.traverse((model: Object3DExtends) => {
// 如果是网格对象(Mesh)且不在排除列表中
if (model.isMesh && !names.includes(model.name as never)) {
// 禁用视锥体剔除,确保模型始终可见
model.frustumCulled = false;
// 启用阴影投射
model.castShadow = true;
}
});
}
// 修改模型颜色和透明度(比如高亮选中的设备)
public setColor(color = "yellow", opacity = 0.5) {
// 如果还没保存过原始材质,则初始化数组
if (!this.isSaveMaterial) this.originMaterials = [];
// 遍历模型的所有子对象
this.gltf.scene.traverse((model: Object3DExtends) => {
if (model.isMesh) {
// 保存原始材质以便日后恢复
if (!this.isSaveMaterial) this.originMaterials.push(model.material as Material);
// 创建新材质并应用到模型
model.material = new THREE.MeshPhongMaterial({
// 设置双面可见
side: THREE.DoubleSide,
// 启用透明效果
transparent: true,
// 关闭深度测试,使半透明物体可以正确显示
depthTest: false,
// 启用深度写入
depthWrite: true,
// 设置材质颜色
color: new THREE.Color(color),
// 设置透明度
opacity: opacity,
});
}
});
// 标记已保存原始材质
this.isSaveMaterial = true;
}
// 启动模型动画(使模型动起来)
public startAnima(i = 0) {
// 保存动画索引
this.animaIndex = i;
// 如果混合器不存在,则创建新的动画混合器
if (!this.mixer) this.mixer = new THREE.AnimationMixer(this.object);
// 如果没有动画,则直接返回
if (this.gltf.animations.length < 1) return;
// 播放指定索引的动画
this.mixer.clipAction(this.gltf.animations[i]).play();
// 创建动画对象并添加到全局动画列表
this.animaObject = {
// 动画更新函数
fun: this.updateAnima,
// 动画上下文对象
content: this,
};
// 注册到Viewer的动画循环中
this.viewer.addAnimate(this.animaObject);
}
}
这个模块像负责对模型进行各种调整:
- 设置模型的大小(缩放比例)
- 添加阴影效果,让场景更真实
- 修改模型的颜色和透明度(如高亮选中的设备)
- 播放模型的动画效果(如设备运行状态变化)
4. 交互事件处理 (Viewer/Events 模块)
场景中的鼠标交互处理,实现了点击、双击、悬停等功能:
// 在Viewer类中的代码片段
// 初始化射线检测器,用于处理3D场景中的鼠标事件
public initRaycaster() {
// 创建射线检测器(像一道激光从鼠标位置射入3D场景)
this.raycaster = new Raycaster();
// 创建事件处理函数
const initRaycasterEvent: Function = (eventName: keyof HTMLElementEventMap): void => {
// 使用节流函数包装事件处理器,防止过于频繁触发
const funWrap = throttle(
(event: any) => {
// 保存原始事件对象
this.mouseEvent = event;
// 计算鼠标在3D空间中的标准化坐标(把屏幕坐标转换为3D场景坐标)
this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
this.mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
// 发出事件,通知相关组件处理,传递射线检测结果
this.emitter.emit(Events[eventName].raycaster, this.getRaycasterIntersectObjects());
},
50 // 节流时间:50ms内只触发一次(防止太频繁)
);
// 向DOM元素添加事件监听器
this.viewerDom.addEventListener(eventName, funWrap, false);
};
// 注册各种鼠标事件
initRaycasterEvent('click'); // 单击事件
initRaycasterEvent('dblclick'); // 双击事件
initRaycasterEvent('mousemove'); // 鼠标移动事件
}
// 在Sence.vue组件中的代码片段
onMounted(() => {
// 初始化3D场景和加载模型
init();
initModel();
// 注册双击事件处理函数
viewer.emitter.on(Event.dblclick.raycaster, (list: THREE.Intersection[]) => {
// 处理用户点击3D对象
onMouseClick(list);
});
// 注册鼠标移动事件处理函数
viewer.emitter.on(Event.mousemove.raycaster, (list: THREE.Intersection[]) => {
// 处理鼠标悬停效果
onMouseMove(list);
});
});
// 处理鼠标点击事件
const onMouseClick = (intersects: THREE.Intersection[]) => {
// 如果没有点击到任何对象,则直接返回
if (!intersects.length) return;
// 获取第一个被点击的对象
const selectedObject = intersects[0].object;
// 根据点击的对象类型执行不同操作
if (selectedObject.name.includes('zuo')) {
// 如果点击的是办公楼模型,选中该区域
selectOffice(selectedObject.parent);
}
// 更多处理逻辑...
}
这部分代码负责处理用户与 3D 场景的互动:
- 检测用户鼠标点击或悬停在哪个 3D 对象上
- 计算出精确的点击位置(从 2D 屏幕坐标转换到 3D 空间坐标)
- 根据用户行为触发相应的操作(如显示设备信息、高亮选中的设备等)
- 使用节流技术,防止事件过于频繁导致性能问题
5. 场景与组件集成 (Vue 组件部分)
在 Vue 组件中整合 Three.js 场景,实现界面与 3D 场景的结合:
<!-- 简化的Sence.vue组件结构 -->
<template>
<!-- 3D场景容器 -->
<div id="three"></div>
<!-- 悬浮信息框组件 -->
<Popover ref="popoverRef" :top="popoverTop" :left="popoverLeft" :data="popoverData"></Popover>
</template>
<script lang="ts" setup>
// 组件挂载完成后初始化3D场景
onMounted(() => {
// 初始化3D场景
init();
// 加载3D模型
initModel();
});
// 初始化3D场景和相关组件
const init = () => {
// 创建核心3D场景(舞台)
viewer = new Viewer("three");
// 初始化射线检测(用于鼠标交互)
viewer.initRaycaster();
// 创建模型加载器(搬运工)
modelLoader = new ModelLoader(viewer);
// 创建选中框辅助对象(用于显示选中的物体)
boxHelperWrap = new BoxHelperWrap(viewer);
// 注册事件监听
// 双击事件
viewer.emitter.on(Event.dblclick.raycaster, onMouseClick);
// 鼠标移动事件
viewer.emitter.on(Event.mousemove.raycaster, onMouseMove);
};
// 加载3D模型并设置属性
const initModel = () => {
// 加载数据中心模型
modelLoader.loadModelToScene("/models/datacenter.glb", (baseModel) => {
// 设置模型缩放比例(调整大小)
baseModel.setScalc(0.2);
// 获取模型场景
const model = baseModel.gltf.scene;
// 设置模型位置(放在场景中心)
model.position.set(0, 0, 0);
// 设置模型名称
model.name = "机房";
// 启用阴影效果(让画面更真实)
baseModel.openCastShadow();
// 保存模型引用,便于后续操作
dataCenter = baseModel;
// 克隆一份模型用于状态恢复
oldDataCenter = model.clone();
// 遍历所有机柜,添加到可点击对象列表
const rackList: any[] = [];
// 遍历模型中的所有对象
model.traverse((item) => {
// 检查是否是机柜对象
if (checkIsRack(item)) {
// 将机柜添加到列表中
rackList.push(item);
}
});
// 设置可交互对象列表(告诉系统哪些物体可以被点击)
viewer.setRaycasterObjects(rackList);
});
};
</script>
这个组件负责:
- 将 Three.js 场景嵌入到 Vue 界面中
- 初始化 3D 场景和各个功能模块
- 加载数据中心的 3D 模型
- 设置模型的位置、大小和各种属性
- 建立交互事件与 Vue 组件的连接,使点击 3D 对象可以触发界面上的信息显示
本地运行
# 安装依赖
npm i
# 启动项目
npm start
代码
https://github.com/fh332393900/threejs-demo/tree/main (opens in a new tab)