mirror of
https://github.com/galacean/engine.git
synced 2026-06-09 09:12:48 +08:00
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:
111
e2e/case/particleRenderer-noise.ts
Normal file
111
e2e/case/particleRenderer-noise.ts
Normal 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);
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:6ddb5bc9d8f18f18a37a69aefa0aa58a9371dd35e0af6038e9a220c14f36767e
|
||||
size 24260
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -18,5 +18,6 @@ export enum ParticleRandomSubSeeds {
|
||||
Shape = 0xaf502044,
|
||||
GravityModifier = 0xa47b8c4d,
|
||||
ForceOverLifetime = 0xe6fb937c,
|
||||
LimitVelocityOverLifetime = 0xb5a21f7e
|
||||
LimitVelocityOverLifetime = 0xb5a21f7e,
|
||||
Noise = 0xf4b2c8a1
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -241,7 +241,7 @@ export class LimitVelocityOverLifetimeModule extends ParticleGeneratorModule {
|
||||
return;
|
||||
}
|
||||
this._enabled = value;
|
||||
this._generator._setTransformFeedback(value);
|
||||
this._generator._setTransformFeedback();
|
||||
this._generator._renderer._onGeneratorParamsChanged();
|
||||
}
|
||||
}
|
||||
|
||||
334
packages/core/src/particle/modules/NoiseModule.ts
Normal file
334
packages/core/src/particle/modules/NoiseModule.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
89
packages/core/src/shaderlib/particle/noise_module.glsl
Normal file
89
packages/core/src/shaderlib/particle/noise_module.glsl
Normal 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
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user