Skip to content

feat(particle): add NoiseModule for simplex noise turbulence#2953

Open
hhhhkrx wants to merge 1 commit intogalacean:dev/2.0from
hhhhkrx:feat/noise
Open

feat(particle): add NoiseModule for simplex noise turbulence#2953
hhhhkrx wants to merge 1 commit intogalacean:dev/2.0from
hhhhkrx:feat/noise

Conversation

@hhhhkrx
Copy link
Copy Markdown
Contributor

@hhhhkrx hhhhkrx commented Apr 3, 2026

Summary

  • Add NoiseModule to the particle system, referencing Unity's Noise Module
  • GPU-computed simplex noise displacement using existing noise_common + noise_simplex_3D shader libraries
  • Supports per-axis strength, frequency, scroll speed, damping, and up to 3 octaves
  • No instance buffer changes needed — purely uniform-driven
  • Noise bounds expansion applied in world space (after rotation transform) to ensure correct culling with rotated emitters

Usage

const generator = particleRenderer.generator;
generator.noise.enabled = true;
generator.noise.strengthX = 2.0;
generator.noise.frequency = 1.0;
generator.noise.scrollSpeed = 0.5;
generator.noise.octaves = 2;

Test plan

  • npm run build passes
  • Enable noise on a particle system, verify turbulence displacement
  • Adjust frequency, strength, scrollSpeed parameters and confirm visual changes
  • Enable octaves = 3, verify increased detail
  • Toggle damping off, confirm equal-amplitude noise across full lifetime
  • Test with rotated emitter to verify bounds correctness
  • Test with StretchedBillboard mode (known limitation: stretch direction does not account for noise velocity, consistent with Unity)

Summary by CodeRabbit

  • New Features
    • Added configurable noise effects to particle systems (strength, frequency, scroll speed, damping, octaves) for richer animations.
    • Particle shaders updated to support noise-driven behavior for lifetime effects.
  • Bug Fixes
    • Improved world-space bounds calculation so noisy particle motion is correctly accounted for in rendering and culling.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 3, 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: e6ef00bf-dddf-49d5-8b0f-18eb011cc673

📥 Commits

Reviewing files that changed from the base of the PR and between dcdfec0 and 64f726c.

⛔ Files ignored due to path filters (2)
  • packages/core/src/shaderlib/extra/particle.vs.glsl is excluded by !**/*.glsl
  • packages/core/src/shaderlib/particle/noise_over_lifetime_module.glsl is excluded by !**/*.glsl
📒 Files selected for processing (4)
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/index.ts
  • packages/core/src/particle/modules/NoiseModule.ts
  • packages/core/src/shaderlib/particle/index.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/core/src/particle/index.ts
  • packages/core/src/shaderlib/particle/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/modules/NoiseModule.ts

Walkthrough

Added a new NoiseModule for particle noise control, integrated it into ParticleGenerator (property, shader updates, bounds expansion), exported the module from the particle package, and registered a new GLSL noise module in the particle shader library.

Changes

Cohort / File(s) Summary
Noise Module Implementation
packages/core/src/particle/modules/NoiseModule.ts
New exported NoiseModule class extending ParticleGeneratorModule with public getters/setters for strengthX/Y/Z, frequency, scrollSpeed, damping, octaves (clamped 1–3), octaveMultiplier, octaveScale; updates renderer on changes and writes noise uniforms/macros in _updateShaderData.
ParticleGenerator Integration
packages/core/src/particle/ParticleGenerator.ts
Added @deepClone readonly noise: NoiseModule property; call to this.noise._updateShaderData(shaderData) in _updateShaderData; expanded _calculateTransformedBounds to include axis-aligned margin based on noise.enabled, strengthX/Y/Z and octave-derived maxAmplitude.
Exports and Shader Index
packages/core/src/particle/index.ts, packages/core/src/shaderlib/particle/index.ts
Exported NoiseModule from package particle index; added noise_over_lifetime_module GLSL import/property to particle shader library index.

Sequence Diagram

