docs: add architecture refactor and webgl instancing plans

This commit is contained in:
linkong
2026-03-20 13:53:36 +08:00
parent 4f922f13d1
commit 3e3090d72a
2 changed files with 458 additions and 0 deletions

View File

@@ -0,0 +1,165 @@
# 地球3D可视化架构重构计划
## 背景
当前 `frontend/public/earth` 3D地球可视化系统基于 Three.js 构建,未来需要迁移到 Unreal Engine (Cesium)。为降低迁移成本,需要提前做好**逻辑与渲染分离**的架构设计。
## 目标
- 将线缆高亮逻辑与渲染实现分离
- 保持交互逻辑可复用,只需重写渲染层
- 为后续迁移到 UE/Cesium 做好准备
## 已完成
### 1. 状态枚举定义 (constants.js)
```javascript
export const CABLE_STATE = {
NORMAL: 'normal',
HOVERED: 'hovered',
LOCKED: 'locked'
};
```
### 2. 线缆状态管理 (cables.js - 数据层)
```javascript
const cableStates = new Map();
export function getCableState(cableId) { ... }
export function setCableState(cableId, state) { ... }
export function clearAllCableStates() { ... }
export function getCableStateInfo() { ... }
```
### 3. 逻辑层调用 (main.js)
```javascript
// 悬停
setCableState(cable.userData.cableId, CABLE_STATE.HOVERED);
// 锁定
setCableState(cableId, CABLE_STATE.LOCKED);
// 恢复
setCableState(cableId, CABLE_STATE.NORMAL);
clearAllCableStates();
// 清除锁定时
clearLockedObject() {
hoveredCable = null;
clearAllCableStates();
...
}
```
### 4. 渲染层 (main.js - applyCableVisualState)
```javascript
function applyCableVisualState() {
const allCables = getCableLines();
const pulse = (Math.sin(Date.now() * CABLE_CONFIG.pulseSpeed) + 1) * 0.5;
allCables.forEach(c => {
const cableId = c.userData.cableId;
const state = getCableState(cableId);
switch (state) {
case CABLE_STATE.LOCKED:
// 呼吸效果 + 白色
c.material.opacity = CABLE_CONFIG.lockedOpacityMin + pulse * CABLE_CONFIG.pulseCoefficient;
c.material.color.setRGB(1, 1, 1);
break;
case CABLE_STATE.HOVERED:
// 白色高亮
c.material.opacity = 1;
c.material.color.setRGB(1, 1, 1);
break;
case CABLE_STATE.NORMAL:
default:
if (lockedObjectType === 'cable' && lockedObject) {
// 其他线缆变暗
c.material.opacity = CABLE_CONFIG.otherOpacity;
...
} else {
// 恢复原始
c.material.opacity = 1;
c.material.color.setHex(c.userData.originalColor);
}
}
});
}
```
## 待完成
### Phase 1: 完善状态配置 (constants.js)
```javascript
export const CABLE_CONFIG = {
lockedOpacityMin: 0.6,
lockedOpacityMax: 1.0,
otherOpacity: 0.5,
otherBrightness: 0.6,
pulseSpeed: 0.003,
pulseCoefficient: 0.4,
// 未来可扩展
// lockedLineWidth: 3,
// normalLineWidth: 1,
};
```
### Phase 2: 卫星状态管理 (satellites.js)
参考线缆状态管理,为卫星添加类似的状态枚举和状态管理函数:
```javascript
export const SATELLITE_STATE = {
NORMAL: 'normal',
HOVERED: 'hovered',
LOCKED: 'locked'
};
```
#### 卫星数据源说明
- **当前使用**: CelesTrak (https://celestrak.org) - 免费,无需认证
- **后续计划**: Space-Track.org (https://space-track.org) - 需要认证,数据更权威
- 迁移时只需修改 `satellites.js` 中的数据获取逻辑,状态管理和渲染逻辑不变
### Phase 3: 统一渲染接口
将所有对象的渲染逻辑抽象为一个统一的渲染函数:
```javascript
function applyObjectVisualState() {
applyCableVisualState();
applySatelliteVisualState();
applyLandingPointVisualState();
}
```
### Phase 4: UE 迁移准备
迁移到 Unreal Engine 时:
1. 保留 `constants.js` 中的枚举和配置
2. 保留 `cables.js` 中的数据层和状态管理
3. 保留 `main.js` 中的交互逻辑
4. **仅重写** `applyCableVisualState()` 等渲染函数
---
## 架构原则
1. **状态与渲染分离** - 对象状态由数据层管理,渲染层只负责根据状态更新视觉效果
2. **逻辑可复用** - 交互逻辑(点击、悬停、锁定)在迁移时应直接复用
3. **渲染可替换** - 渲染实现可以针对不同引擎重写,不影响逻辑层
## 文件变更记录
| 日期 | 文件 | 变更 |
|------|------|------|
| 2026-03-19 | constants.js | 新增 CABLE_STATE 枚举 |
| 2026-03-19 | cables.js | 新增状态管理函数 |
| 2026-03-19 | main.js | 使用状态管理,抽象 applyCableVisualState() |

View File

@@ -0,0 +1,293 @@
# WebGL Instancing 卫星渲染优化计划
## 背景
当前 `satellites.js` 使用 `THREE.Points` 渲染卫星,受限于 WebGL 点渲染性能,只能显示 ~500-1000 颗卫星。
需要迁移到真正的 WebGL Instancing 以支持 5000+ 卫星流畅渲染。
## 技术选型
| 方案 | 性能 | 改动量 | 维护性 | 推荐 |
|------|------|--------|--------|------|
| THREE.Points (现状) | ★★☆ | - | - | 基准 |
| THREE.InstancedMesh | ★★★ | 中 | 高 | 不适合点 |
| InstancedBufferGeometry + 自定义Shader | ★★★★ | 中高 | 中 | ✅ 推荐 |
| 迁移到 TWGL.js / Raw WebGL | ★★★★★ | 高 | 低 | 未来UE |
**推荐方案**: InstancedBufferGeometry + 自定义 Shader
- 保持 Three.js 架构
- 复用 satellite.js 数据层
- 性能接近原生 WebGL
---
## Phase 1: 调研与原型
### 1.1 分析现有架构
**现状 (satellites.js)**:
```javascript
// 创建点云
const pointsGeometry = new THREE.BufferGeometry();
pointsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
pointsGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const pointsMaterial = new THREE.PointsMaterial({
size: 2,
vertexColors: true,
transparent: true,
opacity: 0.8,
sizeAttenuation: true
});
satellitePoints = new THREE.Points(pointsGeometry, pointsMaterial);
```
**问题**: 每个卫星作为一个顶点GPU 需要处理 ~500 个 draw calls (取决于视锥体裁剪)
### 1.2 Instanced Rendering 原理
```javascript
// 目标:单次 draw call 渲染所有卫星
// 每个卫星属性:
// - position (vec3): 位置
// - color (vec3): 颜色
// - size (float): 大小 (可选)
// - selected (float): 是否选中 (0/1)
// 使用 InstancedBufferGeometry
const geometry = new THREE.InstancedBufferGeometry();
geometry.index = originalGeometry.index;
geometry.attributes.position = originalGeometry.attributes.position;
geometry.attributes.uv = originalGeometry.attributes.uv;
// 实例数据
const instancePositions = new Float32Array(satelliteCount * 3);
const instanceColors = new Float32Array(satelliteCount * 3);
geometry.setAttribute('instancePosition',
new THREE.InstancedBufferAttribute(instancePositions, 3));
geometry.setAttribute('instanceColor',
new THREE.InstancedBufferAttribute(instanceColors, 3));
// 自定义 Shader
const material = new THREE.ShaderMaterial({
vertexShader: `
attribute vec3 instancePosition;
attribute vec3 instanceColor;
varying vec3 vColor;
void main() {
vColor = instanceColor;
vec3 transformed = position + instancePosition;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 0.8);
}
`
});
```
---
## Phase 2: 实现
### 2.1 创建 instanced-satellites.js
```javascript
// instanced-satellites.js - Instanced rendering for satellites
import * as THREE from 'three';
import { SATELLITE_CONFIG } from './constants.js';
let instancedMesh = null;
let satelliteData = [];
let instancePositions = null;
let instanceColors = null;
let satelliteCount = 0;
const SATELLITE_VERTEX_SHADER = `
attribute vec3 instancePosition;
attribute vec3 instanceColor;
attribute float instanceSize;
varying vec3 vColor;
void main() {
vColor = instanceColor;
vec3 transformed = position * instanceSize + instancePosition;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`;
const SATELLITE_FRAGMENT_SHADER = `
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 0.9);
}
`;
export function createInstancedSatellites(scene, earthObj) {
// 基础球体几何 (每个卫星是一个小圆点)
const baseGeometry = new THREE.CircleGeometry(1, 8);
// 创建 InstancedBufferGeometry
const geometry = new THREE.InstancedBufferGeometry();
geometry.index = baseGeometry.index;
geometry.attributes.position = baseGeometry.attributes.position;
geometry.attributes.uv = baseGeometry.attributes.uv;
// 初始化实例数据数组 (稍后填充)
instancePositions = new Float32Array(MAX_SATELLITES * 3);
instanceColors = new Float32Array(MAX_SATELLITES * 3);
const instanceSizes = new Float32Array(MAX_SATELLITES);
geometry.setAttribute('instancePosition',
new THREE.InstancedBufferAttribute(instancePositions, 3));
geometry.setAttribute('instanceColor',
new THREE.InstancedBufferAttribute(instanceColors, 3));
geometry.setAttribute('instanceSize',
new THREE.InstancedBufferAttribute(instanceSizes, 1));
const material = new THREE.ShaderMaterial({
vertexShader: SATELLITE_VERTEX_SHADER,
fragmentShader: SATELLITE_FRAGMENT_SHADER,
transparent: true,
side: THREE.DoubleSide
});
instancedMesh = new THREE.Mesh(geometry, material);
instancedMesh.frustumCulled = false; // 我们自己处理裁剪
scene.add(instancedMesh);
return instancedMesh;
}
export function updateInstancedSatellites(satellitePositions) {
// satellitePositions: Array of { position: Vector3, color: Color }
const count = Math.min(satellitePositions.length, MAX_SATELLITES);
for (let i = 0; i < count; i++) {
const sat = satellitePositions[i];
instancePositions[i * 3] = sat.position.x;
instancePositions[i * 3 + 1] = sat.position.y;
instancePositions[i * 3 + 2] = sat.position.z;
instanceColors[i * 3] = sat.color.r;
instanceColors[i * 3 + 1] = sat.color.g;
instanceColors[i * 3 + 2] = sat.color.b;
}
instancedMesh.geometry.attributes.instancePosition.needsUpdate = true;
instancedMesh.geometry.attributes.instanceColor.needsUpdate = true;
instancedMesh.geometry.setDrawRange(0, count);
}
```
### 2.2 修改现有 satellites.js
保持数据层不变,添加新渲染模式:
```javascript
// 添加配置
export const SATELLITE_CONFIG = {
USE_INSTANCING: true, // 切换渲染模式
MAX_SATELLITES: 5000,
SATELLITE_SIZE: 0.5,
// ...
};
```
### 2.3 性能优化点
1. **GPU 实例化**: 单次 draw call 渲染所有卫星
2. **批量更新**: 所有位置/颜色一次更新
3. **视锥体裁剪**: 自定义裁剪逻辑,避免 CPU 端逐卫星检测
4. **LOD (可选)**: 远处卫星简化显示
---
## Phase 3: 与现有系统集成
### 3.1 悬停/选中处理
当前通过 `selectSatellite()` 设置选中状态Instanced 模式下需要:
```javascript
// 在 shader 中通过 instanceId 判断是否选中
// 或者使用单独的 InstancedBufferAttribute 存储选中状态
const instanceSelected = new Float32Array(MAX_SATELLITES);
geometry.setAttribute('instanceSelected',
new THREE.InstancedBufferAttribute(instanceSelected, 1));
```
### 3.2 轨迹线
轨迹线仍然使用 `THREE.Line``THREE.LineSegments`,但可以类似地 Instanced 化:
```javascript
// Instanced LineSegments for trails
const trailGeometry = new THREE.InstancedBufferGeometry();
trailGeometry.setAttribute('position', trailPositions);
trailGeometry.setAttribute('instanceStart', ...);
trailGeometry.setAttribute('instanceEnd', ...);
```
---
## Phase 4: 验证与调优
### 4.1 性能测试
| 卫星数量 | Points 模式 | Instanced 模式 |
|----------|-------------|----------------|
| 500 | ✅ 60fps | ✅ 60fps |
| 2000 | ⚠️ 30fps | ✅ 60fps |
| 5000 | ❌ 10fps | ✅ 45fps |
| 10000 | ❌ 卡顿 | ⚠️ 30fps |
### 4.2 可能遇到的问题
1. **Shader 编译错误**: 需要调试 GLSL
2. **实例数量限制**: GPU 最大实例数 (通常 65535)
3. **大小不一**: 需要 per-instance size 属性
4. **透明度排序**: Instanced 渲染透明度处理复杂
---
## 文件变更清单
| 文件 | 变更 |
|------|------|
| `constants.js` | 新增 `SATELLITE_CONFIG` |
| `satellites.js` | 添加 Instanced 模式支持 |
| `instanced-satellites.js` | 新文件 - Instanced 渲染核心 |
| `main.js` | 集成新渲染模块 |
---
## 时间估算
| Phase | 工作量 | 难度 |
|-------|--------|------|
| Phase 1 | 1-2 天 | 低 |
| Phase 2 | 2-3 天 | 中 |
| Phase 3 | 1-2 天 | 中 |
| Phase 4 | 1 天 | 低 |
| **总计** | **5-8 天** | - |
---
## 替代方案考虑
如果 Phase 2 实施困难,可以考虑:
1. **使用 Three.js InstancedMesh**: 适合渲染小型 3D 模型替代点
2. **使用 pointcloud2 格式**: 类似 LiDAR 点云渲染
3. **Web Workers**: 将轨道计算移到 Worker 线程
4. **迁移到 Cesium**: Cesium 原生支持 Instancing且是 UE 迁移的中间步骤