docs: add architecture refactor and webgl instancing plans
This commit is contained in:
165
.sisyphus/plans/earth-architecture-refactor.md
Normal file
165
.sisyphus/plans/earth-architecture-refactor.md
Normal 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() |
|
||||||
293
.sisyphus/plans/webgl-instancing-satellites.md
Normal file
293
.sisyphus/plans/webgl-instancing-satellites.md
Normal 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 迁移的中间步骤
|
||||||
Reference in New Issue
Block a user