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

* feat(particle): add NoiseModule for simplex noise turbulence

(cherry picked from commit 3189475648)
This commit is contained in:
hhhhkrx
2026-04-09 20:15:10 +08:00
committed by luzhuang
parent e5228d76d6
commit a3cbbbef97
12 changed files with 603 additions and 14 deletions

View File

@@ -0,0 +1,111 @@
/**
* @title Particle Noise
* @category Particle
*/
import {
AssetType,
BlendMode,
Camera,
Color,
Engine,
Entity,
ParticleCompositeCurve,
ParticleCurveMode,
ParticleGradientMode,
ParticleMaterial,
ParticleRenderer,
ParticleSimulationSpace,
ConeShape,
Texture2D,
WebGLEngine
} from "@galacean/engine";
import { initScreenshot, updateForE2E } from "./.mockForE2E";
WebGLEngine.create({
canvas: "canvas"
}).then((engine) => {
engine.canvas.resizeByClientSize();
const scene = engine.sceneManager.activeScene;
const rootEntity = scene.createRootEntity();
scene.background.solidColor = new Color(0, 0, 0, 1);
const cameraEntity = rootEntity.createChild("camera");
cameraEntity.transform.setPosition(0, 0, 0);
const camera = cameraEntity.addComponent(Camera);
camera.fieldOfView = 60;
engine.resourceManager
.load({
url: "https://mdn.alipayobjects.com/huamei_b4l2if/afts/img/A*JPsCSK5LtYkAAAAAAAAAAAAADil6AQ/original",
type: AssetType.Texture
})
.then((texture) => {
createNoiseParticle(engine, rootEntity, <Texture2D>texture);
updateForE2E(engine, 200);
initScreenshot(engine, camera);
});
});
function createNoiseParticle(engine: Engine, rootEntity: Entity, texture: Texture2D): void {
const particleEntity = new Entity(engine, "Noise");
particleEntity.transform.setPosition(0, 0, -2);
const particleRenderer = particleEntity.addComponent(ParticleRenderer);
const generator = particleRenderer.generator;
generator.useAutoRandomSeed = false;
const material = new ParticleMaterial(engine);
material.baseColor = new Color(0.4, 0.8, 1.0, 1.0);
material.blendMode = BlendMode.Additive;
material.baseTexture = texture;
particleRenderer.setMaterial(material);
const { main, emission, noise, colorOverLifetime } = generator;
// Main
main.duration = 3;
main.isLoop = true;
main.startLifetime.constantMin = 0.3;
main.startLifetime.constantMax = 0.6;
main.startLifetime.mode = ParticleCurveMode.TwoConstants;
main.startSpeed.constantMin = 4;
main.startSpeed.constantMax = 4;
main.startSpeed.mode = ParticleCurveMode.TwoConstants;
main.startSize.constantMin = 0.05;
main.startSize.constantMax = 0.1;
main.startSize.mode = ParticleCurveMode.TwoConstants;
main.gravityModifier.constant = -0.5;
main.simulationSpace = ParticleSimulationSpace.Local;
main.maxParticles = 200;
// Emission
emission.rateOverTime.constant = 40;
const coneShape = new ConeShape();
coneShape.angle = 25;
coneShape.radius = 0.00001;
emission.shape = coneShape;
// Color over lifetime
// colorOverLifetime.enabled = true;
// colorOverLifetime.color.mode = ParticleGradientMode.Gradient;
// const gradient = colorOverLifetime.color.gradient;
// gradient.alphaKeys[0].alpha = 0;
// gradient.alphaKeys[1].alpha = 0;
// gradient.addAlphaKey(0.1, 1.0);
// gradient.addAlphaKey(0.8, 1.0);
// Noise
noise.enabled = true;
noise.strengthX = new ParticleCompositeCurve(1);
noise.strengthY = new ParticleCompositeCurve(1);
noise.strengthZ = new ParticleCompositeCurve(1);
noise.frequency = 1;
noise.scrollSpeed = 0;
noise.octaveCount = 1;
noise.octaveIntensityMultiplier = 0.5;
noise.octaveFrequencyMultiplier = 2.0;
rootEntity.addChild(particleEntity);
}

