UNPKG

@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
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