From 3e3090d72ac10ad337c32519f20458733b8ec8d5 Mon Sep 17 00:00:00 2001 From: linkong Date: Fri, 20 Mar 2026 13:53:36 +0800 Subject: [PATCH] docs: add architecture refactor and webgl instancing plans --- .../plans/earth-architecture-refactor.md | 165 ++++++++++ .../plans/webgl-instancing-satellites.md | 293 ++++++++++++++++++ 2 files changed, 458 insertions(+) create mode 100644 .sisyphus/plans/earth-architecture-refactor.md create mode 100644 .sisyphus/plans/webgl-instancing-satellites.md diff --git a/.sisyphus/plans/earth-architecture-refactor.md b/.sisyphus/plans/earth-architecture-refactor.md new file mode 100644 index 00000000..2ce150a7 --- /dev/null +++ b/.sisyphus/plans/earth-architecture-refactor.md @@ -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() | diff --git a/.sisyphus/plans/webgl-instancing-satellites.md b/.sisyphus/plans/webgl-instancing-satellites.md new file mode 100644 index 00000000..c9a61e95 --- /dev/null +++ b/.sisyphus/plans/webgl-instancing-satellites.md @@ -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 迁移的中间步骤