Skip to content

Commit 4813496

Browse files
[NPE] Adding gradients support for lifetime (#17449)
- targetStopDuration is now a block input for SystemBlock - Adds functions to generate gradients from FactorValues - Introduces a ConversionContext object to make sure that certain blocks are re-used in the graph instead of being regenerated constantly. PG to test: #0K3AQ2#3727 PG to test: #H4GGRI Ratio block group <img width="1375" height="879" alt="NewRatio" src="https://github.com/user-attachments/assets/788d1d1d-c197-43d6-9787-4f9a9eb82dde" /> Gradients block group <img width="560" height="668" alt="LifetimeGradients" src="https://github.com/user-attachments/assets/e8a4d336-c35d-4c08-a78c-5584ac163cfc" />
1 parent e721b03 commit 4813496

File tree

3 files changed

+158
-29
lines changed

3 files changed

+158
-29
lines changed

packages/dev/core/src/Particles/Node/Blocks/systemBlock.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,6 @@ export class SystemBlock extends NodeParticleBlock {
5151
@editableInPropertyPage("Manual emit count", PropertyTypeForEdition.Int, "ADVANCED", { embedded: true, notifiers: { rebuild: true }, min: -1 })
5252
public manualEmitCount = -1;
5353

54-
/**
55-
* Gets or sets the target stop duration for the particle system
56-
*/
57-
@editableInPropertyPage("Target duration", PropertyTypeForEdition.Float, "ADVANCED", { embedded: true, notifiers: { rebuild: true }, min: 0 })
58-
public targetStopDuration = 0;
59-
6054
/**
6155
* Gets or sets the target stop duration for the particle system
6256
*/
@@ -123,6 +117,7 @@ export class SystemBlock extends NodeParticleBlock {
123117
this.registerInput("onEnd", NodeParticleBlockConnectionPointTypes.System, true);
124118
this.registerInput("translationPivot", NodeParticleBlockConnectionPointTypes.Vector2, true);
125119
this.registerInput("textureMask", NodeParticleBlockConnectionPointTypes.Color4, true);
120+
this.registerInput("targetStopDuration", NodeParticleBlockConnectionPointTypes.Float, true, 0, 0);
126121
this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System);
127122
}
128123

@@ -176,6 +171,13 @@ export class SystemBlock extends NodeParticleBlock {
176171
return this._inputs[5];
177172
}
178173

174+
/**
175+
* Gets the targetStopDuration input component
176+
*/
177+
public get targetStopDuration(): NodeParticleConnectionPoint {
178+
return this._inputs[6];
179+
}
180+
179181
/**
180182
* Gets the system output component
181183
*/
@@ -203,7 +205,7 @@ export class SystemBlock extends NodeParticleBlock {
203205
particleSystem.preWarmStepOffset = this.preWarmStepOffset;
204206
particleSystem.blendMode = this.blendMode;
205207
particleSystem.name = this.name;
206-
particleSystem._targetStopDuration = this.targetStopDuration;
208+
particleSystem._targetStopDuration = (this.targetStopDuration.getConnectedValue(state) as number) ?? 0;
207209
particleSystem.startDelay = this.startDelay;
208210
particleSystem.isBillboardBased = this.isBillboardBased;
209211
particleSystem.translationPivot = (this.translationPivot.getConnectedValue(state) as Vector2) || Vector2.Zero();
@@ -268,7 +270,6 @@ export class SystemBlock extends NodeParticleBlock {
268270
serializationObject.isLocal = this.isLocal;
269271
serializationObject.disposeOnStop = this.disposeOnStop;
270272
serializationObject.doNoStart = this.doNoStart;
271-
serializationObject.targetStopDuration = this.targetStopDuration;
272273
serializationObject.startDelay = this.startDelay;
273274

274275
return serializationObject;
@@ -292,10 +293,6 @@ export class SystemBlock extends NodeParticleBlock {
292293
this.blendMode = serializationObject.blendMode;
293294
}
294295

295-
if (serializationObject.targetStopDuration !== undefined) {
296-
this.targetStopDuration = serializationObject.targetStopDuration;
297-
}
298-
299296
if (serializationObject.startDelay !== undefined) {
300297
this.startDelay = serializationObject.startDelay;
301298
}

packages/dev/core/src/Particles/Node/nodeParticleSystemSet.helper.ts

Lines changed: 148 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Nullable } from "core/types";
22
import type { Color4 } from "core/Maths/math.color";
33
import type { Texture } from "core/Materials/Textures/texture";
44
import type { Mesh } from "core/Meshes/mesh";
5-
import type { ColorGradient } from "core/Misc";
5+
import type { ColorGradient, FactorGradient } from "core/Misc";
66
import type { ParticleSystem } from "core/Particles/particleSystem";
77
import type { IParticleSystem } from "core/Particles/IParticleSystem";
88
import type { BoxParticleEmitter } from "core/Particles/EmitterTypes/boxParticleEmitter";
@@ -42,6 +42,14 @@ import { UpdateColorBlock } from "./Blocks/Update/updateColorBlock";
4242
import { UpdateDirectionBlock } from "./Blocks/Update/updateDirectionBlock";
4343
import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock";
4444

45+
/** Represents blocks or groups of blocks that can be used in multiple places in the graph, so they are stored in this context to be reused */
46+
type ConversionContext = {
47+
targetStopDurationBlockOutput: NodeParticleConnectionPoint;
48+
timeToStopTimeRatioBlockGroupOutput: NodeParticleConnectionPoint;
49+
};
50+
51+
type RuntimeConversionContext = Partial<ConversionContext>;
52+
4553
/**
4654
* Converts a ParticleSystem to a NodeParticleSystemSet.
4755
* @param name The name of the node particle system set.
@@ -59,16 +67,16 @@ export async function ConvertToNodeParticleSystemSetAsync(name: string, particle
5967
const promises: Promise<void>[] = [];
6068

6169
for (const particleSystem of particleSystemsList) {
62-
promises.push(_ExtractDatafromParticleSystemAsync(nodeParticleSystemSet, particleSystem));
70+
promises.push(_ExtractDatafromParticleSystemAsync(nodeParticleSystemSet, particleSystem, {}));
6371
}
6472

6573
await Promise.all(promises);
6674
return nodeParticleSystemSet;
6775
}
6876

69-
async function _ExtractDatafromParticleSystemAsync(newSet: NodeParticleSystemSet, oldSystem: ParticleSystem): Promise<void> {
77+
async function _ExtractDatafromParticleSystemAsync(newSet: NodeParticleSystemSet, oldSystem: ParticleSystem, context: RuntimeConversionContext): Promise<void> {
7078
// CreateParticle block
71-
const createParticleBlock = _CreateCreateParticleBlock(oldSystem);
79+
const createParticleBlock = _CreateCreateParticleBlockGroup(oldSystem, context);
7280

7381
// Emitter Shape block
7482
const shapeBlock = _CreateEmitterShapeBlock(oldSystem);
@@ -82,27 +90,26 @@ async function _ExtractDatafromParticleSystemAsync(newSet: NodeParticleSystemSet
8290
positionUpdatedParticle.connectTo(colorUpdateBlock.particle);
8391

8492
// System block
85-
const newSystem = _CreateSystemBlock(oldSystem);
93+
const newSystem = _CreateSystemBlock(oldSystem, context);
8694
colorUpdateBlock.output.connectTo(newSystem.particle);
8795

8896
// Register
8997
newSet.systemBlocks.push(newSystem);
9098
}
9199

92-
function _CreateSystemBlock(oldSystem: ParticleSystem): SystemBlock {
100+
function _CreateSystemBlock(oldSystem: ParticleSystem, context: RuntimeConversionContext): SystemBlock {
93101
const newSystem = new SystemBlock(oldSystem.name);
94102

95103
_CreateAndConnectInput("Translation pivot", oldSystem.translationPivot, newSystem.translationPivot);
96104
_CreateAndConnectInput("Texture mask", oldSystem.textureMask, newSystem.textureMask);
105+
const targetStopDurationOutput = _CreateTargetStopDurationInputBlock(oldSystem, context);
106+
targetStopDurationOutput.connectTo(newSystem.targetStopDuration);
97107

98108
newSystem.emitRate = oldSystem.emitRate;
99109
newSystem.manualEmitCount = oldSystem.manualEmitCount;
100-
101110
newSystem.blendMode = oldSystem.blendMode;
102111
newSystem.capacity = oldSystem.getCapacity();
103-
newSystem.targetStopDuration = oldSystem.targetStopDuration;
104112
newSystem.startDelay = oldSystem.startDelay;
105-
newSystem.targetStopDuration = oldSystem.targetStopDuration;
106113
newSystem.updateSpeed = oldSystem.updateSpeed;
107114
newSystem.preWarmCycles = oldSystem.preWarmCycles;
108115
newSystem.preWarmStepOffset = oldSystem.preWarmStepOffset;
@@ -123,10 +130,15 @@ function _CreateSystemBlock(oldSystem: ParticleSystem): SystemBlock {
123130
return newSystem;
124131
}
125132

126-
function _CreateCreateParticleBlock(oldSystem: ParticleSystem): CreateParticleBlock {
133+
// Create Particle Block Group functions
134+
135+
function _CreateCreateParticleBlockGroup(oldSystem: ParticleSystem, context: RuntimeConversionContext): CreateParticleBlock {
127136
// Create particle
128137
const createParticleBlock = new CreateParticleBlock("Create Particle");
129138

139+
// Lifetime
140+
_CreateParticleLifetimeBlockGroup(oldSystem, context).connectTo(createParticleBlock.lifeTime);
141+
130142
// Size
131143
const randomSizeBlock = new ParticleRandomBlock("Random size");
132144
_CreateAndConnectInput("Min size", oldSystem.minSize, randomSizeBlock.min);
@@ -156,15 +168,28 @@ function _CreateCreateParticleBlock(oldSystem: ParticleSystem): CreateParticleBl
156168
_CreateAndConnectInput("Max Rotation", oldSystem.maxInitialRotation, randomRotationBlock.max);
157169
randomRotationBlock.output.connectTo(createParticleBlock.angle);
158170

159-
// Lifetime
160-
const randomLifetimeBlock = new ParticleRandomBlock("Random Lifetime");
161-
_CreateAndConnectInput("Min Lifetime", oldSystem.minLifeTime, randomLifetimeBlock.min);
162-
_CreateAndConnectInput("Max Lifetime", oldSystem.maxLifeTime, randomLifetimeBlock.max);
163-
randomLifetimeBlock.output.connectTo(createParticleBlock.lifeTime);
164-
165171
return createParticleBlock;
166172
}
167173

174+
/**
175+
* Creates the group of blocks that represent the particle lifetime
176+
* @param oldSystem The old particle system to migrate
177+
* @param context The system migration context
178+
* @returns The output of the group of blocks that represent the particle lifetime
179+
*/
180+
function _CreateParticleLifetimeBlockGroup(oldSystem: ParticleSystem, context: RuntimeConversionContext): NodeParticleConnectionPoint {
181+
if (oldSystem.targetStopDuration && oldSystem._lifeTimeGradients && oldSystem._lifeTimeGradients.length > 0) {
182+
context.timeToStopTimeRatioBlockGroupOutput = _CreateTimeToStopTimeRatioBlockGroup(oldSystem, context);
183+
const gradientBlockGroupOutput = _CreateGradientBlockGroup(context.timeToStopTimeRatioBlockGroupOutput, oldSystem._lifeTimeGradients);
184+
return gradientBlockGroupOutput;
185+
} else {
186+
const randomLifetimeBlock = new ParticleRandomBlock("Random Lifetime");
187+
_CreateAndConnectInput("Min Lifetime", oldSystem.minLifeTime, randomLifetimeBlock.min);
188+
_CreateAndConnectInput("Max Lifetime", oldSystem.maxLifeTime, randomLifetimeBlock.max);
189+
return randomLifetimeBlock.output;
190+
}
191+
}
192+
168193
function _CreateEmitterShapeBlock(oldSystem: IParticleSystem): IShapeBlock {
169194
const emitter = oldSystem.particleEmitterType;
170195
if (!emitter) {
@@ -536,3 +561,110 @@ function _CreateAndConnectSystemSource(systemBlockName: string, systemSource: No
536561
input.systemSource = systemSource;
537562
input.output.connectTo(targetToConnectTo);
538563
}
564+
565+
/**
566+
* Creates the target stop duration input block, as it can be shared in multiple places
567+
* This block is stored in the context so the same block is shared in the graph
568+
* @param oldSystem The old particle system to migrate
569+
* @param context The system migration context
570+
* @returns
571+
*/
572+
function _CreateTargetStopDurationInputBlock(oldSystem: ParticleSystem, context: RuntimeConversionContext): NodeParticleConnectionPoint {
573+
// If we have already created the target stop duration input block, return it
574+
if (context.targetStopDurationBlockOutput) {
575+
return context.targetStopDurationBlockOutput;
576+
}
577+
578+
// Create the target stop duration input block if not already created
579+
const targetStopDurationInputBlock = new ParticleInputBlock("Target Stop Duration");
580+
targetStopDurationInputBlock.value = oldSystem.targetStopDuration;
581+
582+
// Save the output in our context to avoid regenerating it again
583+
context.targetStopDurationBlockOutput = targetStopDurationInputBlock.output;
584+
return context.targetStopDurationBlockOutput;
585+
}
586+
587+
/**
588+
* Create a group of blocks that calculates the ratio between the actual frame and the target stop duration, clamped between 0 and 1.
589+
* This is used to simulate the behavior of the old particle system where several particle gradient values are affected by the target stop duration.
590+
* This block group is stored in the context so the same group is shared in the graph
591+
* @param oldSystem The old particle system to migrate
592+
* @param context The system migration context
593+
* @returns The ratio block output connection point
594+
*/
595+
function _CreateTimeToStopTimeRatioBlockGroup(oldSystem: ParticleSystem, context: RuntimeConversionContext): NodeParticleConnectionPoint {
596+
// If we have already generated this group, return it
597+
if (context.timeToStopTimeRatioBlockGroupOutput) {
598+
return context.timeToStopTimeRatioBlockGroupOutput;
599+
}
600+
601+
context.targetStopDurationBlockOutput = _CreateTargetStopDurationInputBlock(oldSystem, context);
602+
603+
// Find the ratio between the actual frame and the target stop duration
604+
const ratio = new ParticleMathBlock("Frame/Stop Ratio");
605+
ratio.operation = ParticleMathBlockOperations.Divide;
606+
_CreateAndConnectSystemSource("Actual Frame", NodeParticleSystemSources.Time, ratio.left);
607+
context.targetStopDurationBlockOutput.connectTo(ratio.right);
608+
609+
// Make sure values is >=0
610+
const clampMin = new ParticleMathBlock("Clamp Min 0");
611+
clampMin.operation = ParticleMathBlockOperations.Max;
612+
_CreateAndConnectInput("Zero", 0, clampMin.left);
613+
ratio.output.connectTo(clampMin.right);
614+
615+
// Make sure values is <=1
616+
const clampMax = new ParticleMathBlock("Clamp Max 1");
617+
clampMax.operation = ParticleMathBlockOperations.Min;
618+
_CreateAndConnectInput("One", 1, clampMax.left);
619+
clampMin.output.connectTo(clampMax.right);
620+
621+
// Save the group output in our context to avoid regenerating it again
622+
context.timeToStopTimeRatioBlockGroupOutput = clampMax.output;
623+
return context.timeToStopTimeRatioBlockGroupOutput;
624+
}
625+
626+
/**
627+
* Creates the blocks that represent a gradient
628+
* @param gradientSelector The value that determines which gradient to use
629+
* @param gradientValues The list of gradient values
630+
* @returns The output connection point of the gradient block
631+
*/
632+
function _CreateGradientBlockGroup(gradientSelector: NodeParticleConnectionPoint, gradientValues: Array<FactorGradient>): NodeParticleConnectionPoint {
633+
// Create the gradient block and connect the value that controls the gradient selection
634+
const gradientBlock = new ParticleGradientBlock("Gradient Block");
635+
gradientSelector.connectTo(gradientBlock.gradient);
636+
637+
// Create the gradient values
638+
for (let i = 0; i < gradientValues.length; i++) {
639+
const gradientValueBlockGroupOutput = _CreateGradientValueBlockGroup(gradientValues[i], i);
640+
gradientValueBlockGroupOutput.connectTo(gradientBlock.inputs[i + 1]);
641+
}
642+
643+
return gradientBlock.output;
644+
}
645+
646+
/**
647+
* Creates the blocks that represent a gradient value
648+
* This can be either a single value or a random between two values
649+
* @param gradientStep The gradient step data
650+
* @param index The index of the gradient step
651+
* @returns The output connection point of the gradient value block
652+
*/
653+
function _CreateGradientValueBlockGroup(gradientStep: FactorGradient, index: number): NodeParticleConnectionPoint {
654+
const gradientValueBlock = new ParticleGradientValueBlock("Gradient Value " + index);
655+
gradientValueBlock.reference = gradientStep.gradient;
656+
657+
if (gradientStep.factor2 !== undefined) {
658+
// Create a random between value1 and value2
659+
const randomBlock = new ParticleRandomBlock("Random Gradient Value " + index);
660+
randomBlock.lockMode = ParticleRandomBlockLocks.PerParticle;
661+
_CreateAndConnectInput("Value 1", gradientStep.factor1, randomBlock.min);
662+
_CreateAndConnectInput("Value 2", gradientStep.factor2, randomBlock.max);
663+
randomBlock.output.connectTo(gradientValueBlock.value);
664+
} else {
665+
// Single value
666+
_CreateAndConnectInput("Value", gradientStep.factor1, gradientValueBlock.value);
667+
}
668+
669+
return gradientValueBlock.output;
670+
}

packages/dev/core/src/Particles/thinParticleSystem.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ export class ThinParticleSystem extends BaseParticleSystem implements IDisposabl
754754
}
755755

756756
/** @internal */
757-
public _emitFromParticle: (particle: Particle) => void = (particle) => {
757+
public _emitFromParticle: (particle: Particle) => void = (_particle) => {
758758
// Do nothing
759759
};
760760

0 commit comments

Comments
 (0)