Skip to content

feat(particle): support custom particle shaders with custom data#3004

Open
hhhhkrx wants to merge 2 commits into
galacean:dev/2.0from
hhhhkrx:feat/particle-custom-fragment
Open

feat(particle): support custom particle shaders with custom data#3004
hhhhkrx wants to merge 2 commits into
galacean:dev/2.0from
hhhhkrx:feat/particle-custom-fragment

Conversation

@hhhhkrx
Copy link
Copy Markdown
Contributor

@hhhhkrx hhhhkrx commented May 13, 2026

Summary

Allow user-authored particle shaders to override vert / frag while reusing the engine's particle simulation, and add a CustomDataModule for feeding per-particle business data into those shaders.

What's in

  • ParticleMaterial now accepts an optional Shader in its constructor, so users can pass a shader built via Shader.create(...) while keeping the existing default behavior.
  • Effect/Particle.shader restructured to match the PBR.shader pattern: the .shader file is the user-facing surface that declares an inline vert / frag, calling helpers brought in by the include. The include ShaderLibrary/Particle/ParticleVert.glsl carries:
    • All engine uniforms / Attributes / Varyings structs / 8 particle-module includes
    • Reusable helpers: computeParticleCenter, computeParticleColor, computeParticleVaryingUV
  • CustomDataModule: two per-particle vec4 streams (data0, data1). Each stream's 4 components share one mode (Constant / TwoConstants / Curve / TwoCurves). Per-particle random factors for the random modes come from hashing a_DirectionTime.w — no vertex-buffer changes needed.
  • ShaderLibrary/Particle/Module/CustomData.glsl exposes sampleParticleCustomData0 / sampleParticleCustomData1 helpers; uniforms (renderer_CustomData0MaxConst, etc.) are also readable directly by name from a user shader.
  • E2E case particleRenderer-customShader: builds a custom particle shader at runtime that reads both renderer_CustomData0MaxConst (color tint) and renderer_CustomData1MaxConst (position offset) directly. Verifies the TS-side customData.dataN.x.constantMax = ..._updateShaderData → GPU uniform → shader read round-trip end to end.

How a user customizes a particle shader

Copy Effect/Particle.shader as a starter, edit the inline vert / 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

  • tsc passes for packages/core and e2e
  • npm run precompile produces all 22 shaders cleanly with the new include layout
  • tests/src/core/particle/ — 77/77 unit tests pass
  • E2E Particle.customShader runs and produces the expected orange-tinted, right-shifted particles
  • Visual regression on remaining Particle.* e2e cases (run on CI)

Summary by CodeRabbit

  • New Features

    • Per-particle custom data streams (two vec4 channels) for richer particle effects and custom shaders.
    • Particle materials can accept a custom shader to control particle rendering.
  • Public API

    • New exports for the custom-data module/stream and two new random-seed constants for custom data.
  • Tests

    • Added an end-to-end test validating custom-shader particle rendering with per-particle data.

Review Change Stack

- 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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a376ff81-e052-4975-a0b4-e431b98b3019

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3daa4 and ada8712.

📒 Files selected for processing (1)
  • e2e/config.ts

Walkthrough

This 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.

Changes

Particle Custom Data Streams

Layer / File(s) Summary
Data structures and public API
packages/core/src/particle/modules/CustomDataModule.ts, packages/core/src/particle/enums/ParticleRandomSubSeeds.ts, packages/core/src/particle/ParticleMaterial.ts, packages/core/src/particle/index.ts
Adds CustomDataStream, new random sub-seed constants, makes ParticleMaterial accept an optional shader parameter, and re-exports the new types.
ParticleGenerator wiring
packages/core/src/particle/ParticleGenerator.ts
Adds readonly customData: CustomDataModule, initializes it in the constructor, and invokes its _updateShaderData and _resetRandomSeed at the appropriate lifecycle points.
CustomDataModule implementation
packages/core/src/particle/modules/CustomDataModule.ts
Implements CustomDataModule with data0/data1 streams, shader macro/property bindings, per-stream upload logic for constant or curve modes, component-mode validation, and per-stream random seed handling.
Shader library & compiled shader
packages/shader/src/ShaderLibrary/index.ts, packages/shader/compiledShaders/Effect/Particle.shaderc
Registers Particle_Module_CustomData and Particle_ParticleVert in shader library and regenerates Effect/Particle.shaderc instruction metadata.
E2E test and config
e2e/case/particleRenderer-customShader.ts, e2e/config.ts
Adds an E2E test that compiles an inline custom shader using renderer_CustomData* inputs, configures generator constants for tint/offset, runs the simulation, captures a screenshot, and registers the test in config.

