# 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 迁移的中间步骤