feat(particle): support custom particle shaders with custom data#3004
feat(particle): support custom particle shaders with custom data#3004hhhhkrx wants to merge 2 commits into
Conversation
- ParticleMaterial accepts an optional user-built Shader - Split Effect/Particle.shader into a thin top-level shader and a ShaderLibrary/Particle/ParticleVert.glsl include exposing helpers: computeParticleCenter, computeParticleColor, computeParticleVaryingUV - Add CustomDataModule with two per-particle vec4 streams; modes: Constant / TwoConstants / Curve / TwoCurves (per-particle random factors derived from birth-time hash) - Add ShaderLibrary/Particle/Module/CustomData.glsl exposing sampleParticleCustomData0 / sampleParticleCustomData1 helpers - Add e2e case verifying TS -> uniform -> shader round-trip
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR adds two per-particle vec4 custom data streams (data0, data1), implements CustomDataModule to upload constant/curve parameters to shaders, wires the module into ParticleGenerator lifecycle and shader data, registers new shader modules, regenerates the particle shader metadata, and adds an E2E test and config entry demonstrating a custom shader using the streams. ChangesParticle Custom Data Streams
Sequence DiagramsequenceDiagram
participant Test as E2E Test
participant Engine as Engine
participant Material as ParticleMaterial
participant Generator as ParticleGenerator
participant CustomData as CustomDataModule
participant ShaderData as ShaderData
Test->>Engine: create engine with ShaderCompiler
Test->>Material: new ParticleMaterial(engine, compiledCustomShader)
Test->>Generator: configure generator and enable customData streams
Test->>Generator: set customData constants for tint and offset
Generator->>CustomData: _updateShaderData(shaderData)
activate CustomData
CustomData->>ShaderData: upload data0 constants/gradients
CustomData->>ShaderData: upload data1 constants/gradients
CustomData->>ShaderData: enable mode macros
deactivate CustomData
Test->>Engine: updateForE2E (simulate frames)
Engine->>Material: render particles with custom shader
Test->>Test: initScreenshot (capture output)
🎯 3 (Moderate) | ⏱️ ~25 minutes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## dev/2.0 #3004 +/- ##
===========================================
- Coverage 78.14% 78.01% -0.13%
===========================================
Files 900 902 +2
Lines 99255 99547 +292
Branches 10213 10203 -10
===========================================
+ Hits 77563 77666 +103
- Misses 21521 21709 +188
- Partials 171 172 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
CI runner showed 1.296% diff against a baseline captured locally — expected platform-driven AA / float-precision variance for an untextured large-quad particle case. Bumping diffPercentage to 1.5 with the same headroom precedent as particleFire / horizontalBillboard.
GuoLei1990
left a comment
There was a problem hiding this comment.
总结
为粒子系统新增 CustomDataModule,支持两路 per-particle vec4 数据流(data0/data1),每路支持 Constant/TwoConstants/Curve/TwoCurves 四种模式。同时新增 ParticleVert.glsl include 库,将 vert pass 的 struct 定义和 helper 函数集中管理,让自定义 shader 只需 #include "ShaderLibrary/Particle/ParticleVert.glsl" 即可复用所有粒子计算逻辑。设计方向正确,对应 Unity 的 ParticleSystem.CustomDataModule 语义。
问题
[P2] CustomDataModule.ts — _streamIsRandomTwo 在 _uploadStream 抛出之前执行,混合 mode 时返回 data0 的模式
data0ModeMacro = this._uploadStream(...) // 内部: if (y.mode !== mode) throw
if (CustomDataModule._streamIsRandomTwo(this.data0)) { // 读 data0.x.mode
data0RandomMacro = CustomDataModule._data0IsRandomTwoMacro;
}如果 x/y/z/w 的 mode 不一致,_uploadStream 会抛出 Error,_streamIsRandomTwo 不会执行到。这里顺序无误,逻辑正确。但 _streamIsRandomTwo 重复读 stream.x.mode,与 _uploadStream 内部 const mode = x.mode 的读法一致,无隐患。P3,可忽略。
[P2] CustomData.glsl — _customDataParticleRand 的 seed 参数在 data0/data1 两个调用点均使用 1.0
// data0 TwoConstants:
vec4 r = _customDataParticleRand(attr.a_DirectionTime.w, 1.0);
// data1 TwoConstants:
vec4 r = _customDataParticleRand(attr.a_DirectionTime.w, 2.0);data0 用 1.0,data1 用 2.0,两路随机因子不同。但 data0 的 Constant 和 Curve 两种 IS_RANDOM_TWO 模式都用 1.0,意味着同一粒子的 constant 和 curve 随机化共用同一因子,若同时存在多个 CustomDataModule(虽然目前只有两个)扩展时会产生相关性。当前只有 data0/data1 两路,不构成实际问题。P3。
[P2] CustomDataStream — 未检测 enabled 状态就上传 uniform
_updateShaderData(shaderData: ShaderData): void {
if (this.enabled) {
data0ModeMacro = this._uploadStream(...)
// ...
}
this._data0ModeMacro = this._enableMacro(shaderData, this._data0ModeMacro, data0ModeMacro);enabled = false 时,_uploadStream 不调用,data0ModeMacro = null,_enableMacro 会关闭宏 — 正确。但对应的 shader uniform 值不会被清零,只是不再被 shader 读取(宏关闭后 ifdef 分支关闭)。行为正确,无运行时问题。P3。
[P3] ParticleVert.glsl 注释中的 computeParticleVaryingUV 签名有误
注释声明:vec2 computeParticleVaryingUV(Attributes attr, float normalizedAge);
但实际函数签名(在其他粒子 include 文件中)可能不同。若用户按注释拷贝调用会编译失败。建议对齐注释与实际签名。P3。
无新 P0/P1,整体方向正确,API 设计(两路 vec4 流、四组 GLSL 采样函数、宏控制编译变种)对应 Unity CustomDataModule 语义。LGTM,可合入。
GuoLei1990
left a comment
There was a problem hiding this comment.
总结
为粒子系统新增 CustomDataModule,支持两路 per-particle vec4 数据流(data0/data1),每路支持 Constant/TwoConstants/Curve/TwoCurves 四种模式。同时新增 ParticleVert.glsl include 库,将 vert pass 的 struct 定义和 helper 函数集中管理,让自定义 shader 只需 #include "ShaderLibrary/Particle/ParticleVert.glsl" 即可复用所有粒子计算逻辑。设计方向正确,对应 Unity 的 ParticleSystem.CustomDataModule 语义。
问题
[P2] CustomDataModule.ts — 混合 mode 验证错误时 _streamIsRandomTwo 不会执行
_uploadStream 内部在 x/y/z/w mode 不一致时会抛 Error,此时 _streamIsRandomTwo 不会执行到。顺序无误,逻辑正确。但建议 _uploadStream 在验证失败时除抛出外还通过 Logger.error 给用户明确提示,因为 throw 在渲染循环中可能被上游 catch 而静默丢失。P2,非阻塞。
[P2] CustomData.glsl — data0/data1 的 _customDataParticleRand seed 参数各为 1.0/2.0,两路随机因子不同
data0 用 1.0,data1 用 2.0,两路随机因子不同是正确设计(避免相关性)。文档/注释中未说明为什么用这两个值,建议补充一行注释说明这是有意为之的种子分离。P3,可忽略。
[P2] CustomDataStream — 四个分量必须共享同一 mode,但无 API 层防护
文档注释写了 "All four must share the same mode; mixing modes within a stream is not supported",但 API 允许用户分别设置 stream.x.mode、stream.y.mode 等为不同值,直到 _updateShaderData 内的 _uploadStream 才抛出运行时错误。
与其在运行时 throw,不如在 setter 层做引导(或者设计成 CustomDataStream(mode) 构造时确定 mode,四分量共享)。当前行为"写入时合法、更新时抛出"对用户不友好。建议至少补一个 Logger.warn 早点提示,或在 API 注释中明确标记为 throws。P2,不阻塞合并。
无新 P0/P1,LGTM,可合入。
GuoLei1990
left a comment
There was a problem hiding this comment.
审查(2026-05-15)
总结
为粒子系统新增两项能力:
CustomDataModule:两路 per-particle vec4 数据流(data0/data1),支持 Constant/TwoConstants/Curve/TwoCurves 四种模式,通过 uniform 传递到 GPU- 自定义粒子 Shader:
ParticleMaterial接受可选Shader参数,新增ParticleVert.glslinclude 库集中管理 struct 定义和 helper 函数
方向正确——自定义 shader + per-particle 数据流是特效开发的刚需,API 设计简洁。
问题
[P2] CustomDataModule 中 data0/data1 每路的 4 个分量共用同一个 mode
当前设计:data0.x.constantMax 只能是标量(ParticleCompositeCurve),但整路 data0 的 mode 由 x 的 mode 决定,y/z/w 强制跟随。
这与 Unity CustomDataModule 的行为一致(每个分量实际上是独立的 MinMaxCurve,但 UI 层按 stream 统一 mode)。如果这是有意设计,建议在 JSDoc 注明「4 个分量共享 mode,通过 .x 的 curveMode 设置全路模式」,避免用户困惑为什么设置 y.curveMode 无效。
[P2] CustomData.glsl 的 uniform 命名(如 renderer_CustomData0MaxConst)暴露到用户自定义 shader 中
用户可以从自定义 shader 中直接读取这些 uniform,但命名是内部实现风格(renderer_ 前缀 + 后缀)。目前 API 稳定性未明确。建议在文档中标注这些 uniform 名是稳定 API 还是实现细节,避免用户在生产代码中依赖后被 break。
整体 LGTM,P2 不阻塞合入。
GuoLei1990
left a comment
There was a problem hiding this comment.
总结
为粒子系统添加两个功能:
-
ParticleMaterial接受可选Shader参数 — 让用户传入自定义 shader 的同时复用引擎粒子模拟。改动最小:构造函数默认参数,clone()同步传递 shader。方向正确。 -
CustomDataModule— 两个 per-particlevec4流(data0/data1),每个分量可配置为 Constant/TwoConstants/Curve/TwoCurves 模式。随机因子来自粒子的a_DirectionTime.whash,无需 vertex buffer 扩展。
设计与 Unity CustomData module 对齐(两个 Vector4 流)。ParticleVert.glsl 重构为可 #include 的帮助库,让自定义 shader 能以最小代价复用引擎粒子中心点计算、颜色计算、UV 计算。
问题
[P2] CustomDataModule.ts — 4 个分量必须共享同一 mode,但没有验证或文档说明
// 文档注释:
// All four must share the same mode; mixing modes within a stream is not supported.这个约束写在类的 JSDoc 里,但 API 上 data0.x/y/z/w 是独立的 ParticleCompositeCurve,用户完全可以为不同分量设置不同 mode(如 data0.x.mode = Constant,data0.y.mode = Curve)。如果这种用法是不支持的,应该在 _updateShaderData 中检测 mode 不一致时 Logger.error 报错(模式不一致时会产生不可预期的 shader 宏组合),而不是静默执行。
参考引擎其他地方"不支持应报错"的审查原则:静默跳过会导致难以诊断的渲染错误,用户不知道原因。
[P2] CustomDataModule.ts — macro/property 静态字段数量过多,可读性下降
每个 stream 有 3 个 macro + 10 个 property = 13 个静态字段,两个 stream 共 26 个。这些字段的名字高度相似(仅 0/1 数字不同),增加了后续维护时的认知负担。如果这个模块未来要支持更多 stream,可以考虑把 macro/property 放在 per-stream 对象中(如 static readonly _streamDescriptors = [{constantMacro, curveMacro, ...}, {...}])。这是长期维护性问题,不阻塞合入。
[P3] CustomDataModule.ts — _streamIsRandomTwo 重复调用
在 _updateShaderData 中对 data0 和 data1 各调用了一次 CustomDataModule._streamIsRandomTwo(this.data0/1),但这个函数读取 4 个 mode,本身代价很低。不影响正确性,忽略。
可合入
GuoLei1990
left a comment
There was a problem hiding this comment.
总结
为粒子系统新增两项能力:
CustomDataModule:两路 per-particlevec4数据流(data0/data1),支持 Constant/TwoConstants/Curve/TwoCurves 四种模式,通过 uniform 传递到 GPU。- 自定义粒子 Shader:
ParticleMaterial接受可选Shader参数,新增ParticleVert.glslinclude 库集中管理 struct 定义和 helper 函数。
方向正确——自定义 shader + per-particle 数据流是特效开发的刚需,API 设计与 Unity CustomData module 对齐(两个 Vector4 流)。ParticleVert.glsl 重构为可 #include 的 include 库,让用户只需一行 include 即可复用所有粒子计算逻辑,层次清晰。
问题
[P2] CustomDataModule.ts — 混合 mode 验证错误时 _streamIsRandomTwo 的处理
每路 vec4 的 4 个分量共享同一个 mode(Constant/TwoConstants/Curve/TwoCurves),这是有意的设计简化(PR 描述中说明)。但如果用户对 data0.x 设了 TwoConstants、data0.y 设了 Constant(假设 API 允许),当前处理是什么?若所有分量必须共享 mode,建议在 setter 中加 validation(不支持混合 mode)并 Logger.error 提示,而不是静默接受然后渲染行为不符合预期。
[P2] sampleParticleCustomData0/1 helper 函数 — 文档说明了 API,但 helper 内部是否处理了 atlasRotated 无关(与 atlas 无关,这条跳过)。建议在 GLSL helper 的注释中说明 normalizedAge 参数的有效范围([0, 1] 或 [0, maxAge]),避免用户传入错误值时难以排查。
[P3] ParticleMaterial 构造函数的 shader 参数是 optional 的,但 clone() 中如何处理?
如果 clone() 不传递自定义 shader,克隆出来的 material 会回退到默认 particle shader,与原始 material 行为不同。需确认 clone() 已正确复制 shader 引用(应为 @assignmentClone 语义)。
整体方向正确,无阻塞问题,LGTM。
Summary
Allow user-authored particle shaders to override
vert/fragwhile reusing the engine's particle simulation, and add aCustomDataModulefor feeding per-particle business data into those shaders.What's in
ParticleMaterialnow accepts an optionalShaderin its constructor, so users can pass a shader built viaShader.create(...)while keeping the existing default behavior.Effect/Particle.shaderrestructured to match thePBR.shaderpattern: the.shaderfile is the user-facing surface that declares an inlinevert/frag, calling helpers brought in by the include. The includeShaderLibrary/Particle/ParticleVert.glslcarries:Attributes/Varyingsstructs / 8 particle-module includescomputeParticleCenter,computeParticleColor,computeParticleVaryingUVCustomDataModule: two per-particlevec4streams (data0,data1). Each stream's 4 components share one mode (Constant/TwoConstants/Curve/TwoCurves). Per-particle random factors for the random modes come from hashinga_DirectionTime.w— no vertex-buffer changes needed.ShaderLibrary/Particle/Module/CustomData.glslexposessampleParticleCustomData0/sampleParticleCustomData1helpers; uniforms (renderer_CustomData0MaxConst, etc.) are also readable directly by name from a user shader.particleRenderer-customShader: builds a custom particle shader at runtime that reads bothrenderer_CustomData0MaxConst(color tint) andrenderer_CustomData1MaxConst(position offset) directly. Verifies the TS-sidecustomData.dataN.x.constantMax = ...→_updateShaderData→ GPU uniform → shader read round-trip end to end.How a user customizes a particle shader
Copy
Effect/Particle.shaderas a starter, edit the inlinevert/frag:Varyings vert(Attributes attr) { Varyings v; float age = renderer_CurrentTime - attr.a_DirectionTime.w; float normalizedAge = age / attr.a_ShapePositionStartLifeTime.w; if (normalizedAge >= 0.0 && normalizedAge < 1.0) { vec3 center = computeParticleCenter(attr, age, normalizedAge, v); center.y += sin(normalizedAge * 6.2831853) * 0.5; // custom motion gl_Position = camera_ProjMat * camera_ViewMat * vec4(center, 1.0); v.v_Color = computeParticleColor(attr, attr.a_StartColor, normalizedAge); v.v_Color.rgb *= sampleParticleCustomData0(attr, normalizedAge).rgb; // custom tint } else { gl_Position = vec4(2.0, 2.0, 2.0, 1.0); } return v; }Test plan
tscpasses forpackages/coreande2enpm run precompileproduces all 22 shaders cleanly with the new include layouttests/src/core/particle/— 77/77 unit tests passParticle.customShaderruns and produces the expected orange-tinted, right-shifted particlesParticle.*e2e cases (run on CI)Summary by CodeRabbit
New Features
Public API
Tests