Files
planet/.sisyphus/plans/webgl-instancing-satellites.md

8.3 KiB
Raw Blame History

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):

// 创建点云
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 原理

// 目标:单次 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

// 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

保持数据层不变,添加新渲染模式:

// 添加配置
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 模式下需要:

// 在 shader 中通过 instanceId 判断是否选中
// 或者使用单独的 InstancedBufferAttribute 存储选中状态
const instanceSelected = new Float32Array(MAX_SATELLITES);
geometry.setAttribute('instanceSelected', 
  new THREE.InstancedBufferAttribute(instanceSelected, 1));

3.2 轨迹线

轨迹线仍然使用 THREE.LineTHREE.LineSegments,但可以类似地 Instanced 化:

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