sequenceDiagram
    participant PG as ParticleGenerator
    participant NM as NoiseModule
    participant Renderer as Renderer
    participant ShaderData as ShaderData
    participant Shader as Shader

    PG->>NM: construct NoiseModule(this)
    PG->>NM: set properties (strength, freq, scroll, octaves, damping)
    NM->>Renderer: _onGeneratorParamsChanged()
    PG->>ShaderData: _updateShaderData(shaderData)
    PG->>NM: NM._updateShaderData(shaderData)
    alt Noise enabled
        NM->>ShaderData: write strengthVec, freq, scroll, octaveVec
        NM->>Shader: _enableMacro("NOISE_ENABLED", true)
        alt damping enabled
            NM->>Shader: _enableMacro("NOISE_DAMPING", true)
        else damping disabled
            NM->>Shader: _enableMacro("NOISE_DAMPING", false)
        end
    else Noise disabled
        NM->>Shader: _enableMacro("NOISE_ENABLED", false)
    end
    PG->>PG: _calculateTransformedBounds()
    PG->>PG: expand bounds by noise axis-aligned margin (maxAmplitude * strength)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I jitter and hum in a powdered noise bloom,
Pushing particles gently out from the gloom,
Octaves and strength weave a soft, sparking trance,
Damping and scroll send the chaos to dance,
Hooray — more motion, more magical prance! ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(particle): add NoiseModule for simplex noise turbulence' accurately describes the main change: adding a NoiseModule to the particle system for simplex noise effects.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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.

@hhhhkrx hhhhkrx requested a review from GuoLei1990 April 3, 2026 06:19
@hhhhkrx hhhhkrx self-assigned this Apr 3, 2026
Add GPU-computed simplex noise displacement to particles, referencing
Unity's Noise Module. Supports per-axis strength, frequency, scroll
speed, damping, and up to 3 octaves. Reuses existing noise_common and
noise_simplex_3D shader libraries. No instance buffer changes needed.
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 3, 2026

Codecov Report

❌ Patch coverage is 63.40426% with 86 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.70%. Comparing base (6c82a45) to head (64f726c).

Files with missing lines Patch % Lines
packages/core/src/particle/modules/NoiseModule.ts 59.24% 86 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #2953      +/-   ##
===========================================
- Coverage    77.73%   77.70%   -0.04%     
===========================================
  Files          898      899       +1     
  Lines        98310    98552     +242     
  Branches      9806     9806              
===========================================
+ Hits         76417    76575     +158     
- Misses       21728    21812      +84     
  Partials       165      165              