View File

@@ -430,6 +430,12 @@ export const E2E_CONFIG = {
caseFileName: "particleRenderer-horizontal-billboard",
threshold: 0,
diffPercentage: 0.2162
},
noiseModule: {
category: "Particle",
caseFileName: "particleRenderer-noise",
threshold: 0,
diffPercentage: 0
}
},
PostProcess: {

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6ddb5bc9d8f18f18a37a69aefa0aa58a9371dd35e0af6038e9a220c14f36767e
size 24260

View File

@@ -33,6 +33,7 @@ import { ParticleCompositeCurve } from "./modules/ParticleCompositeCurve";
import { RotationOverLifetimeModule } from "./modules/RotationOverLifetimeModule";
import { SizeOverLifetimeModule } from "./modules/SizeOverLifetimeModule";
import { TextureSheetAnimationModule } from "./modules/TextureSheetAnimationModule";
import { NoiseModule } from "./modules/NoiseModule";
import { VelocityOverLifetimeModule } from "./modules/VelocityOverLifetimeModule";
/**
@@ -83,6 +84,9 @@ export class ParticleGenerator {
/** Texture sheet animation module. */
@deepClone
readonly textureSheetAnimation = new TextureSheetAnimationModule(this);
/** Noise module. */
@deepClone
readonly noise: NoiseModule;
/** @internal */
_currentParticleCount = 0;
@@ -191,6 +195,7 @@ export class ParticleGenerator {
this.forceOverLifetime = new ForceOverLifetimeModule(this);
this.sizeOverLifetime = new SizeOverLifetimeModule(this);
this.limitVelocityOverLifetime = new LimitVelocityOverLifetimeModule(this);
this.noise = new NoiseModule(this);
this.emission.enabled = true;
}
@@ -614,6 +619,7 @@ export class ParticleGenerator {
this.sizeOverLifetime._updateShaderData(shaderData);
this.rotationOverLifetime._updateShaderData(shaderData);
this.colorOverLifetime._updateShaderData(shaderData);
this.noise._updateShaderData(shaderData);
}
/**
@@ -629,20 +635,23 @@ export class ParticleGenerator {
this.limitVelocityOverLifetime._resetRandomSeed(seed);
this.rotationOverLifetime._resetRandomSeed(seed);
this.colorOverLifetime._resetRandomSeed(seed);
this.noise._resetRandomSeed(seed);
}
/**
* @internal
*/
_setTransformFeedback(enabled: boolean): void {
this._useTransformFeedback = enabled;
_setTransformFeedback(): void {
const needed = this.limitVelocityOverLifetime.enabled || this.noise.enabled;
if (needed === this._useTransformFeedback) return;
this._useTransformFeedback = needed;
// Switching TF mode invalidates all active particle state: feedback buffers and instance
// buffer layout are incompatible between the two paths. Clear rather than show a one-frame
// jump; new particles will fill in naturally from the next emit cycle.
this._clearActiveParticles();
if (enabled) {
if (needed) {
if (!this._feedbackSimulator) {
this._feedbackSimulator = new ParticleTransformFeedbackSimulator(this._renderer.engine);
}
@@ -702,9 +711,7 @@ export class ParticleGenerator {
* @internal
*/
_cloneTo(target: ParticleGenerator): void {
if (target.limitVelocityOverLifetime.enabled) {
target._setTransformFeedback(true);
}
target._setTransformFeedback();
}
/**
@@ -947,7 +954,9 @@ export class ParticleGenerator {
instanceVertices[offset + 20] = colorOverLifetime._colorGradientRand.random();
}
// instanceVertices[offset + 21] = rand.random();
if (this.noise.enabled) {
instanceVertices[offset + 21] = this.noise._noiseRand.random();
}
const rotationOverLifetime = this.rotationOverLifetime;
if (rotationOverLifetime.enabled && rotationOverLifetime.rotationZ.mode === ParticleCurveMode.TwoConstants) {
@@ -1390,6 +1399,22 @@ export class ParticleGenerator {
min.add(worldOffsetMin);
max.add(worldOffsetMax);
// Noise module impact: noise output is normalized to [-1, 1],
// max displacement = |strength_max|
const { noise } = this;
if (noise.enabled) {
let noiseMaxX: number, noiseMaxY: number, noiseMaxZ: number;
if (noise.separateAxes) {
noiseMaxX = Math.abs(noise.strengthX._getMax());
noiseMaxY = Math.abs(noise.strengthY._getMax());
noiseMaxZ = Math.abs(noise.strengthZ._getMax());
} else {
noiseMaxX = noiseMaxY = noiseMaxZ = Math.abs(noise.strengthX._getMax());
}
min.set(min.x - noiseMaxX, min.y - noiseMaxY, min.z - noiseMaxZ);
max.set(max.x + noiseMaxX, max.y + noiseMaxY, max.z + noiseMaxZ);
}
min.add(worldPosition);
max.add(worldPosition);
}

View File

@@ -18,5 +18,6 @@ export enum ParticleRandomSubSeeds {
Shape = 0xaf502044,
GravityModifier = 0xa47b8c4d,
ForceOverLifetime = 0xe6fb937c,
LimitVelocityOverLifetime = 0xb5a21f7e
LimitVelocityOverLifetime = 0xb5a21f7e,
Noise = 0xf4b2c8a1
}

View File

@@ -20,4 +20,5 @@ export { SizeOverLifetimeModule } from "./modules/SizeOverLifetimeModule";
export { TextureSheetAnimationModule } from "./modules/TextureSheetAnimationModule";
export { VelocityOverLifetimeModule } from "./modules/VelocityOverLifetimeModule";
export { LimitVelocityOverLifetimeModule } from "./modules/LimitVelocityOverLifetimeModule";
export { NoiseModule } from "./modules/NoiseModule";
export * from "./modules/shape/index";

View File

@@ -241,7 +241,7 @@ export class LimitVelocityOverLifetimeModule extends ParticleGeneratorModule {
return;
}
this._enabled = value;
this._generator._setTransformFeedback(value);
this._generator._setTransformFeedback();
this._generator._renderer._onGeneratorParamsChanged();
}
}

View File

@@ -0,0 +1,334 @@
import { Rand, Vector3, Vector4 } from "@galacean/engine-math";
import { deepClone, ignoreClone } from "../../clone/CloneManager";
import { ShaderData, ShaderMacro, ShaderProperty } from "../../shader";
import { ParticleGenerator } from "../ParticleGenerator";
import { ParticleCurveMode } from "../enums/ParticleCurveMode";
import { ParticleRandomSubSeeds } from "../enums/ParticleRandomSubSeeds";
import { ParticleCompositeCurve } from "./ParticleCompositeCurve";
import { ParticleGeneratorModule } from "./ParticleGeneratorModule";
/**
* Noise module for particle system.
* Adds simplex noise-based turbulence displacement to particles.
*/
export class NoiseModule extends ParticleGeneratorModule {
static readonly _enabledMacro = ShaderMacro.getByName("RENDERER_NOISE_MODULE_ENABLED");
static readonly _strengthCurveMacro = ShaderMacro.getByName("RENDERER_NOISE_STRENGTH_CURVE");
static readonly _strengthIsRandomTwoMacro = ShaderMacro.getByName("RENDERER_NOISE_STRENGTH_IS_RANDOM_TWO");
static readonly _separateAxesMacro = ShaderMacro.getByName("RENDERER_NOISE_IS_SEPARATE");
static readonly _noiseProperty = ShaderProperty.getByName("renderer_NoiseParams");
static readonly _noiseOctaveProperty = ShaderProperty.getByName("renderer_NoiseOctaveParams");
static readonly _strengthMinConstProperty = ShaderProperty.getByName("renderer_NoiseStrengthMinConst");
static readonly _strengthMaxCurveXProperty = ShaderProperty.getByName("renderer_NoiseStrengthMaxCurveX");
static readonly _strengthMaxCurveYProperty = ShaderProperty.getByName("renderer_NoiseStrengthMaxCurveY");
static readonly _strengthMaxCurveZProperty = ShaderProperty.getByName("renderer_NoiseStrengthMaxCurveZ");
static readonly _strengthMinCurveXProperty = ShaderProperty.getByName("renderer_NoiseStrengthMinCurveX");
static readonly _strengthMinCurveYProperty = ShaderProperty.getByName("renderer_NoiseStrengthMinCurveY");
static readonly _strengthMinCurveZProperty = ShaderProperty.getByName("renderer_NoiseStrengthMinCurveZ");
@ignoreClone
private _enabledModuleMacro: ShaderMacro;
@ignoreClone
private _strengthCurveModeMacro: ShaderMacro;
@ignoreClone
private _strengthIsRandomTwoModeMacro: ShaderMacro;
@ignoreClone
private _separateAxesModeMacro: ShaderMacro;
/** @internal */
@ignoreClone
_noiseRand = new Rand(0, ParticleRandomSubSeeds.Noise);
@ignoreClone
private _noiseParams = new Vector4();
@ignoreClone
private _noiseOctaveParams = new Vector4();
@ignoreClone
private _strengthMinConst = new Vector3();
@deepClone
private _strengthX: ParticleCompositeCurve;
@deepClone
private _strengthY: ParticleCompositeCurve;
@deepClone
private _strengthZ: ParticleCompositeCurve;
private _scrollSpeed = 0;
private _separateAxes = false;
private _frequency = 0.5;
private _octaveCount = 1;
private _octaveIntensityMultiplier = 0.5;
private _octaveFrequencyMultiplier = 2.0;
/**
* Specifies whether the strength is separate on each axis, when disabled, only `strength` is used.
*/
get separateAxes(): boolean {
return this._separateAxes;
}
set separateAxes(value: boolean) {
if (value !== this._separateAxes) {
this._separateAxes = value;
this._generator._renderer._onGeneratorParamsChanged();
}
}
/**
* Noise strength. When `separateAxes` is disabled, applies to all axes.
* When `separateAxes` is enabled, applies only to 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);
}
}
/**
* Noise strength for y axis, used when `separateAxes` is enabled.
*/
get strengthY(): ParticleCompositeCurve {
return this._strengthY;
}
set strengthY(value: ParticleCompositeCurve) {
const lastValue = this._strengthY;
if (value !== lastValue) {
this._strengthY = value;
this._onCompositeCurveChange(lastValue, value);
}
}
/**
* Noise strength for z axis, used when `separateAxes` is enabled.
*/
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.
*/
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();
}
}
/**
* Noise field scroll speed over time.
*/
get scrollSpeed(): number {
return this._scrollSpeed;
}
set scrollSpeed(value: number) {
if (value !== this._scrollSpeed) {
this._scrollSpeed = value;
this._generator._renderer._onGeneratorParamsChanged();
}
}
/**
* Number of noise octave layers (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, only effective when `octaveCount` > 1.
* 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, only effective when `octaveCount` > 1.
* 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;
}
override set enabled(value: boolean) {
if (value !== this._enabled) {
if (value && !this._generator._renderer.engine._hardwareRenderer.isWebGL2) {
return;
}
this._enabled = value;
this._generator._setTransformFeedback();
this._generator._renderer._onGeneratorParamsChanged();
}
}
constructor(generator: ParticleGenerator) {
super(generator);
this.strengthX = new ParticleCompositeCurve(1);
this.strengthY = new ParticleCompositeCurve(1);
this.strengthZ = new ParticleCompositeCurve(1);
}
/**
* @internal
*/
_updateShaderData(shaderData: ShaderData): void {
let enabledMacro = <ShaderMacro>null;
let strengthCurveMacro = <ShaderMacro>null;
let strengthIsRandomTwoMacro = <ShaderMacro>null;
let separateAxesMacro = <ShaderMacro>null;
if (this.enabled) {
enabledMacro = NoiseModule._enabledMacro;
const strengthX = this._strengthX;
const strengthY = this._strengthY;
const strengthZ = this._strengthZ;
const separateAxes = this._separateAxes;
// Determine strength curve mode (following SOL pattern)
const isRandomCurveMode = separateAxes
? strengthX.mode === ParticleCurveMode.TwoCurves &&
strengthY.mode === ParticleCurveMode.TwoCurves &&
strengthZ.mode === ParticleCurveMode.TwoCurves
: strengthX.mode === ParticleCurveMode.TwoCurves;
const isCurveMode =
isRandomCurveMode ||
(separateAxes
? strengthX.mode === ParticleCurveMode.Curve &&
strengthY.mode === ParticleCurveMode.Curve &&
strengthZ.mode === ParticleCurveMode.Curve
: strengthX.mode === ParticleCurveMode.Curve);
const isRandomConstMode = separateAxes
? strengthX.mode === ParticleCurveMode.TwoConstants &&
strengthY.mode === ParticleCurveMode.TwoConstants &&
strengthZ.mode === ParticleCurveMode.TwoConstants
: strengthX.mode === ParticleCurveMode.TwoConstants;
// noiseParams.w = frequency (always needed)
const noiseParams = this._noiseParams;
if (isCurveMode) {
// Curve/TwoCurves: encode curve data as float arrays
shaderData.setFloatArray(NoiseModule._strengthMaxCurveXProperty, strengthX.curveMax._getTypeArray());
if (separateAxes) {
shaderData.setFloatArray(NoiseModule._strengthMaxCurveYProperty, strengthY.curveMax._getTypeArray());
shaderData.setFloatArray(NoiseModule._strengthMaxCurveZProperty, strengthZ.curveMax._getTypeArray());
}
if (isRandomCurveMode) {
shaderData.setFloatArray(NoiseModule._strengthMinCurveXProperty, strengthX.curveMin._getTypeArray());
if (separateAxes) {
shaderData.setFloatArray(NoiseModule._strengthMinCurveYProperty, strengthY.curveMin._getTypeArray());
shaderData.setFloatArray(NoiseModule._strengthMinCurveZProperty, strengthZ.curveMin._getTypeArray());
}
strengthIsRandomTwoMacro = NoiseModule._strengthIsRandomTwoMacro;
}
strengthCurveMacro = NoiseModule._strengthCurveMacro;
// xyz unused in curve mode, just set frequency
noiseParams.set(0, 0, 0, this._frequency);
} else {
// Constant/TwoConstants: pack strength into noiseParams.xyz
if (separateAxes) {
noiseParams.set(strengthX.constantMax, strengthY.constantMax, strengthZ.constantMax, this._frequency);
} else {
const s = strengthX.constantMax;
noiseParams.set(s, s, s, this._frequency);
}
if (isRandomConstMode) {
const minConst = this._strengthMinConst;
if (separateAxes) {
minConst.set(strengthX.constantMin, strengthY.constantMin, strengthZ.constantMin);
} else {
const sMin = strengthX.constantMin;
minConst.set(sMin, sMin, sMin);
}
shaderData.setVector3(NoiseModule._strengthMinConstProperty, minConst);
strengthIsRandomTwoMacro = NoiseModule._strengthIsRandomTwoMacro;
}
}
shaderData.setVector4(NoiseModule._noiseProperty, noiseParams);
if (separateAxes) {
separateAxesMacro = NoiseModule._separateAxesMacro;
}
const noiseOctaveParams = this._noiseOctaveParams;
noiseOctaveParams.set(
this._scrollSpeed,
this._octaveCount,
this._octaveIntensityMultiplier,
this._octaveFrequencyMultiplier
);
shaderData.setVector4(NoiseModule._noiseOctaveProperty, noiseOctaveParams);
}
this._enabledModuleMacro = this._enableMacro(shaderData, this._enabledModuleMacro, enabledMacro);
this._strengthCurveModeMacro = this._enableMacro(shaderData, this._strengthCurveModeMacro, strengthCurveMacro);
this._strengthIsRandomTwoModeMacro = this._enableMacro(
shaderData,
this._strengthIsRandomTwoModeMacro,
strengthIsRandomTwoMacro
);
this._separateAxesModeMacro = this._enableMacro(shaderData, this._separateAxesModeMacro, separateAxesMacro);
}
/**
* @internal
*/
_resetRandomSeed(seed: number): void {
this._noiseRand.reset(seed, ParticleRandomSubSeeds.Noise);
}
}

View File

@@ -74,6 +74,7 @@ uniform int renderer_SimulationSpace;
#include <size_over_lifetime_module>
#include <rotation_over_lifetime_module>
#include <texture_sheet_animation_module>
#include <noise_module>
vec3 computeParticlePosition(in vec3 startVelocity, in float age, in float normalizedAge, vec3 gravityVelocity, vec4 worldRotation, inout vec3 localVelocity, inout vec3 worldVelocity) {
vec3 startPosition = startVelocity * age;

View File

@@ -6,6 +6,7 @@ import color_over_lifetime_module from "./color_over_lifetime_module.glsl";
import texture_sheet_animation_module from "./texture_sheet_animation_module.glsl";
import force_over_lifetime_module from "./force_over_lifetime_module.glsl";
import limit_velocity_over_lifetime_module from "./limit_velocity_over_lifetime_module.glsl";
import noise_module from "./noise_module.glsl";
import particle_feedback_simulation from "./particle_feedback_simulation.glsl";
import sphere_billboard from "./sphere_billboard.glsl";
@@ -23,6 +24,7 @@ export default {
texture_sheet_animation_module,
force_over_lifetime_module,
limit_velocity_over_lifetime_module,
noise_module,
particle_feedback_simulation,
sphere_billboard,

View File

@@ -0,0 +1,89 @@
#ifdef RENDERER_NOISE_MODULE_ENABLED
#include <noise_common>
#include <noise_simplex_3D>
uniform vec4 renderer_NoiseParams; // xyz = strength (constant mode only), w = frequency
uniform vec4 renderer_NoiseOctaveParams; // x = scrollSpeed, y = octaveCount, z = octaveIntensityMultiplier, w = octaveFrequencyMultiplier
#ifdef RENDERER_NOISE_STRENGTH_CURVE
uniform vec2 renderer_NoiseStrengthMaxCurveX[4];
#ifdef RENDERER_NOISE_IS_SEPARATE
uniform vec2 renderer_NoiseStrengthMaxCurveY[4];
uniform vec2 renderer_NoiseStrengthMaxCurveZ[4];
#endif
#ifdef RENDERER_NOISE_STRENGTH_IS_RANDOM_TWO
uniform vec2 renderer_NoiseStrengthMinCurveX[4];
#ifdef RENDERER_NOISE_IS_SEPARATE
uniform vec2 renderer_NoiseStrengthMinCurveY[4];
uniform vec2 renderer_NoiseStrengthMinCurveZ[4];
#endif
#endif
#else
#ifdef RENDERER_NOISE_STRENGTH_IS_RANDOM_TWO
uniform vec3 renderer_NoiseStrengthMinConst;
#endif
#endif
vec3 sampleSimplexNoise3D(vec3 coord) {
float axisOffset = 100.0;
return vec3(
simplex(vec3(coord.z, coord.y, coord.x)),
simplex(vec3(coord.x + axisOffset, coord.z, coord.y)),
simplex(vec3(coord.y, coord.x + axisOffset, coord.z))
);
}
vec3 computeNoiseDisplacement(vec3 currentPosition, float normalizedAge) {
vec3 coord = currentPosition * renderer_NoiseParams.w
+ vec3(renderer_CurrentTime * renderer_NoiseOctaveParams.x);
int octaveCount = int(renderer_NoiseOctaveParams.y);
float octaveIntensityMultiplier = renderer_NoiseOctaveParams.z;
float octaveFrequencyMultiplier = renderer_NoiseOctaveParams.w;
vec3 noiseValue = sampleSimplexNoise3D(coord);
float totalAmplitude = 1.0;
// Unrolled octave loop (GLSL ES 1.0 requires constant loop bounds)
if (octaveCount >= 2) {
float amplitude = octaveIntensityMultiplier;
totalAmplitude += amplitude;
noiseValue += amplitude * sampleSimplexNoise3D(coord * octaveFrequencyMultiplier);
if (octaveCount >= 3) {
amplitude *= octaveIntensityMultiplier;
totalAmplitude += amplitude;
noiseValue += amplitude * sampleSimplexNoise3D(coord * octaveFrequencyMultiplier * octaveFrequencyMultiplier);
}
}
// Evaluate strength (supports Constant, TwoConstants, Curve, TwoCurves).
vec3 strength;
#ifdef RENDERER_NOISE_STRENGTH_CURVE
float sx = evaluateParticleCurve(renderer_NoiseStrengthMaxCurveX, normalizedAge);
#ifdef RENDERER_NOISE_STRENGTH_IS_RANDOM_TWO
sx = mix(evaluateParticleCurve(renderer_NoiseStrengthMinCurveX, normalizedAge), sx, a_Random0.z);
#endif
#ifdef RENDERER_NOISE_IS_SEPARATE
float sy = evaluateParticleCurve(renderer_NoiseStrengthMaxCurveY, normalizedAge);
float sz = evaluateParticleCurve(renderer_NoiseStrengthMaxCurveZ, normalizedAge);
#ifdef RENDERER_NOISE_STRENGTH_IS_RANDOM_TWO
sy = mix(evaluateParticleCurve(renderer_NoiseStrengthMinCurveY, normalizedAge), sy, a_Random0.z);
sz = mix(evaluateParticleCurve(renderer_NoiseStrengthMinCurveZ, normalizedAge), sz, a_Random0.z);
#endif
strength = vec3(sx, sy, sz);
#else
strength = vec3(sx);
#endif
#else
strength = renderer_NoiseParams.xyz;
#ifdef RENDERER_NOISE_STRENGTH_IS_RANDOM_TWO
strength = mix(renderer_NoiseStrengthMinConst, strength, a_Random0.z);
#endif
#endif
return (noiseValue / totalAmplitude) * strength;
}
#endif

View File

@@ -34,6 +34,7 @@ varying vec3 v_FeedbackVelocity;
#include <velocity_over_lifetime_module>
#include <force_over_lifetime_module>
#include <limit_velocity_over_lifetime_module>
#include <noise_module>
// Get VOL instantaneous velocity at normalizedAge
vec3 getVOLVelocity(float normalizedAge) {
@@ -227,16 +228,31 @@ void main() {
// World mode: position in world space, velocity rotated to world
// =====================================================
// FOL is now fully in localVelocity (both local and world-space FOL).
// Only VOL overlay needs to be added here.
// VOL and Noise overlays are added here (not persisted).
vec3 totalVelocity;
if (renderer_SimulationSpace == 0) {
// Local: integrate in local space
totalVelocity = localVelocity + volLocal
+ rotationByQuaternions(volWorld, invWorldRotation);
totalVelocity = localVelocity + volLocal + rotationByQuaternions(volWorld, invWorldRotation);
} else {
// World: integrate in world space
totalVelocity = rotationByQuaternions(localVelocity + volLocal, worldRotation) + volWorld;
}
#ifdef RENDERER_NOISE_MODULE_ENABLED
// Noise velocity overlay (not persisted)
// computeNoiseDisplacement returns noise * strength (position-scale)
// Dividing by lifetime converts to velocity so that integration over lifetime
// recovers the original displacement magnitude
// Use analytical base position (birth + initial velocity * age) instead of
// a_FeedbackPosition to avoid feedback loop: position → noise → velocity → position
vec3 noiseBasePos;
if (renderer_SimulationSpace == 0) {
noiseBasePos = a_ShapePositionStartLifeTime.xyz + a_DirectionTime.xyz * a_StartSpeed * age;
} else {
noiseBasePos = rotationByQuaternions(
a_ShapePositionStartLifeTime.xyz + a_DirectionTime.xyz * a_StartSpeed * age,
worldRotation) + a_SimulationWorldPosition;
}
totalVelocity += computeNoiseDisplacement(noiseBasePos, normalizedAge) / lifetime;
#endif
vec3 position = a_FeedbackPosition + totalVelocity * dt;
v_FeedbackPosition = position;