@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
899 lines • 54.1 kB
JavaScript
import { Vector2, Vector3 } from "../../Maths/math.vector.js";
import { NodeParticleSystemSet } from "./nodeParticleSystemSet.js";
import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources.js";
import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources.js";
import { ParticleConverterBlock } from "./Blocks/particleConverterBlock.js";
import { ParticleFloatToIntBlock, ParticleFloatToIntBlockOperations } from "./Blocks/particleFloatToIntBlock.js";
import { ParticleGradientBlock } from "./Blocks/particleGradientBlock.js";
import { ParticleGradientValueBlock } from "./Blocks/particleGradientValueBlock.js";
import { ParticleInputBlock } from "./Blocks/particleInputBlock.js";
import { ParticleMathBlock, ParticleMathBlockOperations } from "./Blocks/particleMathBlock.js";
import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock.js";
import { ParticleLerpBlock } from "./Blocks/particleLerpBlock.js";
import { ParticleTextureSourceBlock } from "./Blocks/particleSourceTextureBlock.js";
import { ParticleVectorLengthBlock } from "./Blocks/particleVectorLengthBlock.js";
import { SystemBlock } from "./Blocks/systemBlock.js";
import { ParticleConditionBlock, ParticleConditionBlockTests } from "./Blocks/Conditions/particleConditionBlock.js";
import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock.js";
import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock.js";
import { ConeShapeBlock } from "./Blocks/Emitters/coneShapeBlock.js";
import { CylinderShapeBlock } from "./Blocks/Emitters/cylinderShapeBlock.js";
import { CustomShapeBlock } from "./Blocks/Emitters/customShapeBlock.js";
import { MeshShapeBlock } from "./Blocks/Emitters/meshShapeBlock.js";
import { PointShapeBlock } from "./Blocks/Emitters/pointShapeBlock.js";
import { SetupSpriteSheetBlock } from "./Blocks/Emitters/setupSpriteSheetBlock.js";
import { SphereShapeBlock } from "./Blocks/Emitters/sphereShapeBlock.js";
import { UpdateAngleBlock } from "./Blocks/Update/updateAngleBlock.js";
import { BasicSpriteUpdateBlock } from "./Blocks/Update/basicSpriteUpdateBlock.js";
import { UpdateAttractorBlock } from "./Blocks/Update/updateAttractorBlock.js";
import { UpdateColorBlock } from "./Blocks/Update/updateColorBlock.js";
import { UpdateDirectionBlock } from "./Blocks/Update/updateDirectionBlock.js";
import { UpdateFlowMapBlock } from "./Blocks/Update/updateFlowMapBlock.js";
import { UpdateNoiseBlock } from "./Blocks/Update/updateNoiseBlock.js";
import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock.js";
import { UpdateSizeBlock } from "./Blocks/Update/updateSizeBlock.js";
import { GenerateBase64StringFromPixelData } from "../../Misc/copyTools.js";
/**
* Converts a ParticleSystem to a NodeParticleSystemSet.
* @param name The name of the node particle system set.
* @param particleSystemsList The particle systems to convert.
* @returns The converted node particle system set or null if conversion failed.
*/
export async function ConvertToNodeParticleSystemSetAsync(name, particleSystemsList) {
if (!particleSystemsList || !particleSystemsList.length) {
return null;
}
const nodeParticleSystemSet = new NodeParticleSystemSet(name);
const promises = [];
for (const particleSystem of particleSystemsList) {
promises.push(_ExtractDatafromParticleSystemAsync(nodeParticleSystemSet, particleSystem, {}));
}
await Promise.all(promises);
return nodeParticleSystemSet;
}
async function _ExtractDatafromParticleSystemAsync(newSet, oldSystem, context) {
// CreateParticle block group
const createParticleOutput = _CreateParticleBlockGroup(oldSystem, context);
// UpdateParticle block group
const updateParticleOutput = _UpdateParticleBlockGroup(createParticleOutput, oldSystem, context);
// System block
const newSystem = _SystemBlockGroup(updateParticleOutput, oldSystem, context);
// Register
newSet.systemBlocks.push(newSystem);
}
// ------------- CREATE PARTICLE FUNCTIONS -------------
// The creation of the different properties follows the order they are added to the CreationQueue in ThinParticleSystem:
// Lifetime, Emit Power, Size, Scale/StartSize, Angle, Color, Noise, ColorDead, Sheet
function _CreateParticleBlockGroup(oldSystem, context) {
// Create particle block
const createParticleBlock = new CreateParticleBlock("Create Particle");
let createdParticle = createParticleBlock.particle;
_CreateParticleLifetimeBlockGroup(oldSystem, context).connectTo(createParticleBlock.lifeTime);
_CreateParticleEmitPowerBlockGroup(oldSystem).connectTo(createParticleBlock.emitPower);
_CreateParticleSizeBlockGroup(oldSystem, context).connectTo(createParticleBlock.size);
_CreateParticleScaleBlockGroup(oldSystem, context).connectTo(createParticleBlock.scale);
_CreateParticleAngleBlockGroup(oldSystem).connectTo(createParticleBlock.angle);
_CreateParticleColorBlockGroup(oldSystem, context).connectTo(createParticleBlock.color);
// Dead color
_CreateAndConnectInput("Dead Color", oldSystem.colorDead.clone(), createParticleBlock.colorDead);
// Emitter shape
createdParticle = _EmitterShapeBlock(createdParticle, oldSystem);
// Sprite sheet setup
if (oldSystem.isAnimationSheetEnabled) {
createdParticle = _SpriteSheetBlock(createdParticle, oldSystem);
}
return createdParticle;
}
/**
* Creates the group of blocks that represent the particle lifetime
* @param oldSystem The old particle system to convert
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle lifetime
*/
function _CreateParticleLifetimeBlockGroup(oldSystem, context) {
if (oldSystem.targetStopDuration && oldSystem._lifeTimeGradients && oldSystem._lifeTimeGradients.length > 0) {
context.timeToStopTimeRatioBlockGroupOutput = _CreateTimeToStopTimeRatioBlockGroup(oldSystem.targetStopDuration, context);
const gradientBlockGroupOutput = _CreateGradientBlockGroup(context.timeToStopTimeRatioBlockGroupOutput, oldSystem._lifeTimeGradients, ParticleRandomBlockLocks.PerParticle, "Lifetime");
return gradientBlockGroupOutput;
}
else {
const randomLifetimeBlock = new ParticleRandomBlock("Random Lifetime");
_CreateAndConnectInput("Min Lifetime", oldSystem.minLifeTime, randomLifetimeBlock.min);
_CreateAndConnectInput("Max Lifetime", oldSystem.maxLifeTime, randomLifetimeBlock.max);
return randomLifetimeBlock.output;
}
}
/**
* Creates the group of blocks that represent the particle emit power
* @param oldSystem The old particle system to convert
* @returns The output of the group of blocks that represent the particle emit power
*/
function _CreateParticleEmitPowerBlockGroup(oldSystem) {
const randomEmitPowerBlock = new ParticleRandomBlock("Random Emit Power");
_CreateAndConnectInput("Min Emit Power", oldSystem.minEmitPower, randomEmitPowerBlock.min);
_CreateAndConnectInput("Max Emit Power", oldSystem.maxEmitPower, randomEmitPowerBlock.max);
return randomEmitPowerBlock.output;
}
/**
* Creates the group of blocks that represent the particle size
* @param oldSystem The old particle system to convert
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle size
*/
function _CreateParticleSizeBlockGroup(oldSystem, context) {
if (oldSystem._sizeGradients && oldSystem._sizeGradients.length > 0) {
context.sizeGradientValue0Output = _CreateParticleInitialValueFromGradient(oldSystem._sizeGradients);
return context.sizeGradientValue0Output;
}
else {
const randomSizeBlock = new ParticleRandomBlock("Random size");
_CreateAndConnectInput("Min size", oldSystem.minSize, randomSizeBlock.min);
_CreateAndConnectInput("Max size", oldSystem.maxSize, randomSizeBlock.max);
return randomSizeBlock.output;
}
}
/**
* Creates the group of blocks that represent the particle scale
* @param oldSystem The old particle system to convert
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle scale
*/
function _CreateParticleScaleBlockGroup(oldSystem, context) {
// Create the random scale
const randomScaleBlock = new ParticleRandomBlock("Random Scale");
_CreateAndConnectInput("Min Scale", new Vector2(oldSystem.minScaleX, oldSystem.minScaleY), randomScaleBlock.min);
_CreateAndConnectInput("Max Scale", new Vector2(oldSystem.maxScaleX, oldSystem.maxScaleY), randomScaleBlock.max);
if (oldSystem.targetStopDuration && oldSystem._startSizeGradients && oldSystem._startSizeGradients.length > 0) {
// Create the start size gradient
context.timeToStopTimeRatioBlockGroupOutput = _CreateTimeToStopTimeRatioBlockGroup(oldSystem.targetStopDuration, context);
const gradientBlockGroupOutput = _CreateGradientBlockGroup(context.timeToStopTimeRatioBlockGroupOutput, oldSystem._startSizeGradients, ParticleRandomBlockLocks.PerParticle, "Start Size");
// Multiply the initial random scale by the start size gradient
const multiplyScaleBlock = new ParticleMathBlock("Multiply Scale by Start Size Gradient");
multiplyScaleBlock.operation = ParticleMathBlockOperations.Multiply;
randomScaleBlock.output.connectTo(multiplyScaleBlock.left);
gradientBlockGroupOutput.connectTo(multiplyScaleBlock.right);
return multiplyScaleBlock.output;
}
else {
return randomScaleBlock.output;
}
}
/**
* Creates the group of blocks that represent the particle angle (rotation)
* @param oldSystem The old particle system to convert
* @returns The output of the group of blocks that represent the particle angle (rotation)
*/
function _CreateParticleAngleBlockGroup(oldSystem) {
const randomRotationBlock = new ParticleRandomBlock("Random Rotation");
_CreateAndConnectInput("Min Rotation", oldSystem.minInitialRotation, randomRotationBlock.min);
_CreateAndConnectInput("Max Rotation", oldSystem.maxInitialRotation, randomRotationBlock.max);
return randomRotationBlock.output;
}
/**
* Creates the group of blocks that represent the particle color
* @param oldSystem The old particle system to convert
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle color
*/
function _CreateParticleColorBlockGroup(oldSystem, context) {
if (oldSystem._colorGradients && oldSystem._colorGradients.length > 0) {
context.colorGradientValue0Output = _CreateParticleInitialValueFromGradient(oldSystem._colorGradients);
return context.colorGradientValue0Output;
}
else {
const randomStepBlock = new ParticleRandomBlock("Random color step");
_CreateAndConnectInput("Min", 0, randomStepBlock.min);
_CreateAndConnectInput("Max", 1, randomStepBlock.max);
const lerpColorBlock = new ParticleLerpBlock("Lerp color");
_CreateAndConnectInput("Color 1", oldSystem.color1.clone(), lerpColorBlock.left);
_CreateAndConnectInput("Color 2", oldSystem.color2.clone(), lerpColorBlock.right);
randomStepBlock.output.connectTo(lerpColorBlock.gradient);
return lerpColorBlock.output;
}
}
function _CreateParticleInitialValueFromGradient(gradients) {
if (gradients.length === 0) {
throw new Error("No gradients provided.");
}
const gradientStep = gradients[0];
const value1 = gradientStep.factor1 ?? gradientStep.color1;
const value2 = gradientStep.factor2 ?? gradientStep.color2;
if (value2 !== undefined) {
// Create a random between value1 and value2
const randomBlock = new ParticleRandomBlock("Random Value 0");
randomBlock.lockMode = ParticleRandomBlockLocks.OncePerParticle;
_CreateAndConnectInput("Value 1", value1, randomBlock.min);
_CreateAndConnectInput("Value 2", value2, randomBlock.max);
return randomBlock.output;
}
else {
// Single value
const sizeBlock = new ParticleInputBlock("Value");
sizeBlock.value = value1;
return sizeBlock.output;
}
}
function _EmitterShapeBlock(particle, oldSystem) {
const emitter = oldSystem.particleEmitterType;
if (!emitter) {
throw new Error("Particle system has no emitter type.");
}
let shapeBlock = null;
switch (emitter.getClassName()) {
case "BoxParticleEmitter": {
const source = emitter;
shapeBlock = new BoxShapeBlock("Box Shape");
const target = shapeBlock;
_CreateAndConnectInput("Direction 1", source.direction1.clone(), target.direction1);
_CreateAndConnectInput("Direction 2", source.direction2.clone(), target.direction2);
_CreateAndConnectInput("Min Emit Box", source.minEmitBox.clone(), target.minEmitBox);
_CreateAndConnectInput("Max Emit Box", source.maxEmitBox.clone(), target.maxEmitBox);
break;
}
case "ConeParticleEmitter": {
const source = emitter;
shapeBlock = new ConeShapeBlock("Cone Shape");
const target = shapeBlock;
target.emitFromSpawnPointOnly = source.emitFromSpawnPointOnly;
_CreateAndConnectInput("Radius", source.radius, target.radius);
_CreateAndConnectInput("Angle", source.angle, target.angle);
_CreateAndConnectInput("Radius Range", source.radiusRange, target.radiusRange);
_CreateAndConnectInput("Height Range", source.heightRange, target.heightRange);
_CreateAndConnectInput("Direction Randomizer", source.directionRandomizer, target.directionRandomizer);
break;
}
case "ConeDirectedParticleEmitter": {
const source = emitter;
shapeBlock = new ConeShapeBlock("Cone Shape");
const target = shapeBlock;
target.emitFromSpawnPointOnly = source.emitFromSpawnPointOnly;
_CreateAndConnectInput("Radius", source.radius, target.radius);
_CreateAndConnectInput("Angle", source.angle, target.angle);
_CreateAndConnectInput("Radius Range", source.radiusRange, target.radiusRange);
_CreateAndConnectInput("Height Range", source.heightRange, target.heightRange);
_CreateAndConnectInput("Direction 1", source.direction1.clone(), target.direction1);
_CreateAndConnectInput("Direction 2", source.direction2.clone(), target.direction2);
break;
}
case "CustomParticleEmitter": {
const source = emitter;
shapeBlock = new CustomShapeBlock("Custom Shape");
const target = shapeBlock;
target.particlePositionGenerator = source.particlePositionGenerator;
target.particleDestinationGenerator = source.particleDestinationGenerator;
target.particleDirectionGenerator = source.particleDirectionGenerator;
break;
}
case "CylinderParticleEmitter": {
const source = emitter;
shapeBlock = new CylinderShapeBlock("Cylinder Shape");
const target = shapeBlock;
_CreateAndConnectInput("Height", source.height, target.height);
_CreateAndConnectInput("Radius", source.radius, target.radius);
_CreateAndConnectInput("Radius Range", source.radiusRange, target.radiusRange);
_CreateAndConnectInput("Direction Randomizer", source.directionRandomizer, target.directionRandomizer);
break;
}
case "CylinderDirectedParticleEmitter": {
const source = emitter;
shapeBlock = new CylinderShapeBlock("Cylinder Shape");
const target = shapeBlock;
_CreateAndConnectInput("Height", source.height, target.height);
_CreateAndConnectInput("Radius", source.radius, target.radius);
_CreateAndConnectInput("Radius Range", source.radiusRange, target.radiusRange);
_CreateAndConnectInput("Direction 1", source.direction1.clone(), target.direction1);
_CreateAndConnectInput("Direction 2", source.direction2.clone(), target.direction2);
break;
}
case "HemisphericParticleEmitter": {
const source = emitter;
shapeBlock = new SphereShapeBlock("Sphere Shape");
const target = shapeBlock;
target.isHemispheric = true;
_CreateAndConnectInput("Radius", source.radius, target.radius);
_CreateAndConnectInput("Radius Range", source.radiusRange, target.radiusRange);
_CreateAndConnectInput("Direction Randomizer", source.directionRandomizer, target.directionRandomizer);
break;
}
case "MeshParticleEmitter": {
const source = emitter;
shapeBlock = new MeshShapeBlock("Mesh Shape");
const target = shapeBlock;
target.useMeshNormalsForDirection = source.useMeshNormalsForDirection;
_CreateAndConnectInput("Direction 1", source.direction1.clone(), target.direction1);
_CreateAndConnectInput("Direction 2", source.direction2.clone(), target.direction2);
target.mesh = source.mesh;
break;
}
case "PointParticleEmitter": {
const source = emitter;
shapeBlock = new PointShapeBlock("Point Shape");
const target = shapeBlock;
_CreateAndConnectInput("Direction 1", source.direction1.clone(), target.direction1);
_CreateAndConnectInput("Direction 2", source.direction2.clone(), target.direction2);
break;
}
case "SphereParticleEmitter": {
const source = emitter;
shapeBlock = new SphereShapeBlock("Sphere Shape");
const target = shapeBlock;
_CreateAndConnectInput("Radius", source.radius, target.radius);
_CreateAndConnectInput("Radius Range", source.radiusRange, target.radiusRange);
_CreateAndConnectInput("Direction Randomizer", source.directionRandomizer, target.directionRandomizer);
break;
}
case "SphereDirectedParticleEmitter": {
const source = emitter;
shapeBlock = new SphereShapeBlock("Sphere Shape");
const target = shapeBlock;
_CreateAndConnectInput("Radius", source.radius, target.radius);
_CreateAndConnectInput("Radius Range", source.radiusRange, target.radiusRange);
_CreateAndConnectInput("Direction1", source.direction1.clone(), target.direction1);
_CreateAndConnectInput("Direction2", source.direction2.clone(), target.direction2);
break;
}
}
if (!shapeBlock) {
throw new Error(`Unsupported particle emitter type: ${emitter.getClassName()}`);
}
particle.connectTo(shapeBlock.particle);
return shapeBlock.output;
}
function _SpriteSheetBlock(particle, oldSystem) {
const spriteSheetBlock = new SetupSpriteSheetBlock("Sprite Sheet Setup");
particle.connectTo(spriteSheetBlock.particle);
spriteSheetBlock.start = oldSystem.startSpriteCellID;
spriteSheetBlock.end = oldSystem.endSpriteCellID;
spriteSheetBlock.width = oldSystem.spriteCellWidth;
spriteSheetBlock.height = oldSystem.spriteCellHeight;
spriteSheetBlock.spriteCellChangeSpeed = oldSystem.spriteCellChangeSpeed;
spriteSheetBlock.loop = oldSystem.spriteCellLoop;
spriteSheetBlock.randomStartCell = oldSystem.spriteRandomStartCell;
return spriteSheetBlock.output;
}
// ------------- UPDATE PARTICLE FUNCTIONS -------------
/**
* Creates the group of blocks that represent the particle system update
* The creation of the different properties follows the order they are added to the ProcessQueue in ThinParticleSystem:
* Color, AngularSpeedGradients, AngularSpeed, VelocityGradients, Direction, LimitVelocityGradients, DragGradients, Position, Noise, SizeGradients, Gravity
* @param inputParticle The particle input connection point
* @param oldSystem The old particle system to convert
* @param context The runtime conversion context
* @returns The output connection point after all updates have been applied
*/
function _UpdateParticleBlockGroup(inputParticle, oldSystem, context) {
let updatedParticle = inputParticle;
updatedParticle = _UpdateParticleColorBlockGroup(updatedParticle, oldSystem._colorGradients, context);
updatedParticle = _UpdateParticleAngleBlockGroup(updatedParticle, oldSystem._angularSpeedGradients, oldSystem.minAngularSpeed, oldSystem.maxAngularSpeed, context);
if (oldSystem._velocityGradients && oldSystem._velocityGradients.length > 0) {
context.scaledDirection = _UpdateParticleVelocityGradientBlockGroup(oldSystem._velocityGradients, context);
}
if (oldSystem._dragGradients && oldSystem._dragGradients.length > 0) {
context.scaledDirection = _UpdateParticleDragGradientBlockGroup(oldSystem._dragGradients, context);
}
updatedParticle = _UpdateParticlePositionBlockGroup(updatedParticle, oldSystem.isLocal, context);
if (oldSystem.attractors && oldSystem.attractors.length > 0) {
updatedParticle = _UpdateParticleAttractorBlockGroup(updatedParticle, oldSystem.attractors);
}
if (oldSystem.flowMap) {
updatedParticle = _UpdateParticleFlowMapBlockGroup(updatedParticle, oldSystem.flowMap, oldSystem.flowMapStrength);
}
if (oldSystem._limitVelocityGradients && oldSystem._limitVelocityGradients.length > 0 && oldSystem.limitVelocityDamping !== 0) {
updatedParticle = _UpdateParticleVelocityLimitGradientBlockGroup(updatedParticle, oldSystem._limitVelocityGradients, oldSystem.limitVelocityDamping, context);
}
if (oldSystem.noiseTexture && oldSystem.noiseStrength) {
updatedParticle = _UpdateParticleNoiseBlockGroup(updatedParticle, oldSystem.noiseTexture.clone(), oldSystem.noiseStrength.clone());
}
if (oldSystem._sizeGradients && oldSystem._sizeGradients.length > 0) {
updatedParticle = _UpdateParticleSizeGradientBlockGroup(updatedParticle, oldSystem._sizeGradients, context);
}
if (oldSystem.gravity.equalsToFloats(0, 0, 0) === false) {
updatedParticle = _UpdateParticleGravityBlockGroup(updatedParticle, oldSystem.gravity);
}
if (oldSystem.isAnimationSheetEnabled) {
updatedParticle = _UpdateParticleSpriteCellBlockGroup(updatedParticle);
}
return updatedParticle;
}
/**
* Creates the group of blocks that represent the particle color update
* @param inputParticle The input particle to update
* @param colorGradients The color gradients (if any)
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle color update
*/
function _UpdateParticleColorBlockGroup(inputParticle, colorGradients, context) {
let colorCalculation;
if (colorGradients && colorGradients.length > 0) {
if (context.colorGradientValue0Output === undefined) {
throw new Error("Initial color gradient values not found in context.");
}
context.ageToLifeTimeRatioBlockGroupOutput = _CreateAgeToLifeTimeRatioBlockGroup(context);
colorCalculation = _CreateGradientBlockGroup(context.ageToLifeTimeRatioBlockGroupOutput, colorGradients, ParticleRandomBlockLocks.OncePerParticle, "Color", [
context.colorGradientValue0Output,
]);
}
else {
colorCalculation = _BasicColorUpdateBlockGroup();
}
// Create the color update block clamping alpha >= 0
const colorUpdateBlock = new UpdateColorBlock("Color update");
inputParticle.connectTo(colorUpdateBlock.particle);
_ClampUpdateColorAlpha(colorCalculation).connectTo(colorUpdateBlock.color);
return colorUpdateBlock.output;
}
/**
* Creates the group of blocks that represent the particle angle update
* @param inputParticle The input particle to update
* @param angularSpeedGradients The angular speed gradients (if any)
* @param minAngularSpeed The minimum angular speed
* @param maxAngularSpeed The maximum angular speed
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle color update
*/
function _UpdateParticleAngleBlockGroup(inputParticle, angularSpeedGradients, minAngularSpeed, maxAngularSpeed, context) {
// We will try to use gradients if they exist
// If not, we will try to use min/max angular speed
let angularSpeedCalculation = null;
if (angularSpeedGradients && angularSpeedGradients.length > 0) {
angularSpeedCalculation = _UpdateParticleAngularSpeedGradientBlockGroup(angularSpeedGradients, context);
}
else if (minAngularSpeed !== 0 || maxAngularSpeed !== 0) {
angularSpeedCalculation = _UpdateParticleAngularSpeedBlockGroup(minAngularSpeed, maxAngularSpeed);
}
// If we have an angular speed calculation, then update the angle
if (angularSpeedCalculation) {
// Create the angular speed delta
const angleSpeedDeltaOutput = _CreateDeltaModifiedInput("Angular Speed", angularSpeedCalculation);
// Add it to the angle
const addAngle = new ParticleMathBlock("Add Angular Speed to Angle");
addAngle.operation = ParticleMathBlockOperations.Add;
_CreateAndConnectContextualSource("Angle", NodeParticleContextualSources.Angle, addAngle.left);
angleSpeedDeltaOutput.connectTo(addAngle.right);
// Update the particle angle
const updateAngle = new UpdateAngleBlock("Angle Update with Angular Speed");
inputParticle.connectTo(updateAngle.particle);
addAngle.output.connectTo(updateAngle.angle);
return updateAngle.output;
}
else {
return inputParticle;
}
}
/**
* Creates the group of blocks that represent the particle velocity update
* @param velocityGradients The velocity gradients
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle velocity update
*/
function _UpdateParticleVelocityGradientBlockGroup(velocityGradients, context) {
context.ageToLifeTimeRatioBlockGroupOutput = _CreateAgeToLifeTimeRatioBlockGroup(context);
// Generate the gradient
const velocityValueOutput = _CreateGradientBlockGroup(context.ageToLifeTimeRatioBlockGroupOutput, velocityGradients, ParticleRandomBlockLocks.OncePerParticle, "Velocity");
// Update the direction scale based on the velocity
const multiplyScaleByVelocity = new ParticleMathBlock("Multiply Direction Scale by Velocity");
multiplyScaleByVelocity.operation = ParticleMathBlockOperations.Multiply;
velocityValueOutput.connectTo(multiplyScaleByVelocity.left);
_CreateAndConnectContextualSource("Direction Scale", NodeParticleContextualSources.DirectionScale, multiplyScaleByVelocity.right);
// Update the particle direction scale
const multiplyDirection = new ParticleMathBlock("Scaled Direction");
multiplyDirection.operation = ParticleMathBlockOperations.Multiply;
multiplyScaleByVelocity.output.connectTo(multiplyDirection.left);
_CreateAndConnectContextualSource("Direction", NodeParticleContextualSources.Direction, multiplyDirection.right);
// Store the new calculation of the scaled direction in the context
context.scaledDirection = multiplyDirection.output;
return multiplyDirection.output;
}
/**
* Creates the group of blocks that represent the particle velocity limit update
* @param inputParticle The input particle to update
* @param velocityLimitGradients The velocity limit gradients
* @param limitVelocityDamping The limit velocity damping factor
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle velocity limit update
*/
function _UpdateParticleVelocityLimitGradientBlockGroup(inputParticle, velocityLimitGradients, limitVelocityDamping, context) {
context.ageToLifeTimeRatioBlockGroupOutput = _CreateAgeToLifeTimeRatioBlockGroup(context);
// Calculate the current speed
const currentSpeedBlock = new ParticleVectorLengthBlock("Current Speed");
_CreateAndConnectContextualSource("Direction", NodeParticleContextualSources.Direction, currentSpeedBlock.input);
// Calculate the velocity limit from the gradient
const velocityLimitValueOutput = _CreateGradientBlockGroup(context.ageToLifeTimeRatioBlockGroupOutput, velocityLimitGradients, ParticleRandomBlockLocks.OncePerParticle, "Velocity Limit");
// Blocks that will calculate the new velocity if over the limit
const damped = new ParticleMathBlock("Damped Speed");
damped.operation = ParticleMathBlockOperations.Multiply;
_CreateAndConnectContextualSource("Direction", NodeParticleContextualSources.Direction, damped.left);
_CreateAndConnectInput("Limit Velocity Damping", limitVelocityDamping, damped.right);
// Compare current speed and limit
const compareSpeed = new ParticleConditionBlock("Compare Speed to Limit");
compareSpeed.test = ParticleConditionBlockTests.GreaterThan;
currentSpeedBlock.output.connectTo(compareSpeed.left);
velocityLimitValueOutput.connectTo(compareSpeed.right);
damped.output.connectTo(compareSpeed.ifTrue);
_CreateAndConnectContextualSource("Direction", NodeParticleContextualSources.Direction, compareSpeed.ifFalse);
// Update the direction based on the calculted value
const updateDirection = new UpdateDirectionBlock("Direction Update");
inputParticle.connectTo(updateDirection.particle);
compareSpeed.output.connectTo(updateDirection.direction);
return updateDirection.output;
}
/**
* Creates the group of blocks that represent the particle noise update
* @param inputParticle The particle to update
* @param noiseTexture The noise texture
* @param noiseStrength The strength of the noise
* @returns The output of the group of blocks that represent the particle noise update
*/
function _UpdateParticleNoiseBlockGroup(inputParticle, noiseTexture, noiseStrength) {
const noiseUpdate = new UpdateNoiseBlock("Noise Update");
inputParticle.connectTo(noiseUpdate.particle);
_CreateTextureBlock(noiseTexture).connectTo(noiseUpdate.noiseTexture);
_CreateAndConnectInput("Noise Strength", noiseStrength, noiseUpdate.strength);
return noiseUpdate.output;
}
/**
* Creates the group of blocks that represent the particle drag update
* @param dragGradients The drag gradients
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle drag update
*/
function _UpdateParticleDragGradientBlockGroup(dragGradients, context) {
context.ageToLifeTimeRatioBlockGroupOutput = _CreateAgeToLifeTimeRatioBlockGroup(context);
// Generate the gradient
const dragValueOutput = _CreateGradientBlockGroup(context.ageToLifeTimeRatioBlockGroupOutput, dragGradients, ParticleRandomBlockLocks.OncePerParticle, "Drag");
// Calculate drag factor
const oneMinusDragBlock = new ParticleMathBlock("1 - Drag");
oneMinusDragBlock.operation = ParticleMathBlockOperations.Subtract;
_CreateAndConnectInput("One", 1, oneMinusDragBlock.left);
dragValueOutput.connectTo(oneMinusDragBlock.right);
// Multiply the scaled direction by drag factor
const multiplyDirection = new ParticleMathBlock("Scaled Direction with Drag");
multiplyDirection.operation = ParticleMathBlockOperations.Multiply;
oneMinusDragBlock.output.connectTo(multiplyDirection.left);
if (context.scaledDirection === undefined) {
_CreateAndConnectContextualSource("Scaled Direction", NodeParticleContextualSources.ScaledDirection, multiplyDirection.right);
}
else {
context.scaledDirection.connectTo(multiplyDirection.right);
}
// Store the new calculation of the scaled direction in the context
context.scaledDirection = multiplyDirection.output;
return multiplyDirection.output;
}
/**
* Creates the group of blocks that represent the particle position update
* @param inputParticle The input particle to update
* @param isLocal Whether the particle coordinate system is local or not
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle position update
*/
function _UpdateParticlePositionBlockGroup(inputParticle, isLocal, context) {
// Update the particle position
const updatePosition = new UpdatePositionBlock("Position Update");
inputParticle.connectTo(updatePosition.particle);
if (isLocal) {
_CreateAndConnectContextualSource("Local Position Updated", NodeParticleContextualSources.LocalPositionUpdated, updatePosition.position);
}
else {
// Calculate the new position
const addPositionBlock = new ParticleMathBlock("Add Position");
addPositionBlock.operation = ParticleMathBlockOperations.Add;
_CreateAndConnectContextualSource("Position", NodeParticleContextualSources.Position, addPositionBlock.left);
if (context.scaledDirection === undefined) {
_CreateAndConnectContextualSource("Scaled Direction", NodeParticleContextualSources.ScaledDirection, addPositionBlock.right);
}
else {
context.scaledDirection.connectTo(addPositionBlock.right);
}
addPositionBlock.output.connectTo(updatePosition.position);
}
return updatePosition.output;
}
/**
* Creates the group of blocks that represent the particle attractor update
* @param inputParticle The input particle to update
* @param attractors The attractors (if any)
* @returns The output of the group of blocks that represent the particle attractor update
*/
function _UpdateParticleAttractorBlockGroup(inputParticle, attractors) {
let outputParticle = inputParticle;
// Chain update attractor blocks for each attractor
for (let i = 0; i < attractors.length; i++) {
const attractor = attractors[i];
const attractorBlock = new UpdateAttractorBlock(`Attractor Block ${i}`);
outputParticle.connectTo(attractorBlock.particle);
_CreateAndConnectInput("Attractor Position", attractor.position.clone(), attractorBlock.attractor);
_CreateAndConnectInput("Attractor Strength", attractor.strength, attractorBlock.strength);
outputParticle = attractorBlock.output;
}
return outputParticle;
}
/**
* Creates the group of blocks that represent the particle flow map update
* @param inputParticle The input particle to update
* @param flowMap The flow map data
* @param flowMapStrength The strength of the flow map
* @returns The output of the group of blocks that represent the particle flow map update
*/
function _UpdateParticleFlowMapBlockGroup(inputParticle, flowMap, flowMapStrength) {
// Create the flow map update block
const updateFlowMapBlock = new UpdateFlowMapBlock("Flow Map Update");
inputParticle.connectTo(updateFlowMapBlock.particle);
// Create a texture block from the flow map data
// The FlowMap only stores raw pixel data, so we need to convert it to a base64 data URL
// Y has to be flipped as the texture data is flipped between CPU (canvas, Y=0 at top) and GPU (texture, Y=0 at bottom)
const flowMapTextureBlock = new ParticleTextureSourceBlock("Flow Map Texture");
flowMapTextureBlock.serializedCachedData = true;
flowMapTextureBlock.textureDataUrl = GenerateBase64StringFromPixelData(flowMap.data, { width: flowMap.width, height: flowMap.height }, true) ?? "";
flowMapTextureBlock.textureOutput.connectTo(updateFlowMapBlock.flowMap);
_CreateAndConnectInput("Flow Map Strength", flowMapStrength, updateFlowMapBlock.strength);
return updateFlowMapBlock.output;
}
/**
* Creates the group of blocks that represent the particle size update
* @param inputParticle The input particle to update
* @param sizeGradients The size gradients (if any)
* @param context The context of the current conversion
* @returns The output of the group of blocks that represent the particle size update
*/
function _UpdateParticleSizeGradientBlockGroup(inputParticle, sizeGradients, context) {
if (context.sizeGradientValue0Output === undefined) {
throw new Error("Initial size gradient values not found in context.");
}
context.ageToLifeTimeRatioBlockGroupOutput = _CreateAgeToLifeTimeRatioBlockGroup(context);
// Generate the gradient
const sizeValueOutput = _CreateGradientBlockGroup(context.ageToLifeTimeRatioBlockGroupOutput, sizeGradients, ParticleRandomBlockLocks.OncePerParticle, "Size", [
context.sizeGradientValue0Output,
]);
// Create the update size
const updateSizeBlock = new UpdateSizeBlock("Size Update");
inputParticle.connectTo(updateSizeBlock.particle);
sizeValueOutput.connectTo(updateSizeBlock.size);
return updateSizeBlock.output;
}
/**
* Creates the group of blocks that represent the particle gravity update
* @param inputParticle The input particle to update
* @param gravity The gravity vector to apply
* @returns The output of the group of blocks that represent the particle gravity update
*/
function _UpdateParticleGravityBlockGroup(inputParticle, gravity) {
// Create the gravity delta
const gravityDeltaOutput = _CreateDeltaModifiedInput("Gravity", gravity);
// Add it to the direction
const addDirectionBlock = new ParticleMathBlock("Add Gravity to Direction");
addDirectionBlock.operation = ParticleMathBlockOperations.Add;
_CreateAndConnectContextualSource("Direction", NodeParticleContextualSources.Direction, addDirectionBlock.left);
gravityDeltaOutput.connectTo(addDirectionBlock.right);
// Update the particle direction
const updateDirection = new UpdateDirectionBlock("Direction Update with Gravity");
inputParticle.connectTo(updateDirection.particle);
addDirectionBlock.output.connectTo(updateDirection.direction);
return updateDirection.output;
}
/**
* Creates the group of blocks that represent the particle sprite cell update
* @param inputParticle The input particle to update
* @returns The output of the group of blocks that represent the particle sprite cell update #2MI0A1#3
*/
function _UpdateParticleSpriteCellBlockGroup(inputParticle) {
const updateSpriteCell = new BasicSpriteUpdateBlock("Sprite Cell Update");
inputParticle.connectTo(updateSpriteCell.particle);
return updateSpriteCell.output;
}
function _UpdateParticleAngularSpeedGradientBlockGroup(angularSpeedGradients, context) {
context.ageToLifeTimeRatioBlockGroupOutput = _CreateAgeToLifeTimeRatioBlockGroup(context);
// Generate the gradient
const angularSpeedValueOutput = _CreateGradientBlockGroup(context.ageToLifeTimeRatioBlockGroupOutput, angularSpeedGradients, ParticleRandomBlockLocks.OncePerParticle, "Angular Speed");
return angularSpeedValueOutput;
}
function _UpdateParticleAngularSpeedBlockGroup(minAngularSpeed, maxAngularSpeed) {
// Random value between for the angular speed of the particle
const randomAngularSpeedBlock = new ParticleRandomBlock("Random Angular Speed");
randomAngularSpeedBlock.lockMode = ParticleRandomBlockLocks.OncePerParticle;
_CreateAndConnectInput("Min Angular Speed", minAngularSpeed, randomAngularSpeedBlock.min);
_CreateAndConnectInput("Max Angular Speed", maxAngularSpeed, randomAngularSpeedBlock.max);
return randomAngularSpeedBlock.output;
}
function _BasicColorUpdateBlockGroup() {
const addColorBlock = new ParticleMathBlock("Add Color");
addColorBlock.operation = ParticleMathBlockOperations.Add;
_CreateAndConnectContextualSource("Color", NodeParticleContextualSources.Color, addColorBlock.left);
_CreateAndConnectContextualSource("Scaled Color Step", NodeParticleContextualSources.ScaledColorStep, addColorBlock.right);
return addColorBlock.output;
}
function _ClampUpdateColorAlpha(colorCalculationOutput) {
// Decompose color to clamp alpha
const decomposeColorBlock = new ParticleConverterBlock("Decompose Color");
colorCalculationOutput.connectTo(decomposeColorBlock.colorIn);
// Clamp alpha to be >= 0
const maxAlphaBlock = new ParticleMathBlock("Alpha >= 0");
maxAlphaBlock.operation = ParticleMathBlockOperations.Max;
decomposeColorBlock.wOut.connectTo(maxAlphaBlock.left);
_CreateAndConnectInput("Zero", 0, maxAlphaBlock.right);
// Recompose color
const composeColorBlock = new ParticleConverterBlock("Compose Color");
decomposeColorBlock.xyzOut.connectTo(composeColorBlock.xyzIn);
maxAlphaBlock.output.connectTo(composeColorBlock.wIn);
return composeColorBlock.colorOut;
}
// ------------- SYSTEM FUNCTIONS -------------
function _SystemBlockGroup(updateParticleOutput, oldSystem, context) {
const newSystem = new SystemBlock(oldSystem.name);
newSystem.translationPivot.value = oldSystem.translationPivot.clone();
newSystem.textureMask.value = oldSystem.textureMask.clone();
newSystem.manualEmitCount = oldSystem.manualEmitCount;
newSystem.blendMode = oldSystem.blendMode;
newSystem.capacity = oldSystem.getCapacity();
newSystem.startDelay = oldSystem.startDelay;
newSystem.updateSpeed = oldSystem.updateSpeed;
newSystem.preWarmCycles = oldSystem.preWarmCycles;
newSystem.preWarmStepOffset = oldSystem.preWarmStepOffset;
newSystem.isBillboardBased = oldSystem.isBillboardBased;
newSystem.billBoardMode = oldSystem.billboardMode;
newSystem.isLocal = oldSystem.isLocal;
newSystem.disposeOnStop = oldSystem.disposeOnStop;
newSystem.renderingGroupId = oldSystem.renderingGroupId;
const emitter = oldSystem.emitter;
if (emitter instanceof Vector3) {
newSystem.emitter = emitter.clone();
}
else {
newSystem.emitter = emitter;
}
_SystemCustomShader(oldSystem, newSystem);
_SystemEmitRateValue(oldSystem.getEmitRateGradients(), oldSystem.targetStopDuration, oldSystem.emitRate, newSystem, context);
_SystemTargetStopDuration(oldSystem.targetStopDuration, newSystem, context);
const texture = oldSystem.particleTexture;
if (texture) {
_CreateTextureBlock(texture).connectTo(newSystem.texture);
}
updateParticleOutput.connectTo(newSystem.particle);
return newSystem;
}
function _SystemCustomShader(oldSystem, newSystem) {
if (oldSystem.customShader) {
// Copy the custom shader configuration so it can be recreated when building the system
newSystem.customShader = {
shaderPath: {
fragmentElement: oldSystem.customShader.shaderPath.fragmentElement,
},
shaderOptions: {
uniforms: oldSystem.customShader.shaderOptions.uniforms.slice(),
samplers: oldSystem.customShader.shaderOptions.samplers.slice(),
defines: oldSystem.customShader.shaderOptions.defines.slice(),
},
};
}
else {
// Check if there's a custom effect set directly without customShader metadata
// This happens when using the ThinParticleSystem constructor with a customEffect parameter or when calling setCustomEffect directly
const customEffect = oldSystem.getCustomEffect(0);
if (customEffect) {
const effectName = customEffect.name;
const fragmentElement = typeof effectName === "string"
? effectName
: (effectName.fragmentElement ?? effectName.fragment);
newSystem.customShader = {
shaderPath: {
fragmentElement: fragmentElement ?? "",
},
shaderOptions: {
uniforms: customEffect._uniformsNames.slice(),
samplers: customEffect._samplerList.slice(),
defines: customEffect.defines ? customEffect.defines.split("\n").filter((d) => d.length > 0) : [],
},
};
}
}
}
function _SystemEmitRateValue(emitGradients, targetStopDuration, emitRate, newSystem, context) {
if (emitGradients && emitGradients.length > 0 && targetStopDuration > 0) {
// Create the emit gradients
context.timeToStopTimeRatioBlockGroupOutput = _CreateTimeToStopTimeRatioBlockGroup(targetStopDuration, context);
const gradientValue = _CreateGradientBlockGroup(context.timeToStopTimeRatioBlockGroupOutput, emitGradients, ParticleRandomBlockLocks.PerSystem, "Emit Rate");
// Round the value to an int
const roundBlock = new ParticleFloatToIntBlock("Round to Int");
roundBlock.operation = ParticleFloatToIntBlockOperations.Round;
gradientValue.connectTo(roundBlock.input);
roundBlock.output.connectTo(newSystem.emitRate);
}
else {
newSystem.emitRate.value = emitRate;
}
}
function _SystemTargetStopDuration(targetStopDuration, newSystem, context) {
// If something else uses the target stop duration (like a gradient),
// then the block is already created and stored in the context
if (context.targetStopDurationBlockOutput) {
context.targetStopDurationBlockOutput.connectTo(newSystem.targetStopDuration);
}
else {
// If no one used it, do not create a block just set the value
newSystem.targetStopDuration.value = targetStopDuration;
}
}
// ------------- UTILITY FUNCTIONS -------------
function _CreateDeltaModifiedInput(name, value) {
const multiplyBlock = new ParticleMathBlock("Multiply by Delta");
multiplyBlock.operation = ParticleMathBlockOperations.Multiply;
if (value instanceof Vector3) {
_CreateAndConnectInput(name, value, multiplyBlock.left);
}
else {
value.connectTo(multiplyBlock.left);
}
_CreateAndConnectSystemSource("Delta", NodeParticleSystemSources.Delta, multiplyBlock.right);
return multiplyBlock.output;
}
function _CreateAndConnectInput(inputBlockName, value, targetToConnectTo, inputType) {
const input = new ParticleInputBlock(inputBlockName, inputType);
input.value = value;
input.output.connectTo(targetToConnectTo);
}
function _CreateAndConnectContextualSource(contextualBlockName, contextSource, targetToConnectTo) {
const input = new ParticleInputBlock(contextualBlockName);
input.contextualValue = contextSource;
input.output.connectTo(targetToConnectTo);
}
function _CreateAndConnectSystemSource(systemBlockName, systemSource, targetToConnectTo) {
const input = new ParticleInputBlock(systemBlockName);
input.systemSource = systemSource;
input.output.connectTo(targetToConnectTo);
}
/**
* Creates the target stop duration input block, as it can be shared in multiple places
* This block is stored in the context so the same block is shared in the graph
* @param targetStopDuration The target stop duration value
* @param context The context of the current conversion
* @returns
*/
function _CreateTargetStopDurationInputBlock(targetStopDuration, context) {
// If we have already created the target stop duration input block, return it
if (context.targetStopDurationBlockOutput) {
return context.targetStopDurationBlockOutput;
}
// Create the target stop duration input block if not already created
const targetStopDurationInputBlock = new ParticleInputBlock("Target Stop Duration");
targetStopDurationInputBlock.value = targetStopDuration;
// Save the output in our context to avoid regenerating it again
context.targetStopDurationBlockOutput = targetStopDurationInputBlock.output;
return context.targetStopDurationBlockOutput;
}
/**
* Create a group of blocks that calculates the ratio between the actual frame and the target stop duration, clamped between 0 and 1.
* This is used to simulate the behavior of the old particle system where several particle gradient values are affected by the target stop duration.
* This block group is stored in the context so the same group is shared in the graph
* @param targetStopDuration The target stop duration value
* @param context The context of the current conversion
* @returns The ratio block output connection point
*/
function _CreateTimeToStopTimeRatioBlockGroup(targetStopDuration, context) {
// If we have already generated this group, return it
if (context.timeToStopTimeRatioBlockGroupOutput) {
return context.timeToStopTimeRatioBlockGroupOutput;
}
context.targetStopDurationBlockOutput = _CreateTargetStopDurationInputBlock(targetStopDuration, context);
// Find the ratio between the actual frame and the target stop duration
const ratio = new ParticleMathBlock("Frame/Stop Ratio");
ratio.operation = ParticleMathBlockOperations.Divide;
_CreateAndConnectSystemSource("Actual Frame", NodeParticleSystemSources.Time, ratio.left);
context.targetStopDurationBlockOutput.connectTo(ratio.right);
// Make sure values is >=0
const clampMin = new ParticleMathBlock("Clamp Min 0");
clampMin.operation = ParticleMathBlockOperations.Max;
_CreateAndConnectInput("Zero", 0, clampMin.left);
ratio.output.connectTo(clampMin.right);
// Make sure values is <=1
const clampMax = new ParticleMathBlock("Clamp Max 1");
clampMax.operation = ParticleMathBlockOperations.Min;
_CreateAndConnectInput("One", 1, clampMax.left);
clampMin.output.connectTo(clampMax.right);
// Save the group output in our context to avoid regenerating it again
context.timeToStopTimeRatioBlockGroupOutput = clampMax.output;
return context.timeToStopTimeRatioBlockGroupOutput;
}
function _CreateAgeToLifeTimeRatioBlockGroup(context) {
// If we have already generated this group, return it
if (context.ageToLifeTimeRatioBlockGroupOutput) {
return context.a