Flag Coverage Δ
unittests 77.70% <63.40%> (-0.04%) ⬇️

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/particle/modules/NoiseModule.ts`:
- Around line 146-150: The octaveMultiplier setter currently allows negative
values which can invert bounds; update set octaveMultiplier(value: number) to
validate and reject or clamp values to a safe non-negative range (e.g., clamp to
>= 0.0 or a chosen min like 0.0/1.0), only assign to this._octaveMultiplier and
call this._generator._renderer._onGeneratorParamsChanged() when the
validated/clamped value differs from the current value; ensure any invalid
inputs are normalized (or throw a clear error) so downstream bounds calculations
in ParticleGenerator no longer receive negative multipliers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: bd9486dd-e2cd-4d0b-91a3-d78d7fd9eb2b

📥 Commits

Reviewing files that changed from the base of the PR and between 6c82a45 and dcdfec0.

⛔ Files ignored due to path filters (2)
  • packages/core/src/shaderlib/extra/particle.vs.glsl is excluded by !**/*.glsl
  • packages/core/src/shaderlib/particle/noise_over_lifetime_module.glsl is excluded by !**/*.glsl
📒 Files selected for processing (4)
  • packages/core/src/particle/ParticleGenerator.ts
  • packages/core/src/particle/index.ts
  • packages/core/src/particle/modules/NoiseModule.ts
  • packages/core/src/shaderlib/particle/index.ts

Comment on lines +146 to +150
set octaveMultiplier(value: number) {
if (value !== this._octaveMultiplier) {
this._octaveMultiplier = value;
this._generator._renderer._onGeneratorParamsChanged();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate octaveMultiplier to avoid invalid bounds shrinkage.

Line 146 currently accepts negative values. That propagates into transformed-bounds expansion (packages/core/src/particle/ParticleGenerator.ts, Line 1397 onward) where signed amplitude accumulation can under-estimate or invert expected expansion, causing culling artifacts.

Proposed fix
   set octaveMultiplier(value: number) {
-    if (value !== this._octaveMultiplier) {
-      this._octaveMultiplier = value;
+    const nextValue = Number.isFinite(value) ? Math.max(0, value) : 0;
+    if (nextValue !== this._octaveMultiplier) {
+      this._octaveMultiplier = nextValue;
       this._generator._renderer._onGeneratorParamsChanged();
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
set octaveMultiplier(value: number) {
if (value !== this._octaveMultiplier) {
this._octaveMultiplier = value;
this._generator._renderer._onGeneratorParamsChanged();
}
set octaveMultiplier(value: number) {
const nextValue = Number.isFinite(value) ? Math.max(0, value) : 0;
if (nextValue !== this._octaveMultiplier) {
this._octaveMultiplier = nextValue;
this._generator._renderer._onGeneratorParamsChanged();
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/particle/modules/NoiseModule.ts` around lines 146 - 150,
The octaveMultiplier setter currently allows negative values which can invert
bounds; update set octaveMultiplier(value: number) to validate and reject or
clamp values to a safe non-negative range (e.g., clamp to >= 0.0 or a chosen min
like 0.0/1.0), only assign to this._octaveMultiplier and call
this._generator._renderer._onGeneratorParamsChanged() when the validated/clamped
value differs from the current value; ensure any invalid inputs are normalized
(or throw a clear error) so downstream bounds calculations in ParticleGenerator
no longer receive negative multipliers.

@GuoLei1990
Copy link
Copy Markdown
Member

Code Review: PR #2953 — NoiseModule for Particle System

总结

PR 为粒子系统新增 Simplex Noise 模块,整体方向正确。主要问题集中在:噪声采样坐标空间错误、多八度噪声缺少归一化、damping 语义实现错误、包围盒计算偏大、API 类型/命名不够清晰。以下逐条说明。


问题

1. [Bug] 噪声采样坐标空间错误 + 缺少仿真空间变换

原代码 (particle.vs.glsl):

#ifdef RENDERER_NOISE_MODULE_ENABLED
    center += computeNoisePositionOffset(a_ShapePositionStartLifeTime.xyz, normalizedAge, age);
#endif

问题

  • a_ShapePositionStartLifeTime.xyz 是粒子的发射形状局部坐标(birth position),不是粒子当前位置。噪声应该基于粒子在仿真空间下的当前位置采样,这样 Local 模式下噪声场跟随粒子系统移动,视觉更稳定。noise 启用时会强制开启 Transform Feedback,a_FeedbackPosition 始终可用,它就是仿真空间下的粒子当前位置。
  • 噪声偏移是在仿真空间下计算的,直接加到世界坐标 center 上是错的。Local 模式下需要经过 worldRotation 变换后再加。
  • normalizedAgeage 参数是为 damping(见第 3 条)准备的,修正后不需要。

修正:

#ifdef RENDERER_NOISE_MODULE_ENABLED
    vec3 noiseOffset = computeNoisePositionOffset(a_FeedbackPosition);
    if (renderer_SimulationSpace == 0) {
        noiseOffset = rotationByQuaternions(noiseOffset, worldRotation);
    }
    center += noiseOffset;
#endif

2. [Bug] 多八度噪声未归一化

原代码 (noise_over_lifetime_module.glsl):

float amplitude = 1.0;
float frequency = 1.0;
vec3 noiseValue = sampleNoise3D(coord);

int octaves = int(renderer_NoiseOctaveInfo.x);
if (octaves >= 2) {
    amplitude *= renderer_NoiseOctaveInfo.y;
    frequency *= renderer_NoiseOctaveInfo.z;
    noiseValue += amplitude * sampleNoise3D(coord * frequency);
}
if (octaves >= 3) {
    amplitude *= renderer_NoiseOctaveInfo.y;
    frequency *= renderer_NoiseOctaveInfo.z;
    noiseValue += amplitude * sampleNoise3D(coord * frequency);
}

