Three.js + WebGL 粒子动画实测:10 万粒子,流畅无压力

测试环境
Windows 桌面,WinForms + OpenTK (OpenGL 3.3)。
处理器 Intel(R) Core(TM) i9-14900HX (2.20 GHz)
机带 RAM 32.0 GB (31.7 GB 可用)
系统类型 64 位操作系统, 基于 x64 的处理器
操作系统版本 Windows 11 家庭中文版
实现原理
核心思路是:用 C# 承载页面,用 Three.js 组织 3D 场景,用 WebGL 做底层 GPU 绘制。
整体如下:
1. `WPF` 窗口中嵌入 `WebView2`。
2. 在 C# 中动态拼接 HTML,并通过 `NavigateToString` 注入页面。
3. 页面侧加载 `three.min.js`,创建 `Scene / Camera / WebGLRenderer`。
4. 粒子通过 `THREE.BufferGeometry` 与多个 `BufferAttribute` 存储。
5. 每帧在 JS 中更新粒子位置与生命周期,提交属性更新后由 GPU 渲染。
6. 页面每秒统计一次 FPS,通过 `window.chrome.webview.postMessage(...)` 回传给 C#,在 UI 上实时显示。
总结为:这是一个“CPU 负责粒子状态更新,GPU 负责大规模点精灵绘制”的混合型架构。
关键技术点
1. 跨技术栈集成:WPF 与 Web 3D 的桥接
- C# 端调用 `EnsureCoreWebView2Async()` 初始化浏览器内核。
- 使用 `WebMessageReceived` 接收前端消息,把 FPS 同步回桌面 UI。
- 这样可以让原生桌面应用拥有 WebGL 渲染能力,同时保留 WPF 的业务壳层。
2. 大规模粒子数据结构:BufferGeometry
- 粒子位置、颜色、尺寸、生命周期都以 `Float32Array` 组织。
- 通过 `geometry.setAttribute(...)` 绑定到 GPU 可消费的缓冲属性。
- 相比逐对象管理,**结构化数组 + 批量绘制**是海量粒子的基础。
3. 自定义 Shader:粒子视觉在 GPU 完成
- 顶点着色器负责点大小透视缩放、生命周期透明度系数计算。
- 片段着色器利用 `gl_PointCoord` 裁剪圆形粒子并做柔和边缘。
- 材质配置启用 `AdditiveBlending`,形成更亮的粒子叠加效果。
仿真更新模型:每帧 CPU 循环
当前粒子运动(重力、阻力、边界反弹、重生)都在 JS `for` 循环中执行。
这使逻辑直观易控,但也带来一个关键瓶颈:**粒子数线性增长时,CPU 计算和属性回写成本同步上升。
核心代码
1. **粒子总量入口**
- `private const int ParticleCount = 100000;`
- 这是压测的直接控制阀,影响所有数组长度和每帧循环规模。
2. **GPU 友好数据准备**
- `positions / colors / sizes / lifetimes / maxLifetimes` 都是 `Float32Array`。
- `BufferAttribute` 绑定后由 `THREE.Points` 进行一次性批量绘制。
3. **渲染材质与 Shader**
- `THREE.ShaderMaterial({...})` 挂载 `vertexShader` 与 `fragmentShader`。
- 开启 `transparent + AdditiveBlending + depthWrite:false`,确保粒子叠加效果。
4. **每帧更新与提交**
- `updateParticles(deltaTime)` 内遍历全部粒子更新状态。
- 更新后设置:
- `particles.geometry.attributes.position.needsUpdate = true`
- `particles.geometry.attributes.lifetime.needsUpdate = true`
- 这一步会触发 GPU 侧缓冲同步,粒子越多,带宽与提交压力越大。
5. **桌面侧 FPS 可观测性**
- 页面端每秒 `postMessage('FPS: ' + fps)`。
- C# 侧 `WebMessageReceived` 更新 `txtFPS.Text`。
- 这让测试过程具备了稳定的在线观测能力。
实测结果
1 万粒子: FPS 145 , 非常流畅 ;
10 万粒子: FPS 53 ,流畅 ;
50 万粒子: FPS 14 ,明显卡顿 ;
100 万粒子: FPS 7,严重卡顿,接近不可用 ;
性能分析
1. 为什么 1 万到 10 万仍能跑得动?
- WebGL 批量绘制能力较强,点精灵渲染天然适合并行。
- 数据结构使用 `BufferGeometry`,减少了对象级 draw call。
- Shader 逻辑相对轻量,没有复杂纹理采样或后处理。
2. 为什么到 50 万、100 万会急剧下滑?
核心原因不是“GPU 不能画点”,而是**CPU + 数据传输 + GPU 渲染**三者叠加后的总成本爆炸:
- **CPU O(N) 更新成本**:每帧遍历所有粒子,计算速度、位置、碰撞、重生。
- **缓冲更新成本**:`position/lifetime` 每帧都标记 `needsUpdate`,意味着大体量数据频繁上传。
- **像素填充压力**:粒子数量增大后,叠加混合(Additive)会显著增加片段处理负担。
- **浏览器容器开销**:WebView2 本质是嵌入式浏览器,不是纯原生图形管线,存在额外调度成本。
3. 从数据看当前实现的可用区间
- **推荐实时区间**:1 万 ~ 10 万(视业务目标而定)。
- **风险区间**:>10 万 后帧率明显衰减,尤其在复杂效果叠加时。
- **极限演示区间**:几十万到百万可以“展示数量级”,但不适合交互型实时动画。
总结
1. 它验证了 `WebView2 + Three.js + WebGL` 路线在桌面应用中的**可行性**。
2. 它给出了当前实现的**性能基线**:10 万以内可用,数十万后显著下降,百万级不可实时。
3. 它说明了后续优化的重点不只是“继续堆 GPU”,而是要系统处理**CPU 更新模型与数据上传路径**。

从这个演示效果,我们看到three.js的性能仅仅是opentk(opengl)的1/10 (https://blog.ZEEKLOG.net/LateFrames/article/details/158291013?spm=1001.2014.3001.5501), 可以看出来明显的区别:
