Skip to content

Commit dcdfec0

Browse files
hhhhkrxclaude
andcommitted
feat(particle): add NoiseModule for simplex noise turbulence
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. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6c82a45 commit dcdfec0

6 files changed

Lines changed: 290 additions & 0 deletions

File tree

packages/core/src/particle/ParticleGenerator.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { ParticleCompositeCurve } from "./modules/ParticleCompositeCurve";
3333
import { RotationOverLifetimeModule } from "./modules/RotationOverLifetimeModule";
3434
import { SizeOverLifetimeModule } from "./modules/SizeOverLifetimeModule";
3535
import { TextureSheetAnimationModule } from "./modules/TextureSheetAnimationModule";
36+
import { NoiseModule } from "./modules/NoiseModule";
3637
import { VelocityOverLifetimeModule } from "./modules/VelocityOverLifetimeModule";
3738

3839
/**
@@ -83,6 +84,9 @@ export class ParticleGenerator {
8384
/** Texture sheet animation module. */
8485
@deepClone
8586
readonly textureSheetAnimation = new TextureSheetAnimationModule(this);
87+
/** Noise module. */
88+
@deepClone
89+
readonly noise = new NoiseModule(this);
8690

8791
/** @internal */
8892
_currentParticleCount = 0;
@@ -613,6 +617,7 @@ export class ParticleGenerator {
613617
this.sizeOverLifetime._updateShaderData(shaderData);
614618
this.rotationOverLifetime._updateShaderData(shaderData);
615619
this.colorOverLifetime._updateShaderData(shaderData);
620+
this.noise._updateShaderData(shaderData);
616621
}
617622

618623
/**
@@ -1385,6 +1390,23 @@ export class ParticleGenerator {
13851390
min.add(worldOffsetMin);
13861391
max.add(worldOffsetMax);
13871392

1393+
// Noise module impact: noise is applied in world space (directly to center),
1394+
// so it must be added after the rotation transform.
1395+
const { noise } = this;
1396+
if (noise.enabled) {
1397+
let maxAmplitude = 1.0;
1398+
let amp = 1.0;
1399+
for (let i = 1; i < noise.octaves; i++) {
1400+
amp *= noise.octaveMultiplier;
1401+
maxAmplitude += amp;
1402+
}
1403+
const noiseMaxX = Math.abs(noise.strengthX) * maxAmplitude;
1404+
const noiseMaxY = Math.abs(noise.strengthY) * maxAmplitude;
1405+
const noiseMaxZ = Math.abs(noise.strengthZ) * maxAmplitude;
1406+
min.set(min.x - noiseMaxX, min.y - noiseMaxY, min.z - noiseMaxZ);
1407+
max.set(max.x + noiseMaxX, max.y + noiseMaxY, max.z + noiseMaxZ);
1408+
}
1409+
13881410
min.add(worldPosition);
13891411
max.add(worldPosition);
13901412
}

packages/core/src/particle/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export { SizeOverLifetimeModule } from "./modules/SizeOverLifetimeModule";
2020
export { TextureSheetAnimationModule } from "./modules/TextureSheetAnimationModule";
2121
export { VelocityOverLifetimeModule } from "./modules/VelocityOverLifetimeModule";
2222
export { LimitVelocityOverLifetimeModule } from "./modules/LimitVelocityOverLifetimeModule";
23+
export { NoiseModule } from "./modules/NoiseModule";
2324
export * from "./modules/shape/index";
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import { Vector3 } from "@galacean/engine-math";
2+
import { ignoreClone } from "../../clone/CloneManager";
3+
import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
4+
import { ParticleGenerator } from "../ParticleGenerator";
5+
import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
6+
7+
/**
8+
* Noise module for particle system.
9+
* Adds simplex noise-based turbulence displacement to particles.
10+
*/
11+
export class NoiseModule extends ParticleGeneratorModule {
12+
static readonly _enabledMacro = ShaderMacro.getByName("RENDERER_NOISE_MODULE_ENABLED");
13+
static readonly _dampingMacro = ShaderMacro.getByName("RENDERER_NOISE_DAMPING");
14+
15+
static readonly _strengthProperty = ShaderProperty.getByName("renderer_NoiseStrength");
16+
static readonly _frequencyProperty = ShaderProperty.getByName("renderer_NoiseFrequency");
17+
static readonly _scrollSpeedProperty = ShaderProperty.getByName("renderer_NoiseScrollSpeed");
18+
static readonly _octaveInfoProperty = ShaderProperty.getByName("renderer_NoiseOctaveInfo");
19+
20+
@ignoreClone
21+
private _enabledModuleMacro: ShaderMacro;
22+
@ignoreClone
23+
private _dampingModuleMacro: ShaderMacro;
24+
25+
@ignoreClone
26+
private _strengthVec = new Vector3();
27+
@ignoreClone
28+
private _octaveInfoVec = new Vector3();
29+
30+
private _strengthX: number = 1.0;
31+
private _strengthY: number = 1.0;
32+
private _strengthZ: number = 1.0;
33+
private _frequency: number = 0.5;
34+
private _scrollSpeed: number = 0.0;
35+
private _damping: boolean = true;
36+
private _octaves: number = 1;
37+
private _octaveMultiplier: number = 0.5;
38+
private _octaveScale: number = 2.0;
39+
40+
/**
41+
* Noise strength for x axis.
42+
*/
43+
get strengthX(): number {
44+
return this._strengthX;
45+
}
46+
47+
set strengthX(value: number) {
48+
if (value !== this._strengthX) {
49+
this._strengthX = value;
50+
this._generator._renderer._onGeneratorParamsChanged();
51+
}
52+
}
53+
54+
/**
55+
* Noise strength for y axis.
56+
*/
57+
get strengthY(): number {
58+
return this._strengthY;
59+
}
60+
61+
set strengthY(value: number) {
62+
if (value !== this._strengthY) {
63+
this._strengthY = value;
64+
this._generator._renderer._onGeneratorParamsChanged();
65+
}
66+
}
67+
68+
/**
69+
* Noise strength for z axis.
70+
*/
71+
get strengthZ(): number {
72+
return this._strengthZ;
73+
}
74+
75+
set strengthZ(value: number) {
76+
if (value !== this._strengthZ) {
77+
this._strengthZ = value;
78+
this._generator._renderer._onGeneratorParamsChanged();
79+
}
80+
}
81+
82+
/**
83+
* Noise spatial frequency.
84+
*/
85+
get frequency(): number {
86+
return this._frequency;
87+
}
88+
89+
set frequency(value: number) {
90+
if (value !== this._frequency) {
91+
this._frequency = value;
92+
this._generator._renderer._onGeneratorParamsChanged();
93+
}
94+
}
95+
96+
/**
97+
* Noise field scroll speed over time.
98+
*/
99+
get scrollSpeed(): number {
100+
return this._scrollSpeed;
101+
}
102+
103+
set scrollSpeed(value: number) {
104+
if (value !== this._scrollSpeed) {
105+
this._scrollSpeed = value;
106+
this._generator._renderer._onGeneratorParamsChanged();
107+
}
108+
}
109+
110+
/**
111+
* Whether noise strength diminishes with particle age.
112+
*/
113+
get damping(): boolean {
114+
return this._damping;
115+
}
116+
117+
set damping(value: boolean) {
118+
if (value !== this._damping) {
119+
this._damping = value;
120+
this._generator._renderer._onGeneratorParamsChanged();
121+
}
122+
}
123+
124+
/**
125+
* Number of noise octaves (1-3).
126+
*/
127+
get octaves(): number {
128+
return this._octaves;
129+
}
130+
131+
set octaves(value: number) {
132+
value = Math.max(1, Math.min(3, Math.floor(value)));
133+
if (value !== this._octaves) {
134+
this._octaves = value;
135+
this._generator._renderer._onGeneratorParamsChanged();
136+
}
137+
}
138+
139+
/**
140+
* Amplitude decay factor per octave.
141+
*/
142+
get octaveMultiplier(): number {
143+
return this._octaveMultiplier;
144+
}
145+
146+
set octaveMultiplier(value: number) {
147+
if (value !== this._octaveMultiplier) {
148+
this._octaveMultiplier = value;
149+
this._generator._renderer._onGeneratorParamsChanged();
150+
}
151+
}
152+
153+
/**
154+
* Frequency increase factor per octave.
155+
*/
156+
get octaveScale(): number {
157+
return this._octaveScale;
158+
}
159+
160+
set octaveScale(value: number) {
161+
if (value !== this._octaveScale) {
162+
this._octaveScale = value;
163+
this._generator._renderer._onGeneratorParamsChanged();
164+
}
165+
}
166+
167+
override get enabled(): boolean {
168+
return this._enabled;
169+
}
170+
171+
override set enabled(value: boolean) {
172+
if (value !== this._enabled) {
173+
this._enabled = value;
174+
this._generator._renderer._onGeneratorParamsChanged();
175+
}
176+
}
177+
178+
constructor(generator: ParticleGenerator) {
179+
super(generator);
180+
}
181+
182+
/**
183+
* @internal
184+
*/
185+
_updateShaderData(shaderData: ShaderData): void {
186+
let enabledMacro = <ShaderMacro>null;
187+
let dampingMacro = <ShaderMacro>null;
188+
189+
if (this.enabled) {
190+
enabledMacro = NoiseModule._enabledMacro;
191+
192+
const strength = this._strengthVec;
193+
strength.set(this._strengthX, this._strengthY, this._strengthZ);
194+
shaderData.setVector3(NoiseModule._strengthProperty, strength);
195+
196+
shaderData.setFloat(NoiseModule._frequencyProperty, this._frequency);
197+
shaderData.setFloat(NoiseModule._scrollSpeedProperty, this._scrollSpeed);
198+
199+
const octaveInfo = this._octaveInfoVec;
200+
octaveInfo.set(this._octaves, this._octaveMultiplier, this._octaveScale);
201+
shaderData.setVector3(NoiseModule._octaveInfoProperty, octaveInfo);
202+
203+
if (this._damping) {
204+
dampingMacro = NoiseModule._dampingMacro;
205+
}
206+
}
207+
208+
this._enabledModuleMacro = this._enableMacro(shaderData, this._enabledModuleMacro, enabledMacro);
209+
this._dampingModuleMacro = this._enableMacro(shaderData, this._dampingModuleMacro, dampingMacro);
210+
}
211+
}

packages/core/src/shaderlib/extra/particle.vs.glsl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ uniform int renderer_SimulationSpace;
7474
#include <size_over_lifetime_module>
7575
#include <rotation_over_lifetime_module>
7676
#include <texture_sheet_animation_module>
77+
#include <noise_over_lifetime_module>
7778

7879
vec3 computeParticlePosition(in vec3 startVelocity, in float age, in float normalizedAge, vec3 gravityVelocity, vec4 worldRotation, inout vec3 localVelocity, inout vec3 worldVelocity) {
7980
vec3 startPosition = startVelocity * age;
@@ -164,6 +165,10 @@ void main() {
164165
vec3 center = computeParticlePosition(startVelocity, age, normalizedAge, gravityVelocity, worldRotation, localVelocity, worldVelocity);
165166
#endif
166167

168+
#ifdef RENDERER_NOISE_MODULE_ENABLED
169+
center += computeNoisePositionOffset(a_ShapePositionStartLifeTime.xyz, normalizedAge, age);
170+
#endif
171+
167172
#include <sphere_billboard>
168173
#include <stretched_billboard>
169174
#include <horizontal_billboard>

packages/core/src/shaderlib/particle/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import color_over_lifetime_module from "./color_over_lifetime_module.glsl";
66
import texture_sheet_animation_module from "./texture_sheet_animation_module.glsl";
77
import force_over_lifetime_module from "./force_over_lifetime_module.glsl";
88
import limit_velocity_over_lifetime_module from "./limit_velocity_over_lifetime_module.glsl";
9+
import noise_over_lifetime_module from "./noise_over_lifetime_module.glsl";
910
import particle_feedback_simulation from "./particle_feedback_simulation.glsl";
1011

1112
import sphere_billboard from "./sphere_billboard.glsl";
@@ -23,6 +24,7 @@ export default {
2324
texture_sheet_animation_module,
2425
force_over_lifetime_module,
2526
limit_velocity_over_lifetime_module,
27+
noise_over_lifetime_module,
2628
particle_feedback_simulation,
2729

2830
sphere_billboard,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#ifdef RENDERER_NOISE_MODULE_ENABLED
2+
3+
#include <noise_common>
4+
#include <noise_simplex_3D>
5+
6+
uniform vec3 renderer_NoiseStrength;
7+
uniform float renderer_NoiseFrequency;
8+
uniform float renderer_NoiseScrollSpeed;
9+
uniform vec3 renderer_NoiseOctaveInfo; // x=octaveCount, y=octaveMultiplier, z=octaveScale
10+
11+
vec3 sampleNoise3D(vec3 coord) {
12+
return vec3(
13+
simplex(coord),
14+
simplex(coord + vec3(17.0, 31.0, 47.0)),
15+
simplex(coord + vec3(67.0, 89.0, 113.0))
16+
);
17+
}
18+
19+
vec3 computeNoisePositionOffset(vec3 birthPosition, float normalizedAge, float age) {
20+
vec3 coord = birthPosition * renderer_NoiseFrequency
21+
+ vec3(renderer_CurrentTime * renderer_NoiseScrollSpeed);
22+
23+
float amplitude = 1.0;
24+
float frequency = 1.0;
25+
vec3 noiseValue = sampleNoise3D(coord);
26+
27+
// Unrolled octave loop (GLSL ES 1.0 requires constant loop bounds)
28+
int octaves = int(renderer_NoiseOctaveInfo.x);
29+
if (octaves >= 2) {
30+
amplitude *= renderer_NoiseOctaveInfo.y;
31+
frequency *= renderer_NoiseOctaveInfo.z;
32+
noiseValue += amplitude * sampleNoise3D(coord * frequency);
33+
}
34+
if (octaves >= 3) {
35+
amplitude *= renderer_NoiseOctaveInfo.y;
36+
frequency *= renderer_NoiseOctaveInfo.z;
37+
noiseValue += amplitude * sampleNoise3D(coord * frequency);
38+
}
39+
40+
vec3 offset = noiseValue * renderer_NoiseStrength;
41+
42+
#ifdef RENDERER_NOISE_DAMPING
43+
offset *= (1.0 - normalizedAge);
44+
#endif
45+
46+
return offset;
47+
}
48+
49+
#endif

0 commit comments

Comments
 (0)