数字孪生与 Web 三维展示

# 数字孪生与 Web 三维展示

把一台水泵模型放到网页上,用户能旋转查看,这是三维展示。如果它对应车间里的 pump-01,并能显示这台水泵最新的温度、运行状态和告警,就有了数字孪生应用的数据基础。

先把模型显示出来,再把真实状态对应上。 本篇先给出可独立运行的网页示例,再说明设备数据怎么接;三维模型本身不会自动知道设备是否正在工作。

# GLB、glTF 和 USDZ 分别是什么

格式 用途 注意事项
glTF 三维场景交换格式,可引用外部二进制和纹理 部署时要保留资源相对关系
GLB glTF 的二进制封装,常把模型和纹理放在一个文件中 单文件方便传输,但也可能很大
USDZ Apple Quick Look 等 AR 场景常用的资产包 材质与动画要在目标设备验证

GLB 更适合直接交给 Web 查看器,CAD 的 STEP 文件通常需要转换和简化。转换后可能丢失工程参数,所以展示资产与原始工程数据应分别保存。

# 最小网页展示:model-viewer

只需要展示和旋转单个模型时,可以使用 Google 的 model-viewer (opens new window)。它封装了加载、相机控制和部分 AR 入口,不需要先写完整的 Three.js 场景管理。

以下示例放在独立的 Vite vanilla 项目中,不是在笔记站直接执行。需要 Node.js 环境和一份有权使用的 product.glb。

# 创建独立练习项目;安装后提交 lockfile 以固定依赖。
npm create vite@latest model-viewer-demo -- --template vanilla
cd model-viewer-demo
npm install

# model-viewer 依赖 three;若出现 peer 版本冲突,按组件声明选择兼容版本。
npm install @google/model-viewer three
1
2
3
4
5
6
7