vec3 offset = noiseValue * renderer_NoiseStrength;

问题

  • 单层 simplex noise 输出范围是 [-1, 1]。多层叠加后值域扩大(2 层 [-1.5, 1.5],3 层 [-1.75, 1.75]),但未除以累计振幅范围归一化回 [-1, 1],导致 octaveCount > 1 时噪声偏移偏大。
  • amplitude 初始值 1.0 和 frequency 初始值 1.0 是多余的——第一层直接采样 coord,不需要这两个变量参与。
  • 变量名有歧义:amplitude 看起来像绝对振幅但实际是层间递推的强度倍率,frequency 看起来像绝对频率但实际是层间递推的频率缩放。应该用更准确的名字。

修正:

vec3 noiseValue = sampleSimplexNoise3D(coord);

int octaveCount = int(renderer_NoiseOctaveInfo.x);
if (octaveCount >= 2) {
    float intensity = renderer_NoiseOctaveInfo.y;
    float frequencyScale = renderer_NoiseOctaveInfo.z;
    float range = 1.0 + intensity;
    noiseValue += intensity * sampleSimplexNoise3D(coord * frequencyScale);

    if (octaveCount >= 3) {
        intensity *= renderer_NoiseOctaveInfo.y;
        frequencyScale *= renderer_NoiseOctaveInfo.z;
        range += intensity;
        noiseValue += intensity * sampleSimplexNoise3D(coord * frequencyScale);
    }

    noiseValue /= range;
}

3. [Bug] damping 语义实现错误

原代码 (noise_over_lifetime_module.glsl):

#ifdef RENDERER_NOISE_DAMPING
    offset *= (1.0 - normalizedAge);
#endif

原代码 (NoiseModule.ts):

/** Whether noise strength diminishes with particle age. */
get damping(): boolean { ... }

// _updateShaderData 中:
if (this._damping) {
    dampingMacro = NoiseModule._dampingMacro;
}

问题

  • 原实现的 damping 是"随粒子生命衰减噪声"(1 - normalizedAge),但正确的 damping 语义应该是 strength / frequency——当 frequency 增大时自动缩小 strength,防止高频率下粒子飞散。这两个是完全不同的功能。
  • "随生命衰减"这个功能本身是冗余的——未来 ParticleCompositeCurve 支持曲线模式后,用户可以通过 strength 曲线自定义任意衰减形态,不需要单独的开关。
  • damping(strength / frequency)几乎没有需要关闭的场景——关闭后调 frequency 会同时影响粗细和幅度,参数不正交,调参不直觉。应该固定为 true,不暴露 API。
  • 用宏 RENDERER_NOISE_DAMPING 控制也不必要,会增加编译变种。

修正:删除 damping 属性、_damping 字段、RENDERER_NOISE_DAMPING 宏和 _dampingMacro/_dampingModuleMacro。在 _updateShaderData 中固定 strength / frequency

const dampingScale = 1.0 / this._frequency;
params.set(
    this._strengthX.constantMax * dampingScale,
    this._strengthY.constantMax * dampingScale,
    this._strengthZ.constantMax * dampingScale,
    this._frequency
);

4. [Bug] 包围盒计算偏大 + 缺少 frequency 补偿

原代码 (ParticleGenerator.ts):

let maxAmplitude = 1.0;
let amp = 1.0;
for (let i = 1; i < noise.octaves; i++) {
    amp *= noise.octaveMultiplier;
    maxAmplitude += amp;
}
const noiseMaxX = Math.abs(noise.strengthX) * maxAmplitude;
const noiseMaxY = Math.abs(noise.strengthY) * maxAmplitude;
const noiseMaxZ = Math.abs(noise.strengthZ) * maxAmplitude;

问题

  • shader 中噪声已经归一化(/= range),输出始终在 [-1, 1],所以乘 maxAmplitude 是多余的,导致 bounds 偏大。
  • 没有考虑 strength / frequency 补偿,bounds 未反映实际的 strength 缩放。

修正:

// Noise offset is computed in simulation space then rotated to world.
// Adding axis-aligned max offset in world space is a conservative bound.
const { noise } = this;
if (noise.enabled) {
    // Noise output is normalized to [-1, 1] after octave accumulation,
    // then multiplied by strength / frequency on CPU side.
    const invFrequency = 1.0 / noise.frequency;
    const noiseMaxX = Math.abs(noise.strengthX.constantMax) * invFrequency;
    const noiseMaxY = Math.abs(noise.strengthY.constantMax) * invFrequency;
    const noiseMaxZ = Math.abs(noise.strengthZ.constantMax) * invFrequency;
    min.set(min.x - noiseMaxX, min.y - noiseMaxY, min.z - noiseMaxZ);
    max.set(max.x + noiseMaxX, max.y + noiseMaxY, max.z + noiseMaxZ);
}

5. [API 类型] strengthX/Y/Z、scrollSpeed 应为 ParticleCompositeCurve

原代码:

private _strengthX: number = 1.0;
private _strengthY: number = 1.0;
private _strengthZ: number = 1.0;
private _scrollSpeed: number = 0.0;

set strengthX(value: number) {
    if (value !== this._strengthX) {
        this._strengthX = value;
        this._generator._renderer._onGeneratorParamsChanged();
    }
}

问题:Galacean 粒子系统中 MinMaxCurve 对应的类型是 ParticleCompositeCurve,其他模块(如 SizeOverLifetime、RotationOverLifetime)都使用这个类型。当前只走常量路径(.constantMax),但类型对齐后未来支持曲线模式时不需要改 API。同时缺少 @deepClone 装饰器和 _onCompositeCurveChange setter 模式。

修正

@deepClone private _strengthX: ParticleCompositeCurve;
@deepClone private _strengthY: ParticleCompositeCurve;
@deepClone private _strengthZ: ParticleCompositeCurve;
@deepClone private _scrollSpeed: ParticleCompositeCurve;

set strengthX(value: ParticleCompositeCurve) {
    const lastValue = this._strengthX;
    if (value !== lastValue) {
        this._strengthX = value;
        this._onCompositeCurveChange(lastValue, value);
    }
}
// strengthY, strengthZ, scrollSpeed 同理

constructor(generator: ParticleGenerator) {
    super(generator);
    this.strengthX = new ParticleCompositeCurve(1.0);
    this.strengthY = new ParticleCompositeCurve(1.0);
    this.strengthZ = new ParticleCompositeCurve(1.0);
    this.scrollSpeed = new ParticleCompositeCurve(0.0);
}

6. [API 命名] 多处命名改良

6a. octavesoctaveCount

octaveCount 语义更明确——"层数",而 octaves 是音乐术语,含义模糊。

6b. octaveMultiplieroctaveIntensityMultiplier

原名看不出是什么的 multiplier。改后明确表示"每层强度的倍率"。

/**
 * Intensity multiplier for each successive octave.
 * Each layer's contribution is scaled by this factor relative to the previous layer, range [0, 1].
 */
get octaveIntensityMultiplier(): number

6c. octaveScaleoctaveFrequencyMultiplier

原名看不出和 frequency 的关系。改后明确表示"每层频率的倍率",与 octaveIntensityMultiplier 形成对称。

/**
 * Frequency multiplier for each successive octave.
 * Each layer samples at this multiple of the previous layer's frequency, range [1, 4].
 */
get octaveFrequencyMultiplier(): number

6d. shader 函数名 sampleNoise3DsampleSimplexNoise3D

明确标注噪声算法类型,可读性更好。


7. [API 缺失] 参数范围约束

原代码frequencyoctaveMultiplieroctaveScale 的 setter 缺少 clamp。

修正

set frequency(value: number) {
    value = Math.max(1e-6, value); // 防止 1/frequency 除零
    // ...
}
set octaveIntensityMultiplier(value: number) {
    value = Math.max(0, Math.min(1, value));
    // ...
}
set octaveFrequencyMultiplier(value: number) {
    value = Math.max(1, Math.min(4, value));
    // ...
}

8. [Bug] enabled 未触发 Transform Feedback 状态更新

原代码:

override set enabled(value: boolean) {
    if (value !== this._enabled) {
        this._enabled = value;
        this._generator._renderer._onGeneratorParamsChanged();
    }
}

问题:噪声模块依赖 Transform Feedback,启用/禁用时需要调用 _updateTransformFeedbackState() 切换渲染路径。同时 TF 需要 WebGL2,WebGL1 环境下应静默忽略启用。

修正:

override set enabled(value: boolean) {
    if (value !== this._enabled) {
        if (value && !this._generator._renderer.engine._hardwareRenderer.isWebGL2) {
            return;
        }
        this._enabled = value;
        this._generator._updateTransformFeedbackState();
        this._generator._renderer._onGeneratorParamsChanged();
    }
}

9. [性能] 4 个 uniform 合并为 2 个 vec4

原代码

uniform vec3 renderer_NoiseStrength;
uniform float renderer_NoiseFrequency;
uniform float renderer_NoiseScrollSpeed;
uniform vec3 renderer_NoiseOctaveInfo;

4 个 uniform 可以打包为 2 个 vec4,减少 uniform 槽位占用。

修正

uniform vec4 renderer_NoiseParams;     // xyz=strength, w=frequency
uniform vec4 renderer_NoiseOctaveInfo; // x=octaveCount, y=octaveIntensityMultiplier, z=octaveFrequencyMultiplier, w=scrollSpeed

TS 侧对应改为 Vector4 + setVector4


10. [改进] 噪声三轴采样改为分量重排

原代码:

vec3 sampleNoise3D(vec3 coord) {
    return vec3(
        simplex(coord),
        simplex(coord + vec3(17.0, 31.0, 47.0)),
        simplex(coord + vec3(67.0, 89.0, 113.0))
    );
}

问题:用魔法数偏移来让三轴采到不同噪声值。分量重排的方式不依赖随意选的常数,通过交换坐标轴本身就保证了各轴采样的独立性,更有保证。

修正:

vec3 sampleSimplexNoise3D(vec3 coord) {
    float d = 100.0;
    return vec3(
        simplex(vec3(coord.z, coord.y, coord.x)),
        simplex(vec3(coord.x + d, coord.z, coord.y)),
        simplex(vec3(coord.y, coord.x + d, coord.z))
    );
}

11. [辅助] _setTransformFeedback 改为 private

该方法只在 ParticleGenerator 内部调用(_updateTransformFeedbackState),是实现细节,不需要 @internal,应改为 private


最终代码

NoiseModule.ts

import { Vector4 } from "@galacean/engine-math";
import { deepClone, ignoreClone } from "../../clone/CloneManager";
import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
import { ParticleGenerator } from "../ParticleGenerator";
import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
import { ParticleGeneratorModule } from "./ParticleGeneratorModule";

/**
 * Noise module for particle system.
 * Adds simplex noise-based turbulence to particle position.
 */
export class NoiseModule extends ParticleGeneratorModule {
  static readonly _enabledMacro = ShaderMacro.getByName("RENDERER_NOISE_MODULE_ENABLED");
  static readonly _paramsProperty = ShaderProperty.getByName("renderer_NoiseParams");
  static readonly _octaveInfoProperty = ShaderProperty.getByName("renderer_NoiseOctaveInfo");

  @ignoreClone private _enabledModuleMacro: ShaderMacro;
  @ignoreClone private _paramsVec = new Vector4();
  @ignoreClone private _octaveInfoVec = new Vector4();

  @deepClone private _strengthX: ParticleCompositeCurve;
  @deepClone private _strengthY: ParticleCompositeCurve;
  @deepClone private _strengthZ: ParticleCompositeCurve;
  private _frequency: number = 0.5;
  @deepClone private _scrollSpeed: ParticleCompositeCurve;
  private _octaveCount: number = 1;
  private _octaveIntensityMultiplier: number = 0.5;
  private _octaveFrequencyMultiplier: number = 2.0;

  /**
   * Maximum position displacement along the x axis.
   */
  get strengthX(): ParticleCompositeCurve {
    return this._strengthX;
  }

