HarmonyOS 首个 GPU 粒子系统:从 Transform Feedback 到工程化接入
在 HarmonyOS 上做 GPU 粒子系统,你可能会遇到这些问题:
- Compute Shader? ES 3.1 才有,兼容性不够
- SSBO? 同样是 ES 3.1,很多设备不支持
- CPU 更新? 10 万粒子直接卡成 PPT
那么,在 ES 3.0 设备上,怎么做 GPU 粒子系统?
答案是:Transform Feedback + Ping-Pong + Curl Noise。
本文基于我在 Resonance 项目中的真实代码,拆解这套方案的完整实现,并给出工程化接入路径。
先说清楚当前状态,避免误导
- ✅ GPUParticleSystem 与 StarryNightEffectsManager 已实现(Transform Feedback / Ping-Pong / Curl Noise 都在)
- ✅ NAPI 与 ArkTS 的配置接口也已打通
- ⚠️ 当前 StarryNightScene 主渲染仍以程序化笔触几何为主,TF 粒子模块在这个分支里尚未完整接入渲染主路径
这篇文章重点是:可复用的 TF 粒子实现 + 在 HarmonyOS 工程里的正确接法。
一、为什么在 ES 3.0 里选 Transform Feedback
在 OpenGL ES 3.0 设备上,想把粒子更新放到 GPU,Transform Feedback 是最稳妥的通路:
| 方案 | OpenGL ES 版本 | 兼容性 | 性能 |
|---|---|---|---|
| Compute Shader | ES 3.1+ | ❌ 很多设备不支持 | ⭐⭐⭐⭐⭐ |
| SSBO | ES 3.1+ | ❌ 很多设备不支持 | ⭐⭐⭐⭐⭐ |
| Transform Feedback | ES 3.0 | ✅ 核心能力,广泛支持 | ⭐⭐⭐⭐ |
所以对"ES 3.0 基线 + 兼容面优先"的项目,TF 是现实方案。
二、项目中的真实落点
核心代码都在 treeLife 模块:
Application/feature/treeLife/src/main/cpp/include/effects/
├── GPUParticleSystem.h
├── StarryNightEffects.h
├── CurlNoise.h
├── PostProcessing.h
└── ...
Native 链接配置是 ES3/EGL:
target_link_libraries(lifetree
libace_napi.z.so
libGLESv3.so
libEGL.so
)
ArkTS 到 Native 的接口在 liblifetree.so.d.ts,包含:
setStarryNightConfig(...)switchScene(...)updateGyroscope(...)onTouchStart/Move/End(...)
三、数据结构:32 字节粒子布局
粒子结构体(位置、速度、生命周期、大小、色相):
struct GPUParticle {
float x, y;
float vx, vy;
float life;
float age;
float size;
float hue;
};
设计点:
- ✅ 连续内存,便于 VBO 传输与顶点属性映射
- ✅ 纯 float,贴合 GPU 处理路径
- ✅ 一份结构同时服务更新 pass 与渲染 pass
四、Transform Feedback + Ping-Pong 的核心循环
1) 先声明 TF 输出变量,再链接程序
const char* feedbackVaryings[] = {
"vPosition", "vVelocity", "vLife", "vAge", "vSize", "vHue"
};
glTransformFeedbackVaryings(program, 6, feedbackVaryings, GL_INTERLEAVED_ATTRIBS);
glLinkProgram(program);
2) 双缓冲避免"同缓冲读写冲突"
int nextBuffer = 1 - currentBuffer_;
glUseProgram(updateProgram_);
glEnable(GL_RASTERIZER_DISCARD);
glBindVertexArray(vao_[currentBuffer_]); // 读
glBindTransformFeedback(GL_TRANSFORM_FEEDBACK, transformFeedback_[nextBuffer]); // 写
glBeginTransformFeedback(GL_POINTS);
glDrawArrays(GL_POINTS, 0, particleCount_);
glEndTransformFeedback();
glDisable(GL_RASTERIZER_DISCARD);
currentBuffer_ = nextBuffer;
这就是标准的 Ping-Pong:A 读 B 写,下一帧交换。
五、Shader:Curl Noise 让流场更自然
更新着色器里做三件事:
- 生命周期推进与重生
- curlNoise + vortex 合成速度场
- 输出下一帧粒子状态到 TF buffer
为什么用 Curl Noise?
| 噪声类型 | 视觉效果 | 运动特点 | 适用场景 |
|---|---|---|---|
| Perlin Noise | 随机飘动 | 运动僵硬,容易"抖动" | 云、地形 |
| Simplex Noise | 随机飘动 | 比 Perlin 平滑,但仍有抖动 | 云、地形 |
| Curl Noise | 流体旋涡 | 场连续,运动自然 | 星空、烟雾、能量流 |
Curl Noise 的核心优势是"无散度"(divergence-free) ,这意味着粒子不会"凭空消失"或"凭空出现",运动轨迹更像真实的流体。
六、HarmonyOS 接入链路(ArkTS -> NAPI -> Scene)
当前工程里的配置链路是:
- ArkTS 调用
lifetree.setStarryNightConfig(config) - NAPI
SetStarryNightConfig解析字段 - SceneRenderer
::setStarryNightConfig(...) - SceneManager
::setStarryNightConfig(...) - StarryNightScene
::setEffectsConfig(...)
这条链是通的,参数能下发到场景层。
七、当前分支的接入边界
当前 StarryNightScene 里有几个明显信号:
initParticles()为空实现(注释明确写了"简化:不用粒子系统")update()当前是静态路径setEffectsConfig()主要保存配置和视差参数
八、常见坑
| 坑 | 为什么 | 后果 |
|---|---|---|
| glTransformFeedbackVaryings 必须在 glLinkProgram 前 | TF 输出变量需要在链接时确定 | 否则 TF 不生效,数据不会写入 |
| 更新阶段必须 GL_RASTERIZER_DISCARD | TF 只需要更新数据,不需要光栅化 | 否则会浪费 GPU 资源 |
| 必须双缓冲 | OpenGL 不允许同一 VBO 同时读写 | 否则会出现"未定义行为",数据错乱 |
| 不要在每帧热路径反复 glGetUniformLocation | glGetUniformLocation 是 CPU 操作,很慢 | 会拖累帧率 |
| ES 3.0 调试读取不要写 glGetBufferSubData | ES 3.0 没有这个 API | 改用 glMapBufferRange |
| 大粒子 + 全屏高分辨率 + 加法混合 | 会放大 fill-rate 压力 | GPU 占用过高,掉帧 |
九、总结
这套实现的价值,不是"炫参数",而是给出一条在 HarmonyOS + ES 3.0 上可工程化复用的 GPU 粒子路线:
- ✅ 能力层:Transform Feedback + Ping-Pong + Curl Noise
- ✅ 工程层:ArkTS/NAPI/Scene 配置链路
- ✅ 落地层:可渐进接入,不必一次性重写整条渲染管线
- 如有需要请添加本人联系方式(备明来意):YunShen1933
下一步:
- 完整接入 StarryNightScene 主渲染路径
- 补充压测数据(2w/4w/8w/10w 粒子)
- 开源到 GitHub,供社区复用. 由于原项目为本人耗尽心力,从0--1开发出来的原生纯血鸿蒙UGC社区app,所以拆分和剥离工作比较精力有限, 如果各位觉得需要本项目原代码, 我可以单独提供,望见谅.
- 正在抽时间把原项目一点点拆分并完全开源.
如果你也在做 HarmonyOS 原生渲染,这条路线值得直接上手。
发布时间:2026-03-03
发布人:云深
关键词:HarmonyOS、OpenGL ES 3.0、Transform Feedback、GPU 粒子系统、Curl Nois