Three.js案例
三维数据中心可视化系统

三维数据中心可视化系统

这是一个基于 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)