Sequence Diagram

sequenceDiagram
  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)
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 I stitched two vec4 streams so bright,
Constants and curves leap into light,
Shaders sip colors, offsets take flight,
Particles shimmer through the night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature: enabling custom particle shaders with custom data support through ParticleMaterial shader parameter and CustomDataModule.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 51.34228% with 145 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.01%. Comparing base (25ba6eb) to head (ada8712).
⚠️ Report is 1 commits behind head on dev/2.0.

Files with missing lines Patch % Lines
...ages/core/src/particle/modules/CustomDataModule.ts 59.64% 92 Missing ⚠️
e2e/case/particleRenderer-customShader.ts 0.00% 45 Missing and 1 partial ⚠️
e2e/config.ts 0.00% 6 Missing ⚠️
packages/core/src/particle/ParticleMaterial.ts 75.00% 1 Missing ⚠️
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     
Flag Coverage Δ
unittests 78.01% <51.34%> (-0.13%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.
@hhhhkrx hhhhkrx self-assigned this May 13, 2026
Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

总结

为粒子系统新增 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_customDataParticleRandseed 参数在 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,可合入。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

总结

为粒子系统新增 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.modestream.y.mode 等为不同值,直到 _updateShaderData 内的 _uploadStream 才抛出运行时错误。

与其在运行时 throw,不如在 setter 层做引导(或者设计成 CustomDataStream(mode) 构造时确定 mode,四分量共享)。当前行为"写入时合法、更新时抛出"对用户不友好。建议至少补一个 Logger.warn 早点提示,或在 API 注释中明确标记为 throws。P2,不阻塞合并。

无新 P0/P1,LGTM,可合入。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查(2026-05-15)

总结

为粒子系统新增两项能力:

  1. CustomDataModule:两路 per-particle vec4 数据流(data0/data1),支持 Constant/TwoConstants/Curve/TwoCurves 四种模式,通过 uniform 传递到 GPU
  2. 自定义粒子 ShaderParticleMaterial 接受可选 Shader 参数,新增 ParticleVert.glsl include 库集中管理 struct 定义和 helper 函数

方向正确——自定义 shader + per-particle 数据流是特效开发的刚需,API 设计简洁。

问题

[P2] CustomDataModuledata0/data1 每路的 4 个分量共用同一个 mode

当前设计:data0.x.constantMax 只能是标量(ParticleCompositeCurve),但整路 data0 的 mode 由 x 的 mode 决定,y/z/w 强制跟随。

这与 Unity CustomDataModule 的行为一致(每个分量实际上是独立的 MinMaxCurve,但 UI 层按 stream 统一 mode)。如果这是有意设计,建议在 JSDoc 注明「4 个分量共享 mode,通过 .xcurveMode 设置全路模式」,避免用户困惑为什么设置 y.curveMode 无效。

[P2] CustomData.glsl 的 uniform 命名(如 renderer_CustomData0MaxConst)暴露到用户自定义 shader 中

用户可以从自定义 shader 中直接读取这些 uniform,但命名是内部实现风格(renderer_ 前缀 + 后缀)。目前 API 稳定性未明确。建议在文档中标注这些 uniform 名是稳定 API 还是实现细节,避免用户在生产代码中依赖后被 break。

整体 LGTM,P2 不阻塞合入。

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

总结

为粒子系统添加两个功能:

  1. ParticleMaterial 接受可选 Shader 参数 — 让用户传入自定义 shader 的同时复用引擎粒子模拟。改动最小:构造函数默认参数,clone() 同步传递 shader。方向正确。

  2. CustomDataModule — 两个 per-particle vec4 流(data0/data1),每个分量可配置为 Constant/TwoConstants/Curve/TwoCurves 模式。随机因子来自粒子的 a_DirectionTime.w hash,无需 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 = Constantdata0.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,本身代价很低。不影响正确性,忽略。

可合入

Copy link
Copy Markdown
Member

@GuoLei1990 GuoLei1990 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

总结

为粒子系统新增两项能力:

  1. CustomDataModule:两路 per-particle vec4 数据流(data0/data1),支持 Constant/TwoConstants/Curve/TwoCurves 四种模式,通过 uniform 传递到 GPU。
  2. 自定义粒子 ShaderParticleMaterial 接受可选 Shader 参数,新增 ParticleVert.glsl include 库集中管理 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。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants