数字孪生与 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
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>
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 = '模型加载失败,请检查文件格式和网络';
});
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>
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>
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>
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 → 更新温度、颜色、标签和告警
2
3
比如后端发来以下消息。字段是业务协议示例,需要由设备采集服务提供,不是 model-viewer 自动生成的数据:
{
"deviceId": "pump-01",
"temperature": 82,
"unit": "celsius",
"status": "running",
"observedAt": "2026-09-27T02:00:00Z"
}
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 再根据此状态查找场景对象,更新标签、颜色;不要把渲染对象当作数据库。
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 关联场景对象和实时数据,并处理数据过期、告警和权限。如果要预测故障或模拟运行,还要接入经过验证的分析或物理模型,不能只靠外观相似。