  set strengthX(value: ParticleCompositeCurve) {
    const lastValue = this._strengthX;
    if (value !== lastValue) {
      this._strengthX = value;
      this._onCompositeCurveChange(lastValue, value);
    }
  }

  /**
   * Maximum position displacement along the y axis.
   */
  get strengthY(): ParticleCompositeCurve {
    return this._strengthY;
  }

  set strengthY(value: ParticleCompositeCurve) {
    const lastValue = this._strengthY;
    if (value !== lastValue) {
      this._strengthY = value;
      this._onCompositeCurveChange(lastValue, value);
    }
  }

  /**
   * Maximum position displacement along the z axis.
   */
  get strengthZ(): ParticleCompositeCurve {
    return this._strengthZ;
  }

  set strengthZ(value: ParticleCompositeCurve) {
    const lastValue = this._strengthZ;
    if (value !== lastValue) {
      this._strengthZ = value;
      this._onCompositeCurveChange(lastValue, value);
    }
  }

  /**
   * Noise spatial frequency. Higher values produce denser, more rapid variation.
   */
  get frequency(): number {
    return this._frequency;
  }

  set frequency(value: number) {
    value = Math.max(1e-6, value);
    if (value !== this._frequency) {
      this._frequency = value;
      this._generator._renderer._onGeneratorParamsChanged();
    }
  }

  /**
   * Speed at which the noise field scrolls over time.
   */
  get scrollSpeed(): ParticleCompositeCurve {
    return this._scrollSpeed;
  }

  set scrollSpeed(value: ParticleCompositeCurve) {
    const lastValue = this._scrollSpeed;
    if (value !== lastValue) {
      this._scrollSpeed = value;
      this._onCompositeCurveChange(lastValue, value);
    }
  }

  /**
   * Number of noise layers combined for finer detail, range [1, 3].
   */
  get octaveCount(): number {
    return this._octaveCount;
  }

  set octaveCount(value: number) {
    value = Math.max(1, Math.min(3, Math.floor(value)));
    if (value !== this._octaveCount) {
      this._octaveCount = value;
      this._generator._renderer._onGeneratorParamsChanged();
    }
  }

  /**
   * Intensity multiplier for each successive octave.
   * Each layer's contribution is scaled by this factor relative to the previous layer, range [0, 1].
   */
  get octaveIntensityMultiplier(): number {
    return this._octaveIntensityMultiplier;
  }

  set octaveIntensityMultiplier(value: number) {
    value = Math.max(0, Math.min(1, value));
    if (value !== this._octaveIntensityMultiplier) {
      this._octaveIntensityMultiplier = value;
      this._generator._renderer._onGeneratorParamsChanged();
    }
  }

  /**
   * Frequency multiplier for each successive octave.
   * Each layer samples at this multiple of the previous layer's frequency, range [1, 4].
   */
  get octaveFrequencyMultiplier(): number {
    return this._octaveFrequencyMultiplier;
  }

  set octaveFrequencyMultiplier(value: number) {
    value = Math.max(1, Math.min(4, value));
    if (value !== this._octaveFrequencyMultiplier) {
      this._octaveFrequencyMultiplier = value;
      this._generator._renderer._onGeneratorParamsChanged();
    }
  }

  override get enabled(): boolean {
    return this._enabled;
  }

  /**
   * @remarks This module requires WebGL2. On WebGL1, enabling will be silently ignored.
   */
  override set enabled(value: boolean) {
    if (value !== this._enabled) {
      if (value && !this._generator._renderer.engine._hardwareRenderer.isWebGL2) {
        return;
      }
      this._enabled = value;
      this._generator._updateTransformFeedbackState();
      this._generator._renderer._onGeneratorParamsChanged();
    }
  }

  constructor(generator: ParticleGenerator) {
    super(generator);
    this.strengthX = new ParticleCompositeCurve(1.0);
    this.strengthY = new ParticleCompositeCurve(1.0);
    this.strengthZ = new ParticleCompositeCurve(1.0);
    this.scrollSpeed = new ParticleCompositeCurve(0.0);
  }