将模型放在 public/product.glb,把入口 index.html 设为:

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>商品三维展示</title>
    <style>
      /* 明确高度,避免自定义元素没有可见的展示空间。 */
      model-viewer { width: 100%; height: 70vh; background: #f5f5f5; }
    </style>
  </head>
  <body>
    <!-- camera-controls 允许拖拽旋转;关闭自动旋转,避免无意义持续运动。 -->
    <model-viewer src="/product.glb" alt="商品三维模型" camera-controls></model-viewer>
    <p id="status" role="status">模型加载中</p>
    <script type="module" src="/main.js"></script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

新建 main.js:

import '@google/model-viewer';

const viewer = document.querySelector('model-viewer');
const status = document.querySelector('#status');

// load 表示模型资源已加载,不代表每种设备的显示效果都已验收。
viewer.addEventListener('load', () => {
  status.textContent = '拖拽可旋转,缩放可查看细节';
});

// 明确失败反馈,避免网络或格式错误时页面只剩一个空框。
viewer.addEventListener('error', () => {
  status.textContent = '模型加载失败,请检查文件格式和网络';
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

运行 npm run dev,检查网络面板、加载反馈、旋转和手机性能。这里没有开启 AR;增加 ar 及对应模式前,需验证 HTTPS、设备能力和资产兼容性。Apple Quick Look 可使用 ios-src 指向 USDZ,不能保证每台设备都有相同的 AR 体验。

如果页面只有空框,先看状态文字,再看浏览器 Network 中 product.glb 是否返回 200:404 通常是路径或部署位置错误;下载成功但解码失败则检查文件格式。能旋转但加载慢,先查看模型和纹理体积,不要先怀疑相机控制代码。

# 增加 AR:网页预览和真实空间放置不是一回事

在上面已经导入 model-viewer 的页面中,将展示标签改为下面这样,并把对应文件放入 public:

<!-- GLB 用于网页和兼容的 AR 路径;ios-src 为 Apple 路径提供 USDZ。 -->
<model-viewer
  src="/product.glb"
  ios-src="/product.usdz"
  poster="/product-poster.jpg"
  alt="可放入真实空间预览的商品"
  camera-controls
  ar
  ar-modes="webxr scene-viewer quick-look"
></model-viewer>
1
2
3
4
5
6
7
8
9
10

ar-modes 提供可尝试的 AR 路径,不意味着所有浏览器都支持全部模式。移动设备可能进入系统查看器,WebXR 路径则需要对应浏览器和安全上下文。普通网页里能旋转模型,不代表 AR 环境识别和真实尺度也已通过验证。

Apple Quick Look (opens new window) 还可以从一个普通链接打开受支持的 USDZ 资产:

<!-- 支持 Quick Look 的设备可进入系统 AR;其他设备应保留普通网页预览入口。 -->
<a rel="ar" href="/product.usdz">
  <img src="/product-poster.jpg" alt="在真实空间查看商品" width="240" />
</a>
1
2
3
4

实际测试要看商品是否以合理尺寸出现在地面或桌面、透明材质是否正确,以及不支持 AR 时用户能否继续使用普通预览。USDZ 和 GLB 不一定支持完全相同的材质效果,最好分别验收。

# 什么情况下使用 Three.js 或高斯渲染器

需求 选择
单个商品预览、旋转与简单 AR model-viewer
多设备场景、点击选中、告警着色、复杂交互 Three.js (opens new window) 与业务状态管理
浏览实景采集的 3DGS 资产 支持对应格式的渲染器,例如 Spark (opens new window)

3DGS 资产不是普通 Mesh,不能仅改文件后缀当成 GLB 加载。选择渲染器时应检查格式、压缩方式、排序与移动端支持。

# Spark:在浏览器渲染 3DGS

Spark (opens new window) 是基于 Three.js / WebGL2 的高斯渲染工具。Three.js 负责场景、相机和渲染基础,Spark 负责高斯资产的加载与绘制;它不是从照片训练 3DGS 的工具。

下面采用官方 README 的 Three.js 0.180.0 与 Spark 2.2.0 组合,使用官方公开蝴蝶样例。保存为独立练习项目的 index.html,通过本地 HTTP 服务打开,不需要自己的 GPU 训练环境,但浏览器需要 WebGL2,并能访问示例的 CDN 和资产地址。

<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>3DGS 网页展示</title>
  <style>
    body { margin: 0; background: #111; color: white; }
    canvas { display: block; }
    p { position: absolute; top: 0; left: 16px; }
  </style>
  <!-- 固定依赖版本,避免 CDN 更新后出现 Three.js 实例或 API 不匹配。 -->
  <script type="importmap">
    { "imports": {
      "three": "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js",
      "@sparkjsdev/spark": "https://sparkjs.dev/releases/spark/2.2.0/spark.module.js"
    } }
  </script>
</head>
<body>
  <p>3DGS 旋转预览;空白时检查 WebGL2、网络和浏览器控制台。</p>
  <script type="module">
    import * as THREE from 'three';
    import { SparkRenderer, SplatMesh } from '@sparkjsdev/spark';

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.01, 1000);
    const renderer = new THREE.WebGLRenderer({ antialias: false });
    // 限制像素比,避免高分屏无意义地放大填充和透明混合开销。
    renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
    renderer.setSize(innerWidth, innerHeight);
    document.body.appendChild(renderer.domElement);

    // 高斯渲染器与普通 Three.js 场景一起工作。
    scene.add(new SparkRenderer({ renderer }));
    const splat = new SplatMesh({
      url: 'https://sparkjs.dev/assets/splats/butterfly.spz',
    });
    // 这是官方蝴蝶样例的摆放方式;其他数据需检查自己的朝向与中心。
    splat.quaternion.set(1, 0, 0, 0);
    splat.position.set(0, 0, -3);
    scene.add(splat);

    let previous;
    renderer.setAnimationLoop(time => {
      // 按经过时间旋转,不按帧数旋转,避免不同刷新率速度不同。
      if (previous !== undefined) splat.rotation.y += Math.min((time - previous) / 1000, 0.1) * 0.3;
      previous = time;
      renderer.render(scene, camera);
    });
    addEventListener('resize', () => {
      camera.aspect = innerWidth / innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(innerWidth, innerHeight);
    });
  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

这个例子只验证高斯资产能加载和渲染。接入 React 等页面时,还应在组件卸载时停止动画、移除事件监听并按库 API 释放 GPU 资源,避免页面反复进入后内存持续上涨。

Spark 支持多种高斯格式,如 PLY、SPZ、SPLAT、KSPLAT、SOG;但普通点云 PLY 不一定含有高斯所需的属性,不能只按后缀判断。大场景需要匹配的分层资产与加载方案,不是换一个渲染器就能保证任意手机流畅。

LOD 和流式加载分别解决什么

LOD(Level of Detail,细节层次)按距离或屏幕贡献选择不同精细度,例如远处设备先显示简化形态。流式加载按需要分批获取数据,避免先下载整个场景。前者控制显示多少细节,后者控制何时加载哪些数据;两者通常配合,但都需要资产组织和运行时支持。

# 数字孪生怎么和业务数据连接

真实设备 → 数据采集 → 校验、存储与权限控制 → HTTP / WebSocket
                                                   ↓
设备 ID ↔ 场景对象 ID → 更新温度、颜色、标签和告警
1
2
3

比如后端发来以下消息。字段是业务协议示例,需要由设备采集服务提供,不是 model-viewer 自动生成的数据:

{
  "deviceId": "pump-01",
  "temperature": 82,
  "unit": "celsius",
  "status": "running",
  "observedAt": "2026-09-27T02:00:00Z"
}
1
2
3
4
5
6
7

前端先通过 deviceId 找到对应水泵,再把标签更新成 82 °C;假设业务规定超过 80 °C 告警,就显示红色告警。80 是例子,实际阈值应来自设备规范。温度、状态和时间都由后端提供,不能从三维模型外观推断。

如果超过允许时间没有新数据,应改成数据已过期,不继续显示正常。收到乱序消息时,也要避免旧温度覆盖新温度。旋转模型只是视觉操作,远程停机则是独立业务命令,需要鉴权、确认、审计及真实执行回执。

# 设备数据如何避免更新错对象

最容易出错的是把场景数组下标当成设备编号:模型一旦重新导出,对象顺序可能变化,温度就会显示在另一台设备上。应为设备保留稳定 ID,并建立显式映射。

下面是可独立在浏览器运行的状态处理示例,只演示校验、映射和乱序保护,不假装已经接入真实设备或 Three.js:

// sceneObjectName 对应建模时约定的对象名;生产环境从资产配置加载。
const devices = new Map([
  ['pump-01', { sceneObjectName: 'Pump_01', observedAt: 0, temperature: null }],
]);

function applyTelemetry(message) {
  const device = devices.get(message.deviceId);
  const timestamp = Date.parse(message.observedAt);
  // 不认识的设备、错误单位或非法值不能直接修改场景。
  if (!device || message.unit !== 'celsius' ||
      !Number.isFinite(message.temperature) || !Number.isFinite(timestamp)) return false;
  // 简化示例使用采集时间;真实多源系统可能需要服务端序列号和时钟校准。
  if (timestamp <= device.observedAt) return false;

  device.observedAt = timestamp;
  device.temperature = message.temperature;
  return true;
}

const accepted = applyTelemetry({
  deviceId: 'pump-01', temperature: 82, unit: 'celsius',
  observedAt: new Date().toISOString(),
});
console.log(accepted, devices.get('pump-01'));
// UI 再根据此状态查找场景对象,更新标签、颜色;不要把渲染对象当作数据库。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

断线和数据过期要单独显示。例如设备本身仍标记 running,但超过业务允许时长没有新数据,页面应显示状态未知或数据过期,而不是继续展示绿色正常。重新连接后先获取当前快照,再接增量事件,避免把遗漏的数据当成从未发生。

# 网页卡顿时,先定位资源还是渲染

现象 优先检查 常用处理
很久才出现模型 下载体积、网络、解压和解析耗时 海报占位、延迟加载、资产压缩与缓存
出现后旋转掉帧 面数、材质数量、阴影、分辨率;高斯场景还看重叠与数量 减面、减少 draw call、降低像素比、适当 LOD
手机过一会崩溃 纹理与 GPU 内存、重复创建且未释放资源 降纹理尺寸、复用资源、卸载时释放
模型看起来漂浮或巨大 原点、坐标方向、单位和相机范围 在资产侧统一,而不是每页写不同补丁

draw call 可以理解为一次向 GPU 提交绘制工作的调用。模型总面数不高,但碎成大量对象和材质,也可能带来较高 CPU 提交成本。优化要比较同设备、同镜头下的加载、帧率和内存,不只看电脑开发环境。

# 交付时怎样验收

  • 资源:模型体积、纹理分辨率、压缩兼容性、来源许可与访问权限。
  • 展示:首屏加载时间、移动端帧率、内存、材质和坐标单位。
  • 状态:设备映射正确、时间戳与单位明确、断线和过期状态可见。
  • 控制:不把动画播放当成真实执行成功,业务命令需要独立结果确认。

# 面试问答

1. 数字孪生和普通三维可视化的区别是什么?参考答案

三维可视化主要回答对象长什么样,数字孪生还要回答它在现实里是谁、现在是什么状态。我会用稳定的设备 ID 关联场景对象和实时数据,并处理数据过期、告警和权限。如果要预测故障或模拟运行,还要接入经过验证的分析或物理模型,不能只靠外观相似。

# 参考资料