数字孪生概念与架构 / 三维可视化引擎
1. 数字孪生概述
1.1 定义
数字孪生(Digital Twin)是指利用数字技术创建物理实体(如设备、产线、建筑、城市)的虚拟映射,通过采集物理实体的实时数据,在虚拟空间中进行仿真、诊断、预测和优化,最终形成物理实体 + 虚拟模型 + 数据连接 + 闭环优化的完整体系。
- 物理实体:现实世界中存在的设备、系统或流程,是数据的源头和优化的落脚点。
- 虚拟模型:物理实体在数字空间中的高保真映射,覆盖几何结构、物理属性、行为逻辑等多维度信息。
- 数据连接:通过 IoT 传感器、API 网关、工业总线等手段实现物理世界与虚拟世界的双向实时数据同步。
- 闭环优化:基于虚拟模型的仿真分析结果反向驱动物理实体执行优化指令,形成持续改进的闭环。
1.2 五维模型
数字孪生的五维模型是对经典三维模型(物理实体、虚拟模型、服务系统)的扩展,加入了孪生数据和连接映射两个维度:
| 维度 | 说明 |
|---|---|
| 物理实体 (PE) | 现实世界的物理对象,包含设备、环境、人员等 |
| 虚拟模型 (VM) | 物理实体的数字化描述,包括几何模型、物理模型、行为模型、规则模型 |
| 服务系统 (SS) | 面向数字孪生的数据管理、仿真分析、决策支持等服务模块 |
| 孪生数据 (DD) | 贯穿物理实体、虚拟模型和服务系统的融合数据,包含历史数据、实时数据、仿真数据 |
| 连接映射 (CN) | 四个组成部分之间的互联互通接口,保障数据在各维度间高效流转 |
五维模型的关系可用公式表示为:
M_DT = (PE, VM, SS, DD, CN)1.3 典型应用场景
智慧工厂
在智能制造领域,数字孪生覆盖从单台设备到整条产线再到整个工厂的多层级映射。通过实时采集 PLC(可编程逻辑控制器)、SCADA(监控与数据采集系统)数据,在虚拟环境中监控设备运行状态、分析产线瓶颈、模拟工艺调整效果。
智慧城市
城市级数字孪生融合 GIS(地理信息系统)、BIM(建筑信息模型)、IoT 传感器等多源数据,构建城市三维数字底座。应用于城市规划、交通仿真、应急推演、环境监测等场景,辅助管理者进行可视化决策。
建筑运维
基于 BIM 模型构建建筑数字孪生,整合楼宇自控系统(BAS)、能源管理系统(EMS)数据,实现设备远程监控、能耗优化、空间管理、安防联动等运维功能,显著降低建筑全生命周期运营成本。
设备预测维护
通过对旋转设备、压缩机、风机等关键设备建立数字孪生模型,利用振动分析、温度趋势、油液检测等多维数据,结合机器学习算法实现设备剩余寿命预测和故障预警,变被动维修为主动维护。
2. 数字孪生架构
2.1 四层架构
数字孪生系统通常采用四层架构设计,自下而上分别为数据采集层、建模层、仿真层和可视化层。
+---------------------------------------------------------------+
| 可视化层 (Visualization Layer) |
| 三维引擎渲染 / 2D 仪表盘 / AR/VR 交互 / 大屏展示 |
+---------------------------------------------------------------+
| 仿真层 (Simulation Layer) |
| 物理仿真 / 机理模型 / 数据驱动模型 / 混合仿真 / 优化算法 |
+---------------------------------------------------------------+
| 建模层 (Modeling Layer) |
| 几何建模 / 行为建模 / 规则建模 / 数据关联 / 模型资产管理 |
+---------------------------------------------------------------+
| 数据采集层 (Data Acquisition Layer) |
| IoT 网关 / 协议适配 / 数据清洗 / 时序存储 / 消息队列 |
+---------------------------------------------------------------+数据采集层
负责从物理实体采集原始数据。关键技术包括:
- 协议适配:支持 Modbus、OPC UA、MQTT、CoAP 等工业协议
- 边缘计算:在靠近数据源的边缘节点进行预处理和过滤,降低云端压力
- 数据管道:通过 Kafka / Pulsar 等消息中间件实现数据的可靠传输
- 时序数据库:使用 InfluxDB / TDengine / TimescaleDB 存储高频时序数据
建模层
将采集到的数据与物理实体的数字描述进行关联。建模层维护数字孪生模型的全生命周期,包括模型的创建、关联、版本管理、一致性校验。
仿真层
利用机理模型(基于物理方程)和数据驱动模型(基于机器学习)对物理实体的行为进行模拟和预测。仿真结果可用于工艺优化、故障诊断、调度决策等。
可视化层
将数字孪生模型和数据以直观的方式呈现给用户。三维可视化引擎是该层的核心组件,负责渲染高保真虚拟场景并叠加实时数据。
2.2 实时数据同步机制
数字孪生的时效性取决于数据同步的延迟和一致性。常见的同步机制包括:
| 机制 | 延迟 | 适用场景 |
|---|---|---|
| WebSocket 推送 | < 100ms | 实时监控、动态数据绑定 |
| MQTT Broker | 100ms - 1s | IoT 设备数据采集 |
| gRPC Stream | < 50ms | 高频率内部服务间通信 |
| 轮询查询 | 1s - 30s | 非实时指标、统计数据 |
| 变更数据捕获 (CDC) | 秒级 | 数据库层面的数据同步 |
2.3 数字孪生成熟度分级
业界通常将数字孪生能力划分为五个成熟度等级:
| 等级 | 名称 | 特征 |
|---|---|---|
| L1 | 可视化 (Visualize) | 在三维场景中展示物理实体的几何外观和实时状态数据,实现"所见即所得" |
| L2 | 诊断 (Diagnose) | 基于规则引擎和统计分析,对异常状态进行告警和根因分析 |
| L3 | 预测 (Predict) | 结合机器学习算法,预测设备剩余寿命、性能衰退趋势和潜在故障 |
| L4 | 自优化 (Self-optimize) | 仿真层输出优化方案并自动执行,实现不依赖人工干预的参数调优 |
| L5 | 自治 (Autonomous) | 数字孪生系统具备自主决策和进化能力,可在复杂环境下持续自适应运行 |
大多数企业目前处于 L1-L2 阶段,少数领先企业正在向 L3-L4 迈进。
3. 三维可视化引擎技术选型
3.1 Three.js
- 渲染能力:基于 WebGL,支持标准 3D 渲染(PBR、阴影、后处理)
- 优势:轻量级(核心库约 200KB gzip)、生态丰富(大量插件和示例)、学习曲线平缓
- 局限:不适合超大规模地理空间场景、原生不支持 3D Tiles 标准
- 适用场景:设备级/产线级数字孪生、Web 端轻量展示、原型验证
3.2 Cesium.js
- 渲染能力:WebGL,专为地理空间三维场景设计
- 优势:支持全球级别三维地球渲染、3D Tiles 标准、高精度地形和影像、时间动态模拟
- 局限:库体积较大(约 3MB gzip)、对非地理场景支持不够友好
- 适用场景:智慧城市、航空航天、地理信息可视化
3.3 Unity3D / Unreal Engine
- 渲染能力:DirectX / Vulkan / Metal,支持高保真实时光影和物理模拟
- 优势:画质顶级(光线追踪、全局光照)、成熟的物理引擎、支持 XR 设备
- 局限:需安装客户端或 WebGL 导出(性能折损)、开发和部署成本高、Web 兼容性差
- 适用场景:高保真工业仿真、数字孪生驾驶舱、VR/AR 培训
3.4 Babylon.js
- 渲染能力:WebGL / WebGPU,微软开源
- 优势:率先支持 WebGPU、内置节点材质编辑器、完善的文档和 Inspector 调试工具
- 局限:社区规模小于 Three.js、国内资料相对较少
- 适用场景:需要 WebGPU 高性能渲染、对微软技术栈有依赖的项目
3.5 Blender(建模工具)
Blender 虽非运行时引擎,但在数字孪生工作流中扮演重要角色:
- 用于创建和编辑三维模型(设备、建筑、场景)
- 支持 GLTF/GLB 格式导出,可直接被 Three.js / Babylon.js 使用
- 提供减面优化、UV 展开、烘焙贴图等模型轻量化功能
- Python API 支持批量处理和自动化工作流
3.6 对比表
| 维度 | Three.js | Cesium.js | Babylon.js | Unity3D | Unreal Engine |
|---|---|---|---|---|---|
| 渲染质量 | 中等 | 中等 | 中等-高 | 高 | 极高 |
| 性能 | 中等 | 中等 | 中等-高(WebGPU) | 高 | 高 |
| Web 兼容性 | 优秀 | 良好 | 良好 | 一般(WebGL) | 一般(WebGL) |
| 学习曲线 | 平缓 | 中等 | 中等 | 较陡 | 陡峭 |
| 行业应用 | 设备孪生/产线/原型 | 智慧城市/GIS | 产品展示/数字工厂 | 工业仿真/培训 | 影视级可视化 |
| 文件体积 | ~200KB gzip | ~3MB gzip | ~500KB gzip | 数十 MB | 数百 MB |
| 开源免费 | 是 (MIT) | 是 (Apache-2.0) | 是 (Apache-2.0) | 部分免费 | 部分免费 |
选型建议:Web 端轻量级数字孪生首选 Three.js;需要地理空间能力叠加 Cesium.js(两者可混用 via cesium-threejs 桥接库);高保真客户端应用选 Unity3D。
4. Three.js 数字孪生实践
4.1 场景初始化
以下是 Three.js 场景的基本初始化流程,包含场景、相机、渲染器和轨道控制器:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// 创建场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e); // 深色科技感背景
// 创建透视相机 (视角, 宽高比, 近裁面, 远裁面)
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(20, 15, 25);
camera.lookAt(0, 0, 0);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true; // 开启阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.getElementById('container').appendChild(renderer.domElement);
// 轨道控制器 (支持旋转/平移/缩放)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.08;
controls.minDistance = 5;
controls.maxDistance = 100;
controls.target.set(0, 0, 0);
// 窗口自适应
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 动画循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();4.2 模型加载
数字孪生中常用的 3D 模型格式包括 GLTF/GLB(标准交换格式)、OBJ(通用格式)、3D Tiles(Cesium 地理空间格式)。
使用 GLTFLoader 加载 GLTF/GLB 模型:
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');
loader.setDRACOLoader(dracoLoader);
loader.load(
'/models/equipment.glb',
(gltf) => {
const model = gltf.scene;
scene.add(model);
},
(progress) => {
const pct = (progress.loaded / progress.total * 100).toFixed(0);
console.log(`加载进度: ${pct}%`);
},
(error) => {
console.error('模型加载失败:', error);
}
);加载 OBJ 格式(配合 MTL 材质文件):
import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
import { MTLLoader } from 'three/addons/loaders/MTLLoader.js';
const mtlLoader = new MTLLoader();
mtlLoader.load('/models/factory.mtl', (materials) => {
materials.preload();
const objLoader = new OBJLoader();
objLoader.setMaterials(materials);
objLoader.load('/models/factory.obj', (obj) => {
scene.add(obj);
});
});4.3 模型变换
对加载后的模型进行位置、旋转、缩放操作,以及管理层级结构:
// 位置变换
model.position.set(10, 0, 5);
model.position.x = 10;
// 旋转变换 (弧度)
model.rotation.x = Math.PI / 2; // 绕 X 轴旋转 90度
model.rotation.y = 0.5;
// 或者使用 quaternion
model.quaternion.setFromEuler(new THREE.Euler(0, Math.PI / 4, 0));
// 缩放
model.scale.set(1, 1, 1);
// 层级结构管理
// 创建分组容器
const factoryGroup = new THREE.Group();
const productionLine = new THREE.Group();
const machineA = ...; // 加载的设备模型
const machineB = ...;
productionLine.add(machineA);
productionLine.add(machineB);
factoryGroup.add(productionLine);
scene.add(factoryGroup);
// 移动分组整体
factoryGroup.position.set(0, 0, 0);
// 查找子节点
const motor = model.getObjectByName('motor_001');
if (motor) {
motor.rotation.z = 0.3;
}
// 遍历所有子节点
model.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});4.4 材质与光照
数字孪生场景中推荐使用 PBR(基于物理的渲染)材质以获得真实感:
import { MeshStandardMaterial, MeshPhysicalMaterial } from 'three';
// 基础 PBR 材质
const metalMaterial = new MeshStandardMaterial({
color: 0xcccccc,
metalness: 0.8,
roughness: 0.2,
});
const plasticMaterial = new MeshStandardMaterial({
color: 0x3366ff,
metalness: 0.1,
roughness: 0.6,
});
// 高精度 PBR 材质 (支持透明涂层、清漆效果)
const glassMaterial = new MeshPhysicalMaterial({
color: 0x88ccff,
metalness: 0.0,
roughness: 0.0,
transparent: true,
opacity: 0.6,
clearcoat: 0.1,
clearcoatRoughness: 0.2,
});环境贴图提供场景的整体反射和光照信息:
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const rgbeLoader = new RGBELoader();
rgbeLoader.load('/textures/studio.hdr', (texture) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture; // 环境反射
scene.background = texture; // 背景 (可选)
});光照系统配置:
// 环境光 - 提供基础照明
const ambientLight = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambientLight);
// 半球光 - 模拟天空和地面的颜色渐变
const hemisphereLight = new THREE.HemisphereLight(0x87ceeb, 0x362d1c, 0.6);
scene.add(hemisphereLight);
// 主方向光 - 产生阴影
const dirLight = new THREE.DirectionalLight(0xffffff, 1.5);
dirLight.position.set(20, 30, 10);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 60;
dirLight.shadow.camera.left = -20;
dirLight.shadow.camera.right = 20;
dirLight.shadow.camera.top = 20;
dirLight.shadow.camera.bottom = -20;
scene.add(dirLight);
// HDR 矩形区域光 (需要额外库)
// 为场景增加局部高光效果
const pointLight = new THREE.PointLight(0xff6600, 0.8, 30);
pointLight.position.set(5, 8, 3);
scene.add(pointLight);5. 数据驱动可视化
5.1 实时数据绑定
数字孪生的核心是实时数据驱动 3D 场景更新。通过 WebSocket 接收设备数据,更新三维对象的属性、颜色、位置和动画:
// WebSocket 连接
const ws = new WebSocket('wss://twin-api.example.com/ws/device-data');
ws.onopen = () => {
console.log('WebSocket 连接已建立');
// 订阅设备数据
ws.send(JSON.stringify({ type: 'subscribe', topics: ['device:sensor:*'] }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
updateTwinScene(data);
};
// 数据驱动的场景更新
function updateTwinScene(data) {
const { deviceId, temperature, vibration, speed, status, position } = data;
// 查找对应的 3D 对象
const device = scene.getObjectByName(deviceId);
if (!device) return;
// 更新颜色 - 根据温度映射颜色
const color = mapTemperatureToColor(temperature);
device.material.color.set(color);
// 更新位置 - 如果是 AGV 或可移动设备
if (position) {
device.position.set(position.x, position.y, position.z);
}
// 更新旋转 - 模拟设备运行状态
if (speed !== undefined) {
device.rotation.y += speed * 0.01;
}
// 更新透明度 - 表示设备离线或故障
if (status === 'offline') {
device.material.opacity = 0.3;
device.material.transparent = true;
}
// 触发设备特定动画
if (vibration > threshold) {
startVibrationAnimation(device, vibration);
}
}
// 温度到颜色的映射函数
function mapTemperatureToColor(temp) {
const t = Math.min(Math.max((temp - 20) / 60, 0), 1); // 20-80°C 范围
const color = new THREE.Color();
color.setHSL(0.7 - t * 0.7, 1.0, 0.5);
return color;
}5.2 传感器数据可视化
数值仪表盘
在 3D 场景中叠加 Sprite 标签或 CSS2DRenderer 标签显示实时数值:
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
// 创建 CSS2D 渲染器
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(window.innerWidth, window.innerHeight);
labelRenderer.domElement.style.position = 'absolute';
labelRenderer.domElement.style.top = '0px';
labelRenderer.domElement.style.left = '0px';
labelRenderer.domElement.style.pointerEvents = 'none'; // 允许点击穿透
document.getElementById('container').appendChild(labelRenderer.domElement);
// 创建数值标签
function createSensorLabel(device, labelText) {
const div = document.createElement('div');
div.className = 'sensor-label';
div.textContent = labelText;
div.style.color = '#00ff88';
div.style.fontSize = '14px';
div.style.fontFamily = 'monospace';
div.style.textShadow = '0 0 10px rgba(0,255,136,0.5)';
div.style.background = 'rgba(0,0,0,0.6)';
div.style.padding = '2px 8px';
div.style.borderRadius = '4px';
div.style.border = '1px solid #00ff88';
const label = new CSS2DObject(div);
label.position.set(0, 2, 0); // 在设备上方 2 单位处
device.add(label);
return label;
}热力图
使用 PlaneGeometry 配合动态更新的 CanvasTexture 实现热力图叠加:
function createHeatmapOverlay(width, height, gridSize) {
const canvas = document.createElement('canvas');
canvas.width = gridSize;
canvas.height = gridSize;
const ctx = canvas.getContext('2d');
// 创建纹理
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
opacity: 0.6,
depthWrite: false,
});
const geometry = new THREE.PlaneGeometry(width, height);
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = -Math.PI / 2; // 平放在地面
return { mesh, ctx, canvas, texture };
}
// 更新热力图数据
function updateHeatmap(heatmap, dataPoints) {
const { ctx, canvas, texture } = heatmap;
const w = canvas.width;
const h = canvas.height;
// 清空画布
ctx.clearRect(0, 0, w, h);
// 绘制温度点
dataPoints.forEach((point) => {
const x = (point.x / maxX) * w;
const y = (point.y / maxY) * h;
const radius = point.value * 2;
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
gradient.addColorStop(0, 'rgba(255, 0, 0, 0.8)');
gradient.addColorStop(0.5, 'rgba(255, 255, 0, 0.4)');
gradient.addColorStop(1, 'rgba(0, 0, 255, 0)');
ctx.fillStyle = gradient;
ctx.fillRect(x - radius, y - radius, radius * 2, radius * 2);
});
texture.needsUpdate = true;
}轨迹动画
通过更新 Points 几何体实现移动轨迹动画:
function createTrajectoryLine(points) {
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(points.length * 3);
points.forEach((p, i) => {
positions[i * 3] = p.x;
positions[i * 3 + 1] = p.y;
positions[i * 3 + 2] = p.z;
});
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const material = new THREE.LineBasicMaterial({ color: 0x00ff88, transparent: true, opacity: 0.6 });
const line = new THREE.Line(geometry, material);
scene.add(line);
// 轨迹上的运动小球
const ballGeo = new THREE.SphereGeometry(0.2, 8, 8);
const ballMat = new THREE.MeshBasicMaterial({ color: 0x00ff88 });
const ball = new THREE.Mesh(ballGeo, ballMat);
scene.add(ball);
let progress = 0;
function animateTrajectory() {
progress += 0.005;
if (progress >= 1) progress = 0;
const idx = Math.floor(progress * (points.length - 1));
const nextIdx = Math.min(idx + 1, points.length - 1);
const t = (progress * (points.length - 1)) % 1;
// 线性插值
ball.position.x = points[idx].x + (points[nextIdx].x - points[idx].x) * t;
ball.position.y = points[idx].y + (points[nextIdx].y - points[idx].y) * t;
ball.position.z = points[idx].z + (points[nextIdx].z - points[idx].z) * t;
requestAnimationFrame(animateTrajectory);
}
animateTrajectory();
}柱状图叠加
在三维场景中使用 BoxGeometry 叠加统计信息柱状图:
function create3DBarChart(data, options = {}) {
const { position = new THREE.Vector3(0, 0, 0), width = 0.5, maxHeight = 10, colors = [0x00ff88, 0xff8800, 0xff0044] } = options;
const group = new THREE.Group();
group.position.copy(position);
const barCount = data.length;
const spacing = width * 1.5;
const startX = -((barCount - 1) * spacing) / 2;
data.forEach((value, index) => {
const height = (value / Math.max(...data)) * maxHeight;
const geo = new THREE.BoxGeometry(width, height, width);
// 根据值大小选择颜色
const colorIdx = Math.min(Math.floor((value / Math.max(...data)) * colors.length), colors.length - 1);
const mat = new THREE.MeshPhysicalMaterial({
color: colors[colorIdx],
metalness: 0.2,
roughness: 0.5,
transparent: true,
opacity: 0.85,
});
const bar = new THREE.Mesh(geo, mat);
bar.position.set(startX + index * spacing, height / 2, 0);
bar.castShadow = true;
group.add(bar);
});
scene.add(group);
return group;
}5.3 事件交互
设备点击、悬停、框选等交互是数字孪生操作的基础:
import { Raycaster } from 'three';
const raycaster = new Raycaster();
const pointer = new THREE.Vector2();
// 悬停高亮
renderer.domElement.addEventListener('pointermove', (event) => {
pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
// 重置之前高亮的对象
if (hoveredObject) {
hoveredObject.material.emissive.setHex(0x000000);
hoveredObject = null;
}
if (intersects.length > 0) {
const obj = intersects[0].object;
if (obj.isMesh && obj.userData.selectable) {
hoveredObject = obj;
obj.material.emissive.setHex(0x222244);
renderer.domElement.style.cursor = 'pointer';
}
} else {
renderer.domElement.style.cursor = 'default';
}
});
// 点击选中
renderer.domElement.addEventListener('click', (event) => {
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const obj = intersects[0].object;
if (obj.userData.selectable) {
// 触发选中回调
onDeviceSelect(obj);
}
}
});
function onDeviceSelect(device) {
console.log(`选中设备: ${device.name}`, device.userData);
// 可在此处弹出设备详情面板
}
// 框选 (使用 BoxSelection 辅助类)
import { SelectionBox } from 'three/addons/interactive/SelectionBox.js';
import { SelectionHelper } from 'three/addons/interactive/SelectionHelper.js';
const selectionBox = new SelectionBox(camera, scene);
const helper = new SelectionHelper(selectionBox, renderer.domElement, 'selectBox');
renderer.domElement.addEventListener('pointerup', () => {
if (selectionBox.isEnabled) {
const selected = selectionBox.collection;
selected.forEach((obj) => {
// 高亮选中对象
if (obj.isMesh && obj.material) {
obj.material.emissive.setHex(0x004488);
}
});
selectionBox.isEnabled = false;
}
});5.4 时间轴回放
历史数据回放功能允许用户以不同速度查看设备状态随时间的变化:
class TimelinePlayer {
constructor(dataFrames) {
this.dataFrames = dataFrames; // [{ timestamp, devices: [...] }]
this.currentIndex = 0;
this.isPlaying = false;
this.speed = 1.0; // 倍速
this.onFrame = null; // 帧更新回调
}
play() {
if (this.isPlaying) return;
this.isPlaying = true;
this._tick();
}
pause() {
this.isPlaying = false;
}
stop() {
this.isPlaying = false;
this.currentIndex = 0;
this._applyFrame(0);
}
setSpeed(speed) {
this.speed = Math.max(0.1, Math.min(10, speed));
}
seekTo(timestamp) {
const idx = this.dataFrames.findIndex((f) => f.timestamp >= timestamp);
if (idx !== -1) {
this.currentIndex = idx;
this._applyFrame(idx);
}
}
_tick() {
if (!this.isPlaying) return;
if (this.currentIndex >= this.dataFrames.length - 1) {
this.isPlaying = false;
return;
}
this._applyFrame(this.currentIndex);
this.currentIndex++;
const interval = 1000 / (30 * this.speed);
setTimeout(() => this._tick(), interval);
}
_applyFrame(index) {
const frame = this.dataFrames[index];
if (frame && this.onFrame) {
this.onFrame(frame, index, this.dataFrames.length);
}
}
}
// 使用示例
const player = new TimelinePlayer(historyData);
player.onFrame = (frame, current, total) => {
frame.devices.forEach((deviceData) => {
const obj = scene.getObjectByName(deviceData.id);
if (obj) {
obj.position.set(deviceData.x, deviceData.y, deviceData.z);
obj.rotation.y = deviceData.rotation;
obj.material.color.set(deviceData.temp > 50 ? 0xff0000 : 0x00ff00);
}
});
// 更新进度条
updateProgressBar(current / total * 100);
};
// 播放控制
document.getElementById('btn-play').onclick = () => player.play();
document.getElementById('btn-pause').onclick = () => player.pause();
document.getElementById('btn-speed').onchange = (e) => player.setSpeed(parseFloat(e.target.value));
document.getElementById('btn-seek').oninput = (e) => {
const pct = e.target.value / 100;
const timestamp = historyData[Math.floor(pct * historyData.length)].timestamp;
player.seekTo(timestamp);
};6. 性能优化
6.1 LOD 层级细节
LOD(Level of Detail)根据相机距离切换不同精度的模型,是三维场景最重要的性能优化手段:
import { LOD } from 'three';
function createLODDevice() {
const lod = new LOD();
// 高精度模型 (近距离)
const highDetail = loadModel('/models/device_high.glb');
lod.addLevel(highDetail, 0);
// 中等精度模型 (中距离)
const midDetail = loadModel('/models/device_mid.glb');
lod.addLevel(midDetail, 20);
// 低精度模型 (远距离)
const lowDetail = loadModel('/models/device_low.glb');
lod.addLevel(lowDetail, 50);
// 代理几何体 (极远距离)
const proxyGeo = new THREE.BoxGeometry(1, 1, 1);
const proxyMat = new THREE.MeshBasicMaterial({ color: 0x666666 });
const proxy = new THREE.Mesh(proxyGeo, proxyMat);
lod.addLevel(proxy, 100);
return lod;
}
// 注意: LOD 需要每帧更新
function updateLODs(camera) {
scene.children.forEach((child) => {
if (child.isLOD) {
child.update(camera);
}
});
}6.2 Instance 实例化渲染
当场景中存在大量相同几何体(如螺栓、管道支架、传感器节点)时,使用 InstancedMesh 可大幅减少 Draw Call:
import { InstancedMesh, Matrix4 } from 'three';
function createInstancedBolts(count = 1000) {
const geometry = new THREE.CylinderGeometry(0.05, 0.05, 0.2, 12);
const material = new THREE.MeshStandardMaterial({
color: 0x888888,
metalness: 0.7,
roughness: 0.3,
});
const instancedMesh = new InstancedMesh(geometry, material, count);
const matrix = new Matrix4();
const position = new THREE.Vector3();
const quaternion = new THREE.Quaternion();
const scale = new THREE.Vector3(1, 1, 1);
for (let i = 0; i < count; i++) {
position.set(
(Math.random() - 0.5) * 50,
0,
(Math.random() - 0.5) * 50
);
quaternion.setFromEuler(new THREE.Euler(Math.random() * Math.PI, 0, 0));
matrix.compose(position, quaternion, scale);
instancedMesh.setMatrixAt(i, matrix);
}
instancedMesh.instanceMatrix.needsUpdate = true;
instancedMesh.castShadow = true;
instancedMesh.receiveShadow = true;
scene.add(instancedMesh);
return instancedMesh;
}
// 更新单个实例的位置
function updateInstancePosition(instancedMesh, index, newPosition) {
const matrix = new Matrix4();
instancedMesh.getMatrixAt(index, matrix);
matrix.setPosition(newPosition);
instancedMesh.setMatrixAt(index, matrix);
instancedMesh.instanceMatrix.needsUpdate = true;
}6.3 合批 Draw Call 优化
将多个静态几何体合并为单个 BufferGeometry,减少渲染提交次数:
import { BufferGeometryUtils } from 'three/addons/utils/BufferGeometryUtils.js';
function mergeStaticMeshes(meshes) {
const geometries = [];
meshes.forEach((mesh) => {
if (mesh.isMesh && mesh.geometry) {
const geo = mesh.geometry.clone();
// 将世界矩阵应用到几何体上
geo.applyMatrix4(mesh.matrixWorld);
geometries.push(geo);
}
});
const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries);
// 使用统一的材质
const material = new THREE.MeshStandardMaterial({
color: 0xaaaaaa,
metalness: 0.5,
roughness: 0.5,
});
const mergedMesh = new THREE.Mesh(mergedGeometry, material);
mergedMesh.castShadow = true;
mergedMesh.receiveShadow = true;
return mergedMesh;
}6.4 纹理压缩
使用 KTX2 / Basis 通用纹理格式可大幅降低 GPU 内存占用和加载带宽:
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
const ktx2Loader = new KTX2Loader();
ktx2Loader.setTranscoderPath('https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/libs/basis/');
ktx2Loader.detectSupport(renderer);
const textureLoader = new THREE.TextureLoader();
// 优先级: KTX2 > WebP > JPEG
function loadOptimizedTexture(url, fallbackUrl) {
// 检测 KTX2 支持
if (renderer.extensions.has('WEBGL_compressed_texture_etc') ||
renderer.extensions.has('WEBGL_compressed_texture_s3tc')) {
return ktx2Loader.loadAsync(url);
}
// 降级到标准格式
return textureLoader.loadAsync(fallbackUrl);
}
// 纹理压缩建议
// - 颜色贴图: KTX2 (ETC2 / BC7) > WebP > JPEG
// - 法线贴图: KTX2 (BC5 / EAC RG) > PNG
// - ORM (Occlusion/Roughness/Metalness): KTX2 合并在单张贴图中6.5 模型轻量化
在建模阶段和生产流程中对模型进行减面和材质合并:
// 模型加载后的后处理轻量化
function optimizeLoadedModel(model) {
model.traverse((child) => {
if (child.isMesh) {
// 优化几何体
if (child.geometry) {
// 删除不必要的顶点属性
child.geometry.deleteAttribute('normal'); // 如果不需光照
child.geometry.deleteAttribute('uv'); // 如果不用贴图
// 合并顶点索引 (减少顶点数)
child.geometry = child.geometry.toNonIndexed();
}
// 合并材质
if (Array.isArray(child.material)) {
// 多材质网格 - 考虑合并或使用单一材质
child.material = child.material[0];
}
}
});
}
// 工具函数: 计算并打印模型统计信息
function logModelStats(model) {
let vertexCount = 0;
let triangleCount = 0;
let meshCount = 0;
model.traverse((child) => {
if (child.isMesh) {
meshCount++;
const geo = child.geometry;
if (geo.index) {
triangleCount += geo.index.count / 3;
} else if (geo.attributes.position) {
triangleCount += geo.attributes.position.count / 3;
}
if (geo.attributes.position) {
vertexCount += geo.attributes.position.count;
}
}
});
console.log(`模型统计: ${meshCount} 个网格, ${vertexCount.toLocaleString()} 顶点, ${triangleCount.toLocaleString()} 三角面`);
}性能优化综合策略
| 优化手段 | 优化效果 | 实施成本 | 适用场景 |
|---|---|---|---|
| LOD 层级切换 | 减少远景 GPU 负载 | 中等 | 大场景、大量设备 |
| InstancedMesh 实例化 | 显著减少 Draw Call | 低 | 重复设备(螺栓、灯杆、管道) |
| 几何体合批 | 减少 Draw Call | 低 | 静态场景元素 |
| 纹理压缩 (KTX2) | 降低显存占用 60-80% | 低 | 所有带贴图的材质 |
| 模型减面优化 | 减少顶点数 50-90% | 中等(需工具) | 高精度 CAD 模型导入 |
| 遮挡剔除 (Occlusion Culling) | 减少不可见物体渲染 | 高 | 室内场景、建筑内部 |
| 使用 BufferGeometry | 比 Geometry 快 2-3 倍 | 低 | 所有动态生成的几何体 |
| 限制阴影范围 | 减少阴影贴图渲染面积 | 低 | 大场景定向光阴影 |
参考资源
- Three.js 官方文档: https://threejs.org/docs/
- Cesium.js 文档: https://cesium.com/learn/
- Babylon.js 文档: https://doc.babylonjs.com/
- GLTF 格式规范: https://github.com/KhronosGroup/glTF
- 3D Tiles 标准: https://github.com/CesiumGS/3d-tiles
- KTX2/Basis 纹理压缩: https://github.com/KhronosGroup/KTX-Software