  /**
   * @internal
   */
  _updateShaderData(shaderData: ShaderData): void {
    let enabledMacro = <ShaderMacro>null;

    if (this.enabled) {
      enabledMacro = NoiseModule._enabledMacro;

      const params = this._paramsVec;
      const dampingScale = 1.0 / this._frequency;
      params.set(
        this._strengthX.constantMax * dampingScale,
        this._strengthY.constantMax * dampingScale,
        this._strengthZ.constantMax * dampingScale,
        this._frequency
      );
      shaderData.setVector4(NoiseModule._paramsProperty, params);

      const octaveInfo = this._octaveInfoVec;
      octaveInfo.set(
        this._octaveCount,
        this._octaveIntensityMultiplier,
        this._octaveFrequencyMultiplier,
        this._scrollSpeed.constantMax
      );
      shaderData.setVector4(NoiseModule._octaveInfoProperty, octaveInfo);
    }

    this._enabledModuleMacro = this._enableMacro(shaderData, this._enabledModuleMacro, enabledMacro);
  }
}

noise_over_lifetime_module.glsl

#ifdef RENDERER_NOISE_MODULE_ENABLED

#include <noise_common>
#include <noise_simplex_3D>

uniform vec4 renderer_NoiseParams;     // xyz=strength, w=frequency
uniform vec4 renderer_NoiseOctaveInfo; // x=octaveCount, y=octaveIntensityMultiplier, z=octaveFrequencyMultiplier, w=scrollSpeed

vec3 sampleSimplexNoise3D(vec3 coord) {
    float d = 100.0;
    return vec3(
        simplex(vec3(coord.z, coord.y, coord.x)),
        simplex(vec3(coord.x + d, coord.z, coord.y)),
        simplex(vec3(coord.y, coord.x + d, coord.z))
    );
}

vec3 computeNoisePositionOffset(vec3 simulationPosition) {
    vec3 coord = simulationPosition * renderer_NoiseParams.w
               + vec3(renderer_CurrentTime * renderer_NoiseOctaveInfo.w);

    vec3 noiseValue = sampleSimplexNoise3D(coord);

    int octaveCount = int(renderer_NoiseOctaveInfo.x);
    if (octaveCount >= 2) {
        float intensity = renderer_NoiseOctaveInfo.y;
        float frequencyScale = renderer_NoiseOctaveInfo.z;
        float range = 1.0 + intensity;
        noiseValue += intensity * sampleSimplexNoise3D(coord * frequencyScale);

        if (octaveCount >= 3) {
            intensity *= renderer_NoiseOctaveInfo.y;
            frequencyScale *= renderer_NoiseOctaveInfo.z;
            range += intensity;
            noiseValue += intensity * sampleSimplexNoise3D(coord * frequencyScale);
        }

        noiseValue /= range;
    }

    // Strength already includes 1/frequency scaling from CPU side
    return noiseValue * renderer_NoiseParams.xyz;
}

#endif

particle.vs.glsl(噪声部分)

#ifdef RENDERER_NOISE_MODULE_ENABLED
    vec3 noiseOffset = computeNoisePositionOffset(a_FeedbackPosition);
    if (renderer_SimulationSpace == 0) {
        noiseOffset = rotationByQuaternions(noiseOffset, worldRotation);
    }
    center += noiseOffset;
#endif

ParticleGenerator.ts(包围盒部分)

// Noise offset is computed in simulation space then rotated to world.
// Adding axis-aligned max offset in world space is a conservative bound.
const { noise } = this;
if (noise.enabled) {
    // Noise output is normalized to [-1, 1] after octave accumulation,
    // then multiplied by strength / frequency on CPU side.
    const invFrequency = 1.0 / noise.frequency;
    const noiseMaxX = Math.abs(noise.strengthX.constantMax) * invFrequency;
    const noiseMaxY = Math.abs(noise.strengthY.constantMax) * invFrequency;
    const noiseMaxZ = Math.abs(noise.strengthZ.constantMax) * invFrequency;
    min.set(min.x - noiseMaxX, min.y - noiseMaxY, min.z - noiseMaxZ);
    max.set(max.x + noiseMaxX, max.y + noiseMaxY, max.z + noiseMaxZ);
}

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