From 4629a7ddae0ca63412433db17eb4fdcca0fea723 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 10 Oct 2025 19:53:17 +0300 Subject: [PATCH 01/68] Add Solid Particle System (SPS) blocks to Node Particle Editor This commit introduces several new blocks for the Solid Particle System (SPS) in the Node Particle Editor, enhancing the particle system capabilities. The following blocks have been added: - SPSMeshSourceBlock: Defines the mesh shape for SPS. - SPSCreateBlock: Creates an SPS with a base mesh. - SPSSystemBlock: Configures the Solid Particle System. - SPSInitParticleBlock: Initializes the update function for a specified range of particles. - SPSUpdatePositionBlock: Updates the position of particles. - SPSUpdateRotationBlock: Updates the rotation of particles. - SPSUpdateScalingBlock: Updates the scaling of particles. - SPSUpdateColorBlock: Updates the color of particles. - SPSUpdateVelocityBlock: Updates the velocity of particles. - SPSPhysicsBlock: Applies physics to particles. - SPSGetParticlePropertyBlock: Retrieves properties of particles. Additionally, the NodeParticleBuildState has been updated to include SPS context and delta time for physics calculations. The editor interface has been modified to accommodate these new blocks, improving the overall functionality and user experience of the particle system editor --- .../core/src/Particles/Node/Blocks/index.ts | 1 + .../src/Particles/Node/Blocks/spsBlocks.ts | 895 ++++++++++++++++++ .../nodeParticleBlockConnectionPointTypes.ts | 24 +- .../Particles/Node/nodeParticleBuildState.ts | 11 + .../Particles/Node/nodeParticleSystemSet.ts | 92 +- .../core/src/Particles/solidParticleSystem.ts | 30 + .../tools/nodeParticleEditor/public/index.js | 2 +- .../nodeParticleEditor/src/blockTools.ts | 77 ++ .../components/nodeList/nodeListComponent.tsx | 24 + .../src/components/preview/previewManager.ts | 6 + .../graphSystem/registerToDisplayLedger.ts | 14 + .../graphSystem/registerToPropertyLedger.ts | 13 + 12 files changed, 1183 insertions(+), 6 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/index.ts b/packages/dev/core/src/Particles/Node/Blocks/index.ts index 3e086086f35..73ae6a2c3a7 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/index.ts @@ -32,3 +32,4 @@ export * from "./Triggers/particleTriggerBlock"; export * from "./particleLocalVariableBlock"; export * from "./particleVectorLengthBlock"; export * from "./particleFresnelBlock"; +export * from "./spsBlocks"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts b/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts new file mode 100644 index 00000000000..db978488567 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts @@ -0,0 +1,895 @@ +import { RegisterClass } from "../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../nodeParticleBuildState"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { SolidParticle } from "core/Particles/solidParticle"; +import { Vector3 } from "core/Maths/math.vector"; +import { Color4 } from "core/Maths/math.color"; +import type { Mesh } from "core/Meshes/mesh"; +import type { Material } from "core/Materials/material"; +import { CreateBox } from "core/Meshes/Builders/boxBuilder"; +import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; +import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; +import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; + +// ============================================================================ +// SPSMeshSourceBlock - Источник меша для SPS +// ============================================================================ + +/** + * Mesh shape types for SPS + */ +export enum SPSMeshShapeType { + Box = 0, + Sphere = 1, + Cylinder = 2, + Plane = 3, + Custom = 4, +} + +/** + * Block used to provide mesh source for SPS + */ +export class SPSMeshSourceBlock extends NodeParticleBlock { + @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { + notifiers: { rebuild: true }, + embedded: true, + options: [ + { label: "Box", value: SPSMeshShapeType.Box }, + { label: "Sphere", value: SPSMeshShapeType.Sphere }, + { label: "Cylinder", value: SPSMeshShapeType.Cylinder }, + { label: "Plane", value: SPSMeshShapeType.Plane }, + { label: "Custom", value: SPSMeshShapeType.Custom }, + ], + }) + public shapeType = SPSMeshShapeType.Box; + + @editableInPropertyPage("Size", PropertyTypeForEdition.Float, "ADVANCED", { + embedded: true, + min: 0.01, + }) + public size = 1; + + @editableInPropertyPage("Segments", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + min: 1, + }) + public segments = 16; + + /** + * Create a new SPSMeshSourceBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("customMesh", NodeParticleBlockConnectionPointTypes.Mesh, true); + this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "SPSMeshSourceBlock"; + } + + /** + * Gets the customMesh input component + */ + public get customMesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the mesh output component + */ + public get mesh(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * @internal + */ + public override _build(state: NodeParticleBuildState) { + let mesh: Mesh; + + if (this.shapeType === SPSMeshShapeType.Custom) { + // Use custom mesh from input + const customMesh = this.customMesh.getConnectedValue(state) as Mesh; + if (customMesh) { + mesh = customMesh; + } else { + // Fallback to box if custom mesh not provided + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + } + } else { + // Create built-in shape + switch (this.shapeType) { + case SPSMeshShapeType.Box: + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + case SPSMeshShapeType.Sphere: + mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); + break; + case SPSMeshShapeType.Cylinder: + mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); + break; + case SPSMeshShapeType.Plane: + mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); + break; + default: + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + } + } + + this.mesh._storedValue = mesh; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.shapeType = this.shapeType; + serializationObject.size = this.size; + serializationObject.segments = this.segments; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.shapeType = serializationObject.shapeType || SPSMeshShapeType.Box; + this.size = serializationObject.size || 1; + this.segments = serializationObject.segments || 16; + } +} + +// ============================================================================ +// SPSCreateBlock - Создание SPS (аналог CreateParticleBlock) +// ============================================================================ + +/** + * Block used to create SPS with base mesh + */ +export class SPSCreateBlock extends NodeParticleBlock { + /** + * Create a new SPSCreateBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("baseMesh", NodeParticleBlockConnectionPointTypes.Mesh); + this.registerInput("particleCount", NodeParticleBlockConnectionPointTypes.Int, true, 100); + + this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "SPSCreateBlock"; + } + + /** + * Gets the baseMesh input component + */ + public get baseMesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the particleCount input component + */ + public get particleCount(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + /** + * Gets the solidParticle output component + */ + public get solidParticle(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * @internal + */ + public override _build(state: NodeParticleBuildState) { + const sps = new SolidParticleSystem(this.name, state.scene); + + const baseMesh = this.baseMesh.getConnectedValue(state) as Mesh; + if (baseMesh) { + const count = this.particleCount.getConnectedValue(state) as number; + sps.addShape(baseMesh, count); + } + + this.solidParticle._storedValue = sps; + } +} + +// ============================================================================ +// SPSSystemBlock - Настройка SPS (аналог SystemBlock) +// ============================================================================ + +/** + * Block used to configure Solid Particle System + */ +export class SPSSystemBlock extends NodeParticleBlock { + private static _IdCounter = 0; + + @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + min: 0, + max: 100000, + }) + public capacity = 1000; + + @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + }) + public billboard = false; + + /** @internal */ + public _internalId = SPSSystemBlock._IdCounter++; + + /** + * Create a new SPSSystemBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this._isSystem = true; + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); + this.registerInput("onStart", NodeParticleBlockConnectionPointTypes.System, true); + this.registerInput("onEnd", NodeParticleBlockConnectionPointTypes.System, true); + this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "SPSSystemBlock"; + } + + /** + * Gets the solidParticle input component + */ + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the material input component + */ + public get material(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + /** + * Gets the onStart input component + */ + public get onStart(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + /** + * Gets the onEnd input component + */ + public get onEnd(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + /** + * Gets the system output component + */ + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * Builds the block and return a functional SPS + * @param state defines the building state + * @returns the built SPS + */ + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { + state.capacity = this.capacity; + state.buildId = this._buildId++; + + this.build(state); + + const sps = this.solidParticle.getConnectedValue(state) as SolidParticleSystem; + + if (!sps) { + throw new Error("SPSSystemBlock: solidParticle input must be connected to SPSCreateBlock"); + } + + sps.billboard = this.billboard; + sps.name = this.name; + + const material = this.material.getConnectedValue(state) as Material; + if (material) { + sps.mesh.material = material; + } + + sps.buildMesh(); + + // Initialize particles with default positions + sps.initParticles(); + + // Start automatic updates + sps.start(); + + this.system._storedValue = this; + + this.onDisposeObservable.addOnce(() => { + sps.dispose(); + }); + + return sps; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.capacity = this.capacity; + serializationObject.billboard = this.billboard; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.capacity = serializationObject.capacity; + this.billboard = !!serializationObject.billboard; + } +} + +// ============================================================================ +// SPSInitParticleBlock - Инициализация updateParticle функции +// ============================================================================ + +/** + * Block used to initialize updateParticle function for specific particle range + */ +export class SPSInitParticleBlock extends NodeParticleBlock { + @editableInPropertyPage("Start Index", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + min: 0, + }) + public startIndex = 0; + + @editableInPropertyPage("End Index", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + min: -1, + }) + public endIndex = -1; // -1 means all particles + + /** + * Create a new SPSInitParticleBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("system", NodeParticleBlockConnectionPointTypes.System); + this.registerInput("updateFunction", NodeParticleBlockConnectionPointTypes.SolidParticle, true); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.System); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "SPSInitParticleBlock"; + } + + /** + * Gets the system input component + */ + public get system(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the updateFunction input component + */ + public get updateFunction(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + /** + * Gets the output component + */ + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * @internal + */ + public override _build(state: NodeParticleBuildState) { + const systemBlock = this.system.getConnectedValue(state) as SPSSystemBlock; + + if (!systemBlock) { + return; + } + + const sps = systemBlock.solidParticle.getConnectedValue(state) as SolidParticleSystem; + + if (!sps) { + return; + } + + // Store the old updateParticle function + const oldUpdateParticle = sps.updateParticle.bind(sps); + + // Create new updateParticle that includes this range + sps.updateParticle = (particle: SolidParticle): SolidParticle => { + // Call previous updateParticle functions + oldUpdateParticle(particle); + + const start = this.startIndex; + const end = this.endIndex === -1 ? sps.nbParticles - 1 : this.endIndex; + + // Only update particles in this range + if (particle.idx >= start && particle.idx <= end) { + state.particleContext = particle as any; + state.spsContext = sps; + + if (this.updateFunction.isConnected) { + this.updateFunction.getConnectedValue(state); + } + } + + return particle; + }; + + this.output._storedValue = systemBlock; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.startIndex = this.startIndex; + serializationObject.endIndex = this.endIndex; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.startIndex = serializationObject.startIndex || 0; + this.endIndex = serializationObject.endIndex || -1; + } +} + +// ============================================================================ +// SPSUpdatePositionBlock - Обновление позиции частицы +// ============================================================================ + +/** + * Block used to update particle position + */ +export class SPSUpdatePositionBlock extends NodeParticleBlock { + /** + * Create a new SPSUpdatePositionBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdatePositionBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newPosition = this.position.getConnectedValue(state) as Vector3; + if (newPosition) { + particle.position.copyFrom(newPosition); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSUpdateRotationBlock - Обновление вращения частицы +// ============================================================================ + +/** + * Block used to update particle rotation + */ +export class SPSUpdateRotationBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdateRotationBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newRotation = this.rotation.getConnectedValue(state) as Vector3; + if (newRotation) { + particle.rotation.copyFrom(newRotation); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSUpdateScalingBlock - Обновление масштаба частицы +// ============================================================================ + +/** + * Block used to update particle scaling + */ +export class SPSUpdateScalingBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdateScalingBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newScaling = this.scaling.getConnectedValue(state) as Vector3; + if (newScaling) { + particle.scaling.copyFrom(newScaling); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSUpdateColorBlock - Обновление цвета частицы +// ============================================================================ + +/** + * Block used to update particle color + */ +export class SPSUpdateColorBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdateColorBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newColor = this.color.getConnectedValue(state) as Color4; + if (newColor) { + if (!particle.color) { + particle.color = new Color4(1, 1, 1, 1); + } + particle.color.copyFrom(newColor); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSUpdateVelocityBlock - Обновление скорости частицы +// ============================================================================ + +/** + * Block used to update particle velocity + */ +export class SPSUpdateVelocityBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdateVelocityBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newVelocity = this.velocity.getConnectedValue(state) as Vector3; + if (newVelocity) { + particle.velocity.copyFrom(newVelocity); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSPhysicsBlock - Физика для частицы +// ============================================================================ + +/** + * Block used to apply physics to SPS particle + */ +export class SPSPhysicsBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("gravity", NodeParticleBlockConnectionPointTypes.Vector3, true, new Vector3(0, -9.81, 0)); + this.registerInput("damping", NodeParticleBlockConnectionPointTypes.Float, true, 0.99); + this.registerInput("forces", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSPhysicsBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get gravity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get damping(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get forces(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const deltaTime = (state as any).deltaTime || 0.016; + + const gravity = this.gravity.getConnectedValue(state) as Vector3; + if (gravity) { + particle.velocity.addInPlace(gravity.scale(deltaTime)); + } + + const forces = this.forces.getConnectedValue(state) as Vector3; + if (forces) { + particle.velocity.addInPlace(forces.scale(deltaTime)); + } + + const damping = this.damping.getConnectedValue(state) as number; + if (damping !== undefined && damping !== null) { + particle.velocity.scaleInPlace(damping); + } + + particle.position.addInPlace(particle.velocity.scale(deltaTime)); + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSGetParticlePropertyBlock - Получение свойств частицы +// ============================================================================ + +/** + * Block used to get particle properties (position, rotation, etc) + */ +export class SPSGetParticlePropertyBlock extends NodeParticleBlock { + @editableInPropertyPage("Property", PropertyTypeForEdition.List, "ADVANCED", { + notifiers: { rebuild: true }, + embedded: true, + options: [ + { label: "Position", value: 0 }, + { label: "Rotation", value: 1 }, + { label: "Scaling", value: 2 }, + { label: "Velocity", value: 3 }, + { label: "Index", value: 4 }, + { label: "Alive", value: 5 }, + { label: "Visible", value: 6 }, + ], + }) + public property = 0; + + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.AutoDetect); + } + + public override getClassName() { + return "SPSGetParticlePropertyBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + switch (this.property) { + case 0: // Position + this.output._storedValue = particle.position; + break; + case 1: // Rotation + this.output._storedValue = particle.rotation; + break; + case 2: // Scaling + this.output._storedValue = particle.scaling; + break; + case 3: // Velocity + this.output._storedValue = particle.velocity; + break; + case 4: // Index + this.output._storedValue = particle.idx; + break; + case 5: // Alive + this.output._storedValue = particle.alive; + break; + case 6: // Visible + this.output._storedValue = particle.isVisible; + break; + } + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.property = this.property; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.property = serializationObject.property || 0; + } +} + +// ============================================================================ +// REGISTRATION +// ============================================================================ + +RegisterClass("BABYLON.SPSMeshSourceBlock", SPSMeshSourceBlock); +RegisterClass("BABYLON.SPSCreateBlock", SPSCreateBlock); +RegisterClass("BABYLON.SPSSystemBlock", SPSSystemBlock); +RegisterClass("BABYLON.SPSInitParticleBlock", SPSInitParticleBlock); +RegisterClass("BABYLON.SPSUpdatePositionBlock", SPSUpdatePositionBlock); +RegisterClass("BABYLON.SPSUpdateRotationBlock", SPSUpdateRotationBlock); +RegisterClass("BABYLON.SPSUpdateScalingBlock", SPSUpdateScalingBlock); +RegisterClass("BABYLON.SPSUpdateColorBlock", SPSUpdateColorBlock); +RegisterClass("BABYLON.SPSUpdateVelocityBlock", SPSUpdateVelocityBlock); +RegisterClass("BABYLON.SPSPhysicsBlock", SPSPhysicsBlock); +RegisterClass("BABYLON.SPSGetParticlePropertyBlock", SPSGetParticlePropertyBlock); diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts index 87d59a00192..c9075b54c59 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts @@ -28,12 +28,28 @@ export enum NodeParticleBlockConnectionPointTypes { Color4Gradient = 0x0800, /** System */ System = 0x1000, + /** SPS - Solid Particle System */ + SPS = 0x2000, + /** SolidParticle */ + SolidParticle = 0x4000, + /** Mesh */ + Mesh = 0x8000, + /** Material */ + Material = 0x10000, + /** Camera */ + Camera = 0x20000, + /** Function */ + Function = 0x40000, + /** Vector4 */ + Vector4 = 0x80000, + /** Boolean */ + Boolean = 0x100000, /** Detect type based on connection */ - AutoDetect = 0x2000, + AutoDetect = 0x200000, /** Output type that will be defined by input type */ - BasedOnInput = 0x4000, + BasedOnInput = 0x400000, /** Undefined */ - Undefined = 0x8000, + Undefined = 0x800000, /** Bitmask of all types */ - All = 0xffff, + All = 0xffffff, } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts index fe8a2800951..0632526dcb3 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -9,6 +9,7 @@ import type { ThinParticleSystem } from "../thinParticleSystem"; import { Color4 } from "core/Maths/math.color"; import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; import type { AbstractMesh } from "core/Meshes/abstractMesh"; +import type { SolidParticleSystem } from "../solidParticleSystem"; /** * Class used to store node based geometry build state @@ -43,6 +44,16 @@ export class NodeParticleBuildState { */ public systemContext: Nullable = null; + /** + * Gets or sets the SPS context for SPS blocks + */ + public spsContext: Nullable = null; + + /** + * Gets or sets the delta time for physics calculations + */ + public deltaTime: number = 0.016; // 60 FPS default + /** * Gets or sets the index of the gradient to use */ diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 325def9bbcf..2b2fd21fc81 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -20,8 +20,20 @@ import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTelepor import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; -import type { Color4 } from "core/Maths/math.color"; import type { Nullable } from "../../types"; +import { + SPSMeshSourceBlock, + SPSCreateBlock, + SPSSystemBlock, + SPSInitParticleBlock, + SPSUpdatePositionBlock, + SPSUpdateColorBlock, + SPSPhysicsBlock, + SPSMeshShapeType, +} from "./Blocks/spsBlocks"; +import { Color4 } from "core/Maths/math.color"; +import { Vector3 } from "core/Maths/math.vector"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -336,6 +348,84 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } + /** + * Create a simple SPS example with 2 groups of particles + * - Particles 0-499: Core particles (golden color) + * - Particles 500-999: Outer particles with physics + */ + public setToDefaultSPS() { + this.clear(); + this.editorData = null; + + // ========== CREATE BASE MESH AND SPS ========== + const meshSource = new SPSMeshSourceBlock("Mesh Source"); + meshSource.shapeType = SPSMeshShapeType.Sphere; + meshSource.size = 0.1; + meshSource.segments = 8; + + const createSPS = new SPSCreateBlock("Create SPS"); + meshSource.mesh.connectTo(createSPS.baseMesh); + + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.capacity = 1000; + spsSystem.billboard = true; + createSPS.solidParticle.connectTo(spsSystem.solidParticle); + + // ========== GROUP 1: CORE (0-499) - Golden color ========== + const coreInit = new SPSInitParticleBlock("Core Init"); + coreInit.startIndex = 0; + coreInit.endIndex = 499; + spsSystem.system.connectTo(coreInit.system); + + // Set color only (positions will be from mesh) + const coreColor = new ParticleInputBlock("Core Color", NodeParticleBlockConnectionPointTypes.Color4); + coreColor.value = new Color4(1, 0.8, 0.2, 1); + + const coreUpdateColor = new SPSUpdateColorBlock("Core Update Color"); + coreColor.output.connectTo(coreUpdateColor.color); + coreInit.updateFunction.connectTo(coreUpdateColor.particle); + + // ========== GROUP 2: OUTER (500-999) - Physics ========== + const outerInit = new SPSInitParticleBlock("Outer Init"); + outerInit.startIndex = 500; + outerInit.endIndex = 999; + spsSystem.system.connectTo(outerInit.system); + + const outerPhysics = new SPSPhysicsBlock("Outer Physics"); + outerInit.updateFunction.connectTo(outerPhysics.particle); + + const gravity = new ParticleInputBlock("Gravity", NodeParticleBlockConnectionPointTypes.Vector3); + gravity.value = new Vector3(0, -1, 0); + gravity.output.connectTo(outerPhysics.gravity); + + const damping = new ParticleInputBlock("Damping", NodeParticleBlockConnectionPointTypes.Float); + damping.value = 0.99; + damping.output.connectTo(outerPhysics.damping); + + const outerColor = new ParticleInputBlock("Outer Color", NodeParticleBlockConnectionPointTypes.Color4); + outerColor.value = new Color4(0.7, 0.7, 0.7, 0.8); + + const outerUpdateColor = new SPSUpdateColorBlock("Outer Update Color"); + outerColor.output.connectTo(outerUpdateColor.color); + outerPhysics.output.connectTo(outerUpdateColor.particle); + + // Add all blocks to attachedBlocks + this.attachedBlocks.push(meshSource); + this.attachedBlocks.push(createSPS); + this.attachedBlocks.push(spsSystem); + this.attachedBlocks.push(coreInit); + this.attachedBlocks.push(coreColor); + this.attachedBlocks.push(coreUpdateColor); + this.attachedBlocks.push(outerInit); + this.attachedBlocks.push(outerPhysics); + this.attachedBlocks.push(gravity); + this.attachedBlocks.push(damping); + this.attachedBlocks.push(outerColor); + this.attachedBlocks.push(outerUpdateColor); + + this._systemBlocks.push(spsSystem as any); + } + /** * Remove a block from the current system set * @param block defines the block to remove diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index fa9a0a8a10b..5a933ce3416 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -7,6 +7,7 @@ import { Mesh } from "../Meshes/mesh"; import { CreateDisc } from "../Meshes/Builders/discBuilder"; import { EngineStore } from "../Engines/engineStore"; import type { Scene, IDisposable } from "../scene"; +import type { Observer } from "../Misc/observable"; import { DepthSortedParticle, SolidParticle, ModelShape, SolidParticleVertex } from "./solidParticle"; import type { TargetCamera } from "../Cameras/targetCamera"; import { BoundingInfo } from "../Culling/boundingInfo"; @@ -1534,10 +1535,39 @@ export class SolidParticleSystem implements IDisposable { return this; } + private _onBeforeRenderObserver: Nullable> = null; + + /** + * Starts the SPS by subscribing to the scene's before render observable + */ + public start(): void { + if (this.mesh && this.mesh.getScene()) { + // Stop any existing observer first + this.stop(); + + const scene = this.mesh.getScene(); + this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { + this.setParticles(); + }); + } + } + + /** + * Stops the SPS by unsubscribing from the scene's before render observable + */ + public stop(): void { + if (this._onBeforeRenderObserver && this.mesh && this.mesh.getScene()) { + const scene = this.mesh.getScene(); + scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + } + /** * Disposes the SPS. */ public dispose(): void { + this.stop(); this.mesh.dispose(); this.vars = null; // drop references to internal big arrays for the GC diff --git a/packages/tools/nodeParticleEditor/public/index.js b/packages/tools/nodeParticleEditor/public/index.js index c7752b2a52c..5d39a35b89f 100644 --- a/packages/tools/nodeParticleEditor/public/index.js +++ b/packages/tools/nodeParticleEditor/public/index.js @@ -223,7 +223,7 @@ checkBabylonVersionAsync().then(() => { scene = new BABYLON.Scene(engine); nodeParticleSet = new BABYLON.NodeParticleSystemSet("System set"); - nodeParticleSet.setToDefault(); + nodeParticleSet.setToDefaultSPS(); nodeParticleSet.buildAsync(scene).then(() => { showEditor(); }); diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 73877983551..67caf2bffd7 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -41,6 +41,19 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; +import { + SPSMeshSourceBlock, + SPSSystemBlock, + SPSCreateBlock, + SPSInitParticleBlock, + SPSUpdatePositionBlock, + SPSUpdateRotationBlock, + SPSUpdateScalingBlock, + SPSUpdateColorBlock, + SPSUpdateVelocityBlock, + SPSPhysicsBlock, + SPSGetParticlePropertyBlock, +} from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -149,6 +162,28 @@ export class BlockTools { return new UpdateAttractorBlock("Update attractor"); case "SystemBlock": return new SystemBlock("System"); + case "SPSMeshSourceBlock": + return new SPSMeshSourceBlock("SPS Mesh Source"); + case "SPSSystemBlock": + return new SPSSystemBlock("SPS System"); + case "SPSCreateBlock": + return new SPSCreateBlock("SPS Create"); + case "SPSInitParticleBlock": + return new SPSInitParticleBlock("SPS Init Particle"); + case "SPSUpdatePositionBlock": + return new SPSUpdatePositionBlock("SPS Update Position"); + case "SPSUpdateRotationBlock": + return new SPSUpdateRotationBlock("SPS Update Rotation"); + case "SPSUpdateScalingBlock": + return new SPSUpdateScalingBlock("SPS Update Scaling"); + case "SPSUpdateColorBlock": + return new SPSUpdateColorBlock("SPS Update Color"); + case "SPSUpdateVelocityBlock": + return new SPSUpdateVelocityBlock("SPS Update Velocity"); + case "SPSPhysicsBlock": + return new SPSPhysicsBlock("SPS Physics"); + case "SPSGetParticlePropertyBlock": + return new SPSGetParticlePropertyBlock("SPS Get Property"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": @@ -435,6 +470,24 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.System: color = "#f20a2e"; break; + case NodeParticleBlockConnectionPointTypes.SPS: + color = "#8b4513"; + break; + case NodeParticleBlockConnectionPointTypes.SolidParticle: + color = "#2e8b57"; + break; + case NodeParticleBlockConnectionPointTypes.Mesh: + color = "#4682b4"; + break; + case NodeParticleBlockConnectionPointTypes.Material: + color = "#daa520"; + break; + case NodeParticleBlockConnectionPointTypes.Camera: + color = "#9370db"; + break; + case NodeParticleBlockConnectionPointTypes.Function: + color = "#ff6347"; + break; } return color; @@ -454,6 +507,18 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Color4; case "Matrix": return NodeParticleBlockConnectionPointTypes.Matrix; + case "SPS": + return NodeParticleBlockConnectionPointTypes.SPS; + case "SolidParticle": + return NodeParticleBlockConnectionPointTypes.SolidParticle; + case "Mesh": + return NodeParticleBlockConnectionPointTypes.Mesh; + case "Material": + return NodeParticleBlockConnectionPointTypes.Material; + case "Camera": + return NodeParticleBlockConnectionPointTypes.Camera; + case "Function": + return NodeParticleBlockConnectionPointTypes.Function; } return NodeParticleBlockConnectionPointTypes.AutoDetect; @@ -473,6 +538,18 @@ export class BlockTools { return "Color4"; case NodeParticleBlockConnectionPointTypes.Matrix: return "Matrix"; + case NodeParticleBlockConnectionPointTypes.SPS: + return "SPS"; + case NodeParticleBlockConnectionPointTypes.SolidParticle: + return "SolidParticle"; + case NodeParticleBlockConnectionPointTypes.Mesh: + return "Mesh"; + case NodeParticleBlockConnectionPointTypes.Material: + return "Material"; + case NodeParticleBlockConnectionPointTypes.Camera: + return "Camera"; + case NodeParticleBlockConnectionPointTypes.Function: + return "Function"; } return ""; diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 4f96ed9a096..b6b61762429 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -23,6 +23,17 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["BasicColorUpdateBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["UpdateFlowMapBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["SystemBlock"] = SystemDisplayManager; + + // SPS Blocks + DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; + DisplayLedger.RegisteredControls["SPSInitParticleBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdatePositionBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateRotationBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateScalingBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateColorBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateVelocityBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSPhysicsBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSGetParticlePropertyBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; DisplayLedger.RegisteredControls["ParticleElbowBlock"] = ElbowDisplayManager; DisplayLedger.RegisteredControls["ParticleTeleportInBlock"] = TeleportInDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 0290e5e97d5..30e6c13e3eb 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -13,4 +13,17 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleDebugBlock"] = DebugPropertyTabComponent; PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; + + // SPS Blocks - use default GenericPropertyComponent + PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSInitParticleBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdatePositionBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateRotationBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateScalingBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateColorBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateVelocityBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSPhysicsBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSGetParticlePropertyBlock"] = GenericPropertyComponent; }; From c5d8db69440f1956391eb56e5f4b212102ce0af4 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 14 Oct 2025 16:13:59 +0300 Subject: [PATCH 02/68] Enhance Node Particle System with Solid Particle Blocks This commit introduces several new blocks for the Solid Particle System (SPS) in the Node Particle Editor, improving the functionality and flexibility of the particle system. The following blocks have been added: - SPSMeshSourceBlock: Defines the mesh shape for SPS. - SPSCreateBlock: Creates an SPS with a base mesh. - SPSSystemBlock: Configures the Solid Particle System. - SPSInitBlock: Initializes particles with configurable parameters (position, velocity, color, etc.). - SPSUpdateBlock: Generates update functions for particle properties. Additionally, the connection point types have been updated to reflect the new Solid Particle System structure, and the editor interface has been modified to accommodate these enhancements, improving the overall user experience. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 37 + .../Blocks/SolidParticle/SPSCreateBlock.ts | 72 ++ .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 110 +++ .../Blocks/SolidParticle/SPSMeshShapeType.ts | 10 + .../SolidParticle/SPSMeshSourceBlock.ts | 114 +++ .../Blocks/SolidParticle/SPSSystemBlock.ts | 271 ++++++ .../Blocks/SolidParticle/SPSUpdateBlock.ts | 119 +++ .../Node/Blocks/SolidParticle/index.ts | 8 + .../core/src/Particles/Node/Blocks/index.ts | 2 +- .../src/Particles/Node/Blocks/spsBlocks.ts | 895 ------------------ .../nodeParticleBlockConnectionPointTypes.ts | 2 +- .../src/Particles/Node/nodeParticleBlock.ts | 13 +- .../Node/nodeParticleBlockConnectionPoint.ts | 18 +- .../nodeGraphSystem/interfaces/portData.ts | 1 + .../nodeParticleEditor/src/blockTools.ts | 42 +- .../components/nodeList/nodeListComponent.tsx | 23 +- .../graphSystem/connectionPointPortData.ts | 4 + .../graphSystem/registerToDisplayLedger.ts | 9 +- .../graphSystem/registerToPropertyLedger.ts | 9 +- 19 files changed, 786 insertions(+), 973 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts delete mode 100644 packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts new file mode 100644 index 00000000000..e4aa711fcdf --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -0,0 +1,37 @@ +import { Vector3 } from "core/Maths/math.vector"; +import { Color4 } from "core/Maths/math.color"; +import type { Mesh } from "core/Meshes/mesh"; +import type { Material } from "core/Materials/material"; + +/** + * Interface for SPS init block data + */ +export interface ISPSInitData { + position?: Vector3 | (() => Vector3); + velocity?: Vector3 | (() => Vector3); + color?: Color4 | (() => Color4); + scaling?: Vector3 | (() => Vector3); + rotation?: Vector3 | (() => Vector3); +} + +/** + * Interface for SPS update block data + */ +export interface ISPSUpdateData { + position?: Vector3 | (() => Vector3); + velocity?: Vector3 | (() => Vector3); + color?: Color4 | (() => Color4); + scaling?: Vector3 | (() => Vector3); + rotation?: Vector3 | (() => Vector3); +} + +/** + * Interface for SPS create block data + */ +export interface ISPSCreateData { + mesh: Mesh; + count: number; + material?: Material; + initBlock?: ISPSInitData; + updateBlock?: ISPSUpdateData; +} diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts new file mode 100644 index 00000000000..04b34827a27 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -0,0 +1,72 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISPSCreateData } from "./ISPSData"; + +/** + * Block used to configure SPS parameters (mesh, count, initBlocks) + */ +export class SPSCreateBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 100); + this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); + this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); + this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); + + this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSCreateBlock"; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get count(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get material(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get initBlock(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get updateBlock(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const mesh = this.mesh.getConnectedValue(state); + const count = (this.count.getConnectedValue(state) as number) || 100; + const material = this.material.getConnectedValue(state); + + const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; + const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; + + const solidParticle: ISPSCreateData = { + mesh, + count, + material, + initBlock, + updateBlock, + }; + + this.solidParticle._storedValue = solidParticle; + } +} + +RegisterClass("BABYLON.SPSCreateBlock", SPSCreateBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts new file mode 100644 index 00000000000..9025cdb74e3 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -0,0 +1,110 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { Vector3 } from "core/Maths/math.vector"; +import { Color4 } from "core/Maths/math.color"; +import type { ISPSInitData } from "./ISPSData"; + +/** + * Block used to generate initialization function for SPS particles + */ +export class SPSInitBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); + + this.registerOutput("initData", NodeParticleBlockConnectionPointTypes.System); + } + + public override getClassName() { + return "SPSInitBlock"; + } + + public get initData(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public override _build(state: NodeParticleBuildState) { + const initData: ISPSInitData = {}; + + if (this.position.isConnected) { + initData.position = () => { + return this.position.getConnectedValue(state) as Vector3; + }; + } else { + initData.position = new Vector3(0, 0, 0); + } + + if (this.velocity.isConnected) { + initData.velocity = () => { + return this.velocity.getConnectedValue(state) as Vector3; + }; + } else { + initData.velocity = new Vector3(0, 0, 0); + } + + if (this.color.isConnected) { + initData.color = () => { + return this.color.getConnectedValue(state) as Color4; + }; + } else { + initData.color = new Color4(1, 1, 1, 1); + } + + if (this.scaling.isConnected) { + initData.scaling = () => { + return this.scaling.getConnectedValue(state) as Vector3; + }; + } else { + initData.scaling = new Vector3(1, 1, 1); + } + + if (this.rotation.isConnected) { + initData.rotation = () => { + return this.rotation.getConnectedValue(state) as Vector3; + }; + } else { + initData.rotation = new Vector3(0, 0, 0); + } + + this.initData._storedValue = initData; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.SPSInitBlock", SPSInitBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts new file mode 100644 index 00000000000..242d6e22f2e --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts @@ -0,0 +1,10 @@ +/** + * Mesh shape types for SPS + */ +export enum SPSMeshShapeType { + Box = 0, + Sphere = 1, + Cylinder = 2, + Plane = 3, + Custom = 4, +} diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts new file mode 100644 index 00000000000..251f051210a --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts @@ -0,0 +1,114 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { CreateBox } from "core/Meshes/Builders/boxBuilder"; +import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; +import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; +import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; +import { SPSMeshShapeType } from "./SPSMeshShapeType"; + +/** + * Block used to provide mesh source for SPS + */ +export class SPSMeshSourceBlock extends NodeParticleBlock { + @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { + notifiers: { rebuild: true }, + embedded: true, + options: [ + { label: "Box", value: SPSMeshShapeType.Box }, + { label: "Sphere", value: SPSMeshShapeType.Sphere }, + { label: "Cylinder", value: SPSMeshShapeType.Cylinder }, + { label: "Plane", value: SPSMeshShapeType.Plane }, + { label: "Custom", value: SPSMeshShapeType.Custom }, + ], + }) + public shapeType = SPSMeshShapeType.Box; + + @editableInPropertyPage("Size", PropertyTypeForEdition.Float, "ADVANCED", { + embedded: true, + min: 0.01, + }) + public size = 1; + + @editableInPropertyPage("Segments", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + min: 1, + }) + public segments = 16; + + public constructor(name: string) { + super(name); + + this.registerInput("customMesh", NodeParticleBlockConnectionPointTypes.Mesh, true); + this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + } + + public override getClassName() { + return "SPSMeshSourceBlock"; + } + + public get customMesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + let mesh; + + if (this.shapeType === SPSMeshShapeType.Custom) { + if (this.customMesh.isConnected) { + const customMesh = this.customMesh.getConnectedValue(state); + if (customMesh) { + mesh = customMesh; + } else { + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + } + } else { + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + } + } else { + switch (this.shapeType) { + case SPSMeshShapeType.Box: + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + case SPSMeshShapeType.Sphere: + mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); + break; + case SPSMeshShapeType.Cylinder: + mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); + break; + case SPSMeshShapeType.Plane: + mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); + break; + default: + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + } + } + + this.mesh._storedValue = mesh; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.shapeType = this.shapeType; + serializationObject.size = this.size; + serializationObject.segments = this.segments; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.shapeType = serializationObject.shapeType || SPSMeshShapeType.Box; + this.size = serializationObject.size || 1; + this.segments = serializationObject.segments || 16; + } +} + +RegisterClass("BABYLON.SPSMeshSourceBlock", SPSMeshSourceBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts new file mode 100644 index 00000000000..3f6b3bafe7b --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -0,0 +1,271 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { ISPSCreateData } from "./ISPSData"; + +/** + * Block used to create SolidParticleSystem and collect all Create blocks + */ +export class SPSSystemBlock extends NodeParticleBlock { + private static _IdCounter = 0; + + @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + min: 0, + max: 100000, + }) + public capacity = 1000; + + @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + }) + public billboard = false; + + public _internalId = SPSSystemBlock._IdCounter++; + + public constructor(name: string) { + super(name); + + this._isSystem = true; + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle, true, null, null, null, true); + this.registerOutput("system", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + } + + public override getClassName() { + return "SPSSystemBlock"; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { + state.capacity = this.capacity; + state.buildId = this._buildId++; + + this.build(state); + + if (!state.scene) { + throw new Error("Scene is not initialized in NodeParticleBuildState"); + } + + const sps = new SolidParticleSystem(this.name, state.scene); + sps.billboard = this.billboard; + sps.name = this.name; + + const createBlocks: ISPSCreateData[] = []; + + if (this.solidParticle.endpoints.length > 0) { + for (const endpoint of this.solidParticle.endpoints) { + const createBlock = endpoint.getConnectedValue(state); + if (createBlock) { + createBlocks.push(createBlock); + } + } + } else if (this.solidParticle.isConnected) { + const createBlock = this.solidParticle.getConnectedValue(state); + if (createBlock) { + createBlocks.push(createBlock); + } + } + + if (createBlocks.length === 0 && this.solidParticle.allowMultipleConnections && this.solidParticle._connectedPoint) { + const connectedPoint = this.solidParticle._connectedPoint; + if (connectedPoint.endpoints && connectedPoint.endpoints.length > 0) { + for (const endpoint of connectedPoint.endpoints) { + const createBlock = endpoint.getConnectedValue(state); + if (createBlock) { + createBlocks.push(createBlock); + } + } + } + } + + for (const createBlock of createBlocks) { + if (createBlock.mesh && createBlock.count) { + sps.addShape(createBlock.mesh, createBlock.count); + createBlock.mesh.dispose(); + } + } + + sps.initParticles = () => { + for (const createBlock of createBlocks) { + if (createBlock.initBlock) { + let startIndex = 0; + for (let i = 0; i < createBlocks.indexOf(createBlock); i++) { + startIndex += createBlocks[i].count; + } + const endIndex = startIndex + createBlock.count - 1; + + for (let p = startIndex; p <= endIndex && p < sps.nbParticles; p++) { + const particle = sps.particles[p]; + + if (createBlock.initBlock?.position) { + const particleContext = { + id: p, + position: particle.position, + velocity: particle.velocity, + color: particle.color, + scaling: particle.scaling, + rotation: particle.rotation, + }; + state.particleContext = particleContext as any; + + const positionValue = createBlock.initBlock.position; + if (typeof positionValue === "function") { + particle.position.copyFrom(positionValue()); + } else { + particle.position.copyFrom(positionValue); + } + } + if (createBlock.initBlock?.velocity) { + const velocityValue = createBlock.initBlock.velocity; + if (typeof velocityValue === "function") { + particle.velocity.copyFrom(velocityValue()); + } else { + particle.velocity.copyFrom(velocityValue); + } + } + if (createBlock.initBlock?.color) { + const colorValue = createBlock.initBlock.color; + if (typeof colorValue === "function") { + particle.color?.copyFrom(colorValue()); + } else { + particle.color?.copyFrom(colorValue); + } + } + if (createBlock.initBlock?.scaling) { + const scalingValue = createBlock.initBlock.scaling; + if (typeof scalingValue === "function") { + particle.scaling.copyFrom(scalingValue()); + } else { + particle.scaling.copyFrom(scalingValue); + } + } + if (createBlock.initBlock?.rotation) { + const rotationValue = createBlock.initBlock.rotation; + if (typeof rotationValue === "function") { + particle.rotation.copyFrom(rotationValue()); + } else { + particle.rotation.copyFrom(rotationValue); + } + } + } + } + } + }; + + sps.updateParticle = (particle: any) => { + let currentParticleIndex = 0; + let targetCreateBlock = null; + + for (const createBlock of createBlocks) { + if (particle.idx >= currentParticleIndex && particle.idx < currentParticleIndex + createBlock.count) { + targetCreateBlock = createBlock; + break; + } + currentParticleIndex += createBlock.count; + } + + if (targetCreateBlock && targetCreateBlock.updateBlock) { + if (targetCreateBlock.updateBlock.position) { + const positionValue = targetCreateBlock.updateBlock.position; + if (typeof positionValue === "function") { + particle.position.copyFrom(positionValue()); + } else { + particle.position.copyFrom(positionValue); + } + } + if (targetCreateBlock.updateBlock.velocity) { + const velocityValue = targetCreateBlock.updateBlock.velocity; + if (typeof velocityValue === "function") { + particle.velocity.copyFrom(velocityValue()); + } else { + particle.velocity.copyFrom(velocityValue); + } + } + if (targetCreateBlock.updateBlock.color) { + const colorValue = targetCreateBlock.updateBlock.color; + if (typeof colorValue === "function") { + particle.color?.copyFrom(colorValue()); + } else { + particle.color?.copyFrom(colorValue); + } + } + if (targetCreateBlock.updateBlock.scaling) { + const scalingValue = targetCreateBlock.updateBlock.scaling; + if (typeof scalingValue === "function") { + particle.scaling.copyFrom(scalingValue()); + } else { + particle.scaling.copyFrom(scalingValue); + } + } + if (targetCreateBlock.updateBlock.rotation) { + const rotationValue = targetCreateBlock.updateBlock.rotation; + if (typeof rotationValue === "function") { + particle.rotation.copyFrom(rotationValue()); + } else { + particle.rotation.copyFrom(rotationValue); + } + } + } + return particle; + }; + + sps.buildMesh(); + + if (sps.initParticles) { + sps.initParticles(); + } + + sps.setParticles(); + + if (state.scene) { + state.scene.onBeforeRenderObservable.add(() => { + if (sps && sps.nbParticles > 0) { + try { + sps.setParticles(); + } catch (error) { + console.error("SPSSystemBlock - error in setParticles:", error); + } + } + }); + } + + sps.start(); + + this.system._storedValue = this; + + this.onDisposeObservable.addOnce(() => { + sps.dispose(); + }); + + return sps; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.capacity = this.capacity; + serializationObject.billboard = this.billboard; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.capacity = serializationObject.capacity; + this.billboard = !!serializationObject.billboard; + } +} + +RegisterClass("BABYLON.SPSSystemBlock", SPSSystemBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts new file mode 100644 index 00000000000..c8dffe9372d --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -0,0 +1,119 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { Vector3 } from "core/Maths/math.vector"; +import { Color4 } from "core/Maths/math.color"; +import type { ISPSUpdateData } from "./ISPSData"; + +/** + * Block used to generate update function for SPS particles + */ +export class SPSUpdateBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); + + this.registerOutput("updateData", NodeParticleBlockConnectionPointTypes.System); + } + + public override getClassName() { + return "SPSUpdateBlock"; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public get updateData(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const updateData: ISPSUpdateData = {}; + + if (this.position.isConnected) { + updateData.position = () => { + const particleContext = { + id: 0, + position: new Vector3(0, 0, 0), + velocity: new Vector3(0, 0, 0), + color: new Color4(1, 1, 1, 1), + scaling: new Vector3(1, 1, 1), + rotation: new Vector3(0, 0, 0), + }; + state.particleContext = particleContext as any; + return this.position.getConnectedValue(state) as Vector3; + }; + } else { + updateData.position = undefined; + } + + if (this.velocity.isConnected) { + updateData.velocity = () => { + return this.velocity.getConnectedValue(state) as Vector3; + }; + } else { + updateData.velocity = undefined; + } + + if (this.color.isConnected) { + updateData.color = () => { + return this.color.getConnectedValue(state) as Color4; + }; + } else { + updateData.color = undefined; + } + + if (this.scaling.isConnected) { + updateData.scaling = () => { + return this.scaling.getConnectedValue(state) as Vector3; + }; + } else { + updateData.scaling = undefined; + } + + if (this.rotation.isConnected) { + updateData.rotation = () => { + return this.rotation.getConnectedValue(state) as Vector3; + }; + } else { + updateData.rotation = undefined; + } + + this.updateData._storedValue = updateData; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.SPSUpdateBlock", SPSUpdateBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts new file mode 100644 index 00000000000..85c5ccedbdf --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -0,0 +1,8 @@ +export * from "./ISPSData"; +export * from "./SPSMeshShapeType"; +export * from "./SPSMeshSourceBlock"; +export * from "./SPSCreateBlock"; +export * from "./SPSSystemBlock"; +export * from "./SPSUpdateBlock"; +export * from "./SPSInitBlock"; +export * from "./SPSRandomBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/index.ts b/packages/dev/core/src/Particles/Node/Blocks/index.ts index 73ae6a2c3a7..c691fcccbf1 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/index.ts @@ -32,4 +32,4 @@ export * from "./Triggers/particleTriggerBlock"; export * from "./particleLocalVariableBlock"; export * from "./particleVectorLengthBlock"; export * from "./particleFresnelBlock"; -export * from "./spsBlocks"; +export * from "./SolidParticle"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts b/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts deleted file mode 100644 index db978488567..00000000000 --- a/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts +++ /dev/null @@ -1,895 +0,0 @@ -import { RegisterClass } from "../../../Misc/typeStore"; -import { NodeParticleBlockConnectionPointTypes } from "../Enums/nodeParticleBlockConnectionPointTypes"; -import { NodeParticleBlock } from "../nodeParticleBlock"; -import type { NodeParticleConnectionPoint } from "../nodeParticleBlockConnectionPoint"; -import type { NodeParticleBuildState } from "../nodeParticleBuildState"; -import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; -import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; -import type { SolidParticle } from "core/Particles/solidParticle"; -import { Vector3 } from "core/Maths/math.vector"; -import { Color4 } from "core/Maths/math.color"; -import type { Mesh } from "core/Meshes/mesh"; -import type { Material } from "core/Materials/material"; -import { CreateBox } from "core/Meshes/Builders/boxBuilder"; -import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; -import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; -import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; - -// ============================================================================ -// SPSMeshSourceBlock - Источник меша для SPS -// ============================================================================ - -/** - * Mesh shape types for SPS - */ -export enum SPSMeshShapeType { - Box = 0, - Sphere = 1, - Cylinder = 2, - Plane = 3, - Custom = 4, -} - -/** - * Block used to provide mesh source for SPS - */ -export class SPSMeshSourceBlock extends NodeParticleBlock { - @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { - notifiers: { rebuild: true }, - embedded: true, - options: [ - { label: "Box", value: SPSMeshShapeType.Box }, - { label: "Sphere", value: SPSMeshShapeType.Sphere }, - { label: "Cylinder", value: SPSMeshShapeType.Cylinder }, - { label: "Plane", value: SPSMeshShapeType.Plane }, - { label: "Custom", value: SPSMeshShapeType.Custom }, - ], - }) - public shapeType = SPSMeshShapeType.Box; - - @editableInPropertyPage("Size", PropertyTypeForEdition.Float, "ADVANCED", { - embedded: true, - min: 0.01, - }) - public size = 1; - - @editableInPropertyPage("Segments", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - min: 1, - }) - public segments = 16; - - /** - * Create a new SPSMeshSourceBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this.registerInput("customMesh", NodeParticleBlockConnectionPointTypes.Mesh, true); - this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); - } - - /** - * Gets the current class name - * @returns the class name - */ - public override getClassName() { - return "SPSMeshSourceBlock"; - } - - /** - * Gets the customMesh input component - */ - public get customMesh(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the mesh output component - */ - public get mesh(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * @internal - */ - public override _build(state: NodeParticleBuildState) { - let mesh: Mesh; - - if (this.shapeType === SPSMeshShapeType.Custom) { - // Use custom mesh from input - const customMesh = this.customMesh.getConnectedValue(state) as Mesh; - if (customMesh) { - mesh = customMesh; - } else { - // Fallback to box if custom mesh not provided - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - } - } else { - // Create built-in shape - switch (this.shapeType) { - case SPSMeshShapeType.Box: - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - break; - case SPSMeshShapeType.Sphere: - mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); - break; - case SPSMeshShapeType.Cylinder: - mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); - break; - case SPSMeshShapeType.Plane: - mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); - break; - default: - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - break; - } - } - - this.mesh._storedValue = mesh; - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.shapeType = this.shapeType; - serializationObject.size = this.size; - serializationObject.segments = this.segments; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.shapeType = serializationObject.shapeType || SPSMeshShapeType.Box; - this.size = serializationObject.size || 1; - this.segments = serializationObject.segments || 16; - } -} - -// ============================================================================ -// SPSCreateBlock - Создание SPS (аналог CreateParticleBlock) -// ============================================================================ - -/** - * Block used to create SPS with base mesh - */ -export class SPSCreateBlock extends NodeParticleBlock { - /** - * Create a new SPSCreateBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this.registerInput("baseMesh", NodeParticleBlockConnectionPointTypes.Mesh); - this.registerInput("particleCount", NodeParticleBlockConnectionPointTypes.Int, true, 100); - - this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - /** - * Gets the current class name - * @returns the class name - */ - public override getClassName() { - return "SPSCreateBlock"; - } - - /** - * Gets the baseMesh input component - */ - public get baseMesh(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the particleCount input component - */ - public get particleCount(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - /** - * Gets the solidParticle output component - */ - public get solidParticle(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * @internal - */ - public override _build(state: NodeParticleBuildState) { - const sps = new SolidParticleSystem(this.name, state.scene); - - const baseMesh = this.baseMesh.getConnectedValue(state) as Mesh; - if (baseMesh) { - const count = this.particleCount.getConnectedValue(state) as number; - sps.addShape(baseMesh, count); - } - - this.solidParticle._storedValue = sps; - } -} - -// ============================================================================ -// SPSSystemBlock - Настройка SPS (аналог SystemBlock) -// ============================================================================ - -/** - * Block used to configure Solid Particle System - */ -export class SPSSystemBlock extends NodeParticleBlock { - private static _IdCounter = 0; - - @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - notifiers: { rebuild: true }, - min: 0, - max: 100000, - }) - public capacity = 1000; - - @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { - embedded: true, - notifiers: { rebuild: true }, - }) - public billboard = false; - - /** @internal */ - public _internalId = SPSSystemBlock._IdCounter++; - - /** - * Create a new SPSSystemBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this._isSystem = true; - - this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); - this.registerInput("onStart", NodeParticleBlockConnectionPointTypes.System, true); - this.registerInput("onEnd", NodeParticleBlockConnectionPointTypes.System, true); - this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System); - } - - /** - * Gets the current class name - * @returns the class name - */ - public override getClassName() { - return "SPSSystemBlock"; - } - - /** - * Gets the solidParticle input component - */ - public get solidParticle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the material input component - */ - public get material(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - /** - * Gets the onStart input component - */ - public get onStart(): NodeParticleConnectionPoint { - return this._inputs[2]; - } - - /** - * Gets the onEnd input component - */ - public get onEnd(): NodeParticleConnectionPoint { - return this._inputs[3]; - } - - /** - * Gets the system output component - */ - public get system(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * Builds the block and return a functional SPS - * @param state defines the building state - * @returns the built SPS - */ - public createSystem(state: NodeParticleBuildState): SolidParticleSystem { - state.capacity = this.capacity; - state.buildId = this._buildId++; - - this.build(state); - - const sps = this.solidParticle.getConnectedValue(state) as SolidParticleSystem; - - if (!sps) { - throw new Error("SPSSystemBlock: solidParticle input must be connected to SPSCreateBlock"); - } - - sps.billboard = this.billboard; - sps.name = this.name; - - const material = this.material.getConnectedValue(state) as Material; - if (material) { - sps.mesh.material = material; - } - - sps.buildMesh(); - - // Initialize particles with default positions - sps.initParticles(); - - // Start automatic updates - sps.start(); - - this.system._storedValue = this; - - this.onDisposeObservable.addOnce(() => { - sps.dispose(); - }); - - return sps; - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.capacity = this.capacity; - serializationObject.billboard = this.billboard; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.capacity = serializationObject.capacity; - this.billboard = !!serializationObject.billboard; - } -} - -// ============================================================================ -// SPSInitParticleBlock - Инициализация updateParticle функции -// ============================================================================ - -/** - * Block used to initialize updateParticle function for specific particle range - */ -export class SPSInitParticleBlock extends NodeParticleBlock { - @editableInPropertyPage("Start Index", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - min: 0, - }) - public startIndex = 0; - - @editableInPropertyPage("End Index", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - min: -1, - }) - public endIndex = -1; // -1 means all particles - - /** - * Create a new SPSInitParticleBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this.registerInput("system", NodeParticleBlockConnectionPointTypes.System); - this.registerInput("updateFunction", NodeParticleBlockConnectionPointTypes.SolidParticle, true); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.System); - } - - /** - * Gets the current class name - * @returns the class name - */ - public override getClassName() { - return "SPSInitParticleBlock"; - } - - /** - * Gets the system input component - */ - public get system(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the updateFunction input component - */ - public get updateFunction(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - /** - * Gets the output component - */ - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * @internal - */ - public override _build(state: NodeParticleBuildState) { - const systemBlock = this.system.getConnectedValue(state) as SPSSystemBlock; - - if (!systemBlock) { - return; - } - - const sps = systemBlock.solidParticle.getConnectedValue(state) as SolidParticleSystem; - - if (!sps) { - return; - } - - // Store the old updateParticle function - const oldUpdateParticle = sps.updateParticle.bind(sps); - - // Create new updateParticle that includes this range - sps.updateParticle = (particle: SolidParticle): SolidParticle => { - // Call previous updateParticle functions - oldUpdateParticle(particle); - - const start = this.startIndex; - const end = this.endIndex === -1 ? sps.nbParticles - 1 : this.endIndex; - - // Only update particles in this range - if (particle.idx >= start && particle.idx <= end) { - state.particleContext = particle as any; - state.spsContext = sps; - - if (this.updateFunction.isConnected) { - this.updateFunction.getConnectedValue(state); - } - } - - return particle; - }; - - this.output._storedValue = systemBlock; - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.startIndex = this.startIndex; - serializationObject.endIndex = this.endIndex; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.startIndex = serializationObject.startIndex || 0; - this.endIndex = serializationObject.endIndex || -1; - } -} - -// ============================================================================ -// SPSUpdatePositionBlock - Обновление позиции частицы -// ============================================================================ - -/** - * Block used to update particle position - */ -export class SPSUpdatePositionBlock extends NodeParticleBlock { - /** - * Create a new SPSUpdatePositionBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdatePositionBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get position(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newPosition = this.position.getConnectedValue(state) as Vector3; - if (newPosition) { - particle.position.copyFrom(newPosition); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSUpdateRotationBlock - Обновление вращения частицы -// ============================================================================ - -/** - * Block used to update particle rotation - */ -export class SPSUpdateRotationBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdateRotationBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get rotation(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newRotation = this.rotation.getConnectedValue(state) as Vector3; - if (newRotation) { - particle.rotation.copyFrom(newRotation); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSUpdateScalingBlock - Обновление масштаба частицы -// ============================================================================ - -/** - * Block used to update particle scaling - */ -export class SPSUpdateScalingBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdateScalingBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get scaling(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newScaling = this.scaling.getConnectedValue(state) as Vector3; - if (newScaling) { - particle.scaling.copyFrom(newScaling); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSUpdateColorBlock - Обновление цвета частицы -// ============================================================================ - -/** - * Block used to update particle color - */ -export class SPSUpdateColorBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdateColorBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get color(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newColor = this.color.getConnectedValue(state) as Color4; - if (newColor) { - if (!particle.color) { - particle.color = new Color4(1, 1, 1, 1); - } - particle.color.copyFrom(newColor); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSUpdateVelocityBlock - Обновление скорости частицы -// ============================================================================ - -/** - * Block used to update particle velocity - */ -export class SPSUpdateVelocityBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdateVelocityBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get velocity(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newVelocity = this.velocity.getConnectedValue(state) as Vector3; - if (newVelocity) { - particle.velocity.copyFrom(newVelocity); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSPhysicsBlock - Физика для частицы -// ============================================================================ - -/** - * Block used to apply physics to SPS particle - */ -export class SPSPhysicsBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("gravity", NodeParticleBlockConnectionPointTypes.Vector3, true, new Vector3(0, -9.81, 0)); - this.registerInput("damping", NodeParticleBlockConnectionPointTypes.Float, true, 0.99); - this.registerInput("forces", NodeParticleBlockConnectionPointTypes.Vector3, true); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSPhysicsBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get gravity(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get damping(): NodeParticleConnectionPoint { - return this._inputs[2]; - } - - public get forces(): NodeParticleConnectionPoint { - return this._inputs[3]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const deltaTime = (state as any).deltaTime || 0.016; - - const gravity = this.gravity.getConnectedValue(state) as Vector3; - if (gravity) { - particle.velocity.addInPlace(gravity.scale(deltaTime)); - } - - const forces = this.forces.getConnectedValue(state) as Vector3; - if (forces) { - particle.velocity.addInPlace(forces.scale(deltaTime)); - } - - const damping = this.damping.getConnectedValue(state) as number; - if (damping !== undefined && damping !== null) { - particle.velocity.scaleInPlace(damping); - } - - particle.position.addInPlace(particle.velocity.scale(deltaTime)); - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSGetParticlePropertyBlock - Получение свойств частицы -// ============================================================================ - -/** - * Block used to get particle properties (position, rotation, etc) - */ -export class SPSGetParticlePropertyBlock extends NodeParticleBlock { - @editableInPropertyPage("Property", PropertyTypeForEdition.List, "ADVANCED", { - notifiers: { rebuild: true }, - embedded: true, - options: [ - { label: "Position", value: 0 }, - { label: "Rotation", value: 1 }, - { label: "Scaling", value: 2 }, - { label: "Velocity", value: 3 }, - { label: "Index", value: 4 }, - { label: "Alive", value: 5 }, - { label: "Visible", value: 6 }, - ], - }) - public property = 0; - - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.AutoDetect); - } - - public override getClassName() { - return "SPSGetParticlePropertyBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - switch (this.property) { - case 0: // Position - this.output._storedValue = particle.position; - break; - case 1: // Rotation - this.output._storedValue = particle.rotation; - break; - case 2: // Scaling - this.output._storedValue = particle.scaling; - break; - case 3: // Velocity - this.output._storedValue = particle.velocity; - break; - case 4: // Index - this.output._storedValue = particle.idx; - break; - case 5: // Alive - this.output._storedValue = particle.alive; - break; - case 6: // Visible - this.output._storedValue = particle.isVisible; - break; - } - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.property = this.property; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.property = serializationObject.property || 0; - } -} - -// ============================================================================ -// REGISTRATION -// ============================================================================ - -RegisterClass("BABYLON.SPSMeshSourceBlock", SPSMeshSourceBlock); -RegisterClass("BABYLON.SPSCreateBlock", SPSCreateBlock); -RegisterClass("BABYLON.SPSSystemBlock", SPSSystemBlock); -RegisterClass("BABYLON.SPSInitParticleBlock", SPSInitParticleBlock); -RegisterClass("BABYLON.SPSUpdatePositionBlock", SPSUpdatePositionBlock); -RegisterClass("BABYLON.SPSUpdateRotationBlock", SPSUpdateRotationBlock); -RegisterClass("BABYLON.SPSUpdateScalingBlock", SPSUpdateScalingBlock); -RegisterClass("BABYLON.SPSUpdateColorBlock", SPSUpdateColorBlock); -RegisterClass("BABYLON.SPSUpdateVelocityBlock", SPSUpdateVelocityBlock); -RegisterClass("BABYLON.SPSPhysicsBlock", SPSPhysicsBlock); -RegisterClass("BABYLON.SPSGetParticlePropertyBlock", SPSGetParticlePropertyBlock); diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts index c9075b54c59..88d31930cd9 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts @@ -29,7 +29,7 @@ export enum NodeParticleBlockConnectionPointTypes { /** System */ System = 0x1000, /** SPS - Solid Particle System */ - SPS = 0x2000, + SolidParticleSystem = 0x2000, /** SolidParticle */ SolidParticle = 0x4000, /** Mesh */ diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index 719e58614b9..6b6dd4c54b3 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -218,10 +218,19 @@ export class NodeParticleBlock { * @param value value to return if there is no connection * @param valueMin min value accepted for value * @param valueMax max value accepted for value + * @param allowMultipleConnections defines if this input allows multiple connections * @returns the current block */ - public registerInput(name: string, type: NodeParticleBlockConnectionPointTypes, isOptional: boolean = false, value?: any, valueMin?: any, valueMax?: any) { - const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input); + public registerInput( + name: string, + type: NodeParticleBlockConnectionPointTypes, + isOptional: boolean = false, + value?: any, + valueMin?: any, + valueMax?: any, + allowMultipleConnections: boolean = false + ) { + const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input, allowMultipleConnections); point.type = type; point.isOptional = isOptional; point.defaultValue = value; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts index 9ca996b3840..14d7215aba2 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts @@ -118,6 +118,11 @@ export class NodeParticleConnectionPoint { */ public valueMax: Nullable = null; + /** + * Gets or sets a boolean indicating that this connection point allows multiple connections + */ + public allowMultipleConnections: boolean = false; + /** * Gets or sets the connection point type (default is float) */ @@ -239,11 +244,13 @@ export class NodeParticleConnectionPoint { * @param name defines the connection point name * @param ownerBlock defines the block hosting this connection point * @param direction defines the direction of the connection point + * @param allowMultipleConnections defines if this point allows multiple connections */ - public constructor(name: string, ownerBlock: NodeParticleBlock, direction: NodeParticleConnectionPointDirection) { + public constructor(name: string, ownerBlock: NodeParticleBlock, direction: NodeParticleConnectionPointDirection, allowMultipleConnections = false) { this._ownerBlock = ownerBlock; this.name = name; this._direction = direction; + this.allowMultipleConnections = allowMultipleConnections; } /** @@ -328,8 +335,13 @@ export class NodeParticleConnectionPoint { throw `Cannot connect these two connectors. source: "${this.ownerBlock.name}".${this.name}, target: "${connectionPoint.ownerBlock.name}".${connectionPoint.name}`; } - this._endpoints.push(connectionPoint); - connectionPoint._connectedPoint = this; + if (this.direction === NodeParticleConnectionPointDirection.Input && this.allowMultipleConnections) { + this._endpoints.push(connectionPoint); + connectionPoint._connectedPoint = this; + } else { + this._endpoints.push(connectionPoint); + connectionPoint._connectedPoint = this; + } this.onConnectionObservable.notifyObservers(connectionPoint); connectionPoint.onConnectionObservable.notifyObservers(this); diff --git a/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts b/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts index 00f285a1a83..917217c927b 100644 --- a/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts +++ b/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts @@ -54,6 +54,7 @@ export interface IPortData { hasEndpoints: boolean; endpoints: Nullable; directValueDefinition?: IPortDirectValueDefinition; + allowMultipleConnections?: boolean; updateDisplayName: (newName: string) => void; canConnectTo: (port: IPortData) => boolean; diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 67caf2bffd7..135a8a23dd6 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -41,19 +41,7 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { - SPSMeshSourceBlock, - SPSSystemBlock, - SPSCreateBlock, - SPSInitParticleBlock, - SPSUpdatePositionBlock, - SPSUpdateRotationBlock, - SPSUpdateScalingBlock, - SPSUpdateColorBlock, - SPSUpdateVelocityBlock, - SPSPhysicsBlock, - SPSGetParticlePropertyBlock, -} from "core/Particles/Node/Blocks"; +import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock } from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -168,22 +156,8 @@ export class BlockTools { return new SPSSystemBlock("SPS System"); case "SPSCreateBlock": return new SPSCreateBlock("SPS Create"); - case "SPSInitParticleBlock": - return new SPSInitParticleBlock("SPS Init Particle"); - case "SPSUpdatePositionBlock": - return new SPSUpdatePositionBlock("SPS Update Position"); - case "SPSUpdateRotationBlock": - return new SPSUpdateRotationBlock("SPS Update Rotation"); - case "SPSUpdateScalingBlock": - return new SPSUpdateScalingBlock("SPS Update Scaling"); - case "SPSUpdateColorBlock": - return new SPSUpdateColorBlock("SPS Update Color"); - case "SPSUpdateVelocityBlock": - return new SPSUpdateVelocityBlock("SPS Update Velocity"); - case "SPSPhysicsBlock": - return new SPSPhysicsBlock("SPS Physics"); - case "SPSGetParticlePropertyBlock": - return new SPSGetParticlePropertyBlock("SPS Get Property"); + case "SPSInitBlock": + return new SPSInitBlock("SPS Init"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": @@ -470,7 +444,7 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.System: color = "#f20a2e"; break; - case NodeParticleBlockConnectionPointTypes.SPS: + case NodeParticleBlockConnectionPointTypes.SolidParticleSystem: color = "#8b4513"; break; case NodeParticleBlockConnectionPointTypes.SolidParticle: @@ -507,8 +481,8 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Color4; case "Matrix": return NodeParticleBlockConnectionPointTypes.Matrix; - case "SPS": - return NodeParticleBlockConnectionPointTypes.SPS; + case "SolidParticleSystem": + return NodeParticleBlockConnectionPointTypes.SolidParticleSystem; case "SolidParticle": return NodeParticleBlockConnectionPointTypes.SolidParticle; case "Mesh": @@ -538,8 +512,8 @@ export class BlockTools { return "Color4"; case NodeParticleBlockConnectionPointTypes.Matrix: return "Matrix"; - case NodeParticleBlockConnectionPointTypes.SPS: - return "SPS"; + case NodeParticleBlockConnectionPointTypes.SolidParticleSystem: + return "SolidParticleSystem"; case NodeParticleBlockConnectionPointTypes.SolidParticle: return "SolidParticle"; case NodeParticleBlockConnectionPointTypes.Mesh: diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index b6b61762429..13d60e67985 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -26,14 +26,7 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; - DisplayLedger.RegisteredControls["SPSInitParticleBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdatePositionBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateRotationBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateScalingBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateColorBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateVelocityBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSPhysicsBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSGetParticlePropertyBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["SPSInitBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; DisplayLedger.RegisteredControls["ParticleElbowBlock"] = ElbowDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 30e6c13e3eb..6c5cdaa6cb6 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -18,12 +18,5 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSInitParticleBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdatePositionBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateRotationBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateScalingBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateColorBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateVelocityBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSPhysicsBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSGetParticlePropertyBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; }; From fc2df4b34035df2af4a84ae8cb30beb19dbb3c48 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 14 Oct 2025 16:20:28 +0300 Subject: [PATCH 03/68] Refactor Solid Particle System initialization and cleanup This commit enhances the Solid Particle System by ensuring that the mesh is built and particles are initialized before rendering. The `buildMesh` and `initParticles` methods are now called during the setup process, improving the overall functionality. Additionally, the cleanup process has been updated to properly dispose of the mesh when the system is stopped, ensuring better resource management. --- .../Blocks/SolidParticle/SPSSystemBlock.ts | 20 ----- .../Particles/Node/nodeParticleSystemSet.ts | 90 ------------------- .../core/src/Particles/solidParticleSystem.ts | 8 ++ 3 files changed, 8 insertions(+), 110 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 3f6b3bafe7b..6f7b412343d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -223,26 +223,6 @@ export class SPSSystemBlock extends NodeParticleBlock { return particle; }; - sps.buildMesh(); - - if (sps.initParticles) { - sps.initParticles(); - } - - sps.setParticles(); - - if (state.scene) { - state.scene.onBeforeRenderObservable.add(() => { - if (sps && sps.nbParticles > 0) { - try { - sps.setParticles(); - } catch (error) { - console.error("SPSSystemBlock - error in setParticles:", error); - } - } - }); - } - sps.start(); this.system._storedValue = this; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 2b2fd21fc81..a3551a90301 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -21,19 +21,7 @@ import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleport import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; -import { - SPSMeshSourceBlock, - SPSCreateBlock, - SPSSystemBlock, - SPSInitParticleBlock, - SPSUpdatePositionBlock, - SPSUpdateColorBlock, - SPSPhysicsBlock, - SPSMeshShapeType, -} from "./Blocks/spsBlocks"; import { Color4 } from "core/Maths/math.color"; -import { Vector3 } from "core/Maths/math.vector"; -import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -348,84 +336,6 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } - /** - * Create a simple SPS example with 2 groups of particles - * - Particles 0-499: Core particles (golden color) - * - Particles 500-999: Outer particles with physics - */ - public setToDefaultSPS() { - this.clear(); - this.editorData = null; - - // ========== CREATE BASE MESH AND SPS ========== - const meshSource = new SPSMeshSourceBlock("Mesh Source"); - meshSource.shapeType = SPSMeshShapeType.Sphere; - meshSource.size = 0.1; - meshSource.segments = 8; - - const createSPS = new SPSCreateBlock("Create SPS"); - meshSource.mesh.connectTo(createSPS.baseMesh); - - const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.capacity = 1000; - spsSystem.billboard = true; - createSPS.solidParticle.connectTo(spsSystem.solidParticle); - - // ========== GROUP 1: CORE (0-499) - Golden color ========== - const coreInit = new SPSInitParticleBlock("Core Init"); - coreInit.startIndex = 0; - coreInit.endIndex = 499; - spsSystem.system.connectTo(coreInit.system); - - // Set color only (positions will be from mesh) - const coreColor = new ParticleInputBlock("Core Color", NodeParticleBlockConnectionPointTypes.Color4); - coreColor.value = new Color4(1, 0.8, 0.2, 1); - - const coreUpdateColor = new SPSUpdateColorBlock("Core Update Color"); - coreColor.output.connectTo(coreUpdateColor.color); - coreInit.updateFunction.connectTo(coreUpdateColor.particle); - - // ========== GROUP 2: OUTER (500-999) - Physics ========== - const outerInit = new SPSInitParticleBlock("Outer Init"); - outerInit.startIndex = 500; - outerInit.endIndex = 999; - spsSystem.system.connectTo(outerInit.system); - - const outerPhysics = new SPSPhysicsBlock("Outer Physics"); - outerInit.updateFunction.connectTo(outerPhysics.particle); - - const gravity = new ParticleInputBlock("Gravity", NodeParticleBlockConnectionPointTypes.Vector3); - gravity.value = new Vector3(0, -1, 0); - gravity.output.connectTo(outerPhysics.gravity); - - const damping = new ParticleInputBlock("Damping", NodeParticleBlockConnectionPointTypes.Float); - damping.value = 0.99; - damping.output.connectTo(outerPhysics.damping); - - const outerColor = new ParticleInputBlock("Outer Color", NodeParticleBlockConnectionPointTypes.Color4); - outerColor.value = new Color4(0.7, 0.7, 0.7, 0.8); - - const outerUpdateColor = new SPSUpdateColorBlock("Outer Update Color"); - outerColor.output.connectTo(outerUpdateColor.color); - outerPhysics.output.connectTo(outerUpdateColor.particle); - - // Add all blocks to attachedBlocks - this.attachedBlocks.push(meshSource); - this.attachedBlocks.push(createSPS); - this.attachedBlocks.push(spsSystem); - this.attachedBlocks.push(coreInit); - this.attachedBlocks.push(coreColor); - this.attachedBlocks.push(coreUpdateColor); - this.attachedBlocks.push(outerInit); - this.attachedBlocks.push(outerPhysics); - this.attachedBlocks.push(gravity); - this.attachedBlocks.push(damping); - this.attachedBlocks.push(outerColor); - this.attachedBlocks.push(outerUpdateColor); - - this._systemBlocks.push(spsSystem as any); - } - /** * Remove a block from the current system set * @param block defines the block to remove diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index 5a933ce3416..5551fbe8217 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -1546,6 +1546,13 @@ export class SolidParticleSystem implements IDisposable { this.stop(); const scene = this.mesh.getScene(); + this.buildMesh(); + + if (this.initParticles) { + this.initParticles(); + } + + this.setParticles(); this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { this.setParticles(); }); @@ -1560,6 +1567,7 @@ export class SolidParticleSystem implements IDisposable { const scene = this.mesh.getScene(); scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); this._onBeforeRenderObserver = null; + this.mesh.dispose(); } } From 0be8117ba77f8d5c9f1ee525ad077a9f9ac3f67a Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 14 Oct 2025 16:21:27 +0300 Subject: [PATCH 04/68] Update NodeParticleEditor to use setToDefault method for NodeParticleSystemSet This commit modifies the initialization of the Node Particle System by changing the method from `setToDefaultSPS` to `setToDefault`, aligning it with the updated API and improving consistency in the codebase. --- packages/tools/nodeParticleEditor/public/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/nodeParticleEditor/public/index.js b/packages/tools/nodeParticleEditor/public/index.js index 5d39a35b89f..c7752b2a52c 100644 --- a/packages/tools/nodeParticleEditor/public/index.js +++ b/packages/tools/nodeParticleEditor/public/index.js @@ -223,7 +223,7 @@ checkBabylonVersionAsync().then(() => { scene = new BABYLON.Scene(engine); nodeParticleSet = new BABYLON.NodeParticleSystemSet("System set"); - nodeParticleSet.setToDefaultSPS(); + nodeParticleSet.setToDefault(); nodeParticleSet.buildAsync(scene).then(() => { showEditor(); }); From 6d723197e3ad419ec33b71d8598885b26543c422 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 17 Oct 2025 09:30:33 +0300 Subject: [PATCH 05/68] Enhance Solid Particle System with isStarted flag and update NodeParticleBlock This commit introduces an `isStarted` flag to the Solid Particle System to track its initialization state, preventing unnecessary operations when stopping the system. Additionally, the NodeParticleBlock has been updated to allow multiple connections for input points, improving flexibility. The SPSUpdateBlock has been integrated into various components, enhancing the Node Particle Editor's functionality and user experience. --- .../Particles/Node/Blocks/SolidParticle/index.ts | 1 - .../core/src/Particles/Node/nodeParticleBlock.ts | 3 ++- .../Node/nodeParticleBlockConnectionPoint.ts | 4 +--- .../dev/core/src/Particles/solidParticleSystem.ts | 14 +++++++++++++- .../tools/nodeParticleEditor/src/blockTools.ts | 4 +++- .../src/components/nodeList/nodeListComponent.tsx | 3 ++- .../src/graphSystem/registerToDisplayLedger.ts | 1 + .../src/graphSystem/registerToPropertyLedger.ts | 1 + 8 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 85c5ccedbdf..2b66eabf628 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -5,4 +5,3 @@ export * from "./SPSCreateBlock"; export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; export * from "./SPSInitBlock"; -export * from "./SPSRandomBlock"; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index 6b6dd4c54b3..c070fa013bc 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -230,13 +230,14 @@ export class NodeParticleBlock { valueMax?: any, allowMultipleConnections: boolean = false ) { - const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input, allowMultipleConnections); + const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input); point.type = type; point.isOptional = isOptional; point.defaultValue = value; point.value = value; point.valueMin = valueMin; point.valueMax = valueMax; + point.allowMultipleConnections = allowMultipleConnections; this._inputs.push(point); diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts index 14d7215aba2..42d9569af52 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts @@ -244,13 +244,11 @@ export class NodeParticleConnectionPoint { * @param name defines the connection point name * @param ownerBlock defines the block hosting this connection point * @param direction defines the direction of the connection point - * @param allowMultipleConnections defines if this point allows multiple connections */ - public constructor(name: string, ownerBlock: NodeParticleBlock, direction: NodeParticleConnectionPointDirection, allowMultipleConnections = false) { + public constructor(name: string, ownerBlock: NodeParticleBlock, direction: NodeParticleConnectionPointDirection) { this._ownerBlock = ownerBlock; this.name = name; this._direction = direction; - this.allowMultipleConnections = allowMultipleConnections; } /** diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index 5551fbe8217..21262babf3a 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -93,6 +93,11 @@ export class SolidParticleSystem implements IDisposable { */ public depthSortedParticles: DepthSortedParticle[]; + /** + * If the SPS has been started. + */ + public isStarted: boolean = false; + /** * If the particle intersection must be computed only with the bounding sphere (no bounding box computation, so faster). (Internal use only) * @internal @@ -1556,6 +1561,7 @@ export class SolidParticleSystem implements IDisposable { this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { this.setParticles(); }); + this.isStarted = true; } } @@ -1563,11 +1569,15 @@ export class SolidParticleSystem implements IDisposable { * Stops the SPS by unsubscribing from the scene's before render observable */ public stop(): void { + if (!this.isStarted) { + return; + } if (this._onBeforeRenderObserver && this.mesh && this.mesh.getScene()) { const scene = this.mesh.getScene(); scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); this._onBeforeRenderObserver = null; this.mesh.dispose(); + this.isStarted = false; } } @@ -1576,7 +1586,9 @@ export class SolidParticleSystem implements IDisposable { */ public dispose(): void { this.stop(); - this.mesh.dispose(); + if (this.mesh) { + this.mesh.dispose(); + } this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 135a8a23dd6..bf33c72c2d2 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -41,7 +41,7 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock } from "core/Particles/Node/Blocks"; +import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock, SPSUpdateBlock } from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -158,6 +158,8 @@ export class BlockTools { return new SPSCreateBlock("SPS Create"); case "SPSInitBlock": return new SPSInitBlock("SPS Init"); + case "SPSUpdateBlock": + return new SPSUpdateBlock("SPS Update"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 13d60e67985..9b542949ad6 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -25,6 +25,7 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; DisplayLedger.RegisteredControls["SPSInitBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; DisplayLedger.RegisteredControls["ParticleElbowBlock"] = ElbowDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 6c5cdaa6cb6..cd5287e3d42 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -19,4 +19,5 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateBlock"] = GenericPropertyComponent; }; From 5a6e5b9ede59a55f182f46e61bf2d3f6450671c9 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 17 Oct 2025 10:13:40 +0300 Subject: [PATCH 06/68] Enhance NodeParticleConnectionPoint to support multiple connections This commit adds a new internal property `_connectedPoints` to the `NodeParticleConnectionPoint` class, allowing it to maintain multiple connections. The connection and disconnection logic has been updated to handle multiple connections appropriately. Additionally, a new getter method `connectedPoints` has been introduced to retrieve the array of connected points. In the `GraphCanvasComponent`, the connection check has been refined to consider the `allowMultipleConnections` property, improving the connection management logic. --- .../Node/nodeParticleBlockConnectionPoint.ts | 22 ++++++++++++++----- .../src/nodeGraphSystem/graphCanvas.tsx | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts index 42d9569af52..4a19ca56da6 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts @@ -35,6 +35,8 @@ export class NodeParticleConnectionPoint { public _ownerBlock: NodeParticleBlock; /** @internal */ public _connectedPoint: Nullable = null; + /** @internal */ + public _connectedPoints = new Array(); /** @internal */ public _storedValue: any = null; @@ -198,6 +200,11 @@ export class NodeParticleConnectionPoint { return this._connectedPoint; } + /** Get the other side of the connection (if any) */ + public get connectedPoints(): Array { + return this._connectedPoints; + } + /** Get the block that owns this connection point */ public get ownerBlock(): NodeParticleBlock { return this._ownerBlock; @@ -333,12 +340,12 @@ export class NodeParticleConnectionPoint { throw `Cannot connect these two connectors. source: "${this.ownerBlock.name}".${this.name}, target: "${connectionPoint.ownerBlock.name}".${connectionPoint.name}`; } - if (this.direction === NodeParticleConnectionPointDirection.Input && this.allowMultipleConnections) { - this._endpoints.push(connectionPoint); - connectionPoint._connectedPoint = this; + this._endpoints.push(connectionPoint); + connectionPoint._connectedPoint = this; + if (connectionPoint.allowMultipleConnections) { + connectionPoint._connectedPoints.push(this); } else { - this._endpoints.push(connectionPoint); - connectionPoint._connectedPoint = this; + connectionPoint._connectedPoints = [this]; } this.onConnectionObservable.notifyObservers(connectionPoint); @@ -361,6 +368,11 @@ export class NodeParticleConnectionPoint { this._endpoints.splice(index, 1); endpoint._connectedPoint = null; + if (endpoint.allowMultipleConnections) { + endpoint._connectedPoints.splice(endpoint._connectedPoints.indexOf(this), 1); + } else { + endpoint._connectedPoints = []; + } this.onDisconnectionObservable.notifyObservers(endpoint); endpoint.onDisconnectionObservable.notifyObservers(this); diff --git a/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx b/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx index 158641f525f..b1f6fcbfc8b 100644 --- a/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx +++ b/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx @@ -1354,7 +1354,7 @@ export class GraphCanvasComponent extends React.Component = null; - if (pointB.isConnected) { + if (pointB.isConnected && !pointB.allowMultipleConnections) { const links = nodeB.getLinksForPortData(pointB); linksToNotifyForDispose = links.slice(); From b31248e202f97ebb114351496dde2a413d7fe9bb Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 20 Oct 2025 09:59:06 +0300 Subject: [PATCH 07/68] Refactor Solid Particle System and Node Particle Editor for improved functionality This commit enhances the Solid Particle System by streamlining the start and stop methods, ensuring proper initialization and cleanup of particles. The ISPSData interface has been updated to enforce required properties for SPS update data, while the SPSCreateBlock now defaults the particle count to 1. Additionally, the Node Particle Editor has been updated to introduce a mode selection feature, allowing users to switch between Standard and SPS modes, improving usability and flexibility in particle system management. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 24 ++--- .../Blocks/SolidParticle/SPSCreateBlock.ts | 4 +- .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 88 +++++++++++------- .../Blocks/SolidParticle/SPSUpdateBlock.ts | 89 +++++++++++-------- .../core/src/Particles/solidParticleSystem.ts | 33 +++---- .../propertyTab/propertyTabComponent.tsx | 40 +++++++++ .../nodeParticleEditor/src/globalState.ts | 2 + .../src/nodeParticleModes.ts | 9 ++ 8 files changed, 176 insertions(+), 113 deletions(-) create mode 100644 packages/tools/nodeParticleEditor/src/nodeParticleModes.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index e4aa711fcdf..7a92022a848 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -3,26 +3,15 @@ import { Color4 } from "core/Maths/math.color"; import type { Mesh } from "core/Meshes/mesh"; import type { Material } from "core/Materials/material"; -/** - * Interface for SPS init block data - */ -export interface ISPSInitData { - position?: Vector3 | (() => Vector3); - velocity?: Vector3 | (() => Vector3); - color?: Color4 | (() => Color4); - scaling?: Vector3 | (() => Vector3); - rotation?: Vector3 | (() => Vector3); -} - /** * Interface for SPS update block data */ export interface ISPSUpdateData { - position?: Vector3 | (() => Vector3); - velocity?: Vector3 | (() => Vector3); - color?: Color4 | (() => Color4); - scaling?: Vector3 | (() => Vector3); - rotation?: Vector3 | (() => Vector3); + position: () => Vector3; + velocity: () => Vector3; + color: () => Color4; + scaling: () => Vector3; + rotation: () => Vector3; } /** @@ -32,6 +21,7 @@ export interface ISPSCreateData { mesh: Mesh; count: number; material?: Material; - initBlock?: ISPSInitData; + initBlock?: ISPSUpdateData; updateBlock?: ISPSUpdateData; + shapeId?: number; } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 04b34827a27..3e7c6c2a20f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -13,7 +13,7 @@ export class SPSCreateBlock extends NodeParticleBlock { super(name); this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); - this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 100); + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); @@ -51,7 +51,7 @@ export class SPSCreateBlock extends NodeParticleBlock { public override _build(state: NodeParticleBuildState) { const mesh = this.mesh.getConnectedValue(state); - const count = (this.count.getConnectedValue(state) as number) || 100; + const count = (this.count.getConnectedValue(state) as number) || 1; const material = this.material.getConnectedValue(state); const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts index 9025cdb74e3..cecf932f50f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -5,7 +5,7 @@ import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnect import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { Vector3 } from "core/Maths/math.vector"; import { Color4 } from "core/Maths/math.color"; -import type { ISPSInitData } from "./ISPSData"; +import type { ISPSUpdateData } from "./ISPSData"; /** * Block used to generate initialization function for SPS particles @@ -52,51 +52,71 @@ export class SPSInitBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - const initData: ISPSInitData = {}; + const initData = {} as ISPSUpdateData; + initData.position = () => { + return this.getPosition(state); + }; + initData.velocity = () => { + return this.getVelocity(state); + }; + initData.color = () => { + return this.getColor(state); + }; + initData.scaling = () => { + return this.getScaling(state); + }; + initData.rotation = () => { + return this.getRotation(state); + }; + this.initData._storedValue = initData; + } + + private getPosition(state: NodeParticleBuildState) { if (this.position.isConnected) { - initData.position = () => { - return this.position.getConnectedValue(state) as Vector3; - }; - } else { - initData.position = new Vector3(0, 0, 0); + if (this.position._storedFunction) { + return this.position._storedFunction!(state); + } + return this.position.getConnectedValue(state); } - + return new Vector3(0, 0, 0); + } + private getVelocity(state: NodeParticleBuildState) { if (this.velocity.isConnected) { - initData.velocity = () => { - return this.velocity.getConnectedValue(state) as Vector3; - }; - } else { - initData.velocity = new Vector3(0, 0, 0); + if (this.velocity._storedFunction) { + return this.velocity._storedFunction!(state); + } + return this.velocity.getConnectedValue(state); } - + return new Vector3(0, 0, 0); + } + private getColor(state: NodeParticleBuildState) { if (this.color.isConnected) { - initData.color = () => { - return this.color.getConnectedValue(state) as Color4; - }; - } else { - initData.color = new Color4(1, 1, 1, 1); + if (this.color._storedFunction) { + return this.color._storedFunction!(state); + } + return this.color.getConnectedValue(state); } - + return new Color4(1, 1, 1, 1); + } + private getScaling(state: NodeParticleBuildState) { if (this.scaling.isConnected) { - initData.scaling = () => { - return this.scaling.getConnectedValue(state) as Vector3; - }; - } else { - initData.scaling = new Vector3(1, 1, 1); + if (this.scaling._storedFunction) { + return this.scaling._storedFunction!(state); + } + return this.scaling.getConnectedValue(state); } - + return new Vector3(1, 1, 1); + } + private getRotation(state: NodeParticleBuildState) { if (this.rotation.isConnected) { - initData.rotation = () => { - return this.rotation.getConnectedValue(state) as Vector3; - }; - } else { - initData.rotation = new Vector3(0, 0, 0); + if (this.rotation._storedFunction) { + return this.rotation._storedFunction!(state); + } + return this.rotation.getConnectedValue(state); } - - this.initData._storedValue = initData; + return new Vector3(0, 0, 0); } - public override serialize(): any { const serializationObject = super.serialize(); return serializationObject; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index c8dffe9372d..4183fed9c9d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -52,58 +52,73 @@ export class SPSUpdateBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - const updateData: ISPSUpdateData = {}; + const updateData: ISPSUpdateData = {} as ISPSUpdateData; + updateData.position = () => { + return this.getPosition(state); + }; + updateData.velocity = () => { + return this.getVelocity(state); + }; + updateData.color = () => { + return this.getColor(state); + }; + updateData.scaling = () => { + return this.getScaling(state); + }; + updateData.rotation = () => { + return this.getRotation(state); + }; + this.updateData._storedValue = updateData; + } + private getPosition(state: NodeParticleBuildState) { if (this.position.isConnected) { - updateData.position = () => { - const particleContext = { - id: 0, - position: new Vector3(0, 0, 0), - velocity: new Vector3(0, 0, 0), - color: new Color4(1, 1, 1, 1), - scaling: new Vector3(1, 1, 1), - rotation: new Vector3(0, 0, 0), - }; - state.particleContext = particleContext as any; - return this.position.getConnectedValue(state) as Vector3; - }; - } else { - updateData.position = undefined; + if (this.position._storedFunction) { + return this.position._storedFunction!(state); + } + return this.position.getConnectedValue(state); } + return new Vector3(0, 0, 0); + } + private getVelocity(state: NodeParticleBuildState) { if (this.velocity.isConnected) { - updateData.velocity = () => { - return this.velocity.getConnectedValue(state) as Vector3; - }; - } else { - updateData.velocity = undefined; + if (this.velocity._storedFunction) { + return this.velocity._storedFunction!(state); + } + return this.velocity.getConnectedValue(state); } + return new Vector3(0, 0, 0); + } + private getColor(state: NodeParticleBuildState) { if (this.color.isConnected) { - updateData.color = () => { - return this.color.getConnectedValue(state) as Color4; - }; - } else { - updateData.color = undefined; + if (this.color._storedFunction) { + return this.color._storedFunction!(state); + } + return this.color.getConnectedValue(state); } + return new Color4(1, 1, 1, 1); + } + private getScaling(state: NodeParticleBuildState) { if (this.scaling.isConnected) { - updateData.scaling = () => { - return this.scaling.getConnectedValue(state) as Vector3; - }; - } else { - updateData.scaling = undefined; + if (this.scaling._storedFunction) { + return this.scaling._storedFunction!(state); + } + return this.scaling.getConnectedValue(state); } + return new Vector3(1, 1, 1); + } + private getRotation(state: NodeParticleBuildState) { if (this.rotation.isConnected) { - updateData.rotation = () => { - return this.rotation.getConnectedValue(state) as Vector3; - }; - } else { - updateData.rotation = undefined; + if (this.rotation._storedFunction) { + return this.rotation._storedFunction!(state); + } + return this.rotation.getConnectedValue(state); } - - this.updateData._storedValue = updateData; + return new Vector3(0, 0, 0); } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index 21262babf3a..c798370b496 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -1546,23 +1546,14 @@ export class SolidParticleSystem implements IDisposable { * Starts the SPS by subscribing to the scene's before render observable */ public start(): void { - if (this.mesh && this.mesh.getScene()) { - // Stop any existing observer first - this.stop(); - - const scene = this.mesh.getScene(); - this.buildMesh(); - - if (this.initParticles) { - this.initParticles(); - } - + this.stop(); + this.buildMesh(); + this.initParticles(); + this.setParticles(); + this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { this.setParticles(); - this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { - this.setParticles(); - }); - this.isStarted = true; - } + }); + this.isStarted = true; } /** @@ -1572,13 +1563,9 @@ export class SolidParticleSystem implements IDisposable { if (!this.isStarted) { return; } - if (this._onBeforeRenderObserver && this.mesh && this.mesh.getScene()) { - const scene = this.mesh.getScene(); - scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - this.mesh.dispose(); - this.isStarted = false; - } + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + this.isStarted = false; } /** diff --git a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx index 7a143f2da20..4ede6c475cb 100644 --- a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx @@ -28,6 +28,8 @@ import type { LockObject } from "shared-ui-components/tabs/propertyGrids/lockObj import { TextLineComponent } from "shared-ui-components/lines/textLineComponent"; import { SliderLineComponent } from "shared-ui-components/lines/sliderLineComponent"; import { NodeParticleSystemSet } from "core/Particles"; +import { NodeParticleModes } from "../../nodeParticleModes"; +import { OptionsLine } from "shared-ui-components/lines/optionsLineComponent"; interface IPropertyTabComponentProps { globalState: GlobalState; @@ -210,6 +212,33 @@ export class PropertyTabComponent extends React.Component + this.changeMode(value as NodeParticleModes)} + /> void; customSave?: { label: string; action: (data: string) => Promise }; + mode: NodeParticleModes = NodeParticleModes.Standard; public constructor() { this.stateManager = new StateManager(); diff --git a/packages/tools/nodeParticleEditor/src/nodeParticleModes.ts b/packages/tools/nodeParticleEditor/src/nodeParticleModes.ts new file mode 100644 index 00000000000..a5c6f90ea6a --- /dev/null +++ b/packages/tools/nodeParticleEditor/src/nodeParticleModes.ts @@ -0,0 +1,9 @@ +/** + * Enum used to define the different modes for NodeParticleEditor + */ +export enum NodeParticleModes { + /** Standard particle system */ + Standard = 0, + /** SPS (Solid Particle System) */ + SPS = 1, +} From 3f7d61fb0b1c69e3e96a240f9fdb240db1c2cd06 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 20 Oct 2025 09:59:31 +0300 Subject: [PATCH 08/68] Refactor SPSSystemBlock to streamline particle initialization and update logic This commit simplifies the particle initialization and update processes within the SPSSystemBlock. The logic for creating and managing connected points has been optimized, allowing for a more efficient retrieval of connected values. Additionally, the updateParticle method has been enhanced to directly utilize shape IDs for better performance and clarity. These changes improve the overall functionality and maintainability of the Solid Particle System. --- .../Blocks/SolidParticle/SPSSystemBlock.ts | 165 +++--------------- 1 file changed, 23 insertions(+), 142 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 6f7b412343d..861cbb993ff 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -6,6 +6,7 @@ import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSCreateData } from "./ISPSData"; +import { SolidParticle } from "../../../solidParticle"; /** * Block used to create SolidParticleSystem and collect all Create blocks @@ -64,161 +65,41 @@ export class SPSSystemBlock extends NodeParticleBlock { sps.billboard = this.billboard; sps.name = this.name; - const createBlocks: ISPSCreateData[] = []; - - if (this.solidParticle.endpoints.length > 0) { - for (const endpoint of this.solidParticle.endpoints) { - const createBlock = endpoint.getConnectedValue(state); - if (createBlock) { - createBlocks.push(createBlock); - } - } - } else if (this.solidParticle.isConnected) { - const createBlock = this.solidParticle.getConnectedValue(state); - if (createBlock) { - createBlocks.push(createBlock); - } - } - - if (createBlocks.length === 0 && this.solidParticle.allowMultipleConnections && this.solidParticle._connectedPoint) { - const connectedPoint = this.solidParticle._connectedPoint; - if (connectedPoint.endpoints && connectedPoint.endpoints.length > 0) { - for (const endpoint of connectedPoint.endpoints) { - const createBlock = endpoint.getConnectedValue(state); - if (createBlock) { - createBlocks.push(createBlock); - } - } - } - } + const createBlocks: ISPSCreateData[] = this.solidParticle.connectedPoints.map((connectedPoint) => connectedPoint._storedValue); for (const createBlock of createBlocks) { if (createBlock.mesh && createBlock.count) { - sps.addShape(createBlock.mesh, createBlock.count); - createBlock.mesh.dispose(); + createBlock.shapeId = sps.addShape(createBlock.mesh, createBlock.count); + createBlock.mesh.isVisible = false; } } sps.initParticles = () => { for (const createBlock of createBlocks) { - if (createBlock.initBlock) { - let startIndex = 0; - for (let i = 0; i < createBlocks.indexOf(createBlock); i++) { - startIndex += createBlocks[i].count; - } - const endIndex = startIndex + createBlock.count - 1; - - for (let p = startIndex; p <= endIndex && p < sps.nbParticles; p++) { - const particle = sps.particles[p]; - - if (createBlock.initBlock?.position) { - const particleContext = { - id: p, - position: particle.position, - velocity: particle.velocity, - color: particle.color, - scaling: particle.scaling, - rotation: particle.rotation, - }; - state.particleContext = particleContext as any; - - const positionValue = createBlock.initBlock.position; - if (typeof positionValue === "function") { - particle.position.copyFrom(positionValue()); - } else { - particle.position.copyFrom(positionValue); - } - } - if (createBlock.initBlock?.velocity) { - const velocityValue = createBlock.initBlock.velocity; - if (typeof velocityValue === "function") { - particle.velocity.copyFrom(velocityValue()); - } else { - particle.velocity.copyFrom(velocityValue); - } - } - if (createBlock.initBlock?.color) { - const colorValue = createBlock.initBlock.color; - if (typeof colorValue === "function") { - particle.color?.copyFrom(colorValue()); - } else { - particle.color?.copyFrom(colorValue); - } - } - if (createBlock.initBlock?.scaling) { - const scalingValue = createBlock.initBlock.scaling; - if (typeof scalingValue === "function") { - particle.scaling.copyFrom(scalingValue()); - } else { - particle.scaling.copyFrom(scalingValue); - } + if (createBlock.initBlock && createBlock.shapeId !== undefined) { + const particles = sps.getParticlesByShapeId(createBlock.shapeId); + + particles.forEach((particle) => { + if (createBlock.initBlock) { + particle.position.copyFrom(createBlock.initBlock.position()); + particle.velocity.copyFrom(createBlock.initBlock.velocity()); + particle.color?.copyFrom(createBlock.initBlock.color()); + particle.scaling.copyFrom(createBlock.initBlock.scaling()); + particle.rotation.copyFrom(createBlock.initBlock.rotation()); } - if (createBlock.initBlock?.rotation) { - const rotationValue = createBlock.initBlock.rotation; - if (typeof rotationValue === "function") { - particle.rotation.copyFrom(rotationValue()); - } else { - particle.rotation.copyFrom(rotationValue); - } - } - } + }); } } }; - sps.updateParticle = (particle: any) => { - let currentParticleIndex = 0; - let targetCreateBlock = null; - - for (const createBlock of createBlocks) { - if (particle.idx >= currentParticleIndex && particle.idx < currentParticleIndex + createBlock.count) { - targetCreateBlock = createBlock; - break; - } - currentParticleIndex += createBlock.count; - } - - if (targetCreateBlock && targetCreateBlock.updateBlock) { - if (targetCreateBlock.updateBlock.position) { - const positionValue = targetCreateBlock.updateBlock.position; - if (typeof positionValue === "function") { - particle.position.copyFrom(positionValue()); - } else { - particle.position.copyFrom(positionValue); - } - } - if (targetCreateBlock.updateBlock.velocity) { - const velocityValue = targetCreateBlock.updateBlock.velocity; - if (typeof velocityValue === "function") { - particle.velocity.copyFrom(velocityValue()); - } else { - particle.velocity.copyFrom(velocityValue); - } - } - if (targetCreateBlock.updateBlock.color) { - const colorValue = targetCreateBlock.updateBlock.color; - if (typeof colorValue === "function") { - particle.color?.copyFrom(colorValue()); - } else { - particle.color?.copyFrom(colorValue); - } - } - if (targetCreateBlock.updateBlock.scaling) { - const scalingValue = targetCreateBlock.updateBlock.scaling; - if (typeof scalingValue === "function") { - particle.scaling.copyFrom(scalingValue()); - } else { - particle.scaling.copyFrom(scalingValue); - } - } - if (targetCreateBlock.updateBlock.rotation) { - const rotationValue = targetCreateBlock.updateBlock.rotation; - if (typeof rotationValue === "function") { - particle.rotation.copyFrom(rotationValue()); - } else { - particle.rotation.copyFrom(rotationValue); - } - } + sps.updateParticle = (particle: SolidParticle) => { + const createBlock = createBlocks.find((createBlock) => createBlock.shapeId === particle.shapeId); + if (createBlock && createBlock.updateBlock) { + particle.position.copyFrom(createBlock.updateBlock.position()); + particle.velocity.copyFrom(createBlock.updateBlock.velocity()); + particle.color?.copyFrom(createBlock.updateBlock.color()); + particle.scaling.copyFrom(createBlock.updateBlock.scaling()); + particle.rotation.copyFrom(createBlock.updateBlock.rotation()); } return particle; }; From 75397839404a3d570e8f1287152f8acfe7c15d44 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 20 Oct 2025 23:24:20 +0300 Subject: [PATCH 09/68] Enhance SolidParticleSystem and NodeParticleSystemSet with improved disposal and multi-connection support This commit updates the SolidParticleSystem to ensure proper disposal of the mesh when stopping the system, enhancing resource management. Additionally, the NodeParticleSystemSet has been modified to support multiple connections for input points, improving flexibility in particle system configurations. A new method, setToDefaultSPS, has been introduced to streamline the setup of Solid Particle Systems, further enhancing usability and functionality. --- .../src/Particles/Node/nodeParticleBlock.ts | 8 ++- .../Particles/Node/nodeParticleSystemSet.ts | 72 +++++++++++++++++-- .../core/src/Particles/solidParticleSystem.ts | 4 +- 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index c070fa013bc..1201e47db01 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -300,9 +300,11 @@ export class NodeParticleBlock { continue; } - const block = input.connectedPoint.ownerBlock; - if (block && block !== this && !block.isSystem) { - block.build(state); + const blocks = input.allowMultipleConnections ? input.connectedPoints.map((p) => p.ownerBlock) : [input.connectedPoint.ownerBlock]; + for (const block of blocks) { + if (block && block !== this && !block.isSystem) { + block.build(state); + } } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index a3551a90301..a55cd581085 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,6 +22,9 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; +import { SPSCreateBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock } from "./Blocks"; +import { ParticleSystem } from ".."; +import { Vector3 } from "../../Maths"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -45,7 +48,7 @@ export interface INodeParticleEditorOptions { * PG: #ZT509U#1 */ export class NodeParticleSystemSet { - private _systemBlocks: SystemBlock[] = []; + private _systemBlocks: (SystemBlock | SPSSystemBlock)[] = []; private _buildId: number = 0; /** Define the Url to load node editor script */ @@ -90,7 +93,7 @@ export class NodeParticleSystemSet { /** * Gets the system blocks */ - public get systemBlocks(): SystemBlock[] { + public get systemBlocks(): (SystemBlock | SPSSystemBlock)[] { return this._systemBlocks; } @@ -269,13 +272,13 @@ export class NodeParticleSystemSet { state.verbose = verbose; const system = block.createSystem(state); - system._source = this; - system._blockReference = block._internalId; - + if (system instanceof ParticleSystem) { + system._source = this; + system._blockReference = block._internalId; + output.systems.push(system); + } // Errors state.emitErrors(); - - output.systems.push(system); } this.onBuildObservable.notifyObservers(this); @@ -336,6 +339,61 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } + public setToDefaultSPS() { + this.clear(); + this.editorData = null; + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.capacity = 1000; + spsSystem.billboard = false; + + const spsCreateBox = new SPSCreateBlock("Create Box Particles"); + const spsCreateSphere = new SPSCreateBlock("Create Sphere Particles"); + + spsCreateBox.count.value = 5; + spsCreateSphere.count.value = 1; + + spsCreateBox.solidParticle.connectTo(spsSystem.solidParticle); + spsCreateSphere.solidParticle.connectTo(spsSystem.solidParticle); + + const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); + const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); + + meshSourceBox.shapeType = SPSMeshShapeType.Box; + meshSourceSphere.shapeType = SPSMeshShapeType.Sphere; + + meshSourceBox.size = 1; + meshSourceSphere.size = 1; + + meshSourceBox.mesh.connectTo(spsCreateBox.mesh); + meshSourceSphere.mesh.connectTo(spsCreateSphere.mesh); + + const spsInitBox = new SPSInitBlock("Initialize Box Particles"); + const spsInitSphere = new SPSInitBlock("Initialize Sphere Particles"); + + spsInitBox.initData.connectTo(spsCreateBox.initBlock); + spsInitSphere.initData.connectTo(spsCreateSphere.initBlock); + + const positionBlockBox = new ParticleInputBlock("Position"); + positionBlockBox.value = new Vector3(1, 1, 1); + positionBlockBox.output.connectTo(spsInitBox.position); + + const rotationBlockBox = new ParticleInputBlock("Rotation"); + rotationBlockBox.value = new Vector3(3, 0, 0); + rotationBlockBox.output.connectTo(spsInitBox.rotation); + + const positionBlockSphere = new ParticleInputBlock("Position"); + positionBlockSphere.value = new Vector3(0, 0, 0); + positionBlockSphere.output.connectTo(spsInitSphere.position); + + const spsUpdateBox = new SPSUpdateBlock("Update Box Particles"); + const spsUpdateSphere = new SPSUpdateBlock("Update Sphere Particles"); + + spsUpdateBox.updateData.connectTo(spsCreateBox.updateBlock); + spsUpdateSphere.updateData.connectTo(spsCreateSphere.updateBlock); + + this._systemBlocks.push(spsSystem); + } + /** * Remove a block from the current system set * @param block defines the block to remove diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index c798370b496..aa78047ffd5 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -1565,6 +1565,7 @@ export class SolidParticleSystem implements IDisposable { } this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); this._onBeforeRenderObserver = null; + this.mesh.dispose(); this.isStarted = false; } @@ -1573,9 +1574,6 @@ export class SolidParticleSystem implements IDisposable { */ public dispose(): void { this.stop(); - if (this.mesh) { - this.mesh.dispose(); - } this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; From 4ff96c9b1374b27ed463b72cf1a8a8ca17a7da96 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 21 Oct 2025 11:24:55 +0300 Subject: [PATCH 10/68] Refactor NodeParticleBlock and NodeParticleConnectionPoint to simplify connection logic This commit streamlines the connection management within the NodeParticleBlock and NodeParticleConnectionPoint classes by removing the support for multiple connections. The `registerInput` method has been simplified, and the logic for handling connected points has been optimized. Additionally, the GraphCanvasComponent has been updated to reflect these changes, enhancing the overall clarity and maintainability of the codebase. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 10 +++++ .../Blocks/SolidParticle/SPSSystemBlock.ts | 42 ++++++++++++++++++- .../src/Particles/Node/nodeParticleBlock.ts | 20 ++------- .../Node/nodeParticleBlockConnectionPoint.ts | 22 ---------- .../Particles/Node/nodeParticleSystemSet.ts | 1 + .../src/nodeGraphSystem/graphCanvas.tsx | 2 +- .../graphSystem/connectionPointPortData.ts | 4 -- 7 files changed, 57 insertions(+), 44 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 3e7c6c2a20f..15c331da71e 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -4,6 +4,7 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import type { ISPSCreateData } from "./ISPSData"; +import { SPSSystemBlock } from "./SPSSystemBlock"; /** * Block used to configure SPS parameters (mesh, count, initBlocks) @@ -66,6 +67,15 @@ export class SPSCreateBlock extends NodeParticleBlock { }; this.solidParticle._storedValue = solidParticle; + + // If connected to SPSSystemBlock, add this create block to its particle sources + if (this.solidParticle.isConnected && this.solidParticle.connectedPoint?.ownerBlock instanceof SPSSystemBlock) { + const systemBlock = this.solidParticle.connectedPoint.ownerBlock as SPSSystemBlock; + // Remove existing source if it exists + systemBlock.removeParticleSource(solidParticle); + // Add the new source + systemBlock.addParticleSource(solidParticle); + } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 861cbb993ff..32ce4767290 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -7,6 +7,7 @@ import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSCreateData } from "./ISPSData"; import { SolidParticle } from "../../../solidParticle"; +import { Observable } from "../../../../Misc/observable"; /** * Block used to create SolidParticleSystem and collect all Create blocks @@ -30,6 +31,14 @@ export class SPSSystemBlock extends NodeParticleBlock { public _internalId = SPSSystemBlock._IdCounter++; + /** + * Gets or sets the list of particle sources + */ + public particleSources: ISPSCreateData[] = []; + + /** Gets an observable raised when the particle sources are changed */ + public onParticleSourcesChangedObservable = new Observable(); + public constructor(name: string) { super(name); @@ -51,6 +60,35 @@ export class SPSSystemBlock extends NodeParticleBlock { return this._outputs[0]; } + /** + * Add a particle source to the system + * @param source The particle source data + */ + public addParticleSource(source: ISPSCreateData): void { + this.particleSources.push(source); + this.onParticleSourcesChangedObservable.notifyObservers(this); + } + + /** + * Remove a particle source from the system + * @param source The particle source data to remove + */ + public removeParticleSource(source: ISPSCreateData): void { + const index = this.particleSources.indexOf(source); + if (index !== -1) { + this.particleSources.splice(index, 1); + this.onParticleSourcesChangedObservable.notifyObservers(this); + } + } + + /** + * Clear all particle sources + */ + public clearParticleSources(): void { + this.particleSources.length = 0; + this.onParticleSourcesChangedObservable.notifyObservers(this); + } + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { state.capacity = this.capacity; state.buildId = this._buildId++; @@ -65,7 +103,7 @@ export class SPSSystemBlock extends NodeParticleBlock { sps.billboard = this.billboard; sps.name = this.name; - const createBlocks: ISPSCreateData[] = this.solidParticle.connectedPoints.map((connectedPoint) => connectedPoint._storedValue); + const createBlocks: ISPSCreateData[] = this.particleSources; for (const createBlock of createBlocks) { if (createBlock.mesh && createBlock.count) { @@ -119,6 +157,7 @@ export class SPSSystemBlock extends NodeParticleBlock { const serializationObject = super.serialize(); serializationObject.capacity = this.capacity; serializationObject.billboard = this.billboard; + serializationObject.particleSources = this.particleSources; return serializationObject; } @@ -126,6 +165,7 @@ export class SPSSystemBlock extends NodeParticleBlock { super._deserialize(serializationObject); this.capacity = serializationObject.capacity; this.billboard = !!serializationObject.billboard; + this.particleSources = serializationObject.particleSources || []; } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index 1201e47db01..719e58614b9 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -218,18 +218,9 @@ export class NodeParticleBlock { * @param value value to return if there is no connection * @param valueMin min value accepted for value * @param valueMax max value accepted for value - * @param allowMultipleConnections defines if this input allows multiple connections * @returns the current block */ - public registerInput( - name: string, - type: NodeParticleBlockConnectionPointTypes, - isOptional: boolean = false, - value?: any, - valueMin?: any, - valueMax?: any, - allowMultipleConnections: boolean = false - ) { + public registerInput(name: string, type: NodeParticleBlockConnectionPointTypes, isOptional: boolean = false, value?: any, valueMin?: any, valueMax?: any) { const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input); point.type = type; point.isOptional = isOptional; @@ -237,7 +228,6 @@ export class NodeParticleBlock { point.value = value; point.valueMin = valueMin; point.valueMax = valueMax; - point.allowMultipleConnections = allowMultipleConnections; this._inputs.push(point); @@ -300,11 +290,9 @@ export class NodeParticleBlock { continue; } - const blocks = input.allowMultipleConnections ? input.connectedPoints.map((p) => p.ownerBlock) : [input.connectedPoint.ownerBlock]; - for (const block of blocks) { - if (block && block !== this && !block.isSystem) { - block.build(state); - } + const block = input.connectedPoint.ownerBlock; + if (block && block !== this && !block.isSystem) { + block.build(state); } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts index 4a19ca56da6..9ca996b3840 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts @@ -35,8 +35,6 @@ export class NodeParticleConnectionPoint { public _ownerBlock: NodeParticleBlock; /** @internal */ public _connectedPoint: Nullable = null; - /** @internal */ - public _connectedPoints = new Array(); /** @internal */ public _storedValue: any = null; @@ -120,11 +118,6 @@ export class NodeParticleConnectionPoint { */ public valueMax: Nullable = null; - /** - * Gets or sets a boolean indicating that this connection point allows multiple connections - */ - public allowMultipleConnections: boolean = false; - /** * Gets or sets the connection point type (default is float) */ @@ -200,11 +193,6 @@ export class NodeParticleConnectionPoint { return this._connectedPoint; } - /** Get the other side of the connection (if any) */ - public get connectedPoints(): Array { - return this._connectedPoints; - } - /** Get the block that owns this connection point */ public get ownerBlock(): NodeParticleBlock { return this._ownerBlock; @@ -342,11 +330,6 @@ export class NodeParticleConnectionPoint { this._endpoints.push(connectionPoint); connectionPoint._connectedPoint = this; - if (connectionPoint.allowMultipleConnections) { - connectionPoint._connectedPoints.push(this); - } else { - connectionPoint._connectedPoints = [this]; - } this.onConnectionObservable.notifyObservers(connectionPoint); connectionPoint.onConnectionObservable.notifyObservers(this); @@ -368,11 +351,6 @@ export class NodeParticleConnectionPoint { this._endpoints.splice(index, 1); endpoint._connectedPoint = null; - if (endpoint.allowMultipleConnections) { - endpoint._connectedPoints.splice(endpoint._connectedPoints.indexOf(this), 1); - } else { - endpoint._connectedPoints = []; - } this.onDisconnectionObservable.notifyObservers(endpoint); endpoint.onDisconnectionObservable.notifyObservers(this); diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index a55cd581085..cb9ec3e5a1a 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -352,6 +352,7 @@ export class NodeParticleSystemSet { spsCreateBox.count.value = 5; spsCreateSphere.count.value = 1; + // Connect create blocks to system (this will automatically add them to particleSources) spsCreateBox.solidParticle.connectTo(spsSystem.solidParticle); spsCreateSphere.solidParticle.connectTo(spsSystem.solidParticle); diff --git a/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx b/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx index b1f6fcbfc8b..158641f525f 100644 --- a/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx +++ b/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx @@ -1354,7 +1354,7 @@ export class GraphCanvasComponent extends React.Component = null; - if (pointB.isConnected && !pointB.allowMultipleConnections) { + if (pointB.isConnected) { const links = nodeB.getLinksForPortData(pointB); linksToNotifyForDispose = links.slice(); diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/connectionPointPortData.ts b/packages/tools/nodeParticleEditor/src/graphSystem/connectionPointPortData.ts index bb33a252da0..aba22908d37 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/connectionPointPortData.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/connectionPointPortData.ts @@ -143,10 +143,6 @@ export class ConnectionPointPortData implements IPortData { return endpoints; } - public get allowMultipleConnections() { - return this.data.allowMultipleConnections; - } - public constructor(connectionPoint: NodeParticleConnectionPoint, nodeContainer: INodeContainer) { this.data = connectionPoint; this._nodeContainer = nodeContainer; From 2d7e11c8263624140663dcb5c1e9156fe009d5d1 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 21 Oct 2025 15:50:18 +0300 Subject: [PATCH 11/68] Add unregisterInput method to NodeParticleBlock for dynamic input management This commit introduces the `unregisterInput` method to the NodeParticleBlock class, allowing for the dynamic removal of input connections. The method handles disconnection and notifies observers of input changes. Additionally, it removes commented-out code from the SPSCreateBlock and refines the input management logic in the SPSSystemBlock, enhancing overall clarity and maintainability of the particle system components. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 10 -- .../Blocks/SolidParticle/SPSSystemBlock.ts | 111 ++++++++++++------ .../src/Particles/Node/nodeParticleBlock.ts | 22 ++++ .../Particles/Node/nodeParticleSystemSet.ts | 1 - 4 files changed, 94 insertions(+), 50 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 15c331da71e..3e7c6c2a20f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -4,7 +4,6 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import type { ISPSCreateData } from "./ISPSData"; -import { SPSSystemBlock } from "./SPSSystemBlock"; /** * Block used to configure SPS parameters (mesh, count, initBlocks) @@ -67,15 +66,6 @@ export class SPSCreateBlock extends NodeParticleBlock { }; this.solidParticle._storedValue = solidParticle; - - // If connected to SPSSystemBlock, add this create block to its particle sources - if (this.solidParticle.isConnected && this.solidParticle.connectedPoint?.ownerBlock instanceof SPSSystemBlock) { - const systemBlock = this.solidParticle.connectedPoint.ownerBlock as SPSSystemBlock; - // Remove existing source if it exists - systemBlock.removeParticleSource(solidParticle); - // Add the new source - systemBlock.addParticleSource(solidParticle); - } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 32ce4767290..90acf510db1 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -7,13 +7,15 @@ import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSCreateData } from "./ISPSData"; import { SolidParticle } from "../../../solidParticle"; -import { Observable } from "../../../../Misc/observable"; +import { Observer } from "../../../../Misc"; /** * Block used to create SolidParticleSystem and collect all Create blocks */ export class SPSSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; + private _connectionObservers = new Map>(); + private _disconnectionObservers = new Map>(); @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { embedded: true, @@ -31,62 +33,82 @@ export class SPSSystemBlock extends NodeParticleBlock { public _internalId = SPSSystemBlock._IdCounter++; - /** - * Gets or sets the list of particle sources - */ - public particleSources: ISPSCreateData[] = []; - - /** Gets an observable raised when the particle sources are changed */ - public onParticleSourcesChangedObservable = new Observable(); - public constructor(name: string) { super(name); this._isSystem = true; - this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle, true, null, null, null, true); + this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); this.registerOutput("system", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + + this._manageExtendedInputs(0); } public override getClassName() { return "SPSSystemBlock"; } - public get solidParticle(): NodeParticleConnectionPoint { - return this._inputs[0]; + private _entryCount = 1; + + private _extend() { + this._entryCount++; + this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle, true); + this._manageExtendedInputs(this._entryCount - 1); } - public get system(): NodeParticleConnectionPoint { - return this._outputs[0]; + private _shrink() { + if (this._entryCount > 1) { + this._unmanageExtendedInputs(this._entryCount - 1); + this._entryCount--; + this.unregisterInput(`solidParticle-${this._entryCount}`); + } } - /** - * Add a particle source to the system - * @param source The particle source data - */ - public addParticleSource(source: ISPSCreateData): void { - this.particleSources.push(source); - this.onParticleSourcesChangedObservable.notifyObservers(this); + private _manageExtendedInputs(index: number) { + const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { + console.log("connectionObserver solidParticle", index); + console.log(" connectionObserver this._entryCount", this._entryCount); + if (this._entryCount - 1 > index) { + return; + } + this._extend(); + }); + + const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { + console.log("solidParticle", index); + console.log("this._entryCount", this._entryCount); + if (this._entryCount - 1 > index) { + return; + } + this._shrink(); + }); + + // Store observers for later removal + this._connectionObservers.set(index, connectionObserver); + this._disconnectionObservers.set(index, disconnectionObserver); } - /** - * Remove a particle source from the system - * @param source The particle source data to remove - */ - public removeParticleSource(source: ISPSCreateData): void { - const index = this.particleSources.indexOf(source); - if (index !== -1) { - this.particleSources.splice(index, 1); - this.onParticleSourcesChangedObservable.notifyObservers(this); + private _unmanageExtendedInputs(index: number) { + const connectionObserver = this._connectionObservers.get(index); + const disconnectionObserver = this._disconnectionObservers.get(index); + + if (connectionObserver) { + this._inputs[index].onConnectionObservable.remove(connectionObserver); + this._connectionObservers.delete(index); + } + + if (disconnectionObserver) { + this._inputs[index].onDisconnectionObservable.remove(disconnectionObserver); + this._disconnectionObservers.delete(index); } } - /** - * Clear all particle sources - */ - public clearParticleSources(): void { - this.particleSources.length = 0; - this.onParticleSourcesChangedObservable.notifyObservers(this); + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[this._entryCount - 1]; + } + + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; } public createSystem(state: NodeParticleBuildState): SolidParticleSystem { @@ -103,7 +125,13 @@ export class SPSSystemBlock extends NodeParticleBlock { sps.billboard = this.billboard; sps.name = this.name; - const createBlocks: ISPSCreateData[] = this.particleSources; + // Collect data from all connected solidParticle inputs + const createBlocks: ISPSCreateData[] = []; + for (let i = 0; i < this._inputs.length; i++) { + if (this._inputs[i].isConnected && this._inputs[i]._storedValue) { + createBlocks.push(this._inputs[i]._storedValue); + } + } for (const createBlock of createBlocks) { if (createBlock.mesh && createBlock.count) { @@ -157,7 +185,7 @@ export class SPSSystemBlock extends NodeParticleBlock { const serializationObject = super.serialize(); serializationObject.capacity = this.capacity; serializationObject.billboard = this.billboard; - serializationObject.particleSources = this.particleSources; + serializationObject._entryCount = this._entryCount; return serializationObject; } @@ -165,7 +193,12 @@ export class SPSSystemBlock extends NodeParticleBlock { super._deserialize(serializationObject); this.capacity = serializationObject.capacity; this.billboard = !!serializationObject.billboard; - this.particleSources = serializationObject.particleSources || []; + + if (serializationObject._entryCount && serializationObject._entryCount > 1) { + for (let i = 1; i < serializationObject._entryCount; i++) { + this._extend(); + } + } } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index 719e58614b9..d4d24c45b8b 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -236,6 +236,28 @@ export class NodeParticleBlock { return this; } + /** + * Unregister an input. Used for dynamic input management + * @param name defines the connection point name to remove + * @returns the current block + */ + public unregisterInput(name: string) { + const index = this._inputs.findIndex((input) => input.name === name); + if (index !== -1) { + const point = this._inputs[index]; + + if (point.isConnected) { + point.disconnectFrom(point.connectedPoint!); + } + + this._inputs.splice(index, 1); + + this.onInputChangedObservable.notifyObservers(point); + } + + return this; + } + /** * Register a new output. Must be called inside a block constructor * @param name defines the connection point name diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index cb9ec3e5a1a..a55cd581085 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -352,7 +352,6 @@ export class NodeParticleSystemSet { spsCreateBox.count.value = 5; spsCreateSphere.count.value = 1; - // Connect create blocks to system (this will automatically add them to particleSources) spsCreateBox.solidParticle.connectTo(spsSystem.solidParticle); spsCreateSphere.solidParticle.connectTo(spsSystem.solidParticle); From 396662bc02280f9620fe339c9c4ab0f6ebe5cc82 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 21 Oct 2025 15:52:13 +0300 Subject: [PATCH 12/68] Remove optional `allowMultipleConnections` property from IPortData interface to simplify connection management. This change aligns with recent refactoring efforts to streamline connection logic across the node graph system. --- .../src/nodeGraphSystem/interfaces/portData.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts b/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts index 917217c927b..00f285a1a83 100644 --- a/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts +++ b/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts @@ -54,7 +54,6 @@ export interface IPortData { hasEndpoints: boolean; endpoints: Nullable; directValueDefinition?: IPortDirectValueDefinition; - allowMultipleConnections?: boolean; updateDisplayName: (newName: string) => void; canConnectTo: (port: IPortData) => boolean; From 72c885b68c48976c89cdc799ddd19b8827be67c2 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 28 Oct 2025 12:43:59 +0300 Subject: [PATCH 13/68] Refactor SolidParticleSystem and related blocks for improved initialization and update logic This commit removes the `isStarted` flag from the SolidParticleSystem, streamlining the start and stop methods. The ISPSUpdateData interface has been updated to make properties optional, enhancing flexibility in particle updates. Additionally, the SPSInitBlock and SPSUpdateBlock have been refactored to only assign update functions when the corresponding properties are connected, optimizing performance. The SPSSystemBlock has been improved to manage the SolidParticleSystem lifecycle more effectively, ensuring proper disposal and initialization of particles. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 11 +- .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 92 ++++++----- .../SolidParticle/SPSMeshSourceBlock.ts | 38 +++-- .../Blocks/SolidParticle/SPSSystemBlock.ts | 144 ++++++++++++------ .../Blocks/SolidParticle/SPSUpdateBlock.ts | 85 +++++------ .../core/src/Particles/solidParticleSystem.ts | 37 +---- 6 files changed, 214 insertions(+), 193 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index 7a92022a848..0119aab9b89 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -7,11 +7,11 @@ import type { Material } from "core/Materials/material"; * Interface for SPS update block data */ export interface ISPSUpdateData { - position: () => Vector3; - velocity: () => Vector3; - color: () => Color4; - scaling: () => Vector3; - rotation: () => Vector3; + position?: () => Vector3; + velocity?: () => Vector3; + color?: () => Color4; + scaling?: () => Vector3; + rotation?: () => Vector3; } /** @@ -23,5 +23,4 @@ export interface ISPSCreateData { material?: Material; initBlock?: ISPSUpdateData; updateBlock?: ISPSUpdateData; - shapeId?: number; } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts index cecf932f50f..df424f3d5b3 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -3,8 +3,6 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import { Vector3 } from "core/Maths/math.vector"; -import { Color4 } from "core/Maths/math.color"; import type { ISPSUpdateData } from "./ISPSData"; /** @@ -53,70 +51,70 @@ export class SPSInitBlock extends NodeParticleBlock { public override _build(state: NodeParticleBuildState) { const initData = {} as ISPSUpdateData; - initData.position = () => { - return this.getPosition(state); - }; - initData.velocity = () => { - return this.getVelocity(state); - }; - initData.color = () => { - return this.getColor(state); - }; - initData.scaling = () => { - return this.getScaling(state); - }; - initData.rotation = () => { - return this.getRotation(state); - }; + if (this.position.isConnected) { + initData.position = () => { + return this.getPosition(state); + }; + } + if (this.velocity.isConnected) { + initData.velocity = () => { + return this.getVelocity(state); + }; + } + if (this.color.isConnected) { + initData.color = () => { + return this.getColor(state); + }; + } + if (this.scaling.isConnected) { + initData.scaling = () => { + return this.getScaling(state); + }; + } + if (this.rotation.isConnected) { + initData.rotation = () => { + return this.getRotation(state); + }; + } this.initData._storedValue = initData; } private getPosition(state: NodeParticleBuildState) { - if (this.position.isConnected) { - if (this.position._storedFunction) { - return this.position._storedFunction!(state); - } - return this.position.getConnectedValue(state); + if (this.position._storedFunction) { + return this.position._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.position.getConnectedValue(state); } + private getVelocity(state: NodeParticleBuildState) { - if (this.velocity.isConnected) { - if (this.velocity._storedFunction) { - return this.velocity._storedFunction!(state); - } - return this.velocity.getConnectedValue(state); + if (this.velocity._storedFunction) { + return this.velocity._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.velocity.getConnectedValue(state); } + private getColor(state: NodeParticleBuildState) { - if (this.color.isConnected) { - if (this.color._storedFunction) { - return this.color._storedFunction!(state); - } - return this.color.getConnectedValue(state); + if (this.color._storedFunction) { + return this.color._storedFunction(state); } - return new Color4(1, 1, 1, 1); + return this.color.getConnectedValue(state); } + private getScaling(state: NodeParticleBuildState) { - if (this.scaling.isConnected) { - if (this.scaling._storedFunction) { - return this.scaling._storedFunction!(state); - } - return this.scaling.getConnectedValue(state); + if (this.scaling._storedFunction) { + return this.scaling._storedFunction(state); } - return new Vector3(1, 1, 1); + return this.scaling.getConnectedValue(state); } + private getRotation(state: NodeParticleBuildState) { - if (this.rotation.isConnected) { - if (this.rotation._storedFunction) { - return this.rotation._storedFunction!(state); - } - return this.rotation.getConnectedValue(state); + if (this.rotation._storedFunction) { + return this.rotation._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.rotation.getConnectedValue(state); } + public override serialize(): any { const serializationObject = super.serialize(); return serializationObject; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts index 251f051210a..fd4e56d9a5e 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts @@ -9,11 +9,15 @@ import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; import { SPSMeshShapeType } from "./SPSMeshShapeType"; +import { Mesh } from "../../../../Meshes"; /** * Block used to provide mesh source for SPS */ export class SPSMeshSourceBlock extends NodeParticleBlock { + private _mesh: Mesh | null = null; + private _disposeHandlerAdded = false; + @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { notifiers: { rebuild: true }, embedded: true, @@ -59,40 +63,52 @@ export class SPSMeshSourceBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - let mesh; - + if (this._mesh) { + this._mesh.dispose(); + this._mesh = null; + } if (this.shapeType === SPSMeshShapeType.Custom) { if (this.customMesh.isConnected) { const customMesh = this.customMesh.getConnectedValue(state); if (customMesh) { - mesh = customMesh; + this._mesh = customMesh; } else { - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); } } else { - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); } } else { switch (this.shapeType) { case SPSMeshShapeType.Box: - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); break; case SPSMeshShapeType.Sphere: - mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); + this._mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); break; case SPSMeshShapeType.Cylinder: - mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); + this._mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); break; case SPSMeshShapeType.Plane: - mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); break; default: - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); break; } } + if (this._mesh) { + this._mesh.isVisible = false; + } - this.mesh._storedValue = mesh; + if (!this._disposeHandlerAdded) { + this.onDisposeObservable.addOnce(() => { + this._mesh?.dispose(); + this._mesh = null; + }); + this._disposeHandlerAdded = true; + } + this.mesh._storedValue = this._mesh; } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 90acf510db1..31f738f45ef 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -8,14 +8,19 @@ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSCreateData } from "./ISPSData"; import { SolidParticle } from "../../../solidParticle"; import { Observer } from "../../../../Misc"; +import { Nullable } from "../../../../types"; +import { Scene } from "../../../.."; /** * Block used to create SolidParticleSystem and collect all Create blocks */ export class SPSSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; + private _sps: SolidParticleSystem | null = null; private _connectionObservers = new Map>(); private _disconnectionObservers = new Map>(); + private _onBeforeRenderObserver: Nullable> = null; + private _disposeHandlerAdded = false; @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { embedded: true, @@ -35,9 +40,7 @@ export class SPSSystemBlock extends NodeParticleBlock { public constructor(name: string) { super(name); - this._isSystem = true; - this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); this.registerOutput("system", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); @@ -66,8 +69,6 @@ export class SPSSystemBlock extends NodeParticleBlock { private _manageExtendedInputs(index: number) { const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { - console.log("connectionObserver solidParticle", index); - console.log(" connectionObserver this._entryCount", this._entryCount); if (this._entryCount - 1 > index) { return; } @@ -75,8 +76,6 @@ export class SPSSystemBlock extends NodeParticleBlock { }); const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { - console.log("solidParticle", index); - console.log("this._entryCount", this._entryCount); if (this._entryCount - 1 > index) { return; } @@ -113,72 +112,121 @@ export class SPSSystemBlock extends NodeParticleBlock { public createSystem(state: NodeParticleBuildState): SolidParticleSystem { state.capacity = this.capacity; - state.buildId = this._buildId++; + state.buildId = this._buildId ? this._buildId + 1 : 0; this.build(state); if (!state.scene) { throw new Error("Scene is not initialized in NodeParticleBuildState"); } + if (this._sps) { + // dispose is not working correctly + // this._sps.dispose(); + this._sps = null; + } - const sps = new SolidParticleSystem(this.name, state.scene); - sps.billboard = this.billboard; - sps.name = this.name; + if (this._onBeforeRenderObserver) { + state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } - // Collect data from all connected solidParticle inputs - const createBlocks: ISPSCreateData[] = []; + this._sps = new SolidParticleSystem(this.name, state.scene, { + useModelMaterial: true, + }); + this._sps.billboard = this.billboard; + this._sps.name = this.name; + + const createBlocks = new Map(); for (let i = 0; i < this._inputs.length; i++) { - if (this._inputs[i].isConnected && this._inputs[i]._storedValue) { - createBlocks.push(this._inputs[i]._storedValue); + const creatData = this._inputs[i].getConnectedValue(state) as ISPSCreateData; + if (this._inputs[i].isConnected && creatData) { + if (creatData.mesh && creatData.count) { + const shapeId = this._sps.addShape(creatData.mesh, creatData.count); + createBlocks.set(shapeId, creatData); + creatData.mesh.isVisible = false; + } } } - for (const createBlock of createBlocks) { - if (createBlock.mesh && createBlock.count) { - createBlock.shapeId = sps.addShape(createBlock.mesh, createBlock.count); - createBlock.mesh.isVisible = false; + this._sps.initParticles = () => { + if (!this._sps) { + return; } - } - - sps.initParticles = () => { - for (const createBlock of createBlocks) { - if (createBlock.initBlock && createBlock.shapeId !== undefined) { - const particles = sps.getParticlesByShapeId(createBlock.shapeId); - - particles.forEach((particle) => { - if (createBlock.initBlock) { - particle.position.copyFrom(createBlock.initBlock.position()); - particle.velocity.copyFrom(createBlock.initBlock.velocity()); - particle.color?.copyFrom(createBlock.initBlock.color()); - particle.scaling.copyFrom(createBlock.initBlock.scaling()); - particle.rotation.copyFrom(createBlock.initBlock.rotation()); - } - }); + for (let p = 0; p < this._sps.nbParticles; p++) { + const particle = this._sps.particles[p]; + const particleCreateData = createBlocks.get(particle.shapeId); + const initBlock = particleCreateData?.initBlock; + if (!initBlock) { + continue; + } + if (initBlock.position) { + particle.position.copyFrom(initBlock.position()); + } + if (initBlock.velocity) { + particle.velocity.copyFrom(initBlock.velocity()); + } + if (initBlock.color) { + particle.color?.copyFrom(initBlock.color()); + } + if (initBlock.scaling) { + particle.scaling.copyFrom(initBlock.scaling()); + } + if (initBlock.rotation) { + particle.rotation.copyFrom(initBlock.rotation()); } } }; - sps.updateParticle = (particle: SolidParticle) => { - const createBlock = createBlocks.find((createBlock) => createBlock.shapeId === particle.shapeId); - if (createBlock && createBlock.updateBlock) { - particle.position.copyFrom(createBlock.updateBlock.position()); - particle.velocity.copyFrom(createBlock.updateBlock.velocity()); - particle.color?.copyFrom(createBlock.updateBlock.color()); - particle.scaling.copyFrom(createBlock.updateBlock.scaling()); - particle.rotation.copyFrom(createBlock.updateBlock.rotation()); + this._sps.updateParticle = (particle: SolidParticle) => { + if (!this._sps) { + return particle; + } + const particleCreateData = createBlocks.get(particle.shapeId); + const updateBlock = particleCreateData?.updateBlock; + if (!updateBlock) { + return particle; + } + if (updateBlock.position) { + particle.position.copyFrom(updateBlock.position()); + } + if (updateBlock.velocity) { + particle.velocity.copyFrom(updateBlock.velocity()); + } + if (updateBlock.color) { + particle.color?.copyFrom(updateBlock.color()); + } + if (updateBlock.scaling) { + particle.scaling.copyFrom(updateBlock.scaling()); + } + if (updateBlock.rotation) { + particle.rotation.copyFrom(updateBlock.rotation()); } return particle; }; - sps.start(); + this._sps.buildMesh(); + this._sps.initParticles(); + this._sps.setParticles(); - this.system._storedValue = this; - - this.onDisposeObservable.addOnce(() => { - sps.dispose(); + this._onBeforeRenderObserver = state.scene.onBeforeRenderObservable.add(() => { + this._sps?.setParticles(); }); - return sps; + this.system._storedValue = this; + + if (!this._disposeHandlerAdded) { + this.onDisposeObservable.addOnce(() => { + this._sps?.dispose(); + this._sps = null; + if (this._onBeforeRenderObserver) { + state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + }); + this._disposeHandlerAdded = true; + } + console.log("SPSSystemBlock#createSystem", this._sps.mesh.getScene().meshes.length); + return this._sps; } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index 4183fed9c9d..193f57e4bf8 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -53,72 +53,67 @@ export class SPSUpdateBlock extends NodeParticleBlock { public override _build(state: NodeParticleBuildState) { const updateData: ISPSUpdateData = {} as ISPSUpdateData; - updateData.position = () => { - return this.getPosition(state); - }; - updateData.velocity = () => { - return this.getVelocity(state); - }; - updateData.color = () => { - return this.getColor(state); - }; - updateData.scaling = () => { - return this.getScaling(state); - }; - updateData.rotation = () => { - return this.getRotation(state); - }; + if (this.position.isConnected) { + updateData.position = () => { + return this.getPosition(state); + }; + } + if (this.velocity.isConnected) { + updateData.velocity = () => { + return this.getVelocity(state); + }; + } + if (this.color.isConnected) { + updateData.color = () => { + return this.getColor(state); + }; + } + if (this.scaling.isConnected) { + updateData.scaling = () => { + return this.getScaling(state); + }; + } + if (this.rotation.isConnected) { + updateData.rotation = () => { + return this.getRotation(state); + }; + } this.updateData._storedValue = updateData; } private getPosition(state: NodeParticleBuildState) { - if (this.position.isConnected) { - if (this.position._storedFunction) { - return this.position._storedFunction!(state); - } - return this.position.getConnectedValue(state); + if (this.position._storedFunction) { + return this.position._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.position.getConnectedValue(state); } private getVelocity(state: NodeParticleBuildState) { - if (this.velocity.isConnected) { - if (this.velocity._storedFunction) { - return this.velocity._storedFunction!(state); - } - return this.velocity.getConnectedValue(state); + if (this.velocity._storedFunction) { + return this.velocity._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.velocity.getConnectedValue(state); } private getColor(state: NodeParticleBuildState) { - if (this.color.isConnected) { - if (this.color._storedFunction) { - return this.color._storedFunction!(state); - } - return this.color.getConnectedValue(state); + if (this.color._storedFunction) { + return this.color._storedFunction(state); } - return new Color4(1, 1, 1, 1); + return this.color.getConnectedValue(state); } private getScaling(state: NodeParticleBuildState) { - if (this.scaling.isConnected) { - if (this.scaling._storedFunction) { - return this.scaling._storedFunction!(state); - } - return this.scaling.getConnectedValue(state); + if (this.scaling._storedFunction) { + return this.scaling._storedFunction(state); } - return new Vector3(1, 1, 1); + return this.scaling.getConnectedValue(state); } private getRotation(state: NodeParticleBuildState) { - if (this.rotation.isConnected) { - if (this.rotation._storedFunction) { - return this.rotation._storedFunction!(state); - } - return this.rotation.getConnectedValue(state); + if (this.rotation._storedFunction) { + return this.rotation._storedFunction!(state); } - return new Vector3(0, 0, 0); + return this.rotation.getConnectedValue(state); } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index aa78047ffd5..fa9a0a8a10b 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -7,7 +7,6 @@ import { Mesh } from "../Meshes/mesh"; import { CreateDisc } from "../Meshes/Builders/discBuilder"; import { EngineStore } from "../Engines/engineStore"; import type { Scene, IDisposable } from "../scene"; -import type { Observer } from "../Misc/observable"; import { DepthSortedParticle, SolidParticle, ModelShape, SolidParticleVertex } from "./solidParticle"; import type { TargetCamera } from "../Cameras/targetCamera"; import { BoundingInfo } from "../Culling/boundingInfo"; @@ -93,11 +92,6 @@ export class SolidParticleSystem implements IDisposable { */ public depthSortedParticles: DepthSortedParticle[]; - /** - * If the SPS has been started. - */ - public isStarted: boolean = false; - /** * If the particle intersection must be computed only with the bounding sphere (no bounding box computation, so faster). (Internal use only) * @internal @@ -1540,40 +1534,11 @@ export class SolidParticleSystem implements IDisposable { return this; } - private _onBeforeRenderObserver: Nullable> = null; - - /** - * Starts the SPS by subscribing to the scene's before render observable - */ - public start(): void { - this.stop(); - this.buildMesh(); - this.initParticles(); - this.setParticles(); - this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { - this.setParticles(); - }); - this.isStarted = true; - } - - /** - * Stops the SPS by unsubscribing from the scene's before render observable - */ - public stop(): void { - if (!this.isStarted) { - return; - } - this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - this.mesh.dispose(); - this.isStarted = false; - } - /** * Disposes the SPS. */ public dispose(): void { - this.stop(); + this.mesh.dispose(); this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; From 014681f1a5aed8174fe7860c9c8964407a61dddd Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Wed, 29 Oct 2025 10:59:10 +0300 Subject: [PATCH 14/68] Refactor SolidParticleSystem blocks to introduce SPSParticleConfigBlock and enhance particle configuration This commit introduces the new SPSParticleConfigBlock for configuring particle parameters such as mesh, count, material, and initialization/update blocks. The SPSCreateBlock has been refactored to utilize this new block, streamlining the creation of particles. Additionally, the ISPSData interface has been updated to reflect the new configuration structure, and various blocks have been adjusted to improve their connection logic and overall functionality within the particle system. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 2 +- .../Blocks/SolidParticle/SPSCreateBlock.ts | 211 +++++++++++++++--- .../SolidParticle/SPSParticleConfigBlock.ts | 72 ++++++ .../Blocks/SolidParticle/SPSSystemBlock.ts | 208 ++--------------- .../Blocks/SolidParticle/SPSUpdateBlock.ts | 2 - .../Node/Blocks/SolidParticle/index.ts | 3 +- .../Particles/Node/nodeParticleSystemSet.ts | 14 +- .../nodeParticleEditor/src/blockTools.ts | 4 +- .../components/nodeList/nodeListComponent.tsx | 7 +- .../graphSystem/registerToDisplayLedger.ts | 2 +- .../graphSystem/registerToPropertyLedger.ts | 2 +- 11 files changed, 285 insertions(+), 242 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index 0119aab9b89..8d0e2f0227f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -17,7 +17,7 @@ export interface ISPSUpdateData { /** * Interface for SPS create block data */ -export interface ISPSCreateData { +export interface ISPSParticleConfigData { mesh: Mesh; count: number; material?: Material; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 3e7c6c2a20f..25823dd45e5 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -3,69 +3,212 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISPSCreateData } from "./ISPSData"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { ISPSParticleConfigData } from "./ISPSData"; +import { SolidParticle } from "../../../solidParticle"; +import { Observer } from "../../../../Misc"; +import { Nullable } from "../../../../types"; +import { Scene } from "../../../.."; /** - * Block used to configure SPS parameters (mesh, count, initBlocks) + * Block used to create SolidParticleSystem and collect all Create blocks */ export class SPSCreateBlock extends NodeParticleBlock { + private _connectionObservers = new Map>(); + private _disconnectionObservers = new Map>(); + private _onBeforeRenderObserver: Nullable> = null; + private _disposeHandlerAdded = false; + public constructor(name: string) { super(name); + this.registerInput(`particleConfig-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); - this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); - this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); - this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); - this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); - this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); - - this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this._manageExtendedInputs(0); } public override getClassName() { return "SPSCreateBlock"; } - public get mesh(): NodeParticleConnectionPoint { - return this._inputs[0]; + private _entryCount = 1; + + private _extend() { + this._entryCount++; + this.registerInput(`particleConfig-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle, true); + this._manageExtendedInputs(this._entryCount - 1); } - public get count(): NodeParticleConnectionPoint { - return this._inputs[1]; + private _shrink() { + if (this._entryCount > 1) { + this._unmanageExtendedInputs(this._entryCount - 1); + this._entryCount--; + this.unregisterInput(`particleConfig-${this._entryCount}`); + } } - public get material(): NodeParticleConnectionPoint { - return this._inputs[2]; + private _manageExtendedInputs(index: number) { + const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { + if (this._entryCount - 1 > index) { + return; + } + this._extend(); + }); + + const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { + if (this._entryCount - 1 > index) { + return; + } + this._shrink(); + }); + + // Store observers for later removal + this._connectionObservers.set(index, connectionObserver); + this._disconnectionObservers.set(index, disconnectionObserver); } - public get initBlock(): NodeParticleConnectionPoint { - return this._inputs[3]; + private _unmanageExtendedInputs(index: number) { + const connectionObserver = this._connectionObservers.get(index); + const disconnectionObserver = this._disconnectionObservers.get(index); + + if (connectionObserver) { + this._inputs[index].onConnectionObservable.remove(connectionObserver); + this._connectionObservers.delete(index); + } + + if (disconnectionObserver) { + this._inputs[index].onDisconnectionObservable.remove(disconnectionObserver); + this._disconnectionObservers.delete(index); + } } - public get updateBlock(): NodeParticleConnectionPoint { - return this._inputs[4]; + public get particleConfig(): NodeParticleConnectionPoint { + return this._inputs[this._entryCount - 1]; } - public get solidParticle(): NodeParticleConnectionPoint { + public get solidParticleSystem(): NodeParticleConnectionPoint { return this._outputs[0]; } public override _build(state: NodeParticleBuildState) { - const mesh = this.mesh.getConnectedValue(state); - const count = (this.count.getConnectedValue(state) as number) || 1; - const material = this.material.getConnectedValue(state); - - const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; - const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; - - const solidParticle: ISPSCreateData = { - mesh, - count, - material, - initBlock, - updateBlock, + if (!state.scene) { + throw new Error("Scene is not initialized in NodeParticleBuildState"); + } + + if (this._onBeforeRenderObserver) { + state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + + const sps = new SolidParticleSystem(this.name, state.scene, { + useModelMaterial: true, + }); + + const createBlocks = new Map(); + for (let i = 0; i < this._inputs.length; i++) { + const creatData = this._inputs[i].getConnectedValue(state) as ISPSParticleConfigData; + if (this._inputs[i].isConnected && creatData) { + if (creatData.mesh && creatData.count) { + const shapeId = sps.addShape(creatData.mesh, creatData.count); + createBlocks.set(shapeId, creatData); + creatData.mesh.isVisible = false; + } + } + } + + sps.initParticles = () => { + if (!sps) { + return; + } + for (let p = 0; p < sps.nbParticles; p++) { + const particle = sps.particles[p]; + const particleCreateData = createBlocks.get(particle.shapeId); + const initBlock = particleCreateData?.initBlock; + if (!initBlock) { + continue; + } + if (initBlock.position) { + particle.position.copyFrom(initBlock.position()); + } + if (initBlock.velocity) { + particle.velocity.copyFrom(initBlock.velocity()); + } + if (initBlock.color) { + particle.color?.copyFrom(initBlock.color()); + } + if (initBlock.scaling) { + particle.scaling.copyFrom(initBlock.scaling()); + } + if (initBlock.rotation) { + particle.rotation.copyFrom(initBlock.rotation()); + } + } + }; + + sps.updateParticle = (particle: SolidParticle) => { + if (!sps) { + return particle; + } + const particleCreateData = createBlocks.get(particle.shapeId); + const updateBlock = particleCreateData?.updateBlock; + if (!updateBlock) { + return particle; + } + if (updateBlock.position) { + particle.position.copyFrom(updateBlock.position()); + } + if (updateBlock.velocity) { + particle.velocity.copyFrom(updateBlock.velocity()); + } + if (updateBlock.color) { + particle.color?.copyFrom(updateBlock.color()); + } + if (updateBlock.scaling) { + particle.scaling.copyFrom(updateBlock.scaling()); + } + if (updateBlock.rotation) { + particle.rotation.copyFrom(updateBlock.rotation()); + } + return particle; }; - this.solidParticle._storedValue = solidParticle; + sps.buildMesh(); + sps.initParticles(); + sps.setParticles(); + + this._onBeforeRenderObserver = state.scene.onBeforeRenderObservable.add(() => { + sps?.setParticles(); + }); + + // this.solidParticleSystem._storedValue?.dispose(); + this.solidParticleSystem._storedValue = sps; + + if (!this._disposeHandlerAdded) { + this.onDisposeObservable.addOnce(() => { + sps?.dispose(); + if (this._onBeforeRenderObserver) { + state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + }); + this._disposeHandlerAdded = true; + } + return sps; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject._entryCount = this._entryCount; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + if (serializationObject._entryCount && serializationObject._entryCount > 1) { + for (let i = 1; i < serializationObject._entryCount; i++) { + this._extend(); + } + } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts new file mode 100644 index 00000000000..d585cbd1258 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -0,0 +1,72 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISPSParticleConfigData } from "./ISPSData"; + +/** + * Block used to configure SPS particle parameters (mesh, count, material, initBlock, updateBlock) + */ +export class SPSParticleConfigBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); + this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); + this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); + this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); + + this.registerOutput("particleConfig", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSParticleConfigBlock"; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get count(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get material(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get initBlock(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get updateBlock(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public get particleConfig(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const mesh = this.mesh.getConnectedValue(state); + const count = (this.count.getConnectedValue(state) as number) || 1; + const material = this.material.getConnectedValue(state); + + const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; + const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; + + const particleConfig: ISPSParticleConfigData = { + mesh, + count, + material, + initBlock, + updateBlock, + }; + + this.particleConfig._storedValue = particleConfig; + } +} + +RegisterClass("BABYLON.SPSParticleConfigBlock", SPSParticleConfigBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 31f738f45ef..80b89056897 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -5,30 +5,17 @@ import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnect import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; -import type { ISPSCreateData } from "./ISPSData"; -import { SolidParticle } from "../../../solidParticle"; -import { Observer } from "../../../../Misc"; -import { Nullable } from "../../../../types"; -import { Scene } from "../../../.."; /** * Block used to create SolidParticleSystem and collect all Create blocks */ export class SPSSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; - private _sps: SolidParticleSystem | null = null; - private _connectionObservers = new Map>(); - private _disconnectionObservers = new Map>(); - private _onBeforeRenderObserver: Nullable> = null; - private _disposeHandlerAdded = false; - - @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - notifiers: { rebuild: true }, - min: 0, - max: 100000, - }) - public capacity = 1000; + // private _sps: SolidParticleSystem | null = null; + // private _connectionObservers = new Map>(); + // private _disconnectionObservers = new Map>(); + // private _onBeforeRenderObserver: Nullable> = null; + // private _disposeHandlerAdded = false; @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { embedded: true, @@ -41,69 +28,16 @@ export class SPSSystemBlock extends NodeParticleBlock { public constructor(name: string) { super(name); this._isSystem = true; - this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerOutput("system", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); - - this._manageExtendedInputs(0); + this.registerInput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + this.registerOutput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); } public override getClassName() { return "SPSSystemBlock"; } - private _entryCount = 1; - - private _extend() { - this._entryCount++; - this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle, true); - this._manageExtendedInputs(this._entryCount - 1); - } - - private _shrink() { - if (this._entryCount > 1) { - this._unmanageExtendedInputs(this._entryCount - 1); - this._entryCount--; - this.unregisterInput(`solidParticle-${this._entryCount}`); - } - } - - private _manageExtendedInputs(index: number) { - const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { - if (this._entryCount - 1 > index) { - return; - } - this._extend(); - }); - - const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { - if (this._entryCount - 1 > index) { - return; - } - this._shrink(); - }); - - // Store observers for later removal - this._connectionObservers.set(index, connectionObserver); - this._disconnectionObservers.set(index, disconnectionObserver); - } - - private _unmanageExtendedInputs(index: number) { - const connectionObserver = this._connectionObservers.get(index); - const disconnectionObserver = this._disconnectionObservers.get(index); - - if (connectionObserver) { - this._inputs[index].onConnectionObservable.remove(connectionObserver); - this._connectionObservers.delete(index); - } - - if (disconnectionObserver) { - this._inputs[index].onDisconnectionObservable.remove(disconnectionObserver); - this._disconnectionObservers.delete(index); - } - } - - public get solidParticle(): NodeParticleConnectionPoint { - return this._inputs[this._entryCount - 1]; + public get solidParticleSystem(): NodeParticleConnectionPoint { + return this._inputs[0]; } public get system(): NodeParticleConnectionPoint { @@ -111,142 +45,32 @@ export class SPSSystemBlock extends NodeParticleBlock { } public createSystem(state: NodeParticleBuildState): SolidParticleSystem { - state.capacity = this.capacity; - state.buildId = this._buildId ? this._buildId + 1 : 0; + state.buildId = ++this._buildId; this.build(state); - if (!state.scene) { - throw new Error("Scene is not initialized in NodeParticleBuildState"); - } - if (this._sps) { - // dispose is not working correctly - // this._sps.dispose(); - this._sps = null; - } + const sps = this.solidParticleSystem.getConnectedValue(state) as SolidParticleSystem; - if (this._onBeforeRenderObserver) { - state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; + if (!sps) { + throw new Error("No SolidParticleSystem connected to SPSSystemBlock"); } - this._sps = new SolidParticleSystem(this.name, state.scene, { - useModelMaterial: true, - }); - this._sps.billboard = this.billboard; - this._sps.name = this.name; - - const createBlocks = new Map(); - for (let i = 0; i < this._inputs.length; i++) { - const creatData = this._inputs[i].getConnectedValue(state) as ISPSCreateData; - if (this._inputs[i].isConnected && creatData) { - if (creatData.mesh && creatData.count) { - const shapeId = this._sps.addShape(creatData.mesh, creatData.count); - createBlocks.set(shapeId, creatData); - creatData.mesh.isVisible = false; - } - } - } - - this._sps.initParticles = () => { - if (!this._sps) { - return; - } - for (let p = 0; p < this._sps.nbParticles; p++) { - const particle = this._sps.particles[p]; - const particleCreateData = createBlocks.get(particle.shapeId); - const initBlock = particleCreateData?.initBlock; - if (!initBlock) { - continue; - } - if (initBlock.position) { - particle.position.copyFrom(initBlock.position()); - } - if (initBlock.velocity) { - particle.velocity.copyFrom(initBlock.velocity()); - } - if (initBlock.color) { - particle.color?.copyFrom(initBlock.color()); - } - if (initBlock.scaling) { - particle.scaling.copyFrom(initBlock.scaling()); - } - if (initBlock.rotation) { - particle.rotation.copyFrom(initBlock.rotation()); - } - } - }; - - this._sps.updateParticle = (particle: SolidParticle) => { - if (!this._sps) { - return particle; - } - const particleCreateData = createBlocks.get(particle.shapeId); - const updateBlock = particleCreateData?.updateBlock; - if (!updateBlock) { - return particle; - } - if (updateBlock.position) { - particle.position.copyFrom(updateBlock.position()); - } - if (updateBlock.velocity) { - particle.velocity.copyFrom(updateBlock.velocity()); - } - if (updateBlock.color) { - particle.color?.copyFrom(updateBlock.color()); - } - if (updateBlock.scaling) { - particle.scaling.copyFrom(updateBlock.scaling()); - } - if (updateBlock.rotation) { - particle.rotation.copyFrom(updateBlock.rotation()); - } - return particle; - }; - - this._sps.buildMesh(); - this._sps.initParticles(); - this._sps.setParticles(); - - this._onBeforeRenderObserver = state.scene.onBeforeRenderObservable.add(() => { - this._sps?.setParticles(); - }); + sps.billboard = this.billboard; + sps.name = this.name; this.system._storedValue = this; - - if (!this._disposeHandlerAdded) { - this.onDisposeObservable.addOnce(() => { - this._sps?.dispose(); - this._sps = null; - if (this._onBeforeRenderObserver) { - state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - } - }); - this._disposeHandlerAdded = true; - } - console.log("SPSSystemBlock#createSystem", this._sps.mesh.getScene().meshes.length); - return this._sps; + return sps; } public override serialize(): any { const serializationObject = super.serialize(); - serializationObject.capacity = this.capacity; serializationObject.billboard = this.billboard; - serializationObject._entryCount = this._entryCount; return serializationObject; } public override _deserialize(serializationObject: any) { super._deserialize(serializationObject); - this.capacity = serializationObject.capacity; this.billboard = !!serializationObject.billboard; - - if (serializationObject._entryCount && serializationObject._entryCount > 1) { - for (let i = 1; i < serializationObject._entryCount; i++) { - this._extend(); - } - } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index 193f57e4bf8..cadae7dea94 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -3,8 +3,6 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import { Vector3 } from "core/Maths/math.vector"; -import { Color4 } from "core/Maths/math.color"; import type { ISPSUpdateData } from "./ISPSData"; /** diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 2b66eabf628..070d7ebd205 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -1,7 +1,8 @@ export * from "./ISPSData"; export * from "./SPSMeshShapeType"; export * from "./SPSMeshSourceBlock"; -export * from "./SPSCreateBlock"; +export * from "./SPSParticleConfigBlock"; export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; export * from "./SPSInitBlock"; +export * from "./SPSCreateBlock"; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index a55cd581085..8bd049b54c7 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,7 +22,7 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; -import { SPSCreateBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock } from "./Blocks"; +import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock, SPSCreateBlock } from "./Blocks"; import { ParticleSystem } from ".."; import { Vector3 } from "../../Maths"; @@ -343,17 +343,19 @@ export class NodeParticleSystemSet { this.clear(); this.editorData = null; const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.capacity = 1000; spsSystem.billboard = false; - const spsCreateBox = new SPSCreateBlock("Create Box Particles"); - const spsCreateSphere = new SPSCreateBlock("Create Sphere Particles"); + const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + + const spsCreateBox = new SPSParticleConfigBlock("Create Box Particles"); + const spsCreateSphere = new SPSParticleConfigBlock("Create Sphere Particles"); spsCreateBox.count.value = 5; spsCreateSphere.count.value = 1; - spsCreateBox.solidParticle.connectTo(spsSystem.solidParticle); - spsCreateSphere.solidParticle.connectTo(spsSystem.solidParticle); + spsCreateBox.particleConfig.connectTo(spsCreateBlock.particleConfig); + spsCreateSphere.particleConfig.connectTo(spsCreateBlock.particleConfig); const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index bf33c72c2d2..97fc5e54f37 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -41,7 +41,7 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock, SPSUpdateBlock } from "core/Particles/Node/Blocks"; +import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock, SPSUpdateBlock, SPSParticleConfigBlock } from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -152,6 +152,8 @@ export class BlockTools { return new SystemBlock("System"); case "SPSMeshSourceBlock": return new SPSMeshSourceBlock("SPS Mesh Source"); + case "SPSParticleConfigBlock": + return new SPSParticleConfigBlock("SPS Particle Config"); case "SPSSystemBlock": return new SPSSystemBlock("SPS System"); case "SPSCreateBlock": diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 9b542949ad6..412eed10b4e 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -24,10 +24,11 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["UpdateFlowMapBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["SystemBlock"] = SystemDisplayManager; - // SPS Blocks DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSParticleConfigBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; DisplayLedger.RegisteredControls["SPSInitBlock"] = UpdateDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index cd5287e3d42..57075b01779 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -14,8 +14,8 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; - // SPS Blocks - use default GenericPropertyComponent PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSParticleConfigBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; From 9c25aa66e03075d79a3fcf00f30194f6a0c78655 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 30 Oct 2025 17:15:48 +0300 Subject: [PATCH 15/68] Refactor SPSCreateBlock and SPSSystemBlock to improve lifecycle management and remove unused observers This commit refactors the SPSCreateBlock and SPSSystemBlock classes by removing unused observer properties and simplifying the lifecycle management of SolidParticleSystem instances. The changes enhance clarity and maintainability by eliminating redundant code related to rendering observers and disposal handlers, ensuring a more efficient handling of particle systems. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 26 ------------------- .../Blocks/SolidParticle/SPSSystemBlock.ts | 19 ++++++-------- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 25823dd45e5..41a398781b2 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -7,8 +7,6 @@ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSParticleConfigData } from "./ISPSData"; import { SolidParticle } from "../../../solidParticle"; import { Observer } from "../../../../Misc"; -import { Nullable } from "../../../../types"; -import { Scene } from "../../../.."; /** * Block used to create SolidParticleSystem and collect all Create blocks @@ -16,8 +14,6 @@ import { Scene } from "../../../.."; export class SPSCreateBlock extends NodeParticleBlock { private _connectionObservers = new Map>(); private _disconnectionObservers = new Map>(); - private _onBeforeRenderObserver: Nullable> = null; - private _disposeHandlerAdded = false; public constructor(name: string) { super(name); @@ -95,11 +91,6 @@ export class SPSCreateBlock extends NodeParticleBlock { throw new Error("Scene is not initialized in NodeParticleBuildState"); } - if (this._onBeforeRenderObserver) { - state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - } - const sps = new SolidParticleSystem(this.name, state.scene, { useModelMaterial: true, }); @@ -176,24 +167,7 @@ export class SPSCreateBlock extends NodeParticleBlock { sps.initParticles(); sps.setParticles(); - this._onBeforeRenderObserver = state.scene.onBeforeRenderObservable.add(() => { - sps?.setParticles(); - }); - - // this.solidParticleSystem._storedValue?.dispose(); this.solidParticleSystem._storedValue = sps; - - if (!this._disposeHandlerAdded) { - this.onDisposeObservable.addOnce(() => { - sps?.dispose(); - if (this._onBeforeRenderObserver) { - state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - } - }); - this._disposeHandlerAdded = true; - } - return sps; } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 80b89056897..4d0a8e4340d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -11,11 +11,6 @@ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; */ export class SPSSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; - // private _sps: SolidParticleSystem | null = null; - // private _connectionObservers = new Map>(); - // private _disconnectionObservers = new Map>(); - // private _onBeforeRenderObserver: Nullable> = null; - // private _disposeHandlerAdded = false; @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { embedded: true, @@ -49,17 +44,19 @@ export class SPSSystemBlock extends NodeParticleBlock { this.build(state); - const sps = this.solidParticleSystem.getConnectedValue(state) as SolidParticleSystem; + const solidParticleSystem = this.solidParticleSystem.getConnectedValue(state) as SolidParticleSystem; - if (!sps) { + if (!solidParticleSystem) { throw new Error("No SolidParticleSystem connected to SPSSystemBlock"); } - sps.billboard = this.billboard; - sps.name = this.name; + solidParticleSystem.billboard = this.billboard; + solidParticleSystem.name = this.name; - this.system._storedValue = this; - return sps; + this.onDisposeObservable.addOnce(() => { + solidParticleSystem.dispose(); + }); + return solidParticleSystem; } public override serialize(): any { From 6da73592aa3f7ce92453b3399246c786322656e9 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 31 Oct 2025 13:58:49 +0300 Subject: [PATCH 16/68] Enhance ParticleSystemSet to support SolidParticleSystem integration This commit updates the ParticleSystemSet class to accommodate SolidParticleSystem instances alongside traditional ParticleSystem objects. The systems array is modified to include both types, and conditional checks are added to ensure proper handling of emitters and serialization for SolidParticleSystems. These changes improve the flexibility and functionality of the particle system management. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 4 ---- .../Particles/Node/nodeParticleSystemSet.ts | 2 +- .../core/src/Particles/particleSystemSet.ts | 19 +++++++++++++----- .../core/src/Particles/solidParticleSystem.ts | 20 +++++++++++++++++-- 4 files changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 41a398781b2..6a5c475662d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -163,10 +163,6 @@ export class SPSCreateBlock extends NodeParticleBlock { return particle; }; - sps.buildMesh(); - sps.initParticles(); - sps.setParticles(); - this.solidParticleSystem._storedValue = sps; } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 8bd049b54c7..46c7e66ed5b 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -275,8 +275,8 @@ export class NodeParticleSystemSet { if (system instanceof ParticleSystem) { system._source = this; system._blockReference = block._internalId; - output.systems.push(system); } + output.systems.push(system); // Errors state.emitErrors(); } diff --git a/packages/dev/core/src/Particles/particleSystemSet.ts b/packages/dev/core/src/Particles/particleSystemSet.ts index a3743ed42ee..6ac7698af9a 100644 --- a/packages/dev/core/src/Particles/particleSystemSet.ts +++ b/packages/dev/core/src/Particles/particleSystemSet.ts @@ -9,6 +9,7 @@ import { ParticleSystem } from "../Particles/particleSystem"; import type { Scene, IDisposable } from "../scene"; import { StandardMaterial } from "../Materials/standardMaterial"; import type { Vector3 } from "../Maths/math.vector"; +import type { SolidParticleSystem } from "./solidParticleSystem"; /** Internal class used to store shapes for emitters */ class ParticleSystemSetEmitterCreationOptions { @@ -34,7 +35,7 @@ export class ParticleSystemSet implements IDisposable { /** * Gets the particle system list */ - public systems: IParticleSystem[] = []; + public systems: (IParticleSystem | SolidParticleSystem)[] = []; /** * Gets or sets the emitter node used with this set @@ -52,7 +53,9 @@ export class ParticleSystemSet implements IDisposable { } for (const system of this.systems) { - system.emitter = value; + if (system instanceof ParticleSystem) { + system.emitter = value; + } } this._emitterNode = value; @@ -90,7 +93,9 @@ export class ParticleSystemSet implements IDisposable { emitterMesh.material = material; for (const system of this.systems) { - system.emitter = emitterMesh; + if (system instanceof ParticleSystem) { + system.emitter = emitterMesh; + } } this._emitterNode = emitterMesh; @@ -103,7 +108,9 @@ export class ParticleSystemSet implements IDisposable { public start(emitter?: AbstractMesh): void { for (const system of this.systems) { if (emitter) { - system.emitter = emitter; + if (system instanceof ParticleSystem) { + system.emitter = emitter; + } } system.start(); } @@ -137,7 +144,9 @@ export class ParticleSystemSet implements IDisposable { result.systems = []; for (const system of this.systems) { - result.systems.push(system.serialize(serializeTexture)); + if (system instanceof ParticleSystem) { + result.systems.push(system.serialize(serializeTexture)); + } } if (this._emitterNode) { diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index fa9a0a8a10b..0b002d34fbe 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -17,6 +17,7 @@ import { StandardMaterial } from "../Materials/standardMaterial"; import { MultiMaterial } from "../Materials/multiMaterial"; import type { PickingInfo } from "../Collisions/pickingInfo"; import type { PBRMaterial } from "../Materials/PBR/pbrMaterial"; +import type { Observer } from "../Misc/observable"; /** * The SPS is a single updatable mesh. The solid particles are simply separate parts or faces of this big mesh. @@ -152,7 +153,7 @@ export class SolidParticleSystem implements IDisposable { protected _autoUpdateSubMeshes: boolean = false; protected _tmpVertex: SolidParticleVertex; protected _recomputeInvisibles: boolean = false; - + protected _onBeforeRenderObserver: Nullable> = null; /** * Creates a SPS (Solid Particle System) object. * @param name (String) is the SPS name, this will be the underlying mesh name. @@ -1538,7 +1539,13 @@ export class SolidParticleSystem implements IDisposable { * Disposes the SPS. */ public dispose(): void { - this.mesh.dispose(); + if (this._onBeforeRenderObserver) { + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + if (this.mesh) { + this.mesh.dispose(); + } this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; @@ -1998,6 +2005,15 @@ export class SolidParticleSystem implements IDisposable { */ public initParticles(): void {} + public start(): void { + this.buildMesh(); + this.initParticles(); + this.setParticles(); + this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { + this.setParticles(); + }); + } + /** * This function does nothing. It may be overwritten to recycle a particle. * The SPS doesn't call this function, you may have to call it by your own. From e37f61f1dbb7ce7d5b880e4953fae7f9a64655c6 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 10:26:33 +0300 Subject: [PATCH 17/68] Enhance NodeParticleBuildState and NodeParticleSystemSet for SolidParticle integration This commit updates the NodeParticleBuildState class to support both Particle and SolidParticle contexts, allowing for more flexible handling of particle data. The systemContext property is modified to accommodate both ThinParticleSystem and SolidParticleSystem. Additionally, the NodeParticleSystemSet class is enhanced with a new method, setToTetrahedronSPS, which sets up a SolidParticleSystem with a tetrahedron configuration, including random positioning, rotation, and color generation. These changes improve the overall functionality and versatility of the particle system management. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 98 +++++--- .../Particles/Node/nodeParticleBuildState.ts | 157 +++++++++--- .../Particles/Node/nodeParticleSystemSet.ts | 236 +++++++++++++++--- 3 files changed, 379 insertions(+), 112 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 6a5c475662d..d0798fe21c0 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -111,28 +111,41 @@ export class SPSCreateBlock extends NodeParticleBlock { if (!sps) { return; } - for (let p = 0; p < sps.nbParticles; p++) { - const particle = sps.particles[p]; - const particleCreateData = createBlocks.get(particle.shapeId); - const initBlock = particleCreateData?.initBlock; - if (!initBlock) { - continue; - } - if (initBlock.position) { - particle.position.copyFrom(initBlock.position()); - } - if (initBlock.velocity) { - particle.velocity.copyFrom(initBlock.velocity()); - } - if (initBlock.color) { - particle.color?.copyFrom(initBlock.color()); - } - if (initBlock.scaling) { - particle.scaling.copyFrom(initBlock.scaling()); - } - if (initBlock.rotation) { - particle.rotation.copyFrom(initBlock.rotation()); + + const originalContext = state.particleContext; + const originalSystemContext = state.systemContext; + + try { + for (let p = 0; p < sps.nbParticles; p++) { + const particle = sps.particles[p]; + const particleCreateData = createBlocks.get(particle.shapeId); + const initBlock = particleCreateData?.initBlock; + if (!initBlock) { + continue; + } + + state.particleContext = particle; + state.systemContext = sps; + + if (initBlock.position) { + particle.position.copyFrom(initBlock.position()); + } + if (initBlock.velocity) { + particle.velocity.copyFrom(initBlock.velocity()); + } + if (initBlock.color) { + particle.color?.copyFrom(initBlock.color()); + } + if (initBlock.scaling) { + particle.scaling.copyFrom(initBlock.scaling()); + } + if (initBlock.rotation) { + particle.rotation.copyFrom(initBlock.rotation()); + } } + } finally { + state.particleContext = originalContext; + state.systemContext = originalSystemContext; } }; @@ -140,25 +153,40 @@ export class SPSCreateBlock extends NodeParticleBlock { if (!sps) { return particle; } + const particleCreateData = createBlocks.get(particle.shapeId); const updateBlock = particleCreateData?.updateBlock; if (!updateBlock) { return particle; } - if (updateBlock.position) { - particle.position.copyFrom(updateBlock.position()); - } - if (updateBlock.velocity) { - particle.velocity.copyFrom(updateBlock.velocity()); - } - if (updateBlock.color) { - particle.color?.copyFrom(updateBlock.color()); - } - if (updateBlock.scaling) { - particle.scaling.copyFrom(updateBlock.scaling()); - } - if (updateBlock.rotation) { - particle.rotation.copyFrom(updateBlock.rotation()); + // Set particle context in state for PerParticle lock mode + const originalContext = state.particleContext; + const originalSystemContext = state.systemContext; + + // Temporarily set particle context for PerParticle lock mode + state.particleContext = particle; + state.systemContext = sps; + + try { + if (updateBlock.position) { + particle.position.copyFrom(updateBlock.position()); + } + if (updateBlock.velocity) { + particle.velocity.copyFrom(updateBlock.velocity()); + } + if (updateBlock.color) { + particle.color?.copyFrom(updateBlock.color()); + } + if (updateBlock.scaling) { + particle.scaling.copyFrom(updateBlock.scaling()); + } + if (updateBlock.rotation) { + particle.rotation.copyFrom(updateBlock.rotation()); + } + } finally { + // Restore original context + state.particleContext = originalContext; + state.systemContext = originalSystemContext; } return particle; }; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts index 0632526dcb3..0e966505ae3 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -1,15 +1,16 @@ import type { Scene } from "core/scene"; import type { NodeParticleConnectionPoint } from "./nodeParticleBlockConnectionPoint"; import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; -import type { Particle } from "../particle"; +import { Particle } from "../particle"; import type { Nullable } from "core/types"; import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; import { Vector2, Vector3 } from "core/Maths/math.vector"; -import type { ThinParticleSystem } from "../thinParticleSystem"; +import { SolidParticle } from "../solidParticle"; +import { ThinParticleSystem } from "../thinParticleSystem"; import { Color4 } from "core/Maths/math.color"; import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; import type { AbstractMesh } from "core/Meshes/abstractMesh"; -import type { SolidParticleSystem } from "../solidParticleSystem"; +import { SolidParticleSystem } from "../solidParticleSystem"; /** * Class used to store node based geometry build state @@ -37,17 +38,13 @@ export class NodeParticleBuildState { /** * Gets or sets the particle context for contextual data */ - public particleContext: Nullable = null; + public particleContext: Nullable = null; /** * Gets or sets the system context for contextual data + * Can be either ThinParticleSystem or SolidParticleSystem */ - public systemContext: Nullable = null; - - /** - * Gets or sets the SPS context for SPS blocks - */ - public spsContext: Nullable = null; + public systemContext: Nullable = null; /** * Gets or sets the delta time for physics calculations @@ -116,42 +113,97 @@ export class NodeParticleBuildState { * @returns the value associated with the source */ public getContextualValue(source: NodeParticleContextualSources) { - if (!this.particleContext || !this.systemContext) { + if (!this.particleContext) { return null; } + const isParticle = this.particleContext instanceof Particle; + const isSolidParticle = this.particleContext instanceof SolidParticle; + switch (source) { + // Sources supported by both Particle and SolidParticle case NodeParticleContextualSources.Position: return this.particleContext.position; + case NodeParticleContextualSources.Color: + return this.particleContext.color; + case NodeParticleContextualSources.Scale: + if (isParticle) { + return (this.particleContext as Particle).scale; + } else if (isSolidParticle) { + // Convert Vector3 scaling to Vector2 for compatibility + const scaling = (this.particleContext as SolidParticle).scaling; + return new Vector2(scaling.x, scaling.y); + } + return null; + + // Sources only supported by Particle (require ThinParticleSystem) case NodeParticleContextualSources.Direction: - return this.particleContext.direction; + if (!isParticle || !this.systemContext) { + return null; + } + return (this.particleContext as Particle).direction; case NodeParticleContextualSources.ScaledDirection: - this.particleContext.direction.scaleToRef(this.systemContext._directionScale, this.systemContext._scaledDirection); + if (!isParticle || !this.systemContext) { + return null; + } + // ScaledDirection only works with ThinParticleSystem + if (!(this.systemContext instanceof ThinParticleSystem)) { + return null; + } + const particle = this.particleContext as Particle; + particle.direction.scaleToRef(this.systemContext._directionScale, this.systemContext._scaledDirection); return this.systemContext._scaledDirection; - case NodeParticleContextualSources.Color: - return this.particleContext.color; case NodeParticleContextualSources.InitialColor: - return this.particleContext.initialColor; + if (!isParticle) { + return null; + } + return (this.particleContext as Particle).initialColor; case NodeParticleContextualSources.ColorDead: - return this.particleContext.colorDead; + if (!isParticle) { + return null; + } + return (this.particleContext as Particle).colorDead; case NodeParticleContextualSources.Age: - return this.particleContext.age; + if (!isParticle) { + return null; + } + return (this.particleContext as Particle).age; case NodeParticleContextualSources.Lifetime: - return this.particleContext.lifeTime; + if (!isParticle) { + return null; + } + return (this.particleContext as Particle).lifeTime; case NodeParticleContextualSources.Angle: - return this.particleContext.angle; - case NodeParticleContextualSources.Scale: - return this.particleContext.scale; + if (!isParticle) { + return null; + } + return (this.particleContext as Particle).angle; case NodeParticleContextualSources.AgeGradient: - return this.particleContext.age / this.particleContext.lifeTime; + if (!isParticle) { + return null; + } + const p = this.particleContext as Particle; + return p.age / p.lifeTime; case NodeParticleContextualSources.SpriteCellEnd: + if (!this.systemContext || !(this.systemContext instanceof ThinParticleSystem)) { + return null; + } return this.systemContext.endSpriteCellID; case NodeParticleContextualSources.SpriteCellIndex: - return this.particleContext.cellIndex; + if (!isParticle) { + return null; + } + return (this.particleContext as Particle).cellIndex; case NodeParticleContextualSources.SpriteCellStart: + if (!this.systemContext || !(this.systemContext instanceof ThinParticleSystem)) { + return null; + } return this.systemContext.startSpriteCellID; case NodeParticleContextualSources.InitialDirection: - return this.particleContext._initialDirection; + if (!isParticle) { + return null; + } + return (this.particleContext as Particle)._initialDirection; } return null; @@ -161,7 +213,7 @@ export class NodeParticleBuildState { * Gets a boolean indicating if the emitter is a transform node (or a simple vector3) */ public get isEmitterTransformNode() { - if (!this.systemContext) { + if (!this.systemContext || !(this.systemContext instanceof ThinParticleSystem)) { return false; } @@ -176,7 +228,7 @@ export class NodeParticleBuildState { * Gets the emitter world matrix */ public get emitterWorldMatrix() { - if (!this.systemContext) { + if (!this.systemContext || !(this.systemContext instanceof ThinParticleSystem)) { return null; } return this.systemContext._emitterWorldMatrix; @@ -186,7 +238,7 @@ export class NodeParticleBuildState { * Gets the emitter inverse world matrix */ public get emitterInverseWorldMatrix() { - if (!this.systemContext) { + if (!this.systemContext || !(this.systemContext instanceof ThinParticleSystem)) { return null; } return this.systemContext._emitterInverseWorldMatrix; @@ -200,11 +252,17 @@ export class NodeParticleBuildState { return null; } - if (this.isEmitterTransformNode) { - return (this.systemContext.emitter).absolutePosition; + if (this.systemContext instanceof ThinParticleSystem) { + if (this.isEmitterTransformNode) { + return (this.systemContext.emitter).absolutePosition; + } + return this.systemContext.emitter as Vector3; + } else if (this.systemContext instanceof SolidParticleSystem) { + // For SPS, return mesh position as "emitter" + return this.systemContext.mesh?.absolutePosition || Vector3.Zero(); } - return this.systemContext.emitter as Vector3; + return null; } /** @@ -217,19 +275,42 @@ export class NodeParticleBuildState { return null; } + const isThinParticleSystem = this.systemContext instanceof ThinParticleSystem; + const isSolidParticleSystem = this.systemContext instanceof SolidParticleSystem; + switch (source) { case NodeParticleSystemSources.Time: - return this.systemContext._actualFrame; + if (isThinParticleSystem) { + return (this.systemContext as ThinParticleSystem)._actualFrame; + } else if (isSolidParticleSystem) { + // For SPS, use frameId from scene + return this.scene.getFrameId() || 0; + } + return null; case NodeParticleSystemSources.Delta: - return this.systemContext._scaledUpdateSpeed; + if (isThinParticleSystem) { + return (this.systemContext as ThinParticleSystem)._scaledUpdateSpeed; + } else if (isSolidParticleSystem) { + // For SPS, use deltaTime from engine + return this.scene.getEngine().getDeltaTime() || 0.016; + } + return null; case NodeParticleSystemSources.Emitter: - if (this.isEmitterTransformNode) { - const emitterMesh = this.systemContext.emitter; - return emitterMesh.absolutePosition; - } else { - return this.systemContext.emitter; + if (isThinParticleSystem) { + const thinSystem = this.systemContext as ThinParticleSystem; + if (this.isEmitterTransformNode) { + const emitterMesh = thinSystem.emitter; + return emitterMesh.absolutePosition; + } else { + return thinSystem.emitter; + } + } else if (isSolidParticleSystem) { + // For SPS, return mesh position as "emitter" + return (this.systemContext as SolidParticleSystem).mesh?.absolutePosition || Vector3.Zero(); } + return null; case NodeParticleSystemSources.CameraPosition: + // Works for both through scene return this.scene.activeCamera?.globalPosition || Vector3.Zero(); } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 46c7e66ed5b..9d145aeb24c 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,9 +22,11 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; -import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock, SPSCreateBlock } from "./Blocks"; +import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock } from "./Blocks"; import { ParticleSystem } from ".."; -import { Vector3 } from "../../Maths"; +import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; +import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; +import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -340,58 +342,214 @@ export class NodeParticleSystemSet { } public setToDefaultSPS() { - this.clear(); - this.editorData = null; - const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.billboard = false; + // this.clear(); + // this.editorData = null; + // const spsSystem = new SPSSystemBlock("SPS System"); + // spsSystem.billboard = false; - const spsCreateBlock = new SPSCreateBlock("Create Particles System"); - spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + // const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + // spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + + // const spsCreateBox = new SPSParticleConfigBlock("Create Box Particles"); + // const spsCreateSphere = new SPSParticleConfigBlock("Create Sphere Particles"); + + // spsCreateBox.count.value = 5; + // spsCreateSphere.count.value = 1; + + // spsCreateBox.particleConfig.connectTo(spsCreateBlock.particleConfig); + // spsCreateSphere.particleConfig.connectTo(spsCreateBlock.particleConfig); - const spsCreateBox = new SPSParticleConfigBlock("Create Box Particles"); - const spsCreateSphere = new SPSParticleConfigBlock("Create Sphere Particles"); + // const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); + // const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); - spsCreateBox.count.value = 5; - spsCreateSphere.count.value = 1; + // meshSourceBox.shapeType = SPSMeshShapeType.Box; + // meshSourceSphere.shapeType = SPSMeshShapeType.Sphere; - spsCreateBox.particleConfig.connectTo(spsCreateBlock.particleConfig); - spsCreateSphere.particleConfig.connectTo(spsCreateBlock.particleConfig); + // meshSourceBox.size = 1; + // meshSourceSphere.size = 1; - const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); - const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); + // meshSourceBox.mesh.connectTo(spsCreateBox.mesh); + // meshSourceSphere.mesh.connectTo(spsCreateSphere.mesh); - meshSourceBox.shapeType = SPSMeshShapeType.Box; - meshSourceSphere.shapeType = SPSMeshShapeType.Sphere; + // const spsInitBox = new SPSInitBlock("Initialize Box Particles"); + // const spsInitSphere = new SPSInitBlock("Initialize Sphere Particles"); - meshSourceBox.size = 1; - meshSourceSphere.size = 1; + // spsInitBox.initData.connectTo(spsCreateBox.initBlock); + // spsInitSphere.initData.connectTo(spsCreateSphere.initBlock); - meshSourceBox.mesh.connectTo(spsCreateBox.mesh); - meshSourceSphere.mesh.connectTo(spsCreateSphere.mesh); + // const positionBlockBox = new ParticleInputBlock("Position"); + // positionBlockBox.value = new Vector3(1, 1, 1); + // positionBlockBox.output.connectTo(spsInitBox.position); - const spsInitBox = new SPSInitBlock("Initialize Box Particles"); - const spsInitSphere = new SPSInitBlock("Initialize Sphere Particles"); + // const rotationBlockBox = new ParticleInputBlock("Rotation"); + // rotationBlockBox.value = new Vector3(3, 0, 0); + // rotationBlockBox.output.connectTo(spsInitBox.rotation); - spsInitBox.initData.connectTo(spsCreateBox.initBlock); - spsInitSphere.initData.connectTo(spsCreateSphere.initBlock); + // const positionBlockSphere = new ParticleInputBlock("Position"); + // positionBlockSphere.value = new Vector3(0, 0, 0); + // positionBlockSphere.output.connectTo(spsInitSphere.position); - const positionBlockBox = new ParticleInputBlock("Position"); - positionBlockBox.value = new Vector3(1, 1, 1); - positionBlockBox.output.connectTo(spsInitBox.position); + // const spsUpdateBox = new SPSUpdateBlock("Update Box Particles"); + // const spsUpdateSphere = new SPSUpdateBlock("Update Sphere Particles"); - const rotationBlockBox = new ParticleInputBlock("Rotation"); - rotationBlockBox.value = new Vector3(3, 0, 0); - rotationBlockBox.output.connectTo(spsInitBox.rotation); + // spsUpdateBox.updateData.connectTo(spsCreateBox.updateBlock); + // spsUpdateSphere.updateData.connectTo(spsCreateSphere.updateBlock); - const positionBlockSphere = new ParticleInputBlock("Position"); - positionBlockSphere.value = new Vector3(0, 0, 0); - positionBlockSphere.output.connectTo(spsInitSphere.position); + // this._systemBlocks.push(spsSystem); + this.setToTetrahedronSPS(); + } - const spsUpdateBox = new SPSUpdateBlock("Update Box Particles"); - const spsUpdateSphere = new SPSUpdateBlock("Update Sphere Particles"); + public setToTetrahedronSPS() { + this.clear(); + this.editorData = null; + + // STEP 1: Create basic SPS system + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.billboard = false; + + // STEP 2: Create SPS creation block + const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); - spsUpdateBox.updateData.connectTo(spsCreateBox.updateBlock); - spsUpdateSphere.updateData.connectTo(spsCreateSphere.updateBlock); + // STEP 3: Create particle config block with count + const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); + spsCreateTetra.count.value = 2000; // Start with small number for testing + spsCreateTetra.particleConfig.connectTo(spsCreateBlock.particleConfig); + + // STEP 4: Create mesh source (using Box first to test) + const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); + meshSourceTetra.shapeType = SPSMeshShapeType.Box; // Start with Box to test + meshSourceTetra.size = 0.3; + meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); + + // STEP 5: Create init block + const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); + spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); + + // STEP 6: Random X position (-10 to 10) + const randomXMin = new ParticleInputBlock("Random X Min"); + randomXMin.value = -10; + const randomXMax = new ParticleInputBlock("Random X Max"); + randomXMax.value = 10; + const randomX = new ParticleRandomBlock("Random X"); + randomX.lockMode = ParticleRandomBlockLocks.PerParticle; + randomXMin.output.connectTo(randomX.min); + randomXMax.output.connectTo(randomX.max); + + // STEP 7: Random Z position (-10 to 10) + const randomZMin = new ParticleInputBlock("Random Z Min"); + randomZMin.value = -10; + const randomZMax = new ParticleInputBlock("Random Z Max"); + randomZMax.value = 10; + const randomZ = new ParticleRandomBlock("Random Z"); + randomZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomZMin.output.connectTo(randomZ.min); + randomZMax.output.connectTo(randomZ.max); + + // STEP 8: Random angle (-PI to PI) + const randomAngleMin = new ParticleInputBlock("Random Angle Min"); + randomAngleMin.value = -Math.PI; + const randomAngleMax = new ParticleInputBlock("Random Angle Max"); + randomAngleMax.value = Math.PI; + const randomAngle = new ParticleRandomBlock("Random Angle"); + randomAngle.lockMode = ParticleRandomBlockLocks.PerParticle; + randomAngleMin.output.connectTo(randomAngle.min); + randomAngleMax.output.connectTo(randomAngle.max); + + // STEP 9: Random range (1 to 5) - smaller range for Y position + const randomRangeMin = new ParticleInputBlock("Random Range Min"); + randomRangeMin.value = 1; + const randomRangeMax = new ParticleInputBlock("Random Range Max"); + randomRangeMax.value = 5; + const randomRange = new ParticleRandomBlock("Random Range"); + randomRange.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRangeMin.output.connectTo(randomRange.min); + randomRangeMax.output.connectTo(randomRange.max); + + // STEP 10: Calculate Y position: range * (1 + cos(angle)) + const one = new ParticleInputBlock("One"); + one.value = 1; + const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); + cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; + randomAngle.output.connectTo(cosAngle.input); + const addOne = new ParticleMathBlock("Add One"); + addOne.operation = ParticleMathBlockOperations.Add; + one.output.connectTo(addOne.left); + cosAngle.output.connectTo(addOne.right); + const multiplyRange = new ParticleMathBlock("Multiply Range"); + multiplyRange.operation = ParticleMathBlockOperations.Multiply; + randomRange.output.connectTo(multiplyRange.left); + addOne.output.connectTo(multiplyRange.right); + + // STEP 11: Combine X, Y, Z into Vector3 using ConverterBlock + const positionConverter = new ParticleConverterBlock("Position Converter"); + randomX.output.connectTo(positionConverter.xIn); + multiplyRange.output.connectTo(positionConverter.yIn); + randomZ.output.connectTo(positionConverter.zIn); + positionConverter.xyzOut.connectTo(spsInitTetra.position); + + // STEP 12: Random rotation X (-PI to PI) + const randomRotXMin = new ParticleInputBlock("Random Rot X Min"); + randomRotXMin.value = -Math.PI; + const randomRotXMax = new ParticleInputBlock("Random Rot X Max"); + randomRotXMax.value = Math.PI; + const randomRotX = new ParticleRandomBlock("Random Rot X"); + randomRotX.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotXMin.output.connectTo(randomRotX.min); + randomRotXMax.output.connectTo(randomRotX.max); + + // STEP 13: Random rotation Y (-PI to PI) + const randomRotYMin = new ParticleInputBlock("Random Rot Y Min"); + randomRotYMin.value = -Math.PI; + const randomRotYMax = new ParticleInputBlock("Random Rot Y Max"); + randomRotYMax.value = Math.PI; + const randomRotY = new ParticleRandomBlock("Random Rot Y"); + randomRotY.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotYMin.output.connectTo(randomRotY.min); + randomRotYMax.output.connectTo(randomRotY.max); + + // STEP 14: Random rotation Z (-PI to PI) + const randomRotZMin = new ParticleInputBlock("Random Rot Z Min"); + randomRotZMin.value = -Math.PI; + const randomRotZMax = new ParticleInputBlock("Random Rot Z Max"); + randomRotZMax.value = Math.PI; + const randomRotZ = new ParticleRandomBlock("Random Rot Z"); + randomRotZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotZMin.output.connectTo(randomRotZ.min); + randomRotZMax.output.connectTo(randomRotZ.max); + + // STEP 15: Rotation Vector3 using ConverterBlock + const rotationConverter = new ParticleConverterBlock("Rotation Converter"); + randomRotX.output.connectTo(rotationConverter.xIn); + randomRotY.output.connectTo(rotationConverter.yIn); + randomRotZ.output.connectTo(rotationConverter.zIn); + rotationConverter.xyzOut.connectTo(spsInitTetra.rotation); + + // STEP 16: Random color using ConverterBlock + const randomColorMin = new ParticleInputBlock("Random Color Min"); + randomColorMin.value = 0; + const randomColorMax = new ParticleInputBlock("Random Color Max"); + randomColorMax.value = 1; + const randomColorR = new ParticleRandomBlock("Random Color R"); + randomColorR.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorR.min); + randomColorMax.output.connectTo(randomColorR.max); + const randomColorG = new ParticleRandomBlock("Random Color G"); + randomColorG.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorG.min); + randomColorMax.output.connectTo(randomColorG.max); + const randomColorB = new ParticleRandomBlock("Random Color B"); + randomColorB.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorB.min); + randomColorMax.output.connectTo(randomColorB.max); + const colorOne = new ParticleInputBlock("Color Alpha"); + colorOne.value = 1; + const colorConverter = new ParticleConverterBlock("Color Converter"); + randomColorR.output.connectTo(colorConverter.xIn); + randomColorG.output.connectTo(colorConverter.yIn); + randomColorB.output.connectTo(colorConverter.zIn); + colorOne.output.connectTo(colorConverter.wIn); + colorConverter.colorOut.connectTo(spsInitTetra.color); this._systemBlocks.push(spsSystem); } From dc4db46341e1944f537f9e2488ac7be7157c967d Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 10:32:47 +0300 Subject: [PATCH 18/68] Refactor SPSInitBlock and SPSUpdateBlock to streamline value retrieval This commit refactors the SPSInitBlock and SPSUpdateBlock classes by removing redundant private methods for retrieving position, velocity, color, scaling, and rotation values. Instead, the blocks now directly use the getConnectedValue method for connected properties, optimizing performance and simplifying the code structure. These changes enhance clarity and maintainability within the SolidParticle system. --- .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 45 +++---------------- .../Blocks/SolidParticle/SPSUpdateBlock.ts | 45 +++---------------- 2 files changed, 10 insertions(+), 80 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts index df424f3d5b3..94fa69bb512 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -53,68 +53,33 @@ export class SPSInitBlock extends NodeParticleBlock { const initData = {} as ISPSUpdateData; if (this.position.isConnected) { initData.position = () => { - return this.getPosition(state); + return this.position.getConnectedValue(state); }; } if (this.velocity.isConnected) { initData.velocity = () => { - return this.getVelocity(state); + return this.velocity.getConnectedValue(state); }; } if (this.color.isConnected) { initData.color = () => { - return this.getColor(state); + return this.color.getConnectedValue(state); }; } if (this.scaling.isConnected) { initData.scaling = () => { - return this.getScaling(state); + return this.scaling.getConnectedValue(state); }; } if (this.rotation.isConnected) { initData.rotation = () => { - return this.getRotation(state); + return this.rotation.getConnectedValue(state); }; } this.initData._storedValue = initData; } - private getPosition(state: NodeParticleBuildState) { - if (this.position._storedFunction) { - return this.position._storedFunction(state); - } - return this.position.getConnectedValue(state); - } - - private getVelocity(state: NodeParticleBuildState) { - if (this.velocity._storedFunction) { - return this.velocity._storedFunction(state); - } - return this.velocity.getConnectedValue(state); - } - - private getColor(state: NodeParticleBuildState) { - if (this.color._storedFunction) { - return this.color._storedFunction(state); - } - return this.color.getConnectedValue(state); - } - - private getScaling(state: NodeParticleBuildState) { - if (this.scaling._storedFunction) { - return this.scaling._storedFunction(state); - } - return this.scaling.getConnectedValue(state); - } - - private getRotation(state: NodeParticleBuildState) { - if (this.rotation._storedFunction) { - return this.rotation._storedFunction(state); - } - return this.rotation.getConnectedValue(state); - } - public override serialize(): any { const serializationObject = super.serialize(); return serializationObject; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index cadae7dea94..4fac6e6af07 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -53,67 +53,32 @@ export class SPSUpdateBlock extends NodeParticleBlock { const updateData: ISPSUpdateData = {} as ISPSUpdateData; if (this.position.isConnected) { updateData.position = () => { - return this.getPosition(state); + return this.position.getConnectedValue(state); }; } if (this.velocity.isConnected) { updateData.velocity = () => { - return this.getVelocity(state); + return this.velocity.getConnectedValue(state); }; } if (this.color.isConnected) { updateData.color = () => { - return this.getColor(state); + return this.color.getConnectedValue(state); }; } if (this.scaling.isConnected) { updateData.scaling = () => { - return this.getScaling(state); + return this.scaling.getConnectedValue(state); }; } if (this.rotation.isConnected) { updateData.rotation = () => { - return this.getRotation(state); + return this.rotation.getConnectedValue(state); }; } this.updateData._storedValue = updateData; } - private getPosition(state: NodeParticleBuildState) { - if (this.position._storedFunction) { - return this.position._storedFunction(state); - } - return this.position.getConnectedValue(state); - } - - private getVelocity(state: NodeParticleBuildState) { - if (this.velocity._storedFunction) { - return this.velocity._storedFunction(state); - } - return this.velocity.getConnectedValue(state); - } - - private getColor(state: NodeParticleBuildState) { - if (this.color._storedFunction) { - return this.color._storedFunction(state); - } - return this.color.getConnectedValue(state); - } - - private getScaling(state: NodeParticleBuildState) { - if (this.scaling._storedFunction) { - return this.scaling._storedFunction(state); - } - return this.scaling.getConnectedValue(state); - } - - private getRotation(state: NodeParticleBuildState) { - if (this.rotation._storedFunction) { - return this.rotation._storedFunction!(state); - } - return this.rotation.getConnectedValue(state); - } - public override serialize(): any { const serializationObject = super.serialize(); return serializationObject; From d116da5f504b0cd9df21f617476cca0c12031d8b Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 11:39:51 +0300 Subject: [PATCH 19/68] Refactor NodeParticleSystemSet to streamline random position and rotation handling This commit updates the NodeParticleSystemSet class to simplify the random positioning and rotation of particles by utilizing Vector2 and Vector3 for combined input blocks. The previous individual random blocks for X, Y, and Z positions and rotations have been consolidated into more efficient structures, enhancing clarity and maintainability. Additionally, the random color generation has been updated to use Vector3, improving the overall functionality of the particle system. --- .../Particles/Node/nodeParticleSystemSet.ts | 125 ++++++------------ 1 file changed, 38 insertions(+), 87 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 9d145aeb24c..ac0dff1a1f3 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,6 +22,7 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; +import { Vector2, Vector3 } from "core/Maths/math.vector"; import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock } from "./Blocks"; import { ParticleSystem } from ".."; import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; @@ -403,48 +404,33 @@ export class NodeParticleSystemSet { this.clear(); this.editorData = null; - // STEP 1: Create basic SPS system const spsSystem = new SPSSystemBlock("SPS System"); spsSystem.billboard = false; - // STEP 2: Create SPS creation block const spsCreateBlock = new SPSCreateBlock("Create Particles System"); spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); - // STEP 3: Create particle config block with count const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); - spsCreateTetra.count.value = 2000; // Start with small number for testing + spsCreateTetra.count.value = 2000; spsCreateTetra.particleConfig.connectTo(spsCreateBlock.particleConfig); - // STEP 4: Create mesh source (using Box first to test) const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); - meshSourceTetra.shapeType = SPSMeshShapeType.Box; // Start with Box to test + meshSourceTetra.shapeType = SPSMeshShapeType.Box; meshSourceTetra.size = 0.3; meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); - // STEP 5: Create init block const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); - // STEP 6: Random X position (-10 to 10) - const randomXMin = new ParticleInputBlock("Random X Min"); - randomXMin.value = -10; - const randomXMax = new ParticleInputBlock("Random X Max"); - randomXMax.value = 10; - const randomX = new ParticleRandomBlock("Random X"); - randomX.lockMode = ParticleRandomBlockLocks.PerParticle; - randomXMin.output.connectTo(randomX.min); - randomXMax.output.connectTo(randomX.max); - - // STEP 7: Random Z position (-10 to 10) - const randomZMin = new ParticleInputBlock("Random Z Min"); - randomZMin.value = -10; - const randomZMax = new ParticleInputBlock("Random Z Max"); - randomZMax.value = 10; - const randomZ = new ParticleRandomBlock("Random Z"); - randomZ.lockMode = ParticleRandomBlockLocks.PerParticle; - randomZMin.output.connectTo(randomZ.min); - randomZMax.output.connectTo(randomZ.max); + // STEP 6: Random X, Z position using Vector2 (-10 to 10) + const randomXZMin = new ParticleInputBlock("Random XZ Min"); + randomXZMin.value = new Vector2(-10, -10); + const randomXZMax = new ParticleInputBlock("Random XZ Max"); + randomXZMax.value = new Vector2(10, 10); + const randomXZ = new ParticleRandomBlock("Random XZ"); + randomXZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomXZMin.output.connectTo(randomXZ.min); + randomXZMax.output.connectTo(randomXZ.max); // STEP 8: Random angle (-PI to PI) const randomAngleMin = new ParticleInputBlock("Random Angle Min"); @@ -481,74 +467,39 @@ export class NodeParticleSystemSet { randomRange.output.connectTo(multiplyRange.left); addOne.output.connectTo(multiplyRange.right); - // STEP 11: Combine X, Y, Z into Vector3 using ConverterBlock + const extractXZ = new ParticleConverterBlock("Extract XZ"); + randomXZ.output.connectTo(extractXZ.xyIn); const positionConverter = new ParticleConverterBlock("Position Converter"); - randomX.output.connectTo(positionConverter.xIn); + extractXZ.xOut.connectTo(positionConverter.xIn); multiplyRange.output.connectTo(positionConverter.yIn); - randomZ.output.connectTo(positionConverter.zIn); + extractXZ.yOut.connectTo(positionConverter.zIn); positionConverter.xyzOut.connectTo(spsInitTetra.position); - // STEP 12: Random rotation X (-PI to PI) - const randomRotXMin = new ParticleInputBlock("Random Rot X Min"); - randomRotXMin.value = -Math.PI; - const randomRotXMax = new ParticleInputBlock("Random Rot X Max"); - randomRotXMax.value = Math.PI; - const randomRotX = new ParticleRandomBlock("Random Rot X"); - randomRotX.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotXMin.output.connectTo(randomRotX.min); - randomRotXMax.output.connectTo(randomRotX.max); - - // STEP 13: Random rotation Y (-PI to PI) - const randomRotYMin = new ParticleInputBlock("Random Rot Y Min"); - randomRotYMin.value = -Math.PI; - const randomRotYMax = new ParticleInputBlock("Random Rot Y Max"); - randomRotYMax.value = Math.PI; - const randomRotY = new ParticleRandomBlock("Random Rot Y"); - randomRotY.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotYMin.output.connectTo(randomRotY.min); - randomRotYMax.output.connectTo(randomRotY.max); - - // STEP 14: Random rotation Z (-PI to PI) - const randomRotZMin = new ParticleInputBlock("Random Rot Z Min"); - randomRotZMin.value = -Math.PI; - const randomRotZMax = new ParticleInputBlock("Random Rot Z Max"); - randomRotZMax.value = Math.PI; - const randomRotZ = new ParticleRandomBlock("Random Rot Z"); - randomRotZ.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotZMin.output.connectTo(randomRotZ.min); - randomRotZMax.output.connectTo(randomRotZ.max); - - // STEP 15: Rotation Vector3 using ConverterBlock - const rotationConverter = new ParticleConverterBlock("Rotation Converter"); - randomRotX.output.connectTo(rotationConverter.xIn); - randomRotY.output.connectTo(rotationConverter.yIn); - randomRotZ.output.connectTo(rotationConverter.zIn); - rotationConverter.xyzOut.connectTo(spsInitTetra.rotation); - - // STEP 16: Random color using ConverterBlock + // STEP 12: Random rotation using Vector3 (-PI to PI) + const randomRotMin = new ParticleInputBlock("Random Rot Min"); + randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); + const randomRotMax = new ParticleInputBlock("Random Rot Max"); + randomRotMax.value = new Vector3(Math.PI, Math.PI, Math.PI); + const randomRot = new ParticleRandomBlock("Random Rotation"); + randomRot.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotMin.output.connectTo(randomRot.min); + randomRotMax.output.connectTo(randomRot.max); + randomRot.output.connectTo(spsInitTetra.rotation); + + // STEP 16: Random color using Vector3 random and ConverterBlock const randomColorMin = new ParticleInputBlock("Random Color Min"); - randomColorMin.value = 0; + randomColorMin.value = new Vector3(0, 0, 0); const randomColorMax = new ParticleInputBlock("Random Color Max"); - randomColorMax.value = 1; - const randomColorR = new ParticleRandomBlock("Random Color R"); - randomColorR.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorR.min); - randomColorMax.output.connectTo(randomColorR.max); - const randomColorG = new ParticleRandomBlock("Random Color G"); - randomColorG.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorG.min); - randomColorMax.output.connectTo(randomColorG.max); - const randomColorB = new ParticleRandomBlock("Random Color B"); - randomColorB.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorB.min); - randomColorMax.output.connectTo(randomColorB.max); - const colorOne = new ParticleInputBlock("Color Alpha"); - colorOne.value = 1; + randomColorMax.value = new Vector3(1, 1, 1); + const randomColorRGB = new ParticleRandomBlock("Random Color RGB"); + randomColorRGB.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorRGB.min); + randomColorMax.output.connectTo(randomColorRGB.max); + const colorAlpha = new ParticleInputBlock("Color Alpha"); + colorAlpha.value = 1; const colorConverter = new ParticleConverterBlock("Color Converter"); - randomColorR.output.connectTo(colorConverter.xIn); - randomColorG.output.connectTo(colorConverter.yIn); - randomColorB.output.connectTo(colorConverter.zIn); - colorOne.output.connectTo(colorConverter.wIn); + randomColorRGB.output.connectTo(colorConverter.xyzIn); + colorAlpha.output.connectTo(colorConverter.wIn); colorConverter.colorOut.connectTo(spsInitTetra.color); this._systemBlocks.push(spsSystem); From 69a803a909aaa63bc9ca58efde96a935f2f51f11 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 11:40:02 +0300 Subject: [PATCH 20/68] Remove commented-out steps in NodeParticleSystemSet for improved code clarity This commit cleans up the NodeParticleSystemSet class by removing outdated comments related to random positioning, rotation, and color generation steps. The removal of these comments enhances the readability of the code and focuses on the current implementation without unnecessary distractions. --- .../dev/core/src/Particles/Node/nodeParticleSystemSet.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index ac0dff1a1f3..934635f7c3c 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -422,7 +422,6 @@ export class NodeParticleSystemSet { const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); - // STEP 6: Random X, Z position using Vector2 (-10 to 10) const randomXZMin = new ParticleInputBlock("Random XZ Min"); randomXZMin.value = new Vector2(-10, -10); const randomXZMax = new ParticleInputBlock("Random XZ Max"); @@ -432,7 +431,6 @@ export class NodeParticleSystemSet { randomXZMin.output.connectTo(randomXZ.min); randomXZMax.output.connectTo(randomXZ.max); - // STEP 8: Random angle (-PI to PI) const randomAngleMin = new ParticleInputBlock("Random Angle Min"); randomAngleMin.value = -Math.PI; const randomAngleMax = new ParticleInputBlock("Random Angle Max"); @@ -442,7 +440,6 @@ export class NodeParticleSystemSet { randomAngleMin.output.connectTo(randomAngle.min); randomAngleMax.output.connectTo(randomAngle.max); - // STEP 9: Random range (1 to 5) - smaller range for Y position const randomRangeMin = new ParticleInputBlock("Random Range Min"); randomRangeMin.value = 1; const randomRangeMax = new ParticleInputBlock("Random Range Max"); @@ -452,7 +449,6 @@ export class NodeParticleSystemSet { randomRangeMin.output.connectTo(randomRange.min); randomRangeMax.output.connectTo(randomRange.max); - // STEP 10: Calculate Y position: range * (1 + cos(angle)) const one = new ParticleInputBlock("One"); one.value = 1; const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); @@ -475,7 +471,6 @@ export class NodeParticleSystemSet { extractXZ.yOut.connectTo(positionConverter.zIn); positionConverter.xyzOut.connectTo(spsInitTetra.position); - // STEP 12: Random rotation using Vector3 (-PI to PI) const randomRotMin = new ParticleInputBlock("Random Rot Min"); randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); const randomRotMax = new ParticleInputBlock("Random Rot Max"); @@ -486,7 +481,6 @@ export class NodeParticleSystemSet { randomRotMax.output.connectTo(randomRot.max); randomRot.output.connectTo(spsInitTetra.rotation); - // STEP 16: Random color using Vector3 random and ConverterBlock const randomColorMin = new ParticleInputBlock("Random Color Min"); randomColorMin.value = new Vector3(0, 0, 0); const randomColorMax = new ParticleInputBlock("Random Color Max"); From 50115b21481f3ffa19df2fcb018b1b1779e4becf Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 12:58:17 +0300 Subject: [PATCH 21/68] Add ParticlePropsSetBlock and ParticlePropsGetBlock for dynamic property management in particles This commit introduces two new blocks, ParticlePropsSetBlock and ParticlePropsGetBlock, to facilitate the setting and retrieval of custom properties in particle.props. The ParticlePropsSetBlock allows for storing values, while the ParticlePropsGetBlock enables reading these values dynamically. Additionally, the NodeParticleSystemSet class is updated to utilize these new blocks, enhancing the flexibility and functionality of the particle system. The node list component is also updated to include descriptions for the new blocks, improving user understanding and accessibility. --- .../SolidParticle/ParticlePropsGetBlock.ts | 115 ++++++++++++ .../SolidParticle/ParticlePropsSetBlock.ts | 118 +++++++++++++ .../Node/Blocks/SolidParticle/index.ts | 2 + .../Particles/Node/nodeParticleSystemSet.ts | 164 +++++++++++------- .../components/nodeList/nodeListComponent.tsx | 13 +- 5 files changed, 350 insertions(+), 62 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts new file mode 100644 index 00000000000..9bf23b3d5f9 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts @@ -0,0 +1,115 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { SolidParticle } from "../../../solidParticle"; + +/** + * Block used to get custom properties from particle.props + * Works similar to contextual blocks but for dynamic property names + */ +export class ParticlePropsGetBlock extends NodeParticleBlock { + /** + * Gets or sets the property name to read from particle.props + */ + public propertyName: string = "value"; + + /** + * Gets or sets the connection point type (default float) + */ + private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; + + public constructor(name: string) { + super(name); + + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.AutoDetect); + // Set default type + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + public override getClassName() { + return "ParticlePropsGetBlock"; + } + + /** + * Gets or sets the connection point type + */ + public get type(): NodeParticleBlockConnectionPointTypes { + return this._type; + } + + public set type(value: NodeParticleBlockConnectionPointTypes) { + if (this._type !== value) { + this._type = value; + // Update output type + (this._outputs[0] as any)._type = value; + (this._outputs[0] as any)._defaultConnectionPointType = value; + } + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + // Validate property name + if (!this.propertyName || this.propertyName.trim() === "") { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + // Validate type + if (this._type === NodeParticleBlockConnectionPointTypes.Undefined || this._type === NodeParticleBlockConnectionPointTypes.AutoDetect) { + this._type = NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._type = this._type; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + const propertyName = this.propertyName; + + const func = (state: NodeParticleBuildState) => { + if (!state.particleContext) { + return null; + } + + const particle = state.particleContext as SolidParticle; + + if (!particle.props) { + return null; + } + + const value = particle.props[propertyName]; + + if (value === undefined) { + return null; + } + + return value; + }; + + if (this.output.isConnected) { + this.output._storedFunction = func; + } else { + this.output._storedValue = func(state); + } + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.propertyName = this.propertyName; + serializationObject.type = this._type; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.propertyName = serializationObject.propertyName || "value"; + this._type = serializationObject.type || NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._type = this._type; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } +} + +RegisterClass("BABYLON.ParticlePropsGetBlock", ParticlePropsGetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts new file mode 100644 index 00000000000..76b8e33e837 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts @@ -0,0 +1,118 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { SolidParticle } from "../../../solidParticle"; + +/** + * Block used to set custom properties in particle.props + * Works as a side-effect block that stores values and passes them through + */ +export class ParticlePropsSetBlock extends NodeParticleBlock { + /** + * Gets or sets the property name to store in particle.props + */ + public propertyName: string = "value"; + + /** + * Gets or sets the connection point type (default float) + */ + private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; + + public constructor(name: string) { + super(name); + + this.registerInput("value", NodeParticleBlockConnectionPointTypes.AutoDetect, true); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.BasedOnInput); + + // Link output type to input type + this._outputs[0]._typeConnectionSource = this._inputs[0]; + // Set default type for when input is not connected + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + public override getClassName() { + return "ParticlePropsSetBlock"; + } + + public get value(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * Gets or sets the connection point type + */ + public get type(): NodeParticleBlockConnectionPointTypes { + return this._type; + } + + public set type(value: NodeParticleBlockConnectionPointTypes) { + if (this._type !== value) { + this._type = value; + // Update default type (used when input is not connected) + (this._outputs[0] as any)._defaultConnectionPointType = value; + } + } + + public override _build(state: NodeParticleBuildState) { + // Validate property name + if (!this.propertyName || this.propertyName.trim() === "") { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + if (!this.value.isConnected) { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + const propertyName = this.propertyName; + + const func = (state: NodeParticleBuildState) => { + if (!state.particleContext) { + return null; + } + + const particle = state.particleContext as SolidParticle; + + const value = this.value.getConnectedValue(state); + + if (!particle.props) { + particle.props = {}; + } + + particle.props[propertyName] = value; + + return value; + }; + + if (this.output.isConnected) { + this.output._storedFunction = func; + } else { + this.output._storedValue = func(state); + } + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.propertyName = this.propertyName; + serializationObject.type = this._type; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.propertyName = serializationObject.propertyName || "value"; + this._type = serializationObject.type || NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } +} + +RegisterClass("BABYLON.ParticlePropsSetBlock", ParticlePropsSetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 070d7ebd205..66eb47cd817 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -6,3 +6,5 @@ export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; export * from "./SPSInitBlock"; export * from "./SPSCreateBlock"; +export * from "./ParticlePropsSetBlock"; +export * from "./ParticlePropsGetBlock"; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 934635f7c3c..83bf201fbbb 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -23,11 +23,23 @@ import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; import { Vector2, Vector3 } from "core/Maths/math.vector"; -import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock } from "./Blocks"; +import { + SPSParticleConfigBlock, + SPSInitBlock, + SPSMeshShapeType, + SPSMeshSourceBlock, + SPSSystemBlock, + SPSCreateBlock, + SPSUpdateBlock, + ParticlePropsSetBlock, + ParticlePropsGetBlock, +} from "./Blocks"; import { ParticleSystem } from ".."; import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; +import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -343,64 +355,6 @@ export class NodeParticleSystemSet { } public setToDefaultSPS() { - // this.clear(); - // this.editorData = null; - // const spsSystem = new SPSSystemBlock("SPS System"); - // spsSystem.billboard = false; - - // const spsCreateBlock = new SPSCreateBlock("Create Particles System"); - // spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); - - // const spsCreateBox = new SPSParticleConfigBlock("Create Box Particles"); - // const spsCreateSphere = new SPSParticleConfigBlock("Create Sphere Particles"); - - // spsCreateBox.count.value = 5; - // spsCreateSphere.count.value = 1; - - // spsCreateBox.particleConfig.connectTo(spsCreateBlock.particleConfig); - // spsCreateSphere.particleConfig.connectTo(spsCreateBlock.particleConfig); - - // const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); - // const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); - - // meshSourceBox.shapeType = SPSMeshShapeType.Box; - // meshSourceSphere.shapeType = SPSMeshShapeType.Sphere; - - // meshSourceBox.size = 1; - // meshSourceSphere.size = 1; - - // meshSourceBox.mesh.connectTo(spsCreateBox.mesh); - // meshSourceSphere.mesh.connectTo(spsCreateSphere.mesh); - - // const spsInitBox = new SPSInitBlock("Initialize Box Particles"); - // const spsInitSphere = new SPSInitBlock("Initialize Sphere Particles"); - - // spsInitBox.initData.connectTo(spsCreateBox.initBlock); - // spsInitSphere.initData.connectTo(spsCreateSphere.initBlock); - - // const positionBlockBox = new ParticleInputBlock("Position"); - // positionBlockBox.value = new Vector3(1, 1, 1); - // positionBlockBox.output.connectTo(spsInitBox.position); - - // const rotationBlockBox = new ParticleInputBlock("Rotation"); - // rotationBlockBox.value = new Vector3(3, 0, 0); - // rotationBlockBox.output.connectTo(spsInitBox.rotation); - - // const positionBlockSphere = new ParticleInputBlock("Position"); - // positionBlockSphere.value = new Vector3(0, 0, 0); - // positionBlockSphere.output.connectTo(spsInitSphere.position); - - // const spsUpdateBox = new SPSUpdateBlock("Update Box Particles"); - // const spsUpdateSphere = new SPSUpdateBlock("Update Sphere Particles"); - - // spsUpdateBox.updateData.connectTo(spsCreateBox.updateBlock); - // spsUpdateSphere.updateData.connectTo(spsCreateSphere.updateBlock); - - // this._systemBlocks.push(spsSystem); - this.setToTetrahedronSPS(); - } - - public setToTetrahedronSPS() { this.clear(); this.editorData = null; @@ -453,14 +407,21 @@ export class NodeParticleSystemSet { one.value = 1; const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; - randomAngle.output.connectTo(cosAngle.input); + // Store angle in props so we can reuse during update + const setAnglePropInit = new ParticlePropsSetBlock("Set Angle Prop Init"); + setAnglePropInit.propertyName = "angle"; + randomAngle.output.connectTo(setAnglePropInit.value); + setAnglePropInit.output.connectTo(cosAngle.input); const addOne = new ParticleMathBlock("Add One"); addOne.operation = ParticleMathBlockOperations.Add; one.output.connectTo(addOne.left); cosAngle.output.connectTo(addOne.right); const multiplyRange = new ParticleMathBlock("Multiply Range"); multiplyRange.operation = ParticleMathBlockOperations.Multiply; - randomRange.output.connectTo(multiplyRange.left); + const setRangePropInit = new ParticlePropsSetBlock("Set Range Prop Init"); + setRangePropInit.propertyName = "range"; + randomRange.output.connectTo(setRangePropInit.value); + setRangePropInit.output.connectTo(multiplyRange.left); addOne.output.connectTo(multiplyRange.right); const extractXZ = new ParticleConverterBlock("Extract XZ"); @@ -496,6 +457,87 @@ export class NodeParticleSystemSet { colorAlpha.output.connectTo(colorConverter.wIn); colorConverter.colorOut.connectTo(spsInitTetra.color); + // Create update block + const spsUpdateTetra = new SPSUpdateBlock("Update Tetrahedron Particles"); + spsUpdateTetra.updateData.connectTo(spsCreateTetra.updateBlock); + + // Get current position (X, Z stay the same, Y updates) + const currentPosition = new ParticleInputBlock("Current Position"); + currentPosition.contextualValue = NodeParticleContextualSources.Position; + + // Extract X and Z from current position + const extractPosition = new ParticleConverterBlock("Extract Position"); + currentPosition.output.connectTo(extractPosition.xyzIn); + + // Retrieve stored properties + const getAngleProp = new ParticlePropsGetBlock("Get Angle Prop"); + getAngleProp.propertyName = "angle"; + getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRangeProp = new ParticlePropsGetBlock("Get Range Prop"); + getRangeProp.propertyName = "range"; + getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; + + // Accumulate angle using delta time to avoid relying on absolute frame id + const deltaBlock = new ParticleInputBlock("Delta Time"); + deltaBlock.systemSource = NodeParticleSystemSources.Delta; + + const milliToSecond = new ParticleInputBlock("Milli To Second"); + milliToSecond.value = 0.001; + + const deltaSeconds = new ParticleMathBlock("Delta Seconds"); + deltaSeconds.operation = ParticleMathBlockOperations.Multiply; + deltaBlock.output.connectTo(deltaSeconds.left); + milliToSecond.output.connectTo(deltaSeconds.right); + + const targetFps = new ParticleInputBlock("Target FPS"); + targetFps.value = 60; + + const normalizedDelta = new ParticleMathBlock("Normalized Delta"); + normalizedDelta.operation = ParticleMathBlockOperations.Multiply; + deltaSeconds.output.connectTo(normalizedDelta.left); + targetFps.output.connectTo(normalizedDelta.right); + + const speedPerFrame = new ParticleInputBlock("Speed Per Frame"); + speedPerFrame.value = Math.PI / 100; + + const scaledIncrement = new ParticleMathBlock("Scaled Increment"); + scaledIncrement.operation = ParticleMathBlockOperations.Multiply; + speedPerFrame.output.connectTo(scaledIncrement.left); + normalizedDelta.output.connectTo(scaledIncrement.right); + + const accumulateAngle = new ParticleMathBlock("Accumulate Angle"); + accumulateAngle.operation = ParticleMathBlockOperations.Add; + getAngleProp.output.connectTo(accumulateAngle.left); + scaledIncrement.output.connectTo(accumulateAngle.right); + + const setAnglePropUpdate = new ParticlePropsSetBlock("Set Angle Prop Update"); + setAnglePropUpdate.propertyName = "angle"; + setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + accumulateAngle.output.connectTo(setAnglePropUpdate.value); + + // Calculate new Y position: range * (1 + cos(angle)) + const oneUpdate = new ParticleInputBlock("One Update"); + oneUpdate.value = 1; + const cosUpdatedAngle = new ParticleTrigonometryBlock("Cos Updated Angle"); + cosUpdatedAngle.operation = ParticleTrigonometryBlockOperations.Cos; + setAnglePropUpdate.output.connectTo(cosUpdatedAngle.input); + const addOneUpdate = new ParticleMathBlock("Add One Update"); + addOneUpdate.operation = ParticleMathBlockOperations.Add; + oneUpdate.output.connectTo(addOneUpdate.left); + cosUpdatedAngle.output.connectTo(addOneUpdate.right); + const multiplyRangeUpdate = new ParticleMathBlock("Multiply Range Update"); + multiplyRangeUpdate.operation = ParticleMathBlockOperations.Multiply; + getRangeProp.output.connectTo(multiplyRangeUpdate.left); + addOneUpdate.output.connectTo(multiplyRangeUpdate.right); + + // Combine X (from current position), Y (new), Z (from current position) + const updatePositionConverter = new ParticleConverterBlock("Update Position Converter"); + extractPosition.xOut.connectTo(updatePositionConverter.xIn); + multiplyRangeUpdate.output.connectTo(updatePositionConverter.yIn); + extractPosition.zOut.connectTo(updatePositionConverter.zIn); + updatePositionConverter.xyzOut.connectTo(spsUpdateTetra.position); + this._systemBlocks.push(spsSystem); } diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 412eed10b4e..d27bf6440eb 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -27,6 +27,8 @@ export class NodeListComponent extends React.Component Date: Fri, 14 Nov 2025 13:35:13 +0300 Subject: [PATCH 22/68] Enhance particle property management with String type support and display value retrieval This commit adds support for String properties in the ParticlePropsGetBlock and ParticlePropsSetBlock, allowing for dynamic management of string-type properties in particle systems. The displayValue method is introduced to provide a user-friendly representation of the property name. Additionally, the InputDisplayManager is updated to handle string values appropriately, improving the overall functionality and user experience in the particle editor. The GenericPropertyTabComponent is also enhanced to include a text input for string properties, further streamlining property management. --- .../dev/core/src/Decorators/nodeDecorator.ts | 2 ++ .../SolidParticle/ParticlePropsGetBlock.ts | 14 ++++++++ .../SolidParticle/ParticlePropsSetBlock.ts | 15 ++++++++ .../Particles/Node/nodeParticleSystemSet.ts | 2 +- .../display/inputDisplayManager.ts | 35 ++++++++++++++----- .../genericNodePropertyComponent.tsx | 14 ++++++++ .../graphSystem/registerToDisplayLedger.ts | 2 ++ 7 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/dev/core/src/Decorators/nodeDecorator.ts b/packages/dev/core/src/Decorators/nodeDecorator.ts index cca7a4d3b5f..eff1923a106 100644 --- a/packages/dev/core/src/Decorators/nodeDecorator.ts +++ b/packages/dev/core/src/Decorators/nodeDecorator.ts @@ -19,6 +19,8 @@ export const enum PropertyTypeForEdition { List, /** property is a Color4 */ Color4, + /** property is a string */ + String, /** property (int) should be edited as a combo box with a list of sampling modes */ SamplingMode, /** property (int) should be edited as a combo box with a list of texture formats */ diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts index 9bf23b3d5f9..c0fc904c3de 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts @@ -4,6 +4,8 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { SolidParticle } from "../../../solidParticle"; +import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; +import { serialize } from "../../../../Misc/decorators"; /** * Block used to get custom properties from particle.props @@ -13,6 +15,11 @@ export class ParticlePropsGetBlock extends NodeParticleBlock { /** * Gets or sets the property name to read from particle.props */ + @serialize("propertyName") + @editableInPropertyPage("Property Name", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + notifiers: { rebuild: true }, + }) public propertyName: string = "value"; /** @@ -20,6 +27,13 @@ export class ParticlePropsGetBlock extends NodeParticleBlock { */ private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; + /** + * Gets the value to display (returns propertyName as string) + */ + public get displayValue(): string { + return this.propertyName || "value"; + } + public constructor(name: string) { super(name); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts index 76b8e33e837..6313d5a7c58 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts @@ -4,6 +4,8 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { SolidParticle } from "../../../solidParticle"; +import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; +import { serialize } from "../../../../Misc/decorators"; /** * Block used to set custom properties in particle.props @@ -13,6 +15,11 @@ export class ParticlePropsSetBlock extends NodeParticleBlock { /** * Gets or sets the property name to store in particle.props */ + @serialize("propertyName") + @editableInPropertyPage("Property Name", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + notifiers: { rebuild: true }, + }) public propertyName: string = "value"; /** @@ -40,6 +47,14 @@ export class ParticlePropsSetBlock extends NodeParticleBlock { return this._inputs[0]; } + /** + * Gets the value to display (returns propertyName as string) + * This shadows the connection point name for display purposes + */ + public get displayValue(): string { + return this.propertyName || "value"; + } + public get output(): NodeParticleConnectionPoint { return this._outputs[0]; } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 83bf201fbbb..ba703e9e541 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -370,7 +370,7 @@ export class NodeParticleSystemSet { const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); meshSourceTetra.shapeType = SPSMeshShapeType.Box; - meshSourceTetra.size = 0.3; + meshSourceTetra.size = 0.1; meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts b/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts index b6b8a16e6c3..ebfa1148fa9 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts @@ -121,28 +121,47 @@ export class InputDisplayManager implements IDisplayManager { break; } } else { + const block = nodeData.data as any; + const blockValue = block.displayValue !== undefined ? block.displayValue : (inputBlock as any).value; + const isStringValue = typeof blockValue === "string"; + switch (inputBlock.type) { case NodeParticleBlockConnectionPointTypes.Int: - value = inputBlock.value.toFixed(0); + value = isStringValue ? blockValue : inputBlock.value.toFixed(0); break; case NodeParticleBlockConnectionPointTypes.Float: - value = inputBlock.value.toFixed(4); + value = isStringValue ? blockValue : inputBlock.value.toFixed(4); break; case NodeParticleBlockConnectionPointTypes.Vector2: { - const vec2Value = inputBlock.value as Vector2; - value = `(${vec2Value.x.toFixed(2)}, ${vec2Value.y.toFixed(2)})`; + if (isStringValue) { + value = blockValue; + } else { + const vec2Value = inputBlock.value as Vector2; + value = `(${vec2Value.x.toFixed(2)}, ${vec2Value.y.toFixed(2)})`; + } break; } case NodeParticleBlockConnectionPointTypes.Vector3: { - const vec3Value = inputBlock.value as Vector3; - value = `(${vec3Value.x.toFixed(2)}, ${vec3Value.y.toFixed(2)}, ${vec3Value.z.toFixed(2)})`; + if (isStringValue) { + value = blockValue; + } else { + const vec3Value = inputBlock.value as Vector3; + value = `(${vec3Value.x.toFixed(2)}, ${vec3Value.y.toFixed(2)}, ${vec3Value.z.toFixed(2)})`; + } break; } case NodeParticleBlockConnectionPointTypes.Color4: { - const col4Value = inputBlock.value as Color4; - value = `(${col4Value.r.toFixed(2)}, ${col4Value.g.toFixed(2)}, ${col4Value.b.toFixed(2)}, ${col4Value.a.toFixed(2)})`; + if (isStringValue) { + value = blockValue; + } else { + const col4Value = inputBlock.value as Color4; + value = `(${col4Value.r.toFixed(2)}, ${col4Value.g.toFixed(2)}, ${col4Value.b.toFixed(2)}, ${col4Value.a.toFixed(2)})`; + } break; } + default: + value = isStringValue ? blockValue : String(blockValue); + break; } } contentArea.innerHTML = value; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/genericNodePropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/genericNodePropertyComponent.tsx index 343dff680f5..edee94902df 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/properties/genericNodePropertyComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/genericNodePropertyComponent.tsx @@ -186,6 +186,20 @@ export class GenericPropertyTabComponent extends React.Component ForceRebuild(block, this.props.stateManager, propertyName, options.notifiers)} + throttlePropertyChangedNotification={true} + /> + ); + break; + } case PropertyTypeForEdition.Float: { const cantDisplaySlider = isNaN(options.min as number) || isNaN(options.max as number) || options.min === options.max; if (cantDisplaySlider) { diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts index 327161e98b7..67dde1e5665 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts @@ -49,4 +49,6 @@ export const RegisterToDisplayManagers = () => { DisplayLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutDisplayManager; DisplayLedger.RegisteredControls["BasicConditionBlock"] = ConditionDisplayManager; DisplayLedger.RegisteredControls["ParticleTriggerBlock"] = TriggerDisplayManager; + DisplayLedger.RegisteredControls["ParticlePropsGetBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["ParticlePropsSetBlock"] = InputDisplayManager; }; From 8833c508508fc976c0723d48a34dc65806286d83 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 10 Oct 2025 19:53:17 +0300 Subject: [PATCH 23/68] Add Solid Particle System (SPS) blocks to Node Particle Editor This commit introduces several new blocks for the Solid Particle System (SPS) in the Node Particle Editor, enhancing the particle system capabilities. The following blocks have been added: - SPSMeshSourceBlock: Defines the mesh shape for SPS. - SPSCreateBlock: Creates an SPS with a base mesh. - SPSSystemBlock: Configures the Solid Particle System. - SPSInitParticleBlock: Initializes the update function for a specified range of particles. - SPSUpdatePositionBlock: Updates the position of particles. - SPSUpdateRotationBlock: Updates the rotation of particles. - SPSUpdateScalingBlock: Updates the scaling of particles. - SPSUpdateColorBlock: Updates the color of particles. - SPSUpdateVelocityBlock: Updates the velocity of particles. - SPSPhysicsBlock: Applies physics to particles. - SPSGetParticlePropertyBlock: Retrieves properties of particles. Additionally, the NodeParticleBuildState has been updated to include SPS context and delta time for physics calculations. The editor interface has been modified to accommodate these new blocks, improving the overall functionality and user experience of the particle system editor --- .../core/src/Particles/Node/Blocks/index.ts | 1 + .../src/Particles/Node/Blocks/spsBlocks.ts | 895 ++++++++++++++++++ .../nodeParticleBlockConnectionPointTypes.ts | 24 +- .../Particles/Node/nodeParticleBuildState.ts | 11 + .../Particles/Node/nodeParticleSystemSet.ts | 92 +- .../core/src/Particles/solidParticleSystem.ts | 30 + .../tools/nodeParticleEditor/public/index.js | 2 +- .../nodeParticleEditor/src/blockTools.ts | 77 ++ .../components/nodeList/nodeListComponent.tsx | 24 + .../src/components/preview/previewManager.ts | 6 + .../graphSystem/registerToDisplayLedger.ts | 14 + .../graphSystem/registerToPropertyLedger.ts | 13 + 12 files changed, 1183 insertions(+), 6 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/index.ts b/packages/dev/core/src/Particles/Node/Blocks/index.ts index 3e086086f35..73ae6a2c3a7 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/index.ts @@ -32,3 +32,4 @@ export * from "./Triggers/particleTriggerBlock"; export * from "./particleLocalVariableBlock"; export * from "./particleVectorLengthBlock"; export * from "./particleFresnelBlock"; +export * from "./spsBlocks"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts b/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts new file mode 100644 index 00000000000..db978488567 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts @@ -0,0 +1,895 @@ +import { RegisterClass } from "../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../nodeParticleBuildState"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { SolidParticle } from "core/Particles/solidParticle"; +import { Vector3 } from "core/Maths/math.vector"; +import { Color4 } from "core/Maths/math.color"; +import type { Mesh } from "core/Meshes/mesh"; +import type { Material } from "core/Materials/material"; +import { CreateBox } from "core/Meshes/Builders/boxBuilder"; +import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; +import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; +import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; + +// ============================================================================ +// SPSMeshSourceBlock - Источник меша для SPS +// ============================================================================ + +/** + * Mesh shape types for SPS + */ +export enum SPSMeshShapeType { + Box = 0, + Sphere = 1, + Cylinder = 2, + Plane = 3, + Custom = 4, +} + +/** + * Block used to provide mesh source for SPS + */ +export class SPSMeshSourceBlock extends NodeParticleBlock { + @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { + notifiers: { rebuild: true }, + embedded: true, + options: [ + { label: "Box", value: SPSMeshShapeType.Box }, + { label: "Sphere", value: SPSMeshShapeType.Sphere }, + { label: "Cylinder", value: SPSMeshShapeType.Cylinder }, + { label: "Plane", value: SPSMeshShapeType.Plane }, + { label: "Custom", value: SPSMeshShapeType.Custom }, + ], + }) + public shapeType = SPSMeshShapeType.Box; + + @editableInPropertyPage("Size", PropertyTypeForEdition.Float, "ADVANCED", { + embedded: true, + min: 0.01, + }) + public size = 1; + + @editableInPropertyPage("Segments", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + min: 1, + }) + public segments = 16; + + /** + * Create a new SPSMeshSourceBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("customMesh", NodeParticleBlockConnectionPointTypes.Mesh, true); + this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "SPSMeshSourceBlock"; + } + + /** + * Gets the customMesh input component + */ + public get customMesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the mesh output component + */ + public get mesh(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * @internal + */ + public override _build(state: NodeParticleBuildState) { + let mesh: Mesh; + + if (this.shapeType === SPSMeshShapeType.Custom) { + // Use custom mesh from input + const customMesh = this.customMesh.getConnectedValue(state) as Mesh; + if (customMesh) { + mesh = customMesh; + } else { + // Fallback to box if custom mesh not provided + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + } + } else { + // Create built-in shape + switch (this.shapeType) { + case SPSMeshShapeType.Box: + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + case SPSMeshShapeType.Sphere: + mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); + break; + case SPSMeshShapeType.Cylinder: + mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); + break; + case SPSMeshShapeType.Plane: + mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); + break; + default: + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + } + } + + this.mesh._storedValue = mesh; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.shapeType = this.shapeType; + serializationObject.size = this.size; + serializationObject.segments = this.segments; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.shapeType = serializationObject.shapeType || SPSMeshShapeType.Box; + this.size = serializationObject.size || 1; + this.segments = serializationObject.segments || 16; + } +} + +// ============================================================================ +// SPSCreateBlock - Создание SPS (аналог CreateParticleBlock) +// ============================================================================ + +/** + * Block used to create SPS with base mesh + */ +export class SPSCreateBlock extends NodeParticleBlock { + /** + * Create a new SPSCreateBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("baseMesh", NodeParticleBlockConnectionPointTypes.Mesh); + this.registerInput("particleCount", NodeParticleBlockConnectionPointTypes.Int, true, 100); + + this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "SPSCreateBlock"; + } + + /** + * Gets the baseMesh input component + */ + public get baseMesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the particleCount input component + */ + public get particleCount(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + /** + * Gets the solidParticle output component + */ + public get solidParticle(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * @internal + */ + public override _build(state: NodeParticleBuildState) { + const sps = new SolidParticleSystem(this.name, state.scene); + + const baseMesh = this.baseMesh.getConnectedValue(state) as Mesh; + if (baseMesh) { + const count = this.particleCount.getConnectedValue(state) as number; + sps.addShape(baseMesh, count); + } + + this.solidParticle._storedValue = sps; + } +} + +// ============================================================================ +// SPSSystemBlock - Настройка SPS (аналог SystemBlock) +// ============================================================================ + +/** + * Block used to configure Solid Particle System + */ +export class SPSSystemBlock extends NodeParticleBlock { + private static _IdCounter = 0; + + @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + min: 0, + max: 100000, + }) + public capacity = 1000; + + @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + }) + public billboard = false; + + /** @internal */ + public _internalId = SPSSystemBlock._IdCounter++; + + /** + * Create a new SPSSystemBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this._isSystem = true; + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); + this.registerInput("onStart", NodeParticleBlockConnectionPointTypes.System, true); + this.registerInput("onEnd", NodeParticleBlockConnectionPointTypes.System, true); + this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "SPSSystemBlock"; + } + + /** + * Gets the solidParticle input component + */ + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the material input component + */ + public get material(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + /** + * Gets the onStart input component + */ + public get onStart(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + /** + * Gets the onEnd input component + */ + public get onEnd(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + /** + * Gets the system output component + */ + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * Builds the block and return a functional SPS + * @param state defines the building state + * @returns the built SPS + */ + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { + state.capacity = this.capacity; + state.buildId = this._buildId++; + + this.build(state); + + const sps = this.solidParticle.getConnectedValue(state) as SolidParticleSystem; + + if (!sps) { + throw new Error("SPSSystemBlock: solidParticle input must be connected to SPSCreateBlock"); + } + + sps.billboard = this.billboard; + sps.name = this.name; + + const material = this.material.getConnectedValue(state) as Material; + if (material) { + sps.mesh.material = material; + } + + sps.buildMesh(); + + // Initialize particles with default positions + sps.initParticles(); + + // Start automatic updates + sps.start(); + + this.system._storedValue = this; + + this.onDisposeObservable.addOnce(() => { + sps.dispose(); + }); + + return sps; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.capacity = this.capacity; + serializationObject.billboard = this.billboard; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.capacity = serializationObject.capacity; + this.billboard = !!serializationObject.billboard; + } +} + +// ============================================================================ +// SPSInitParticleBlock - Инициализация updateParticle функции +// ============================================================================ + +/** + * Block used to initialize updateParticle function for specific particle range + */ +export class SPSInitParticleBlock extends NodeParticleBlock { + @editableInPropertyPage("Start Index", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + min: 0, + }) + public startIndex = 0; + + @editableInPropertyPage("End Index", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + min: -1, + }) + public endIndex = -1; // -1 means all particles + + /** + * Create a new SPSInitParticleBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("system", NodeParticleBlockConnectionPointTypes.System); + this.registerInput("updateFunction", NodeParticleBlockConnectionPointTypes.SolidParticle, true); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.System); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "SPSInitParticleBlock"; + } + + /** + * Gets the system input component + */ + public get system(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the updateFunction input component + */ + public get updateFunction(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + /** + * Gets the output component + */ + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * @internal + */ + public override _build(state: NodeParticleBuildState) { + const systemBlock = this.system.getConnectedValue(state) as SPSSystemBlock; + + if (!systemBlock) { + return; + } + + const sps = systemBlock.solidParticle.getConnectedValue(state) as SolidParticleSystem; + + if (!sps) { + return; + } + + // Store the old updateParticle function + const oldUpdateParticle = sps.updateParticle.bind(sps); + + // Create new updateParticle that includes this range + sps.updateParticle = (particle: SolidParticle): SolidParticle => { + // Call previous updateParticle functions + oldUpdateParticle(particle); + + const start = this.startIndex; + const end = this.endIndex === -1 ? sps.nbParticles - 1 : this.endIndex; + + // Only update particles in this range + if (particle.idx >= start && particle.idx <= end) { + state.particleContext = particle as any; + state.spsContext = sps; + + if (this.updateFunction.isConnected) { + this.updateFunction.getConnectedValue(state); + } + } + + return particle; + }; + + this.output._storedValue = systemBlock; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.startIndex = this.startIndex; + serializationObject.endIndex = this.endIndex; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.startIndex = serializationObject.startIndex || 0; + this.endIndex = serializationObject.endIndex || -1; + } +} + +// ============================================================================ +// SPSUpdatePositionBlock - Обновление позиции частицы +// ============================================================================ + +/** + * Block used to update particle position + */ +export class SPSUpdatePositionBlock extends NodeParticleBlock { + /** + * Create a new SPSUpdatePositionBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdatePositionBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newPosition = this.position.getConnectedValue(state) as Vector3; + if (newPosition) { + particle.position.copyFrom(newPosition); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSUpdateRotationBlock - Обновление вращения частицы +// ============================================================================ + +/** + * Block used to update particle rotation + */ +export class SPSUpdateRotationBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdateRotationBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newRotation = this.rotation.getConnectedValue(state) as Vector3; + if (newRotation) { + particle.rotation.copyFrom(newRotation); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSUpdateScalingBlock - Обновление масштаба частицы +// ============================================================================ + +/** + * Block used to update particle scaling + */ +export class SPSUpdateScalingBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdateScalingBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newScaling = this.scaling.getConnectedValue(state) as Vector3; + if (newScaling) { + particle.scaling.copyFrom(newScaling); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSUpdateColorBlock - Обновление цвета частицы +// ============================================================================ + +/** + * Block used to update particle color + */ +export class SPSUpdateColorBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdateColorBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newColor = this.color.getConnectedValue(state) as Color4; + if (newColor) { + if (!particle.color) { + particle.color = new Color4(1, 1, 1, 1); + } + particle.color.copyFrom(newColor); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSUpdateVelocityBlock - Обновление скорости частицы +// ============================================================================ + +/** + * Block used to update particle velocity + */ +export class SPSUpdateVelocityBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSUpdateVelocityBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const newVelocity = this.velocity.getConnectedValue(state) as Vector3; + if (newVelocity) { + particle.velocity.copyFrom(newVelocity); + } + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSPhysicsBlock - Физика для частицы +// ============================================================================ + +/** + * Block used to apply physics to SPS particle + */ +export class SPSPhysicsBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("gravity", NodeParticleBlockConnectionPointTypes.Vector3, true, new Vector3(0, -9.81, 0)); + this.registerInput("damping", NodeParticleBlockConnectionPointTypes.Float, true, 0.99); + this.registerInput("forces", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSPhysicsBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get gravity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get damping(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get forces(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + const deltaTime = (state as any).deltaTime || 0.016; + + const gravity = this.gravity.getConnectedValue(state) as Vector3; + if (gravity) { + particle.velocity.addInPlace(gravity.scale(deltaTime)); + } + + const forces = this.forces.getConnectedValue(state) as Vector3; + if (forces) { + particle.velocity.addInPlace(forces.scale(deltaTime)); + } + + const damping = this.damping.getConnectedValue(state) as number; + if (damping !== undefined && damping !== null) { + particle.velocity.scaleInPlace(damping); + } + + particle.position.addInPlace(particle.velocity.scale(deltaTime)); + + this.output._storedValue = particle; + } +} + +// ============================================================================ +// SPSGetParticlePropertyBlock - Получение свойств частицы +// ============================================================================ + +/** + * Block used to get particle properties (position, rotation, etc) + */ +export class SPSGetParticlePropertyBlock extends NodeParticleBlock { + @editableInPropertyPage("Property", PropertyTypeForEdition.List, "ADVANCED", { + notifiers: { rebuild: true }, + embedded: true, + options: [ + { label: "Position", value: 0 }, + { label: "Rotation", value: 1 }, + { label: "Scaling", value: 2 }, + { label: "Velocity", value: 3 }, + { label: "Index", value: 4 }, + { label: "Alive", value: 5 }, + { label: "Visible", value: 6 }, + ], + }) + public property = 0; + + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.AutoDetect); + } + + public override getClassName() { + return "SPSGetParticlePropertyBlock"; + } + + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const particle = state.particleContext as any as SolidParticle; + + if (!particle) { + return; + } + + switch (this.property) { + case 0: // Position + this.output._storedValue = particle.position; + break; + case 1: // Rotation + this.output._storedValue = particle.rotation; + break; + case 2: // Scaling + this.output._storedValue = particle.scaling; + break; + case 3: // Velocity + this.output._storedValue = particle.velocity; + break; + case 4: // Index + this.output._storedValue = particle.idx; + break; + case 5: // Alive + this.output._storedValue = particle.alive; + break; + case 6: // Visible + this.output._storedValue = particle.isVisible; + break; + } + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.property = this.property; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.property = serializationObject.property || 0; + } +} + +// ============================================================================ +// REGISTRATION +// ============================================================================ + +RegisterClass("BABYLON.SPSMeshSourceBlock", SPSMeshSourceBlock); +RegisterClass("BABYLON.SPSCreateBlock", SPSCreateBlock); +RegisterClass("BABYLON.SPSSystemBlock", SPSSystemBlock); +RegisterClass("BABYLON.SPSInitParticleBlock", SPSInitParticleBlock); +RegisterClass("BABYLON.SPSUpdatePositionBlock", SPSUpdatePositionBlock); +RegisterClass("BABYLON.SPSUpdateRotationBlock", SPSUpdateRotationBlock); +RegisterClass("BABYLON.SPSUpdateScalingBlock", SPSUpdateScalingBlock); +RegisterClass("BABYLON.SPSUpdateColorBlock", SPSUpdateColorBlock); +RegisterClass("BABYLON.SPSUpdateVelocityBlock", SPSUpdateVelocityBlock); +RegisterClass("BABYLON.SPSPhysicsBlock", SPSPhysicsBlock); +RegisterClass("BABYLON.SPSGetParticlePropertyBlock", SPSGetParticlePropertyBlock); diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts index 87d59a00192..c9075b54c59 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts @@ -28,12 +28,28 @@ export enum NodeParticleBlockConnectionPointTypes { Color4Gradient = 0x0800, /** System */ System = 0x1000, + /** SPS - Solid Particle System */ + SPS = 0x2000, + /** SolidParticle */ + SolidParticle = 0x4000, + /** Mesh */ + Mesh = 0x8000, + /** Material */ + Material = 0x10000, + /** Camera */ + Camera = 0x20000, + /** Function */ + Function = 0x40000, + /** Vector4 */ + Vector4 = 0x80000, + /** Boolean */ + Boolean = 0x100000, /** Detect type based on connection */ - AutoDetect = 0x2000, + AutoDetect = 0x200000, /** Output type that will be defined by input type */ - BasedOnInput = 0x4000, + BasedOnInput = 0x400000, /** Undefined */ - Undefined = 0x8000, + Undefined = 0x800000, /** Bitmask of all types */ - All = 0xffff, + All = 0xffffff, } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts index 88b49990200..a86e14068c0 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -9,6 +9,7 @@ import type { ThinParticleSystem } from "../thinParticleSystem"; import { Color4 } from "core/Maths/math.color"; import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; import type { AbstractMesh } from "core/Meshes/abstractMesh"; +import type { SolidParticleSystem } from "../solidParticleSystem"; /** * Class used to store node based geometry build state @@ -43,6 +44,16 @@ export class NodeParticleBuildState { */ public systemContext: Nullable = null; + /** + * Gets or sets the SPS context for SPS blocks + */ + public spsContext: Nullable = null; + + /** + * Gets or sets the delta time for physics calculations + */ + public deltaTime: number = 0.016; // 60 FPS default + /** * Gets or sets the index of the gradient to use */ diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index eb05f23fc1b..d93c824c49a 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -20,8 +20,20 @@ import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTelepor import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; -import type { Color4 } from "core/Maths/math.color"; import type { Nullable } from "../../types"; +import { + SPSMeshSourceBlock, + SPSCreateBlock, + SPSSystemBlock, + SPSInitParticleBlock, + SPSUpdatePositionBlock, + SPSUpdateColorBlock, + SPSPhysicsBlock, + SPSMeshShapeType, +} from "./Blocks/spsBlocks"; +import { Color4 } from "core/Maths/math.color"; +import { Vector3 } from "core/Maths/math.vector"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -336,6 +348,84 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } + /** + * Create a simple SPS example with 2 groups of particles + * - Particles 0-499: Core particles (golden color) + * - Particles 500-999: Outer particles with physics + */ + public setToDefaultSPS() { + this.clear(); + this.editorData = null; + + // ========== CREATE BASE MESH AND SPS ========== + const meshSource = new SPSMeshSourceBlock("Mesh Source"); + meshSource.shapeType = SPSMeshShapeType.Sphere; + meshSource.size = 0.1; + meshSource.segments = 8; + + const createSPS = new SPSCreateBlock("Create SPS"); + meshSource.mesh.connectTo(createSPS.baseMesh); + + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.capacity = 1000; + spsSystem.billboard = true; + createSPS.solidParticle.connectTo(spsSystem.solidParticle); + + // ========== GROUP 1: CORE (0-499) - Golden color ========== + const coreInit = new SPSInitParticleBlock("Core Init"); + coreInit.startIndex = 0; + coreInit.endIndex = 499; + spsSystem.system.connectTo(coreInit.system); + + // Set color only (positions will be from mesh) + const coreColor = new ParticleInputBlock("Core Color", NodeParticleBlockConnectionPointTypes.Color4); + coreColor.value = new Color4(1, 0.8, 0.2, 1); + + const coreUpdateColor = new SPSUpdateColorBlock("Core Update Color"); + coreColor.output.connectTo(coreUpdateColor.color); + coreInit.updateFunction.connectTo(coreUpdateColor.particle); + + // ========== GROUP 2: OUTER (500-999) - Physics ========== + const outerInit = new SPSInitParticleBlock("Outer Init"); + outerInit.startIndex = 500; + outerInit.endIndex = 999; + spsSystem.system.connectTo(outerInit.system); + + const outerPhysics = new SPSPhysicsBlock("Outer Physics"); + outerInit.updateFunction.connectTo(outerPhysics.particle); + + const gravity = new ParticleInputBlock("Gravity", NodeParticleBlockConnectionPointTypes.Vector3); + gravity.value = new Vector3(0, -1, 0); + gravity.output.connectTo(outerPhysics.gravity); + + const damping = new ParticleInputBlock("Damping", NodeParticleBlockConnectionPointTypes.Float); + damping.value = 0.99; + damping.output.connectTo(outerPhysics.damping); + + const outerColor = new ParticleInputBlock("Outer Color", NodeParticleBlockConnectionPointTypes.Color4); + outerColor.value = new Color4(0.7, 0.7, 0.7, 0.8); + + const outerUpdateColor = new SPSUpdateColorBlock("Outer Update Color"); + outerColor.output.connectTo(outerUpdateColor.color); + outerPhysics.output.connectTo(outerUpdateColor.particle); + + // Add all blocks to attachedBlocks + this.attachedBlocks.push(meshSource); + this.attachedBlocks.push(createSPS); + this.attachedBlocks.push(spsSystem); + this.attachedBlocks.push(coreInit); + this.attachedBlocks.push(coreColor); + this.attachedBlocks.push(coreUpdateColor); + this.attachedBlocks.push(outerInit); + this.attachedBlocks.push(outerPhysics); + this.attachedBlocks.push(gravity); + this.attachedBlocks.push(damping); + this.attachedBlocks.push(outerColor); + this.attachedBlocks.push(outerUpdateColor); + + this._systemBlocks.push(spsSystem as any); + } + /** * Remove a block from the current system set * @param block defines the block to remove diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index fa9a0a8a10b..5a933ce3416 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -7,6 +7,7 @@ import { Mesh } from "../Meshes/mesh"; import { CreateDisc } from "../Meshes/Builders/discBuilder"; import { EngineStore } from "../Engines/engineStore"; import type { Scene, IDisposable } from "../scene"; +import type { Observer } from "../Misc/observable"; import { DepthSortedParticle, SolidParticle, ModelShape, SolidParticleVertex } from "./solidParticle"; import type { TargetCamera } from "../Cameras/targetCamera"; import { BoundingInfo } from "../Culling/boundingInfo"; @@ -1534,10 +1535,39 @@ export class SolidParticleSystem implements IDisposable { return this; } + private _onBeforeRenderObserver: Nullable> = null; + + /** + * Starts the SPS by subscribing to the scene's before render observable + */ + public start(): void { + if (this.mesh && this.mesh.getScene()) { + // Stop any existing observer first + this.stop(); + + const scene = this.mesh.getScene(); + this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { + this.setParticles(); + }); + } + } + + /** + * Stops the SPS by unsubscribing from the scene's before render observable + */ + public stop(): void { + if (this._onBeforeRenderObserver && this.mesh && this.mesh.getScene()) { + const scene = this.mesh.getScene(); + scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + } + /** * Disposes the SPS. */ public dispose(): void { + this.stop(); this.mesh.dispose(); this.vars = null; // drop references to internal big arrays for the GC diff --git a/packages/tools/nodeParticleEditor/public/index.js b/packages/tools/nodeParticleEditor/public/index.js index c7752b2a52c..5d39a35b89f 100644 --- a/packages/tools/nodeParticleEditor/public/index.js +++ b/packages/tools/nodeParticleEditor/public/index.js @@ -223,7 +223,7 @@ checkBabylonVersionAsync().then(() => { scene = new BABYLON.Scene(engine); nodeParticleSet = new BABYLON.NodeParticleSystemSet("System set"); - nodeParticleSet.setToDefault(); + nodeParticleSet.setToDefaultSPS(); nodeParticleSet.buildAsync(scene).then(() => { showEditor(); }); diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 735902ecff8..e0d4ef53c25 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -42,6 +42,19 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; +import { + SPSMeshSourceBlock, + SPSSystemBlock, + SPSCreateBlock, + SPSInitParticleBlock, + SPSUpdatePositionBlock, + SPSUpdateRotationBlock, + SPSUpdateScalingBlock, + SPSUpdateColorBlock, + SPSUpdateVelocityBlock, + SPSPhysicsBlock, + SPSGetParticlePropertyBlock, +} from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -150,6 +163,28 @@ export class BlockTools { return new UpdateAttractorBlock("Update attractor"); case "SystemBlock": return new SystemBlock("System"); + case "SPSMeshSourceBlock": + return new SPSMeshSourceBlock("SPS Mesh Source"); + case "SPSSystemBlock": + return new SPSSystemBlock("SPS System"); + case "SPSCreateBlock": + return new SPSCreateBlock("SPS Create"); + case "SPSInitParticleBlock": + return new SPSInitParticleBlock("SPS Init Particle"); + case "SPSUpdatePositionBlock": + return new SPSUpdatePositionBlock("SPS Update Position"); + case "SPSUpdateRotationBlock": + return new SPSUpdateRotationBlock("SPS Update Rotation"); + case "SPSUpdateScalingBlock": + return new SPSUpdateScalingBlock("SPS Update Scaling"); + case "SPSUpdateColorBlock": + return new SPSUpdateColorBlock("SPS Update Color"); + case "SPSUpdateVelocityBlock": + return new SPSUpdateVelocityBlock("SPS Update Velocity"); + case "SPSPhysicsBlock": + return new SPSPhysicsBlock("SPS Physics"); + case "SPSGetParticlePropertyBlock": + return new SPSGetParticlePropertyBlock("SPS Get Property"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": @@ -453,6 +488,24 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.System: color = "#f20a2e"; break; + case NodeParticleBlockConnectionPointTypes.SPS: + color = "#8b4513"; + break; + case NodeParticleBlockConnectionPointTypes.SolidParticle: + color = "#2e8b57"; + break; + case NodeParticleBlockConnectionPointTypes.Mesh: + color = "#4682b4"; + break; + case NodeParticleBlockConnectionPointTypes.Material: + color = "#daa520"; + break; + case NodeParticleBlockConnectionPointTypes.Camera: + color = "#9370db"; + break; + case NodeParticleBlockConnectionPointTypes.Function: + color = "#ff6347"; + break; } return color; @@ -472,6 +525,18 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Color4; case "Matrix": return NodeParticleBlockConnectionPointTypes.Matrix; + case "SPS": + return NodeParticleBlockConnectionPointTypes.SPS; + case "SolidParticle": + return NodeParticleBlockConnectionPointTypes.SolidParticle; + case "Mesh": + return NodeParticleBlockConnectionPointTypes.Mesh; + case "Material": + return NodeParticleBlockConnectionPointTypes.Material; + case "Camera": + return NodeParticleBlockConnectionPointTypes.Camera; + case "Function": + return NodeParticleBlockConnectionPointTypes.Function; } return NodeParticleBlockConnectionPointTypes.AutoDetect; @@ -491,6 +556,18 @@ export class BlockTools { return "Color4"; case NodeParticleBlockConnectionPointTypes.Matrix: return "Matrix"; + case NodeParticleBlockConnectionPointTypes.SPS: + return "SPS"; + case NodeParticleBlockConnectionPointTypes.SolidParticle: + return "SolidParticle"; + case NodeParticleBlockConnectionPointTypes.Mesh: + return "Mesh"; + case NodeParticleBlockConnectionPointTypes.Material: + return "Material"; + case NodeParticleBlockConnectionPointTypes.Camera: + return "Camera"; + case NodeParticleBlockConnectionPointTypes.Function: + return "Function"; } return ""; diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 61452afcc06..428b90caef7 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -23,6 +23,17 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["BasicColorUpdateBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["UpdateFlowMapBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["SystemBlock"] = SystemDisplayManager; + + // SPS Blocks + DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; + DisplayLedger.RegisteredControls["SPSInitParticleBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdatePositionBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateRotationBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateScalingBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateColorBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateVelocityBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSPhysicsBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSGetParticlePropertyBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; DisplayLedger.RegisteredControls["ParticleElbowBlock"] = ElbowDisplayManager; DisplayLedger.RegisteredControls["ParticleTeleportInBlock"] = TeleportInDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 0290e5e97d5..30e6c13e3eb 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -13,4 +13,17 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleDebugBlock"] = DebugPropertyTabComponent; PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; + + // SPS Blocks - use default GenericPropertyComponent + PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSInitParticleBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdatePositionBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateRotationBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateScalingBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateColorBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateVelocityBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSPhysicsBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSGetParticlePropertyBlock"] = GenericPropertyComponent; }; From 3430d2f10463dfc14a50b18733ff7ff2d0fbb85c Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 14 Oct 2025 16:13:59 +0300 Subject: [PATCH 24/68] Enhance Node Particle System with Solid Particle Blocks This commit introduces several new blocks for the Solid Particle System (SPS) in the Node Particle Editor, improving the functionality and flexibility of the particle system. The following blocks have been added: - SPSMeshSourceBlock: Defines the mesh shape for SPS. - SPSCreateBlock: Creates an SPS with a base mesh. - SPSSystemBlock: Configures the Solid Particle System. - SPSInitBlock: Initializes particles with configurable parameters (position, velocity, color, etc.). - SPSUpdateBlock: Generates update functions for particle properties. Additionally, the connection point types have been updated to reflect the new Solid Particle System structure, and the editor interface has been modified to accommodate these enhancements, improving the overall user experience. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 37 + .../Blocks/SolidParticle/SPSCreateBlock.ts | 72 ++ .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 110 +++ .../Blocks/SolidParticle/SPSMeshShapeType.ts | 10 + .../SolidParticle/SPSMeshSourceBlock.ts | 114 +++ .../Blocks/SolidParticle/SPSSystemBlock.ts | 271 ++++++ .../Blocks/SolidParticle/SPSUpdateBlock.ts | 119 +++ .../Node/Blocks/SolidParticle/index.ts | 8 + .../core/src/Particles/Node/Blocks/index.ts | 2 +- .../src/Particles/Node/Blocks/spsBlocks.ts | 895 ------------------ .../nodeParticleBlockConnectionPointTypes.ts | 2 +- .../src/Particles/Node/nodeParticleBlock.ts | 13 +- .../Node/nodeParticleBlockConnectionPoint.ts | 18 +- .../nodeGraphSystem/interfaces/portData.ts | 1 + .../nodeParticleEditor/src/blockTools.ts | 42 +- .../components/nodeList/nodeListComponent.tsx | 23 +- .../graphSystem/connectionPointPortData.ts | 4 + .../graphSystem/registerToDisplayLedger.ts | 9 +- .../graphSystem/registerToPropertyLedger.ts | 9 +- 19 files changed, 786 insertions(+), 973 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts delete mode 100644 packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts new file mode 100644 index 00000000000..e4aa711fcdf --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -0,0 +1,37 @@ +import { Vector3 } from "core/Maths/math.vector"; +import { Color4 } from "core/Maths/math.color"; +import type { Mesh } from "core/Meshes/mesh"; +import type { Material } from "core/Materials/material"; + +/** + * Interface for SPS init block data + */ +export interface ISPSInitData { + position?: Vector3 | (() => Vector3); + velocity?: Vector3 | (() => Vector3); + color?: Color4 | (() => Color4); + scaling?: Vector3 | (() => Vector3); + rotation?: Vector3 | (() => Vector3); +} + +/** + * Interface for SPS update block data + */ +export interface ISPSUpdateData { + position?: Vector3 | (() => Vector3); + velocity?: Vector3 | (() => Vector3); + color?: Color4 | (() => Color4); + scaling?: Vector3 | (() => Vector3); + rotation?: Vector3 | (() => Vector3); +} + +/** + * Interface for SPS create block data + */ +export interface ISPSCreateData { + mesh: Mesh; + count: number; + material?: Material; + initBlock?: ISPSInitData; + updateBlock?: ISPSUpdateData; +} diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts new file mode 100644 index 00000000000..04b34827a27 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -0,0 +1,72 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISPSCreateData } from "./ISPSData"; + +/** + * Block used to configure SPS parameters (mesh, count, initBlocks) + */ +export class SPSCreateBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 100); + this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); + this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); + this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); + + this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSCreateBlock"; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get count(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get material(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get initBlock(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get updateBlock(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const mesh = this.mesh.getConnectedValue(state); + const count = (this.count.getConnectedValue(state) as number) || 100; + const material = this.material.getConnectedValue(state); + + const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; + const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; + + const solidParticle: ISPSCreateData = { + mesh, + count, + material, + initBlock, + updateBlock, + }; + + this.solidParticle._storedValue = solidParticle; + } +} + +RegisterClass("BABYLON.SPSCreateBlock", SPSCreateBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts new file mode 100644 index 00000000000..9025cdb74e3 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -0,0 +1,110 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { Vector3 } from "core/Maths/math.vector"; +import { Color4 } from "core/Maths/math.color"; +import type { ISPSInitData } from "./ISPSData"; + +/** + * Block used to generate initialization function for SPS particles + */ +export class SPSInitBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); + + this.registerOutput("initData", NodeParticleBlockConnectionPointTypes.System); + } + + public override getClassName() { + return "SPSInitBlock"; + } + + public get initData(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public override _build(state: NodeParticleBuildState) { + const initData: ISPSInitData = {}; + + if (this.position.isConnected) { + initData.position = () => { + return this.position.getConnectedValue(state) as Vector3; + }; + } else { + initData.position = new Vector3(0, 0, 0); + } + + if (this.velocity.isConnected) { + initData.velocity = () => { + return this.velocity.getConnectedValue(state) as Vector3; + }; + } else { + initData.velocity = new Vector3(0, 0, 0); + } + + if (this.color.isConnected) { + initData.color = () => { + return this.color.getConnectedValue(state) as Color4; + }; + } else { + initData.color = new Color4(1, 1, 1, 1); + } + + if (this.scaling.isConnected) { + initData.scaling = () => { + return this.scaling.getConnectedValue(state) as Vector3; + }; + } else { + initData.scaling = new Vector3(1, 1, 1); + } + + if (this.rotation.isConnected) { + initData.rotation = () => { + return this.rotation.getConnectedValue(state) as Vector3; + }; + } else { + initData.rotation = new Vector3(0, 0, 0); + } + + this.initData._storedValue = initData; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.SPSInitBlock", SPSInitBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts new file mode 100644 index 00000000000..242d6e22f2e --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts @@ -0,0 +1,10 @@ +/** + * Mesh shape types for SPS + */ +export enum SPSMeshShapeType { + Box = 0, + Sphere = 1, + Cylinder = 2, + Plane = 3, + Custom = 4, +} diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts new file mode 100644 index 00000000000..251f051210a --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts @@ -0,0 +1,114 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { CreateBox } from "core/Meshes/Builders/boxBuilder"; +import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; +import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; +import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; +import { SPSMeshShapeType } from "./SPSMeshShapeType"; + +/** + * Block used to provide mesh source for SPS + */ +export class SPSMeshSourceBlock extends NodeParticleBlock { + @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { + notifiers: { rebuild: true }, + embedded: true, + options: [ + { label: "Box", value: SPSMeshShapeType.Box }, + { label: "Sphere", value: SPSMeshShapeType.Sphere }, + { label: "Cylinder", value: SPSMeshShapeType.Cylinder }, + { label: "Plane", value: SPSMeshShapeType.Plane }, + { label: "Custom", value: SPSMeshShapeType.Custom }, + ], + }) + public shapeType = SPSMeshShapeType.Box; + + @editableInPropertyPage("Size", PropertyTypeForEdition.Float, "ADVANCED", { + embedded: true, + min: 0.01, + }) + public size = 1; + + @editableInPropertyPage("Segments", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + min: 1, + }) + public segments = 16; + + public constructor(name: string) { + super(name); + + this.registerInput("customMesh", NodeParticleBlockConnectionPointTypes.Mesh, true); + this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + } + + public override getClassName() { + return "SPSMeshSourceBlock"; + } + + public get customMesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + let mesh; + + if (this.shapeType === SPSMeshShapeType.Custom) { + if (this.customMesh.isConnected) { + const customMesh = this.customMesh.getConnectedValue(state); + if (customMesh) { + mesh = customMesh; + } else { + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + } + } else { + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + } + } else { + switch (this.shapeType) { + case SPSMeshShapeType.Box: + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + case SPSMeshShapeType.Sphere: + mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); + break; + case SPSMeshShapeType.Cylinder: + mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); + break; + case SPSMeshShapeType.Plane: + mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); + break; + default: + mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + } + } + + this.mesh._storedValue = mesh; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.shapeType = this.shapeType; + serializationObject.size = this.size; + serializationObject.segments = this.segments; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.shapeType = serializationObject.shapeType || SPSMeshShapeType.Box; + this.size = serializationObject.size || 1; + this.segments = serializationObject.segments || 16; + } +} + +RegisterClass("BABYLON.SPSMeshSourceBlock", SPSMeshSourceBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts new file mode 100644 index 00000000000..3f6b3bafe7b --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -0,0 +1,271 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { ISPSCreateData } from "./ISPSData"; + +/** + * Block used to create SolidParticleSystem and collect all Create blocks + */ +export class SPSSystemBlock extends NodeParticleBlock { + private static _IdCounter = 0; + + @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + min: 0, + max: 100000, + }) + public capacity = 1000; + + @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + }) + public billboard = false; + + public _internalId = SPSSystemBlock._IdCounter++; + + public constructor(name: string) { + super(name); + + this._isSystem = true; + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle, true, null, null, null, true); + this.registerOutput("system", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + } + + public override getClassName() { + return "SPSSystemBlock"; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { + state.capacity = this.capacity; + state.buildId = this._buildId++; + + this.build(state); + + if (!state.scene) { + throw new Error("Scene is not initialized in NodeParticleBuildState"); + } + + const sps = new SolidParticleSystem(this.name, state.scene); + sps.billboard = this.billboard; + sps.name = this.name; + + const createBlocks: ISPSCreateData[] = []; + + if (this.solidParticle.endpoints.length > 0) { + for (const endpoint of this.solidParticle.endpoints) { + const createBlock = endpoint.getConnectedValue(state); + if (createBlock) { + createBlocks.push(createBlock); + } + } + } else if (this.solidParticle.isConnected) { + const createBlock = this.solidParticle.getConnectedValue(state); + if (createBlock) { + createBlocks.push(createBlock); + } + } + + if (createBlocks.length === 0 && this.solidParticle.allowMultipleConnections && this.solidParticle._connectedPoint) { + const connectedPoint = this.solidParticle._connectedPoint; + if (connectedPoint.endpoints && connectedPoint.endpoints.length > 0) { + for (const endpoint of connectedPoint.endpoints) { + const createBlock = endpoint.getConnectedValue(state); + if (createBlock) { + createBlocks.push(createBlock); + } + } + } + } + + for (const createBlock of createBlocks) { + if (createBlock.mesh && createBlock.count) { + sps.addShape(createBlock.mesh, createBlock.count); + createBlock.mesh.dispose(); + } + } + + sps.initParticles = () => { + for (const createBlock of createBlocks) { + if (createBlock.initBlock) { + let startIndex = 0; + for (let i = 0; i < createBlocks.indexOf(createBlock); i++) { + startIndex += createBlocks[i].count; + } + const endIndex = startIndex + createBlock.count - 1; + + for (let p = startIndex; p <= endIndex && p < sps.nbParticles; p++) { + const particle = sps.particles[p]; + + if (createBlock.initBlock?.position) { + const particleContext = { + id: p, + position: particle.position, + velocity: particle.velocity, + color: particle.color, + scaling: particle.scaling, + rotation: particle.rotation, + }; + state.particleContext = particleContext as any; + + const positionValue = createBlock.initBlock.position; + if (typeof positionValue === "function") { + particle.position.copyFrom(positionValue()); + } else { + particle.position.copyFrom(positionValue); + } + } + if (createBlock.initBlock?.velocity) { + const velocityValue = createBlock.initBlock.velocity; + if (typeof velocityValue === "function") { + particle.velocity.copyFrom(velocityValue()); + } else { + particle.velocity.copyFrom(velocityValue); + } + } + if (createBlock.initBlock?.color) { + const colorValue = createBlock.initBlock.color; + if (typeof colorValue === "function") { + particle.color?.copyFrom(colorValue()); + } else { + particle.color?.copyFrom(colorValue); + } + } + if (createBlock.initBlock?.scaling) { + const scalingValue = createBlock.initBlock.scaling; + if (typeof scalingValue === "function") { + particle.scaling.copyFrom(scalingValue()); + } else { + particle.scaling.copyFrom(scalingValue); + } + } + if (createBlock.initBlock?.rotation) { + const rotationValue = createBlock.initBlock.rotation; + if (typeof rotationValue === "function") { + particle.rotation.copyFrom(rotationValue()); + } else { + particle.rotation.copyFrom(rotationValue); + } + } + } + } + } + }; + + sps.updateParticle = (particle: any) => { + let currentParticleIndex = 0; + let targetCreateBlock = null; + + for (const createBlock of createBlocks) { + if (particle.idx >= currentParticleIndex && particle.idx < currentParticleIndex + createBlock.count) { + targetCreateBlock = createBlock; + break; + } + currentParticleIndex += createBlock.count; + } + + if (targetCreateBlock && targetCreateBlock.updateBlock) { + if (targetCreateBlock.updateBlock.position) { + const positionValue = targetCreateBlock.updateBlock.position; + if (typeof positionValue === "function") { + particle.position.copyFrom(positionValue()); + } else { + particle.position.copyFrom(positionValue); + } + } + if (targetCreateBlock.updateBlock.velocity) { + const velocityValue = targetCreateBlock.updateBlock.velocity; + if (typeof velocityValue === "function") { + particle.velocity.copyFrom(velocityValue()); + } else { + particle.velocity.copyFrom(velocityValue); + } + } + if (targetCreateBlock.updateBlock.color) { + const colorValue = targetCreateBlock.updateBlock.color; + if (typeof colorValue === "function") { + particle.color?.copyFrom(colorValue()); + } else { + particle.color?.copyFrom(colorValue); + } + } + if (targetCreateBlock.updateBlock.scaling) { + const scalingValue = targetCreateBlock.updateBlock.scaling; + if (typeof scalingValue === "function") { + particle.scaling.copyFrom(scalingValue()); + } else { + particle.scaling.copyFrom(scalingValue); + } + } + if (targetCreateBlock.updateBlock.rotation) { + const rotationValue = targetCreateBlock.updateBlock.rotation; + if (typeof rotationValue === "function") { + particle.rotation.copyFrom(rotationValue()); + } else { + particle.rotation.copyFrom(rotationValue); + } + } + } + return particle; + }; + + sps.buildMesh(); + + if (sps.initParticles) { + sps.initParticles(); + } + + sps.setParticles(); + + if (state.scene) { + state.scene.onBeforeRenderObservable.add(() => { + if (sps && sps.nbParticles > 0) { + try { + sps.setParticles(); + } catch (error) { + console.error("SPSSystemBlock - error in setParticles:", error); + } + } + }); + } + + sps.start(); + + this.system._storedValue = this; + + this.onDisposeObservable.addOnce(() => { + sps.dispose(); + }); + + return sps; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.capacity = this.capacity; + serializationObject.billboard = this.billboard; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.capacity = serializationObject.capacity; + this.billboard = !!serializationObject.billboard; + } +} + +RegisterClass("BABYLON.SPSSystemBlock", SPSSystemBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts new file mode 100644 index 00000000000..c8dffe9372d --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -0,0 +1,119 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { Vector3 } from "core/Maths/math.vector"; +import { Color4 } from "core/Maths/math.color"; +import type { ISPSUpdateData } from "./ISPSData"; + +/** + * Block used to generate update function for SPS particles + */ +export class SPSUpdateBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); + + this.registerOutput("updateData", NodeParticleBlockConnectionPointTypes.System); + } + + public override getClassName() { + return "SPSUpdateBlock"; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public get updateData(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const updateData: ISPSUpdateData = {}; + + if (this.position.isConnected) { + updateData.position = () => { + const particleContext = { + id: 0, + position: new Vector3(0, 0, 0), + velocity: new Vector3(0, 0, 0), + color: new Color4(1, 1, 1, 1), + scaling: new Vector3(1, 1, 1), + rotation: new Vector3(0, 0, 0), + }; + state.particleContext = particleContext as any; + return this.position.getConnectedValue(state) as Vector3; + }; + } else { + updateData.position = undefined; + } + + if (this.velocity.isConnected) { + updateData.velocity = () => { + return this.velocity.getConnectedValue(state) as Vector3; + }; + } else { + updateData.velocity = undefined; + } + + if (this.color.isConnected) { + updateData.color = () => { + return this.color.getConnectedValue(state) as Color4; + }; + } else { + updateData.color = undefined; + } + + if (this.scaling.isConnected) { + updateData.scaling = () => { + return this.scaling.getConnectedValue(state) as Vector3; + }; + } else { + updateData.scaling = undefined; + } + + if (this.rotation.isConnected) { + updateData.rotation = () => { + return this.rotation.getConnectedValue(state) as Vector3; + }; + } else { + updateData.rotation = undefined; + } + + this.updateData._storedValue = updateData; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.SPSUpdateBlock", SPSUpdateBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts new file mode 100644 index 00000000000..85c5ccedbdf --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -0,0 +1,8 @@ +export * from "./ISPSData"; +export * from "./SPSMeshShapeType"; +export * from "./SPSMeshSourceBlock"; +export * from "./SPSCreateBlock"; +export * from "./SPSSystemBlock"; +export * from "./SPSUpdateBlock"; +export * from "./SPSInitBlock"; +export * from "./SPSRandomBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/index.ts b/packages/dev/core/src/Particles/Node/Blocks/index.ts index 73ae6a2c3a7..c691fcccbf1 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/index.ts @@ -32,4 +32,4 @@ export * from "./Triggers/particleTriggerBlock"; export * from "./particleLocalVariableBlock"; export * from "./particleVectorLengthBlock"; export * from "./particleFresnelBlock"; -export * from "./spsBlocks"; +export * from "./SolidParticle"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts b/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts deleted file mode 100644 index db978488567..00000000000 --- a/packages/dev/core/src/Particles/Node/Blocks/spsBlocks.ts +++ /dev/null @@ -1,895 +0,0 @@ -import { RegisterClass } from "../../../Misc/typeStore"; -import { NodeParticleBlockConnectionPointTypes } from "../Enums/nodeParticleBlockConnectionPointTypes"; -import { NodeParticleBlock } from "../nodeParticleBlock"; -import type { NodeParticleConnectionPoint } from "../nodeParticleBlockConnectionPoint"; -import type { NodeParticleBuildState } from "../nodeParticleBuildState"; -import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; -import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; -import type { SolidParticle } from "core/Particles/solidParticle"; -import { Vector3 } from "core/Maths/math.vector"; -import { Color4 } from "core/Maths/math.color"; -import type { Mesh } from "core/Meshes/mesh"; -import type { Material } from "core/Materials/material"; -import { CreateBox } from "core/Meshes/Builders/boxBuilder"; -import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; -import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; -import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; - -// ============================================================================ -// SPSMeshSourceBlock - Источник меша для SPS -// ============================================================================ - -/** - * Mesh shape types for SPS - */ -export enum SPSMeshShapeType { - Box = 0, - Sphere = 1, - Cylinder = 2, - Plane = 3, - Custom = 4, -} - -/** - * Block used to provide mesh source for SPS - */ -export class SPSMeshSourceBlock extends NodeParticleBlock { - @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { - notifiers: { rebuild: true }, - embedded: true, - options: [ - { label: "Box", value: SPSMeshShapeType.Box }, - { label: "Sphere", value: SPSMeshShapeType.Sphere }, - { label: "Cylinder", value: SPSMeshShapeType.Cylinder }, - { label: "Plane", value: SPSMeshShapeType.Plane }, - { label: "Custom", value: SPSMeshShapeType.Custom }, - ], - }) - public shapeType = SPSMeshShapeType.Box; - - @editableInPropertyPage("Size", PropertyTypeForEdition.Float, "ADVANCED", { - embedded: true, - min: 0.01, - }) - public size = 1; - - @editableInPropertyPage("Segments", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - min: 1, - }) - public segments = 16; - - /** - * Create a new SPSMeshSourceBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this.registerInput("customMesh", NodeParticleBlockConnectionPointTypes.Mesh, true); - this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); - } - - /** - * Gets the current class name - * @returns the class name - */ - public override getClassName() { - return "SPSMeshSourceBlock"; - } - - /** - * Gets the customMesh input component - */ - public get customMesh(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the mesh output component - */ - public get mesh(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * @internal - */ - public override _build(state: NodeParticleBuildState) { - let mesh: Mesh; - - if (this.shapeType === SPSMeshShapeType.Custom) { - // Use custom mesh from input - const customMesh = this.customMesh.getConnectedValue(state) as Mesh; - if (customMesh) { - mesh = customMesh; - } else { - // Fallback to box if custom mesh not provided - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - } - } else { - // Create built-in shape - switch (this.shapeType) { - case SPSMeshShapeType.Box: - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - break; - case SPSMeshShapeType.Sphere: - mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); - break; - case SPSMeshShapeType.Cylinder: - mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); - break; - case SPSMeshShapeType.Plane: - mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); - break; - default: - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - break; - } - } - - this.mesh._storedValue = mesh; - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.shapeType = this.shapeType; - serializationObject.size = this.size; - serializationObject.segments = this.segments; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.shapeType = serializationObject.shapeType || SPSMeshShapeType.Box; - this.size = serializationObject.size || 1; - this.segments = serializationObject.segments || 16; - } -} - -// ============================================================================ -// SPSCreateBlock - Создание SPS (аналог CreateParticleBlock) -// ============================================================================ - -/** - * Block used to create SPS with base mesh - */ -export class SPSCreateBlock extends NodeParticleBlock { - /** - * Create a new SPSCreateBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this.registerInput("baseMesh", NodeParticleBlockConnectionPointTypes.Mesh); - this.registerInput("particleCount", NodeParticleBlockConnectionPointTypes.Int, true, 100); - - this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - /** - * Gets the current class name - * @returns the class name - */ - public override getClassName() { - return "SPSCreateBlock"; - } - - /** - * Gets the baseMesh input component - */ - public get baseMesh(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the particleCount input component - */ - public get particleCount(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - /** - * Gets the solidParticle output component - */ - public get solidParticle(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * @internal - */ - public override _build(state: NodeParticleBuildState) { - const sps = new SolidParticleSystem(this.name, state.scene); - - const baseMesh = this.baseMesh.getConnectedValue(state) as Mesh; - if (baseMesh) { - const count = this.particleCount.getConnectedValue(state) as number; - sps.addShape(baseMesh, count); - } - - this.solidParticle._storedValue = sps; - } -} - -// ============================================================================ -// SPSSystemBlock - Настройка SPS (аналог SystemBlock) -// ============================================================================ - -/** - * Block used to configure Solid Particle System - */ -export class SPSSystemBlock extends NodeParticleBlock { - private static _IdCounter = 0; - - @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - notifiers: { rebuild: true }, - min: 0, - max: 100000, - }) - public capacity = 1000; - - @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { - embedded: true, - notifiers: { rebuild: true }, - }) - public billboard = false; - - /** @internal */ - public _internalId = SPSSystemBlock._IdCounter++; - - /** - * Create a new SPSSystemBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this._isSystem = true; - - this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); - this.registerInput("onStart", NodeParticleBlockConnectionPointTypes.System, true); - this.registerInput("onEnd", NodeParticleBlockConnectionPointTypes.System, true); - this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System); - } - - /** - * Gets the current class name - * @returns the class name - */ - public override getClassName() { - return "SPSSystemBlock"; - } - - /** - * Gets the solidParticle input component - */ - public get solidParticle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the material input component - */ - public get material(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - /** - * Gets the onStart input component - */ - public get onStart(): NodeParticleConnectionPoint { - return this._inputs[2]; - } - - /** - * Gets the onEnd input component - */ - public get onEnd(): NodeParticleConnectionPoint { - return this._inputs[3]; - } - - /** - * Gets the system output component - */ - public get system(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * Builds the block and return a functional SPS - * @param state defines the building state - * @returns the built SPS - */ - public createSystem(state: NodeParticleBuildState): SolidParticleSystem { - state.capacity = this.capacity; - state.buildId = this._buildId++; - - this.build(state); - - const sps = this.solidParticle.getConnectedValue(state) as SolidParticleSystem; - - if (!sps) { - throw new Error("SPSSystemBlock: solidParticle input must be connected to SPSCreateBlock"); - } - - sps.billboard = this.billboard; - sps.name = this.name; - - const material = this.material.getConnectedValue(state) as Material; - if (material) { - sps.mesh.material = material; - } - - sps.buildMesh(); - - // Initialize particles with default positions - sps.initParticles(); - - // Start automatic updates - sps.start(); - - this.system._storedValue = this; - - this.onDisposeObservable.addOnce(() => { - sps.dispose(); - }); - - return sps; - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.capacity = this.capacity; - serializationObject.billboard = this.billboard; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.capacity = serializationObject.capacity; - this.billboard = !!serializationObject.billboard; - } -} - -// ============================================================================ -// SPSInitParticleBlock - Инициализация updateParticle функции -// ============================================================================ - -/** - * Block used to initialize updateParticle function for specific particle range - */ -export class SPSInitParticleBlock extends NodeParticleBlock { - @editableInPropertyPage("Start Index", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - min: 0, - }) - public startIndex = 0; - - @editableInPropertyPage("End Index", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - min: -1, - }) - public endIndex = -1; // -1 means all particles - - /** - * Create a new SPSInitParticleBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this.registerInput("system", NodeParticleBlockConnectionPointTypes.System); - this.registerInput("updateFunction", NodeParticleBlockConnectionPointTypes.SolidParticle, true); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.System); - } - - /** - * Gets the current class name - * @returns the class name - */ - public override getClassName() { - return "SPSInitParticleBlock"; - } - - /** - * Gets the system input component - */ - public get system(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the updateFunction input component - */ - public get updateFunction(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - /** - * Gets the output component - */ - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * @internal - */ - public override _build(state: NodeParticleBuildState) { - const systemBlock = this.system.getConnectedValue(state) as SPSSystemBlock; - - if (!systemBlock) { - return; - } - - const sps = systemBlock.solidParticle.getConnectedValue(state) as SolidParticleSystem; - - if (!sps) { - return; - } - - // Store the old updateParticle function - const oldUpdateParticle = sps.updateParticle.bind(sps); - - // Create new updateParticle that includes this range - sps.updateParticle = (particle: SolidParticle): SolidParticle => { - // Call previous updateParticle functions - oldUpdateParticle(particle); - - const start = this.startIndex; - const end = this.endIndex === -1 ? sps.nbParticles - 1 : this.endIndex; - - // Only update particles in this range - if (particle.idx >= start && particle.idx <= end) { - state.particleContext = particle as any; - state.spsContext = sps; - - if (this.updateFunction.isConnected) { - this.updateFunction.getConnectedValue(state); - } - } - - return particle; - }; - - this.output._storedValue = systemBlock; - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.startIndex = this.startIndex; - serializationObject.endIndex = this.endIndex; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.startIndex = serializationObject.startIndex || 0; - this.endIndex = serializationObject.endIndex || -1; - } -} - -// ============================================================================ -// SPSUpdatePositionBlock - Обновление позиции частицы -// ============================================================================ - -/** - * Block used to update particle position - */ -export class SPSUpdatePositionBlock extends NodeParticleBlock { - /** - * Create a new SPSUpdatePositionBlock - * @param name defines the block name - */ - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdatePositionBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get position(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newPosition = this.position.getConnectedValue(state) as Vector3; - if (newPosition) { - particle.position.copyFrom(newPosition); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSUpdateRotationBlock - Обновление вращения частицы -// ============================================================================ - -/** - * Block used to update particle rotation - */ -export class SPSUpdateRotationBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdateRotationBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get rotation(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newRotation = this.rotation.getConnectedValue(state) as Vector3; - if (newRotation) { - particle.rotation.copyFrom(newRotation); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSUpdateScalingBlock - Обновление масштаба частицы -// ============================================================================ - -/** - * Block used to update particle scaling - */ -export class SPSUpdateScalingBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdateScalingBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get scaling(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newScaling = this.scaling.getConnectedValue(state) as Vector3; - if (newScaling) { - particle.scaling.copyFrom(newScaling); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSUpdateColorBlock - Обновление цвета частицы -// ============================================================================ - -/** - * Block used to update particle color - */ -export class SPSUpdateColorBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdateColorBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get color(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newColor = this.color.getConnectedValue(state) as Color4; - if (newColor) { - if (!particle.color) { - particle.color = new Color4(1, 1, 1, 1); - } - particle.color.copyFrom(newColor); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSUpdateVelocityBlock - Обновление скорости частицы -// ============================================================================ - -/** - * Block used to update particle velocity - */ -export class SPSUpdateVelocityBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSUpdateVelocityBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get velocity(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const newVelocity = this.velocity.getConnectedValue(state) as Vector3; - if (newVelocity) { - particle.velocity.copyFrom(newVelocity); - } - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSPhysicsBlock - Физика для частицы -// ============================================================================ - -/** - * Block used to apply physics to SPS particle - */ -export class SPSPhysicsBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerInput("gravity", NodeParticleBlockConnectionPointTypes.Vector3, true, new Vector3(0, -9.81, 0)); - this.registerInput("damping", NodeParticleBlockConnectionPointTypes.Float, true, 0.99); - this.registerInput("forces", NodeParticleBlockConnectionPointTypes.Vector3, true); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); - } - - public override getClassName() { - return "SPSPhysicsBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get gravity(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get damping(): NodeParticleConnectionPoint { - return this._inputs[2]; - } - - public get forces(): NodeParticleConnectionPoint { - return this._inputs[3]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - const deltaTime = (state as any).deltaTime || 0.016; - - const gravity = this.gravity.getConnectedValue(state) as Vector3; - if (gravity) { - particle.velocity.addInPlace(gravity.scale(deltaTime)); - } - - const forces = this.forces.getConnectedValue(state) as Vector3; - if (forces) { - particle.velocity.addInPlace(forces.scale(deltaTime)); - } - - const damping = this.damping.getConnectedValue(state) as number; - if (damping !== undefined && damping !== null) { - particle.velocity.scaleInPlace(damping); - } - - particle.position.addInPlace(particle.velocity.scale(deltaTime)); - - this.output._storedValue = particle; - } -} - -// ============================================================================ -// SPSGetParticlePropertyBlock - Получение свойств частицы -// ============================================================================ - -/** - * Block used to get particle properties (position, rotation, etc) - */ -export class SPSGetParticlePropertyBlock extends NodeParticleBlock { - @editableInPropertyPage("Property", PropertyTypeForEdition.List, "ADVANCED", { - notifiers: { rebuild: true }, - embedded: true, - options: [ - { label: "Position", value: 0 }, - { label: "Rotation", value: 1 }, - { label: "Scaling", value: 2 }, - { label: "Velocity", value: 3 }, - { label: "Index", value: 4 }, - { label: "Alive", value: 5 }, - { label: "Visible", value: 6 }, - ], - }) - public property = 0; - - public constructor(name: string) { - super(name); - - this.registerInput("particle", NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.AutoDetect); - } - - public override getClassName() { - return "SPSGetParticlePropertyBlock"; - } - - public get particle(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - const particle = state.particleContext as any as SolidParticle; - - if (!particle) { - return; - } - - switch (this.property) { - case 0: // Position - this.output._storedValue = particle.position; - break; - case 1: // Rotation - this.output._storedValue = particle.rotation; - break; - case 2: // Scaling - this.output._storedValue = particle.scaling; - break; - case 3: // Velocity - this.output._storedValue = particle.velocity; - break; - case 4: // Index - this.output._storedValue = particle.idx; - break; - case 5: // Alive - this.output._storedValue = particle.alive; - break; - case 6: // Visible - this.output._storedValue = particle.isVisible; - break; - } - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.property = this.property; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.property = serializationObject.property || 0; - } -} - -// ============================================================================ -// REGISTRATION -// ============================================================================ - -RegisterClass("BABYLON.SPSMeshSourceBlock", SPSMeshSourceBlock); -RegisterClass("BABYLON.SPSCreateBlock", SPSCreateBlock); -RegisterClass("BABYLON.SPSSystemBlock", SPSSystemBlock); -RegisterClass("BABYLON.SPSInitParticleBlock", SPSInitParticleBlock); -RegisterClass("BABYLON.SPSUpdatePositionBlock", SPSUpdatePositionBlock); -RegisterClass("BABYLON.SPSUpdateRotationBlock", SPSUpdateRotationBlock); -RegisterClass("BABYLON.SPSUpdateScalingBlock", SPSUpdateScalingBlock); -RegisterClass("BABYLON.SPSUpdateColorBlock", SPSUpdateColorBlock); -RegisterClass("BABYLON.SPSUpdateVelocityBlock", SPSUpdateVelocityBlock); -RegisterClass("BABYLON.SPSPhysicsBlock", SPSPhysicsBlock); -RegisterClass("BABYLON.SPSGetParticlePropertyBlock", SPSGetParticlePropertyBlock); diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts index c9075b54c59..88d31930cd9 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts @@ -29,7 +29,7 @@ export enum NodeParticleBlockConnectionPointTypes { /** System */ System = 0x1000, /** SPS - Solid Particle System */ - SPS = 0x2000, + SolidParticleSystem = 0x2000, /** SolidParticle */ SolidParticle = 0x4000, /** Mesh */ diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index 719e58614b9..6b6dd4c54b3 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -218,10 +218,19 @@ export class NodeParticleBlock { * @param value value to return if there is no connection * @param valueMin min value accepted for value * @param valueMax max value accepted for value + * @param allowMultipleConnections defines if this input allows multiple connections * @returns the current block */ - public registerInput(name: string, type: NodeParticleBlockConnectionPointTypes, isOptional: boolean = false, value?: any, valueMin?: any, valueMax?: any) { - const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input); + public registerInput( + name: string, + type: NodeParticleBlockConnectionPointTypes, + isOptional: boolean = false, + value?: any, + valueMin?: any, + valueMax?: any, + allowMultipleConnections: boolean = false + ) { + const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input, allowMultipleConnections); point.type = type; point.isOptional = isOptional; point.defaultValue = value; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts index 9ca996b3840..14d7215aba2 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts @@ -118,6 +118,11 @@ export class NodeParticleConnectionPoint { */ public valueMax: Nullable = null; + /** + * Gets or sets a boolean indicating that this connection point allows multiple connections + */ + public allowMultipleConnections: boolean = false; + /** * Gets or sets the connection point type (default is float) */ @@ -239,11 +244,13 @@ export class NodeParticleConnectionPoint { * @param name defines the connection point name * @param ownerBlock defines the block hosting this connection point * @param direction defines the direction of the connection point + * @param allowMultipleConnections defines if this point allows multiple connections */ - public constructor(name: string, ownerBlock: NodeParticleBlock, direction: NodeParticleConnectionPointDirection) { + public constructor(name: string, ownerBlock: NodeParticleBlock, direction: NodeParticleConnectionPointDirection, allowMultipleConnections = false) { this._ownerBlock = ownerBlock; this.name = name; this._direction = direction; + this.allowMultipleConnections = allowMultipleConnections; } /** @@ -328,8 +335,13 @@ export class NodeParticleConnectionPoint { throw `Cannot connect these two connectors. source: "${this.ownerBlock.name}".${this.name}, target: "${connectionPoint.ownerBlock.name}".${connectionPoint.name}`; } - this._endpoints.push(connectionPoint); - connectionPoint._connectedPoint = this; + if (this.direction === NodeParticleConnectionPointDirection.Input && this.allowMultipleConnections) { + this._endpoints.push(connectionPoint); + connectionPoint._connectedPoint = this; + } else { + this._endpoints.push(connectionPoint); + connectionPoint._connectedPoint = this; + } this.onConnectionObservable.notifyObservers(connectionPoint); connectionPoint.onConnectionObservable.notifyObservers(this); diff --git a/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts b/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts index 00f285a1a83..917217c927b 100644 --- a/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts +++ b/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts @@ -54,6 +54,7 @@ export interface IPortData { hasEndpoints: boolean; endpoints: Nullable; directValueDefinition?: IPortDirectValueDefinition; + allowMultipleConnections?: boolean; updateDisplayName: (newName: string) => void; canConnectTo: (port: IPortData) => boolean; diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index e0d4ef53c25..1280d96a97c 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -42,19 +42,7 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { - SPSMeshSourceBlock, - SPSSystemBlock, - SPSCreateBlock, - SPSInitParticleBlock, - SPSUpdatePositionBlock, - SPSUpdateRotationBlock, - SPSUpdateScalingBlock, - SPSUpdateColorBlock, - SPSUpdateVelocityBlock, - SPSPhysicsBlock, - SPSGetParticlePropertyBlock, -} from "core/Particles/Node/Blocks"; +import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock } from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -169,22 +157,8 @@ export class BlockTools { return new SPSSystemBlock("SPS System"); case "SPSCreateBlock": return new SPSCreateBlock("SPS Create"); - case "SPSInitParticleBlock": - return new SPSInitParticleBlock("SPS Init Particle"); - case "SPSUpdatePositionBlock": - return new SPSUpdatePositionBlock("SPS Update Position"); - case "SPSUpdateRotationBlock": - return new SPSUpdateRotationBlock("SPS Update Rotation"); - case "SPSUpdateScalingBlock": - return new SPSUpdateScalingBlock("SPS Update Scaling"); - case "SPSUpdateColorBlock": - return new SPSUpdateColorBlock("SPS Update Color"); - case "SPSUpdateVelocityBlock": - return new SPSUpdateVelocityBlock("SPS Update Velocity"); - case "SPSPhysicsBlock": - return new SPSPhysicsBlock("SPS Physics"); - case "SPSGetParticlePropertyBlock": - return new SPSGetParticlePropertyBlock("SPS Get Property"); + case "SPSInitBlock": + return new SPSInitBlock("SPS Init"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": @@ -488,7 +462,7 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.System: color = "#f20a2e"; break; - case NodeParticleBlockConnectionPointTypes.SPS: + case NodeParticleBlockConnectionPointTypes.SolidParticleSystem: color = "#8b4513"; break; case NodeParticleBlockConnectionPointTypes.SolidParticle: @@ -525,8 +499,8 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Color4; case "Matrix": return NodeParticleBlockConnectionPointTypes.Matrix; - case "SPS": - return NodeParticleBlockConnectionPointTypes.SPS; + case "SolidParticleSystem": + return NodeParticleBlockConnectionPointTypes.SolidParticleSystem; case "SolidParticle": return NodeParticleBlockConnectionPointTypes.SolidParticle; case "Mesh": @@ -556,8 +530,8 @@ export class BlockTools { return "Color4"; case NodeParticleBlockConnectionPointTypes.Matrix: return "Matrix"; - case NodeParticleBlockConnectionPointTypes.SPS: - return "SPS"; + case NodeParticleBlockConnectionPointTypes.SolidParticleSystem: + return "SolidParticleSystem"; case NodeParticleBlockConnectionPointTypes.SolidParticle: return "SolidParticle"; case NodeParticleBlockConnectionPointTypes.Mesh: diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 428b90caef7..6e31c4deec6 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -26,14 +26,7 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; - DisplayLedger.RegisteredControls["SPSInitParticleBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdatePositionBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateRotationBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateScalingBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateColorBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateVelocityBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSPhysicsBlock"] = UpdateDisplayManager; - DisplayLedger.RegisteredControls["SPSGetParticlePropertyBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["SPSInitBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; DisplayLedger.RegisteredControls["ParticleElbowBlock"] = ElbowDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 30e6c13e3eb..6c5cdaa6cb6 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -18,12 +18,5 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSInitParticleBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdatePositionBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateRotationBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateScalingBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateColorBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateVelocityBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSPhysicsBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSGetParticlePropertyBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; }; From be2185e007f455e1d1ba65f07d340818ff453f79 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 14 Oct 2025 16:20:28 +0300 Subject: [PATCH 25/68] Refactor Solid Particle System initialization and cleanup This commit enhances the Solid Particle System by ensuring that the mesh is built and particles are initialized before rendering. The `buildMesh` and `initParticles` methods are now called during the setup process, improving the overall functionality. Additionally, the cleanup process has been updated to properly dispose of the mesh when the system is stopped, ensuring better resource management. --- .../Blocks/SolidParticle/SPSSystemBlock.ts | 20 ----- .../Particles/Node/nodeParticleSystemSet.ts | 90 ------------------- .../core/src/Particles/solidParticleSystem.ts | 8 ++ 3 files changed, 8 insertions(+), 110 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 3f6b3bafe7b..6f7b412343d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -223,26 +223,6 @@ export class SPSSystemBlock extends NodeParticleBlock { return particle; }; - sps.buildMesh(); - - if (sps.initParticles) { - sps.initParticles(); - } - - sps.setParticles(); - - if (state.scene) { - state.scene.onBeforeRenderObservable.add(() => { - if (sps && sps.nbParticles > 0) { - try { - sps.setParticles(); - } catch (error) { - console.error("SPSSystemBlock - error in setParticles:", error); - } - } - }); - } - sps.start(); this.system._storedValue = this; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index d93c824c49a..6fbe6d91869 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -21,19 +21,7 @@ import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleport import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; -import { - SPSMeshSourceBlock, - SPSCreateBlock, - SPSSystemBlock, - SPSInitParticleBlock, - SPSUpdatePositionBlock, - SPSUpdateColorBlock, - SPSPhysicsBlock, - SPSMeshShapeType, -} from "./Blocks/spsBlocks"; import { Color4 } from "core/Maths/math.color"; -import { Vector3 } from "core/Maths/math.vector"; -import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -348,84 +336,6 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } - /** - * Create a simple SPS example with 2 groups of particles - * - Particles 0-499: Core particles (golden color) - * - Particles 500-999: Outer particles with physics - */ - public setToDefaultSPS() { - this.clear(); - this.editorData = null; - - // ========== CREATE BASE MESH AND SPS ========== - const meshSource = new SPSMeshSourceBlock("Mesh Source"); - meshSource.shapeType = SPSMeshShapeType.Sphere; - meshSource.size = 0.1; - meshSource.segments = 8; - - const createSPS = new SPSCreateBlock("Create SPS"); - meshSource.mesh.connectTo(createSPS.baseMesh); - - const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.capacity = 1000; - spsSystem.billboard = true; - createSPS.solidParticle.connectTo(spsSystem.solidParticle); - - // ========== GROUP 1: CORE (0-499) - Golden color ========== - const coreInit = new SPSInitParticleBlock("Core Init"); - coreInit.startIndex = 0; - coreInit.endIndex = 499; - spsSystem.system.connectTo(coreInit.system); - - // Set color only (positions will be from mesh) - const coreColor = new ParticleInputBlock("Core Color", NodeParticleBlockConnectionPointTypes.Color4); - coreColor.value = new Color4(1, 0.8, 0.2, 1); - - const coreUpdateColor = new SPSUpdateColorBlock("Core Update Color"); - coreColor.output.connectTo(coreUpdateColor.color); - coreInit.updateFunction.connectTo(coreUpdateColor.particle); - - // ========== GROUP 2: OUTER (500-999) - Physics ========== - const outerInit = new SPSInitParticleBlock("Outer Init"); - outerInit.startIndex = 500; - outerInit.endIndex = 999; - spsSystem.system.connectTo(outerInit.system); - - const outerPhysics = new SPSPhysicsBlock("Outer Physics"); - outerInit.updateFunction.connectTo(outerPhysics.particle); - - const gravity = new ParticleInputBlock("Gravity", NodeParticleBlockConnectionPointTypes.Vector3); - gravity.value = new Vector3(0, -1, 0); - gravity.output.connectTo(outerPhysics.gravity); - - const damping = new ParticleInputBlock("Damping", NodeParticleBlockConnectionPointTypes.Float); - damping.value = 0.99; - damping.output.connectTo(outerPhysics.damping); - - const outerColor = new ParticleInputBlock("Outer Color", NodeParticleBlockConnectionPointTypes.Color4); - outerColor.value = new Color4(0.7, 0.7, 0.7, 0.8); - - const outerUpdateColor = new SPSUpdateColorBlock("Outer Update Color"); - outerColor.output.connectTo(outerUpdateColor.color); - outerPhysics.output.connectTo(outerUpdateColor.particle); - - // Add all blocks to attachedBlocks - this.attachedBlocks.push(meshSource); - this.attachedBlocks.push(createSPS); - this.attachedBlocks.push(spsSystem); - this.attachedBlocks.push(coreInit); - this.attachedBlocks.push(coreColor); - this.attachedBlocks.push(coreUpdateColor); - this.attachedBlocks.push(outerInit); - this.attachedBlocks.push(outerPhysics); - this.attachedBlocks.push(gravity); - this.attachedBlocks.push(damping); - this.attachedBlocks.push(outerColor); - this.attachedBlocks.push(outerUpdateColor); - - this._systemBlocks.push(spsSystem as any); - } - /** * Remove a block from the current system set * @param block defines the block to remove diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index 5a933ce3416..5551fbe8217 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -1546,6 +1546,13 @@ export class SolidParticleSystem implements IDisposable { this.stop(); const scene = this.mesh.getScene(); + this.buildMesh(); + + if (this.initParticles) { + this.initParticles(); + } + + this.setParticles(); this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { this.setParticles(); }); @@ -1560,6 +1567,7 @@ export class SolidParticleSystem implements IDisposable { const scene = this.mesh.getScene(); scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); this._onBeforeRenderObserver = null; + this.mesh.dispose(); } } From 5b797bc631de464400fb2268dda0aaf38c5a1015 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 14 Oct 2025 16:21:27 +0300 Subject: [PATCH 26/68] Update NodeParticleEditor to use setToDefault method for NodeParticleSystemSet This commit modifies the initialization of the Node Particle System by changing the method from `setToDefaultSPS` to `setToDefault`, aligning it with the updated API and improving consistency in the codebase. --- packages/tools/nodeParticleEditor/public/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/nodeParticleEditor/public/index.js b/packages/tools/nodeParticleEditor/public/index.js index 5d39a35b89f..c7752b2a52c 100644 --- a/packages/tools/nodeParticleEditor/public/index.js +++ b/packages/tools/nodeParticleEditor/public/index.js @@ -223,7 +223,7 @@ checkBabylonVersionAsync().then(() => { scene = new BABYLON.Scene(engine); nodeParticleSet = new BABYLON.NodeParticleSystemSet("System set"); - nodeParticleSet.setToDefaultSPS(); + nodeParticleSet.setToDefault(); nodeParticleSet.buildAsync(scene).then(() => { showEditor(); }); From b63f06e44794ff5766d40d4c890b3dbe39528b78 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 17 Oct 2025 09:30:33 +0300 Subject: [PATCH 27/68] Enhance Solid Particle System with isStarted flag and update NodeParticleBlock This commit introduces an `isStarted` flag to the Solid Particle System to track its initialization state, preventing unnecessary operations when stopping the system. Additionally, the NodeParticleBlock has been updated to allow multiple connections for input points, improving flexibility. The SPSUpdateBlock has been integrated into various components, enhancing the Node Particle Editor's functionality and user experience. --- .../Particles/Node/Blocks/SolidParticle/index.ts | 1 - .../core/src/Particles/Node/nodeParticleBlock.ts | 3 ++- .../Node/nodeParticleBlockConnectionPoint.ts | 4 +--- .../dev/core/src/Particles/solidParticleSystem.ts | 14 +++++++++++++- .../tools/nodeParticleEditor/src/blockTools.ts | 4 +++- .../src/components/nodeList/nodeListComponent.tsx | 3 ++- .../src/graphSystem/registerToDisplayLedger.ts | 1 + .../src/graphSystem/registerToPropertyLedger.ts | 1 + 8 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 85c5ccedbdf..2b66eabf628 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -5,4 +5,3 @@ export * from "./SPSCreateBlock"; export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; export * from "./SPSInitBlock"; -export * from "./SPSRandomBlock"; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index 6b6dd4c54b3..c070fa013bc 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -230,13 +230,14 @@ export class NodeParticleBlock { valueMax?: any, allowMultipleConnections: boolean = false ) { - const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input, allowMultipleConnections); + const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input); point.type = type; point.isOptional = isOptional; point.defaultValue = value; point.value = value; point.valueMin = valueMin; point.valueMax = valueMax; + point.allowMultipleConnections = allowMultipleConnections; this._inputs.push(point); diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts index 14d7215aba2..42d9569af52 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts @@ -244,13 +244,11 @@ export class NodeParticleConnectionPoint { * @param name defines the connection point name * @param ownerBlock defines the block hosting this connection point * @param direction defines the direction of the connection point - * @param allowMultipleConnections defines if this point allows multiple connections */ - public constructor(name: string, ownerBlock: NodeParticleBlock, direction: NodeParticleConnectionPointDirection, allowMultipleConnections = false) { + public constructor(name: string, ownerBlock: NodeParticleBlock, direction: NodeParticleConnectionPointDirection) { this._ownerBlock = ownerBlock; this.name = name; this._direction = direction; - this.allowMultipleConnections = allowMultipleConnections; } /** diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index 5551fbe8217..21262babf3a 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -93,6 +93,11 @@ export class SolidParticleSystem implements IDisposable { */ public depthSortedParticles: DepthSortedParticle[]; + /** + * If the SPS has been started. + */ + public isStarted: boolean = false; + /** * If the particle intersection must be computed only with the bounding sphere (no bounding box computation, so faster). (Internal use only) * @internal @@ -1556,6 +1561,7 @@ export class SolidParticleSystem implements IDisposable { this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { this.setParticles(); }); + this.isStarted = true; } } @@ -1563,11 +1569,15 @@ export class SolidParticleSystem implements IDisposable { * Stops the SPS by unsubscribing from the scene's before render observable */ public stop(): void { + if (!this.isStarted) { + return; + } if (this._onBeforeRenderObserver && this.mesh && this.mesh.getScene()) { const scene = this.mesh.getScene(); scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); this._onBeforeRenderObserver = null; this.mesh.dispose(); + this.isStarted = false; } } @@ -1576,7 +1586,9 @@ export class SolidParticleSystem implements IDisposable { */ public dispose(): void { this.stop(); - this.mesh.dispose(); + if (this.mesh) { + this.mesh.dispose(); + } this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 1280d96a97c..60b0ec0f443 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -42,7 +42,7 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock } from "core/Particles/Node/Blocks"; +import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock, SPSUpdateBlock } from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -159,6 +159,8 @@ export class BlockTools { return new SPSCreateBlock("SPS Create"); case "SPSInitBlock": return new SPSInitBlock("SPS Init"); + case "SPSUpdateBlock": + return new SPSUpdateBlock("SPS Update"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 6e31c4deec6..8afabb07a56 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -25,6 +25,7 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; DisplayLedger.RegisteredControls["SPSInitBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; DisplayLedger.RegisteredControls["ParticleElbowBlock"] = ElbowDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 6c5cdaa6cb6..cd5287e3d42 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -19,4 +19,5 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateBlock"] = GenericPropertyComponent; }; From 5ee2820e7d5bb59dbc6ff3666b87729c8e274393 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 17 Oct 2025 10:13:40 +0300 Subject: [PATCH 28/68] Enhance NodeParticleConnectionPoint to support multiple connections This commit adds a new internal property `_connectedPoints` to the `NodeParticleConnectionPoint` class, allowing it to maintain multiple connections. The connection and disconnection logic has been updated to handle multiple connections appropriately. Additionally, a new getter method `connectedPoints` has been introduced to retrieve the array of connected points. In the `GraphCanvasComponent`, the connection check has been refined to consider the `allowMultipleConnections` property, improving the connection management logic. --- .../Node/nodeParticleBlockConnectionPoint.ts | 22 ++++++++++++++----- .../src/nodeGraphSystem/graphCanvas.tsx | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts index 42d9569af52..4a19ca56da6 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts @@ -35,6 +35,8 @@ export class NodeParticleConnectionPoint { public _ownerBlock: NodeParticleBlock; /** @internal */ public _connectedPoint: Nullable = null; + /** @internal */ + public _connectedPoints = new Array(); /** @internal */ public _storedValue: any = null; @@ -198,6 +200,11 @@ export class NodeParticleConnectionPoint { return this._connectedPoint; } + /** Get the other side of the connection (if any) */ + public get connectedPoints(): Array { + return this._connectedPoints; + } + /** Get the block that owns this connection point */ public get ownerBlock(): NodeParticleBlock { return this._ownerBlock; @@ -333,12 +340,12 @@ export class NodeParticleConnectionPoint { throw `Cannot connect these two connectors. source: "${this.ownerBlock.name}".${this.name}, target: "${connectionPoint.ownerBlock.name}".${connectionPoint.name}`; } - if (this.direction === NodeParticleConnectionPointDirection.Input && this.allowMultipleConnections) { - this._endpoints.push(connectionPoint); - connectionPoint._connectedPoint = this; + this._endpoints.push(connectionPoint); + connectionPoint._connectedPoint = this; + if (connectionPoint.allowMultipleConnections) { + connectionPoint._connectedPoints.push(this); } else { - this._endpoints.push(connectionPoint); - connectionPoint._connectedPoint = this; + connectionPoint._connectedPoints = [this]; } this.onConnectionObservable.notifyObservers(connectionPoint); @@ -361,6 +368,11 @@ export class NodeParticleConnectionPoint { this._endpoints.splice(index, 1); endpoint._connectedPoint = null; + if (endpoint.allowMultipleConnections) { + endpoint._connectedPoints.splice(endpoint._connectedPoints.indexOf(this), 1); + } else { + endpoint._connectedPoints = []; + } this.onDisconnectionObservable.notifyObservers(endpoint); endpoint.onDisconnectionObservable.notifyObservers(this); diff --git a/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx b/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx index 158641f525f..b1f6fcbfc8b 100644 --- a/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx +++ b/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx @@ -1354,7 +1354,7 @@ export class GraphCanvasComponent extends React.Component = null; - if (pointB.isConnected) { + if (pointB.isConnected && !pointB.allowMultipleConnections) { const links = nodeB.getLinksForPortData(pointB); linksToNotifyForDispose = links.slice(); From 16177929051d2c9355287d18c3a031074b83b6ee Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 20 Oct 2025 09:59:06 +0300 Subject: [PATCH 29/68] Refactor Solid Particle System and Node Particle Editor for improved functionality This commit enhances the Solid Particle System by streamlining the start and stop methods, ensuring proper initialization and cleanup of particles. The ISPSData interface has been updated to enforce required properties for SPS update data, while the SPSCreateBlock now defaults the particle count to 1. Additionally, the Node Particle Editor has been updated to introduce a mode selection feature, allowing users to switch between Standard and SPS modes, improving usability and flexibility in particle system management. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 24 ++--- .../Blocks/SolidParticle/SPSCreateBlock.ts | 4 +- .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 88 +++++++++++------- .../Blocks/SolidParticle/SPSUpdateBlock.ts | 89 +++++++++++-------- .../core/src/Particles/solidParticleSystem.ts | 33 +++---- .../propertyTab/propertyTabComponent.tsx | 40 +++++++++ .../nodeParticleEditor/src/globalState.ts | 2 + .../src/nodeParticleModes.ts | 9 ++ 8 files changed, 176 insertions(+), 113 deletions(-) create mode 100644 packages/tools/nodeParticleEditor/src/nodeParticleModes.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index e4aa711fcdf..7a92022a848 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -3,26 +3,15 @@ import { Color4 } from "core/Maths/math.color"; import type { Mesh } from "core/Meshes/mesh"; import type { Material } from "core/Materials/material"; -/** - * Interface for SPS init block data - */ -export interface ISPSInitData { - position?: Vector3 | (() => Vector3); - velocity?: Vector3 | (() => Vector3); - color?: Color4 | (() => Color4); - scaling?: Vector3 | (() => Vector3); - rotation?: Vector3 | (() => Vector3); -} - /** * Interface for SPS update block data */ export interface ISPSUpdateData { - position?: Vector3 | (() => Vector3); - velocity?: Vector3 | (() => Vector3); - color?: Color4 | (() => Color4); - scaling?: Vector3 | (() => Vector3); - rotation?: Vector3 | (() => Vector3); + position: () => Vector3; + velocity: () => Vector3; + color: () => Color4; + scaling: () => Vector3; + rotation: () => Vector3; } /** @@ -32,6 +21,7 @@ export interface ISPSCreateData { mesh: Mesh; count: number; material?: Material; - initBlock?: ISPSInitData; + initBlock?: ISPSUpdateData; updateBlock?: ISPSUpdateData; + shapeId?: number; } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 04b34827a27..3e7c6c2a20f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -13,7 +13,7 @@ export class SPSCreateBlock extends NodeParticleBlock { super(name); this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); - this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 100); + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); @@ -51,7 +51,7 @@ export class SPSCreateBlock extends NodeParticleBlock { public override _build(state: NodeParticleBuildState) { const mesh = this.mesh.getConnectedValue(state); - const count = (this.count.getConnectedValue(state) as number) || 100; + const count = (this.count.getConnectedValue(state) as number) || 1; const material = this.material.getConnectedValue(state); const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts index 9025cdb74e3..cecf932f50f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -5,7 +5,7 @@ import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnect import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { Vector3 } from "core/Maths/math.vector"; import { Color4 } from "core/Maths/math.color"; -import type { ISPSInitData } from "./ISPSData"; +import type { ISPSUpdateData } from "./ISPSData"; /** * Block used to generate initialization function for SPS particles @@ -52,51 +52,71 @@ export class SPSInitBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - const initData: ISPSInitData = {}; + const initData = {} as ISPSUpdateData; + initData.position = () => { + return this.getPosition(state); + }; + initData.velocity = () => { + return this.getVelocity(state); + }; + initData.color = () => { + return this.getColor(state); + }; + initData.scaling = () => { + return this.getScaling(state); + }; + initData.rotation = () => { + return this.getRotation(state); + }; + this.initData._storedValue = initData; + } + + private getPosition(state: NodeParticleBuildState) { if (this.position.isConnected) { - initData.position = () => { - return this.position.getConnectedValue(state) as Vector3; - }; - } else { - initData.position = new Vector3(0, 0, 0); + if (this.position._storedFunction) { + return this.position._storedFunction!(state); + } + return this.position.getConnectedValue(state); } - + return new Vector3(0, 0, 0); + } + private getVelocity(state: NodeParticleBuildState) { if (this.velocity.isConnected) { - initData.velocity = () => { - return this.velocity.getConnectedValue(state) as Vector3; - }; - } else { - initData.velocity = new Vector3(0, 0, 0); + if (this.velocity._storedFunction) { + return this.velocity._storedFunction!(state); + } + return this.velocity.getConnectedValue(state); } - + return new Vector3(0, 0, 0); + } + private getColor(state: NodeParticleBuildState) { if (this.color.isConnected) { - initData.color = () => { - return this.color.getConnectedValue(state) as Color4; - }; - } else { - initData.color = new Color4(1, 1, 1, 1); + if (this.color._storedFunction) { + return this.color._storedFunction!(state); + } + return this.color.getConnectedValue(state); } - + return new Color4(1, 1, 1, 1); + } + private getScaling(state: NodeParticleBuildState) { if (this.scaling.isConnected) { - initData.scaling = () => { - return this.scaling.getConnectedValue(state) as Vector3; - }; - } else { - initData.scaling = new Vector3(1, 1, 1); + if (this.scaling._storedFunction) { + return this.scaling._storedFunction!(state); + } + return this.scaling.getConnectedValue(state); } - + return new Vector3(1, 1, 1); + } + private getRotation(state: NodeParticleBuildState) { if (this.rotation.isConnected) { - initData.rotation = () => { - return this.rotation.getConnectedValue(state) as Vector3; - }; - } else { - initData.rotation = new Vector3(0, 0, 0); + if (this.rotation._storedFunction) { + return this.rotation._storedFunction!(state); + } + return this.rotation.getConnectedValue(state); } - - this.initData._storedValue = initData; + return new Vector3(0, 0, 0); } - public override serialize(): any { const serializationObject = super.serialize(); return serializationObject; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index c8dffe9372d..4183fed9c9d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -52,58 +52,73 @@ export class SPSUpdateBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - const updateData: ISPSUpdateData = {}; + const updateData: ISPSUpdateData = {} as ISPSUpdateData; + updateData.position = () => { + return this.getPosition(state); + }; + updateData.velocity = () => { + return this.getVelocity(state); + }; + updateData.color = () => { + return this.getColor(state); + }; + updateData.scaling = () => { + return this.getScaling(state); + }; + updateData.rotation = () => { + return this.getRotation(state); + }; + this.updateData._storedValue = updateData; + } + private getPosition(state: NodeParticleBuildState) { if (this.position.isConnected) { - updateData.position = () => { - const particleContext = { - id: 0, - position: new Vector3(0, 0, 0), - velocity: new Vector3(0, 0, 0), - color: new Color4(1, 1, 1, 1), - scaling: new Vector3(1, 1, 1), - rotation: new Vector3(0, 0, 0), - }; - state.particleContext = particleContext as any; - return this.position.getConnectedValue(state) as Vector3; - }; - } else { - updateData.position = undefined; + if (this.position._storedFunction) { + return this.position._storedFunction!(state); + } + return this.position.getConnectedValue(state); } + return new Vector3(0, 0, 0); + } + private getVelocity(state: NodeParticleBuildState) { if (this.velocity.isConnected) { - updateData.velocity = () => { - return this.velocity.getConnectedValue(state) as Vector3; - }; - } else { - updateData.velocity = undefined; + if (this.velocity._storedFunction) { + return this.velocity._storedFunction!(state); + } + return this.velocity.getConnectedValue(state); } + return new Vector3(0, 0, 0); + } + private getColor(state: NodeParticleBuildState) { if (this.color.isConnected) { - updateData.color = () => { - return this.color.getConnectedValue(state) as Color4; - }; - } else { - updateData.color = undefined; + if (this.color._storedFunction) { + return this.color._storedFunction!(state); + } + return this.color.getConnectedValue(state); } + return new Color4(1, 1, 1, 1); + } + private getScaling(state: NodeParticleBuildState) { if (this.scaling.isConnected) { - updateData.scaling = () => { - return this.scaling.getConnectedValue(state) as Vector3; - }; - } else { - updateData.scaling = undefined; + if (this.scaling._storedFunction) { + return this.scaling._storedFunction!(state); + } + return this.scaling.getConnectedValue(state); } + return new Vector3(1, 1, 1); + } + private getRotation(state: NodeParticleBuildState) { if (this.rotation.isConnected) { - updateData.rotation = () => { - return this.rotation.getConnectedValue(state) as Vector3; - }; - } else { - updateData.rotation = undefined; + if (this.rotation._storedFunction) { + return this.rotation._storedFunction!(state); + } + return this.rotation.getConnectedValue(state); } - - this.updateData._storedValue = updateData; + return new Vector3(0, 0, 0); } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index 21262babf3a..c798370b496 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -1546,23 +1546,14 @@ export class SolidParticleSystem implements IDisposable { * Starts the SPS by subscribing to the scene's before render observable */ public start(): void { - if (this.mesh && this.mesh.getScene()) { - // Stop any existing observer first - this.stop(); - - const scene = this.mesh.getScene(); - this.buildMesh(); - - if (this.initParticles) { - this.initParticles(); - } - + this.stop(); + this.buildMesh(); + this.initParticles(); + this.setParticles(); + this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { this.setParticles(); - this._onBeforeRenderObserver = scene.onBeforeRenderObservable.add(() => { - this.setParticles(); - }); - this.isStarted = true; - } + }); + this.isStarted = true; } /** @@ -1572,13 +1563,9 @@ export class SolidParticleSystem implements IDisposable { if (!this.isStarted) { return; } - if (this._onBeforeRenderObserver && this.mesh && this.mesh.getScene()) { - const scene = this.mesh.getScene(); - scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - this.mesh.dispose(); - this.isStarted = false; - } + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + this.isStarted = false; } /** diff --git a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx index 7a143f2da20..4ede6c475cb 100644 --- a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx @@ -28,6 +28,8 @@ import type { LockObject } from "shared-ui-components/tabs/propertyGrids/lockObj import { TextLineComponent } from "shared-ui-components/lines/textLineComponent"; import { SliderLineComponent } from "shared-ui-components/lines/sliderLineComponent"; import { NodeParticleSystemSet } from "core/Particles"; +import { NodeParticleModes } from "../../nodeParticleModes"; +import { OptionsLine } from "shared-ui-components/lines/optionsLineComponent"; interface IPropertyTabComponentProps { globalState: GlobalState; @@ -210,6 +212,33 @@ export class PropertyTabComponent extends React.Component + this.changeMode(value as NodeParticleModes)} + /> void; customSave?: { label: string; action: (data: string) => Promise }; + mode: NodeParticleModes = NodeParticleModes.Standard; public constructor() { this.stateManager = new StateManager(); diff --git a/packages/tools/nodeParticleEditor/src/nodeParticleModes.ts b/packages/tools/nodeParticleEditor/src/nodeParticleModes.ts new file mode 100644 index 00000000000..a5c6f90ea6a --- /dev/null +++ b/packages/tools/nodeParticleEditor/src/nodeParticleModes.ts @@ -0,0 +1,9 @@ +/** + * Enum used to define the different modes for NodeParticleEditor + */ +export enum NodeParticleModes { + /** Standard particle system */ + Standard = 0, + /** SPS (Solid Particle System) */ + SPS = 1, +} From 5dc1e1d82663452fba4667d5e9669d6646841336 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 20 Oct 2025 09:59:31 +0300 Subject: [PATCH 30/68] Refactor SPSSystemBlock to streamline particle initialization and update logic This commit simplifies the particle initialization and update processes within the SPSSystemBlock. The logic for creating and managing connected points has been optimized, allowing for a more efficient retrieval of connected values. Additionally, the updateParticle method has been enhanced to directly utilize shape IDs for better performance and clarity. These changes improve the overall functionality and maintainability of the Solid Particle System. --- .../Blocks/SolidParticle/SPSSystemBlock.ts | 165 +++--------------- 1 file changed, 23 insertions(+), 142 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 6f7b412343d..861cbb993ff 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -6,6 +6,7 @@ import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSCreateData } from "./ISPSData"; +import { SolidParticle } from "../../../solidParticle"; /** * Block used to create SolidParticleSystem and collect all Create blocks @@ -64,161 +65,41 @@ export class SPSSystemBlock extends NodeParticleBlock { sps.billboard = this.billboard; sps.name = this.name; - const createBlocks: ISPSCreateData[] = []; - - if (this.solidParticle.endpoints.length > 0) { - for (const endpoint of this.solidParticle.endpoints) { - const createBlock = endpoint.getConnectedValue(state); - if (createBlock) { - createBlocks.push(createBlock); - } - } - } else if (this.solidParticle.isConnected) { - const createBlock = this.solidParticle.getConnectedValue(state); - if (createBlock) { - createBlocks.push(createBlock); - } - } - - if (createBlocks.length === 0 && this.solidParticle.allowMultipleConnections && this.solidParticle._connectedPoint) { - const connectedPoint = this.solidParticle._connectedPoint; - if (connectedPoint.endpoints && connectedPoint.endpoints.length > 0) { - for (const endpoint of connectedPoint.endpoints) { - const createBlock = endpoint.getConnectedValue(state); - if (createBlock) { - createBlocks.push(createBlock); - } - } - } - } + const createBlocks: ISPSCreateData[] = this.solidParticle.connectedPoints.map((connectedPoint) => connectedPoint._storedValue); for (const createBlock of createBlocks) { if (createBlock.mesh && createBlock.count) { - sps.addShape(createBlock.mesh, createBlock.count); - createBlock.mesh.dispose(); + createBlock.shapeId = sps.addShape(createBlock.mesh, createBlock.count); + createBlock.mesh.isVisible = false; } } sps.initParticles = () => { for (const createBlock of createBlocks) { - if (createBlock.initBlock) { - let startIndex = 0; - for (let i = 0; i < createBlocks.indexOf(createBlock); i++) { - startIndex += createBlocks[i].count; - } - const endIndex = startIndex + createBlock.count - 1; - - for (let p = startIndex; p <= endIndex && p < sps.nbParticles; p++) { - const particle = sps.particles[p]; - - if (createBlock.initBlock?.position) { - const particleContext = { - id: p, - position: particle.position, - velocity: particle.velocity, - color: particle.color, - scaling: particle.scaling, - rotation: particle.rotation, - }; - state.particleContext = particleContext as any; - - const positionValue = createBlock.initBlock.position; - if (typeof positionValue === "function") { - particle.position.copyFrom(positionValue()); - } else { - particle.position.copyFrom(positionValue); - } - } - if (createBlock.initBlock?.velocity) { - const velocityValue = createBlock.initBlock.velocity; - if (typeof velocityValue === "function") { - particle.velocity.copyFrom(velocityValue()); - } else { - particle.velocity.copyFrom(velocityValue); - } - } - if (createBlock.initBlock?.color) { - const colorValue = createBlock.initBlock.color; - if (typeof colorValue === "function") { - particle.color?.copyFrom(colorValue()); - } else { - particle.color?.copyFrom(colorValue); - } - } - if (createBlock.initBlock?.scaling) { - const scalingValue = createBlock.initBlock.scaling; - if (typeof scalingValue === "function") { - particle.scaling.copyFrom(scalingValue()); - } else { - particle.scaling.copyFrom(scalingValue); - } + if (createBlock.initBlock && createBlock.shapeId !== undefined) { + const particles = sps.getParticlesByShapeId(createBlock.shapeId); + + particles.forEach((particle) => { + if (createBlock.initBlock) { + particle.position.copyFrom(createBlock.initBlock.position()); + particle.velocity.copyFrom(createBlock.initBlock.velocity()); + particle.color?.copyFrom(createBlock.initBlock.color()); + particle.scaling.copyFrom(createBlock.initBlock.scaling()); + particle.rotation.copyFrom(createBlock.initBlock.rotation()); } - if (createBlock.initBlock?.rotation) { - const rotationValue = createBlock.initBlock.rotation; - if (typeof rotationValue === "function") { - particle.rotation.copyFrom(rotationValue()); - } else { - particle.rotation.copyFrom(rotationValue); - } - } - } + }); } } }; - sps.updateParticle = (particle: any) => { - let currentParticleIndex = 0; - let targetCreateBlock = null; - - for (const createBlock of createBlocks) { - if (particle.idx >= currentParticleIndex && particle.idx < currentParticleIndex + createBlock.count) { - targetCreateBlock = createBlock; - break; - } - currentParticleIndex += createBlock.count; - } - - if (targetCreateBlock && targetCreateBlock.updateBlock) { - if (targetCreateBlock.updateBlock.position) { - const positionValue = targetCreateBlock.updateBlock.position; - if (typeof positionValue === "function") { - particle.position.copyFrom(positionValue()); - } else { - particle.position.copyFrom(positionValue); - } - } - if (targetCreateBlock.updateBlock.velocity) { - const velocityValue = targetCreateBlock.updateBlock.velocity; - if (typeof velocityValue === "function") { - particle.velocity.copyFrom(velocityValue()); - } else { - particle.velocity.copyFrom(velocityValue); - } - } - if (targetCreateBlock.updateBlock.color) { - const colorValue = targetCreateBlock.updateBlock.color; - if (typeof colorValue === "function") { - particle.color?.copyFrom(colorValue()); - } else { - particle.color?.copyFrom(colorValue); - } - } - if (targetCreateBlock.updateBlock.scaling) { - const scalingValue = targetCreateBlock.updateBlock.scaling; - if (typeof scalingValue === "function") { - particle.scaling.copyFrom(scalingValue()); - } else { - particle.scaling.copyFrom(scalingValue); - } - } - if (targetCreateBlock.updateBlock.rotation) { - const rotationValue = targetCreateBlock.updateBlock.rotation; - if (typeof rotationValue === "function") { - particle.rotation.copyFrom(rotationValue()); - } else { - particle.rotation.copyFrom(rotationValue); - } - } + sps.updateParticle = (particle: SolidParticle) => { + const createBlock = createBlocks.find((createBlock) => createBlock.shapeId === particle.shapeId); + if (createBlock && createBlock.updateBlock) { + particle.position.copyFrom(createBlock.updateBlock.position()); + particle.velocity.copyFrom(createBlock.updateBlock.velocity()); + particle.color?.copyFrom(createBlock.updateBlock.color()); + particle.scaling.copyFrom(createBlock.updateBlock.scaling()); + particle.rotation.copyFrom(createBlock.updateBlock.rotation()); } return particle; }; From eaeecf4691bca5e3cbd494f32ae5127b81b33bae Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 20 Oct 2025 23:24:20 +0300 Subject: [PATCH 31/68] Enhance SolidParticleSystem and NodeParticleSystemSet with improved disposal and multi-connection support This commit updates the SolidParticleSystem to ensure proper disposal of the mesh when stopping the system, enhancing resource management. Additionally, the NodeParticleSystemSet has been modified to support multiple connections for input points, improving flexibility in particle system configurations. A new method, setToDefaultSPS, has been introduced to streamline the setup of Solid Particle Systems, further enhancing usability and functionality. --- .../src/Particles/Node/nodeParticleBlock.ts | 8 ++- .../Particles/Node/nodeParticleSystemSet.ts | 72 +++++++++++++++++-- .../core/src/Particles/solidParticleSystem.ts | 4 +- 3 files changed, 71 insertions(+), 13 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index c070fa013bc..1201e47db01 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -300,9 +300,11 @@ export class NodeParticleBlock { continue; } - const block = input.connectedPoint.ownerBlock; - if (block && block !== this && !block.isSystem) { - block.build(state); + const blocks = input.allowMultipleConnections ? input.connectedPoints.map((p) => p.ownerBlock) : [input.connectedPoint.ownerBlock]; + for (const block of blocks) { + if (block && block !== this && !block.isSystem) { + block.build(state); + } } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 6fbe6d91869..6b629e0a297 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,6 +22,9 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; +import { SPSCreateBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock } from "./Blocks"; +import { ParticleSystem } from ".."; +import { Vector3 } from "../../Maths"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -45,7 +48,7 @@ export interface INodeParticleEditorOptions { * PG: #ZT509U#1 */ export class NodeParticleSystemSet { - private _systemBlocks: SystemBlock[] = []; + private _systemBlocks: (SystemBlock | SPSSystemBlock)[] = []; private _buildId: number = 0; /** Define the Url to load node editor script */ @@ -90,7 +93,7 @@ export class NodeParticleSystemSet { /** * Gets the system blocks */ - public get systemBlocks(): SystemBlock[] { + public get systemBlocks(): (SystemBlock | SPSSystemBlock)[] { return this._systemBlocks; } @@ -269,13 +272,13 @@ export class NodeParticleSystemSet { state.verbose = verbose; const system = block.createSystem(state); - system._source = this; - system._blockReference = block._internalId; - + if (system instanceof ParticleSystem) { + system._source = this; + system._blockReference = block._internalId; + output.systems.push(system); + } // Errors state.emitErrors(); - - output.systems.push(system); } this.onBuildObservable.notifyObservers(this); @@ -336,6 +339,61 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } + public setToDefaultSPS() { + this.clear(); + this.editorData = null; + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.capacity = 1000; + spsSystem.billboard = false; + + const spsCreateBox = new SPSCreateBlock("Create Box Particles"); + const spsCreateSphere = new SPSCreateBlock("Create Sphere Particles"); + + spsCreateBox.count.value = 5; + spsCreateSphere.count.value = 1; + + spsCreateBox.solidParticle.connectTo(spsSystem.solidParticle); + spsCreateSphere.solidParticle.connectTo(spsSystem.solidParticle); + + const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); + const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); + + meshSourceBox.shapeType = SPSMeshShapeType.Box; + meshSourceSphere.shapeType = SPSMeshShapeType.Sphere; + + meshSourceBox.size = 1; + meshSourceSphere.size = 1; + + meshSourceBox.mesh.connectTo(spsCreateBox.mesh); + meshSourceSphere.mesh.connectTo(spsCreateSphere.mesh); + + const spsInitBox = new SPSInitBlock("Initialize Box Particles"); + const spsInitSphere = new SPSInitBlock("Initialize Sphere Particles"); + + spsInitBox.initData.connectTo(spsCreateBox.initBlock); + spsInitSphere.initData.connectTo(spsCreateSphere.initBlock); + + const positionBlockBox = new ParticleInputBlock("Position"); + positionBlockBox.value = new Vector3(1, 1, 1); + positionBlockBox.output.connectTo(spsInitBox.position); + + const rotationBlockBox = new ParticleInputBlock("Rotation"); + rotationBlockBox.value = new Vector3(3, 0, 0); + rotationBlockBox.output.connectTo(spsInitBox.rotation); + + const positionBlockSphere = new ParticleInputBlock("Position"); + positionBlockSphere.value = new Vector3(0, 0, 0); + positionBlockSphere.output.connectTo(spsInitSphere.position); + + const spsUpdateBox = new SPSUpdateBlock("Update Box Particles"); + const spsUpdateSphere = new SPSUpdateBlock("Update Sphere Particles"); + + spsUpdateBox.updateData.connectTo(spsCreateBox.updateBlock); + spsUpdateSphere.updateData.connectTo(spsCreateSphere.updateBlock); + + this._systemBlocks.push(spsSystem); + } + /** * Remove a block from the current system set * @param block defines the block to remove diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index c798370b496..aa78047ffd5 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -1565,6 +1565,7 @@ export class SolidParticleSystem implements IDisposable { } this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); this._onBeforeRenderObserver = null; + this.mesh.dispose(); this.isStarted = false; } @@ -1573,9 +1574,6 @@ export class SolidParticleSystem implements IDisposable { */ public dispose(): void { this.stop(); - if (this.mesh) { - this.mesh.dispose(); - } this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; From 49944148e93cd0cc2c709e8c7f8bb3bba6b787df Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 21 Oct 2025 11:24:55 +0300 Subject: [PATCH 32/68] Refactor NodeParticleBlock and NodeParticleConnectionPoint to simplify connection logic This commit streamlines the connection management within the NodeParticleBlock and NodeParticleConnectionPoint classes by removing the support for multiple connections. The `registerInput` method has been simplified, and the logic for handling connected points has been optimized. Additionally, the GraphCanvasComponent has been updated to reflect these changes, enhancing the overall clarity and maintainability of the codebase. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 10 +++++ .../Blocks/SolidParticle/SPSSystemBlock.ts | 42 ++++++++++++++++++- .../src/Particles/Node/nodeParticleBlock.ts | 20 ++------- .../Node/nodeParticleBlockConnectionPoint.ts | 22 ---------- .../Particles/Node/nodeParticleSystemSet.ts | 1 + .../src/nodeGraphSystem/graphCanvas.tsx | 2 +- .../graphSystem/connectionPointPortData.ts | 4 -- 7 files changed, 57 insertions(+), 44 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 3e7c6c2a20f..15c331da71e 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -4,6 +4,7 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import type { ISPSCreateData } from "./ISPSData"; +import { SPSSystemBlock } from "./SPSSystemBlock"; /** * Block used to configure SPS parameters (mesh, count, initBlocks) @@ -66,6 +67,15 @@ export class SPSCreateBlock extends NodeParticleBlock { }; this.solidParticle._storedValue = solidParticle; + + // If connected to SPSSystemBlock, add this create block to its particle sources + if (this.solidParticle.isConnected && this.solidParticle.connectedPoint?.ownerBlock instanceof SPSSystemBlock) { + const systemBlock = this.solidParticle.connectedPoint.ownerBlock as SPSSystemBlock; + // Remove existing source if it exists + systemBlock.removeParticleSource(solidParticle); + // Add the new source + systemBlock.addParticleSource(solidParticle); + } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 861cbb993ff..32ce4767290 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -7,6 +7,7 @@ import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSCreateData } from "./ISPSData"; import { SolidParticle } from "../../../solidParticle"; +import { Observable } from "../../../../Misc/observable"; /** * Block used to create SolidParticleSystem and collect all Create blocks @@ -30,6 +31,14 @@ export class SPSSystemBlock extends NodeParticleBlock { public _internalId = SPSSystemBlock._IdCounter++; + /** + * Gets or sets the list of particle sources + */ + public particleSources: ISPSCreateData[] = []; + + /** Gets an observable raised when the particle sources are changed */ + public onParticleSourcesChangedObservable = new Observable(); + public constructor(name: string) { super(name); @@ -51,6 +60,35 @@ export class SPSSystemBlock extends NodeParticleBlock { return this._outputs[0]; } + /** + * Add a particle source to the system + * @param source The particle source data + */ + public addParticleSource(source: ISPSCreateData): void { + this.particleSources.push(source); + this.onParticleSourcesChangedObservable.notifyObservers(this); + } + + /** + * Remove a particle source from the system + * @param source The particle source data to remove + */ + public removeParticleSource(source: ISPSCreateData): void { + const index = this.particleSources.indexOf(source); + if (index !== -1) { + this.particleSources.splice(index, 1); + this.onParticleSourcesChangedObservable.notifyObservers(this); + } + } + + /** + * Clear all particle sources + */ + public clearParticleSources(): void { + this.particleSources.length = 0; + this.onParticleSourcesChangedObservable.notifyObservers(this); + } + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { state.capacity = this.capacity; state.buildId = this._buildId++; @@ -65,7 +103,7 @@ export class SPSSystemBlock extends NodeParticleBlock { sps.billboard = this.billboard; sps.name = this.name; - const createBlocks: ISPSCreateData[] = this.solidParticle.connectedPoints.map((connectedPoint) => connectedPoint._storedValue); + const createBlocks: ISPSCreateData[] = this.particleSources; for (const createBlock of createBlocks) { if (createBlock.mesh && createBlock.count) { @@ -119,6 +157,7 @@ export class SPSSystemBlock extends NodeParticleBlock { const serializationObject = super.serialize(); serializationObject.capacity = this.capacity; serializationObject.billboard = this.billboard; + serializationObject.particleSources = this.particleSources; return serializationObject; } @@ -126,6 +165,7 @@ export class SPSSystemBlock extends NodeParticleBlock { super._deserialize(serializationObject); this.capacity = serializationObject.capacity; this.billboard = !!serializationObject.billboard; + this.particleSources = serializationObject.particleSources || []; } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index 1201e47db01..719e58614b9 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -218,18 +218,9 @@ export class NodeParticleBlock { * @param value value to return if there is no connection * @param valueMin min value accepted for value * @param valueMax max value accepted for value - * @param allowMultipleConnections defines if this input allows multiple connections * @returns the current block */ - public registerInput( - name: string, - type: NodeParticleBlockConnectionPointTypes, - isOptional: boolean = false, - value?: any, - valueMin?: any, - valueMax?: any, - allowMultipleConnections: boolean = false - ) { + public registerInput(name: string, type: NodeParticleBlockConnectionPointTypes, isOptional: boolean = false, value?: any, valueMin?: any, valueMax?: any) { const point = new NodeParticleConnectionPoint(name, this, NodeParticleConnectionPointDirection.Input); point.type = type; point.isOptional = isOptional; @@ -237,7 +228,6 @@ export class NodeParticleBlock { point.value = value; point.valueMin = valueMin; point.valueMax = valueMax; - point.allowMultipleConnections = allowMultipleConnections; this._inputs.push(point); @@ -300,11 +290,9 @@ export class NodeParticleBlock { continue; } - const blocks = input.allowMultipleConnections ? input.connectedPoints.map((p) => p.ownerBlock) : [input.connectedPoint.ownerBlock]; - for (const block of blocks) { - if (block && block !== this && !block.isSystem) { - block.build(state); - } + const block = input.connectedPoint.ownerBlock; + if (block && block !== this && !block.isSystem) { + block.build(state); } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts index 4a19ca56da6..9ca996b3840 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlockConnectionPoint.ts @@ -35,8 +35,6 @@ export class NodeParticleConnectionPoint { public _ownerBlock: NodeParticleBlock; /** @internal */ public _connectedPoint: Nullable = null; - /** @internal */ - public _connectedPoints = new Array(); /** @internal */ public _storedValue: any = null; @@ -120,11 +118,6 @@ export class NodeParticleConnectionPoint { */ public valueMax: Nullable = null; - /** - * Gets or sets a boolean indicating that this connection point allows multiple connections - */ - public allowMultipleConnections: boolean = false; - /** * Gets or sets the connection point type (default is float) */ @@ -200,11 +193,6 @@ export class NodeParticleConnectionPoint { return this._connectedPoint; } - /** Get the other side of the connection (if any) */ - public get connectedPoints(): Array { - return this._connectedPoints; - } - /** Get the block that owns this connection point */ public get ownerBlock(): NodeParticleBlock { return this._ownerBlock; @@ -342,11 +330,6 @@ export class NodeParticleConnectionPoint { this._endpoints.push(connectionPoint); connectionPoint._connectedPoint = this; - if (connectionPoint.allowMultipleConnections) { - connectionPoint._connectedPoints.push(this); - } else { - connectionPoint._connectedPoints = [this]; - } this.onConnectionObservable.notifyObservers(connectionPoint); connectionPoint.onConnectionObservable.notifyObservers(this); @@ -368,11 +351,6 @@ export class NodeParticleConnectionPoint { this._endpoints.splice(index, 1); endpoint._connectedPoint = null; - if (endpoint.allowMultipleConnections) { - endpoint._connectedPoints.splice(endpoint._connectedPoints.indexOf(this), 1); - } else { - endpoint._connectedPoints = []; - } this.onDisconnectionObservable.notifyObservers(endpoint); endpoint.onDisconnectionObservable.notifyObservers(this); diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 6b629e0a297..6317e864cbc 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -352,6 +352,7 @@ export class NodeParticleSystemSet { spsCreateBox.count.value = 5; spsCreateSphere.count.value = 1; + // Connect create blocks to system (this will automatically add them to particleSources) spsCreateBox.solidParticle.connectTo(spsSystem.solidParticle); spsCreateSphere.solidParticle.connectTo(spsSystem.solidParticle); diff --git a/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx b/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx index b1f6fcbfc8b..158641f525f 100644 --- a/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx +++ b/packages/dev/sharedUiComponents/src/nodeGraphSystem/graphCanvas.tsx @@ -1354,7 +1354,7 @@ export class GraphCanvasComponent extends React.Component = null; - if (pointB.isConnected && !pointB.allowMultipleConnections) { + if (pointB.isConnected) { const links = nodeB.getLinksForPortData(pointB); linksToNotifyForDispose = links.slice(); diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/connectionPointPortData.ts b/packages/tools/nodeParticleEditor/src/graphSystem/connectionPointPortData.ts index bb33a252da0..aba22908d37 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/connectionPointPortData.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/connectionPointPortData.ts @@ -143,10 +143,6 @@ export class ConnectionPointPortData implements IPortData { return endpoints; } - public get allowMultipleConnections() { - return this.data.allowMultipleConnections; - } - public constructor(connectionPoint: NodeParticleConnectionPoint, nodeContainer: INodeContainer) { this.data = connectionPoint; this._nodeContainer = nodeContainer; From 140a45e27b23d31489ffaf9e90d9eb7c87d99338 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 21 Oct 2025 15:50:18 +0300 Subject: [PATCH 33/68] Add unregisterInput method to NodeParticleBlock for dynamic input management This commit introduces the `unregisterInput` method to the NodeParticleBlock class, allowing for the dynamic removal of input connections. The method handles disconnection and notifies observers of input changes. Additionally, it removes commented-out code from the SPSCreateBlock and refines the input management logic in the SPSSystemBlock, enhancing overall clarity and maintainability of the particle system components. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 10 -- .../Blocks/SolidParticle/SPSSystemBlock.ts | 111 ++++++++++++------ .../src/Particles/Node/nodeParticleBlock.ts | 22 ++++ .../Particles/Node/nodeParticleSystemSet.ts | 1 - 4 files changed, 94 insertions(+), 50 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 15c331da71e..3e7c6c2a20f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -4,7 +4,6 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import type { ISPSCreateData } from "./ISPSData"; -import { SPSSystemBlock } from "./SPSSystemBlock"; /** * Block used to configure SPS parameters (mesh, count, initBlocks) @@ -67,15 +66,6 @@ export class SPSCreateBlock extends NodeParticleBlock { }; this.solidParticle._storedValue = solidParticle; - - // If connected to SPSSystemBlock, add this create block to its particle sources - if (this.solidParticle.isConnected && this.solidParticle.connectedPoint?.ownerBlock instanceof SPSSystemBlock) { - const systemBlock = this.solidParticle.connectedPoint.ownerBlock as SPSSystemBlock; - // Remove existing source if it exists - systemBlock.removeParticleSource(solidParticle); - // Add the new source - systemBlock.addParticleSource(solidParticle); - } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 32ce4767290..90acf510db1 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -7,13 +7,15 @@ import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSCreateData } from "./ISPSData"; import { SolidParticle } from "../../../solidParticle"; -import { Observable } from "../../../../Misc/observable"; +import { Observer } from "../../../../Misc"; /** * Block used to create SolidParticleSystem and collect all Create blocks */ export class SPSSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; + private _connectionObservers = new Map>(); + private _disconnectionObservers = new Map>(); @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { embedded: true, @@ -31,62 +33,82 @@ export class SPSSystemBlock extends NodeParticleBlock { public _internalId = SPSSystemBlock._IdCounter++; - /** - * Gets or sets the list of particle sources - */ - public particleSources: ISPSCreateData[] = []; - - /** Gets an observable raised when the particle sources are changed */ - public onParticleSourcesChangedObservable = new Observable(); - public constructor(name: string) { super(name); this._isSystem = true; - this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle, true, null, null, null, true); + this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); this.registerOutput("system", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + + this._manageExtendedInputs(0); } public override getClassName() { return "SPSSystemBlock"; } - public get solidParticle(): NodeParticleConnectionPoint { - return this._inputs[0]; + private _entryCount = 1; + + private _extend() { + this._entryCount++; + this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle, true); + this._manageExtendedInputs(this._entryCount - 1); } - public get system(): NodeParticleConnectionPoint { - return this._outputs[0]; + private _shrink() { + if (this._entryCount > 1) { + this._unmanageExtendedInputs(this._entryCount - 1); + this._entryCount--; + this.unregisterInput(`solidParticle-${this._entryCount}`); + } } - /** - * Add a particle source to the system - * @param source The particle source data - */ - public addParticleSource(source: ISPSCreateData): void { - this.particleSources.push(source); - this.onParticleSourcesChangedObservable.notifyObservers(this); + private _manageExtendedInputs(index: number) { + const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { + console.log("connectionObserver solidParticle", index); + console.log(" connectionObserver this._entryCount", this._entryCount); + if (this._entryCount - 1 > index) { + return; + } + this._extend(); + }); + + const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { + console.log("solidParticle", index); + console.log("this._entryCount", this._entryCount); + if (this._entryCount - 1 > index) { + return; + } + this._shrink(); + }); + + // Store observers for later removal + this._connectionObservers.set(index, connectionObserver); + this._disconnectionObservers.set(index, disconnectionObserver); } - /** - * Remove a particle source from the system - * @param source The particle source data to remove - */ - public removeParticleSource(source: ISPSCreateData): void { - const index = this.particleSources.indexOf(source); - if (index !== -1) { - this.particleSources.splice(index, 1); - this.onParticleSourcesChangedObservable.notifyObservers(this); + private _unmanageExtendedInputs(index: number) { + const connectionObserver = this._connectionObservers.get(index); + const disconnectionObserver = this._disconnectionObservers.get(index); + + if (connectionObserver) { + this._inputs[index].onConnectionObservable.remove(connectionObserver); + this._connectionObservers.delete(index); + } + + if (disconnectionObserver) { + this._inputs[index].onDisconnectionObservable.remove(disconnectionObserver); + this._disconnectionObservers.delete(index); } } - /** - * Clear all particle sources - */ - public clearParticleSources(): void { - this.particleSources.length = 0; - this.onParticleSourcesChangedObservable.notifyObservers(this); + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[this._entryCount - 1]; + } + + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; } public createSystem(state: NodeParticleBuildState): SolidParticleSystem { @@ -103,7 +125,13 @@ export class SPSSystemBlock extends NodeParticleBlock { sps.billboard = this.billboard; sps.name = this.name; - const createBlocks: ISPSCreateData[] = this.particleSources; + // Collect data from all connected solidParticle inputs + const createBlocks: ISPSCreateData[] = []; + for (let i = 0; i < this._inputs.length; i++) { + if (this._inputs[i].isConnected && this._inputs[i]._storedValue) { + createBlocks.push(this._inputs[i]._storedValue); + } + } for (const createBlock of createBlocks) { if (createBlock.mesh && createBlock.count) { @@ -157,7 +185,7 @@ export class SPSSystemBlock extends NodeParticleBlock { const serializationObject = super.serialize(); serializationObject.capacity = this.capacity; serializationObject.billboard = this.billboard; - serializationObject.particleSources = this.particleSources; + serializationObject._entryCount = this._entryCount; return serializationObject; } @@ -165,7 +193,12 @@ export class SPSSystemBlock extends NodeParticleBlock { super._deserialize(serializationObject); this.capacity = serializationObject.capacity; this.billboard = !!serializationObject.billboard; - this.particleSources = serializationObject.particleSources || []; + + if (serializationObject._entryCount && serializationObject._entryCount > 1) { + for (let i = 1; i < serializationObject._entryCount; i++) { + this._extend(); + } + } } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts index 719e58614b9..d4d24c45b8b 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBlock.ts @@ -236,6 +236,28 @@ export class NodeParticleBlock { return this; } + /** + * Unregister an input. Used for dynamic input management + * @param name defines the connection point name to remove + * @returns the current block + */ + public unregisterInput(name: string) { + const index = this._inputs.findIndex((input) => input.name === name); + if (index !== -1) { + const point = this._inputs[index]; + + if (point.isConnected) { + point.disconnectFrom(point.connectedPoint!); + } + + this._inputs.splice(index, 1); + + this.onInputChangedObservable.notifyObservers(point); + } + + return this; + } + /** * Register a new output. Must be called inside a block constructor * @param name defines the connection point name diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 6317e864cbc..6b629e0a297 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -352,7 +352,6 @@ export class NodeParticleSystemSet { spsCreateBox.count.value = 5; spsCreateSphere.count.value = 1; - // Connect create blocks to system (this will automatically add them to particleSources) spsCreateBox.solidParticle.connectTo(spsSystem.solidParticle); spsCreateSphere.solidParticle.connectTo(spsSystem.solidParticle); From 62c71437102dd47a4cacffdb182a9ad27a7de0b1 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 21 Oct 2025 15:52:13 +0300 Subject: [PATCH 34/68] Remove optional `allowMultipleConnections` property from IPortData interface to simplify connection management. This change aligns with recent refactoring efforts to streamline connection logic across the node graph system. --- .../src/nodeGraphSystem/interfaces/portData.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts b/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts index 917217c927b..00f285a1a83 100644 --- a/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts +++ b/packages/dev/sharedUiComponents/src/nodeGraphSystem/interfaces/portData.ts @@ -54,7 +54,6 @@ export interface IPortData { hasEndpoints: boolean; endpoints: Nullable; directValueDefinition?: IPortDirectValueDefinition; - allowMultipleConnections?: boolean; updateDisplayName: (newName: string) => void; canConnectTo: (port: IPortData) => boolean; From 9791892e98968285b26bf57debe9345ac76c59c0 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 28 Oct 2025 12:43:59 +0300 Subject: [PATCH 35/68] Refactor SolidParticleSystem and related blocks for improved initialization and update logic This commit removes the `isStarted` flag from the SolidParticleSystem, streamlining the start and stop methods. The ISPSUpdateData interface has been updated to make properties optional, enhancing flexibility in particle updates. Additionally, the SPSInitBlock and SPSUpdateBlock have been refactored to only assign update functions when the corresponding properties are connected, optimizing performance. The SPSSystemBlock has been improved to manage the SolidParticleSystem lifecycle more effectively, ensuring proper disposal and initialization of particles. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 11 +- .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 92 ++++++----- .../SolidParticle/SPSMeshSourceBlock.ts | 38 +++-- .../Blocks/SolidParticle/SPSSystemBlock.ts | 144 ++++++++++++------ .../Blocks/SolidParticle/SPSUpdateBlock.ts | 85 +++++------ .../core/src/Particles/solidParticleSystem.ts | 37 +---- 6 files changed, 214 insertions(+), 193 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index 7a92022a848..0119aab9b89 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -7,11 +7,11 @@ import type { Material } from "core/Materials/material"; * Interface for SPS update block data */ export interface ISPSUpdateData { - position: () => Vector3; - velocity: () => Vector3; - color: () => Color4; - scaling: () => Vector3; - rotation: () => Vector3; + position?: () => Vector3; + velocity?: () => Vector3; + color?: () => Color4; + scaling?: () => Vector3; + rotation?: () => Vector3; } /** @@ -23,5 +23,4 @@ export interface ISPSCreateData { material?: Material; initBlock?: ISPSUpdateData; updateBlock?: ISPSUpdateData; - shapeId?: number; } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts index cecf932f50f..df424f3d5b3 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -3,8 +3,6 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import { Vector3 } from "core/Maths/math.vector"; -import { Color4 } from "core/Maths/math.color"; import type { ISPSUpdateData } from "./ISPSData"; /** @@ -53,70 +51,70 @@ export class SPSInitBlock extends NodeParticleBlock { public override _build(state: NodeParticleBuildState) { const initData = {} as ISPSUpdateData; - initData.position = () => { - return this.getPosition(state); - }; - initData.velocity = () => { - return this.getVelocity(state); - }; - initData.color = () => { - return this.getColor(state); - }; - initData.scaling = () => { - return this.getScaling(state); - }; - initData.rotation = () => { - return this.getRotation(state); - }; + if (this.position.isConnected) { + initData.position = () => { + return this.getPosition(state); + }; + } + if (this.velocity.isConnected) { + initData.velocity = () => { + return this.getVelocity(state); + }; + } + if (this.color.isConnected) { + initData.color = () => { + return this.getColor(state); + }; + } + if (this.scaling.isConnected) { + initData.scaling = () => { + return this.getScaling(state); + }; + } + if (this.rotation.isConnected) { + initData.rotation = () => { + return this.getRotation(state); + }; + } this.initData._storedValue = initData; } private getPosition(state: NodeParticleBuildState) { - if (this.position.isConnected) { - if (this.position._storedFunction) { - return this.position._storedFunction!(state); - } - return this.position.getConnectedValue(state); + if (this.position._storedFunction) { + return this.position._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.position.getConnectedValue(state); } + private getVelocity(state: NodeParticleBuildState) { - if (this.velocity.isConnected) { - if (this.velocity._storedFunction) { - return this.velocity._storedFunction!(state); - } - return this.velocity.getConnectedValue(state); + if (this.velocity._storedFunction) { + return this.velocity._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.velocity.getConnectedValue(state); } + private getColor(state: NodeParticleBuildState) { - if (this.color.isConnected) { - if (this.color._storedFunction) { - return this.color._storedFunction!(state); - } - return this.color.getConnectedValue(state); + if (this.color._storedFunction) { + return this.color._storedFunction(state); } - return new Color4(1, 1, 1, 1); + return this.color.getConnectedValue(state); } + private getScaling(state: NodeParticleBuildState) { - if (this.scaling.isConnected) { - if (this.scaling._storedFunction) { - return this.scaling._storedFunction!(state); - } - return this.scaling.getConnectedValue(state); + if (this.scaling._storedFunction) { + return this.scaling._storedFunction(state); } - return new Vector3(1, 1, 1); + return this.scaling.getConnectedValue(state); } + private getRotation(state: NodeParticleBuildState) { - if (this.rotation.isConnected) { - if (this.rotation._storedFunction) { - return this.rotation._storedFunction!(state); - } - return this.rotation.getConnectedValue(state); + if (this.rotation._storedFunction) { + return this.rotation._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.rotation.getConnectedValue(state); } + public override serialize(): any { const serializationObject = super.serialize(); return serializationObject; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts index 251f051210a..fd4e56d9a5e 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts @@ -9,11 +9,15 @@ import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; import { SPSMeshShapeType } from "./SPSMeshShapeType"; +import { Mesh } from "../../../../Meshes"; /** * Block used to provide mesh source for SPS */ export class SPSMeshSourceBlock extends NodeParticleBlock { + private _mesh: Mesh | null = null; + private _disposeHandlerAdded = false; + @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { notifiers: { rebuild: true }, embedded: true, @@ -59,40 +63,52 @@ export class SPSMeshSourceBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - let mesh; - + if (this._mesh) { + this._mesh.dispose(); + this._mesh = null; + } if (this.shapeType === SPSMeshShapeType.Custom) { if (this.customMesh.isConnected) { const customMesh = this.customMesh.getConnectedValue(state); if (customMesh) { - mesh = customMesh; + this._mesh = customMesh; } else { - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); } } else { - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); } } else { switch (this.shapeType) { case SPSMeshShapeType.Box: - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); break; case SPSMeshShapeType.Sphere: - mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); + this._mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); break; case SPSMeshShapeType.Cylinder: - mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); + this._mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); break; case SPSMeshShapeType.Plane: - mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); break; default: - mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); break; } } + if (this._mesh) { + this._mesh.isVisible = false; + } - this.mesh._storedValue = mesh; + if (!this._disposeHandlerAdded) { + this.onDisposeObservable.addOnce(() => { + this._mesh?.dispose(); + this._mesh = null; + }); + this._disposeHandlerAdded = true; + } + this.mesh._storedValue = this._mesh; } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 90acf510db1..31f738f45ef 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -8,14 +8,19 @@ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSCreateData } from "./ISPSData"; import { SolidParticle } from "../../../solidParticle"; import { Observer } from "../../../../Misc"; +import { Nullable } from "../../../../types"; +import { Scene } from "../../../.."; /** * Block used to create SolidParticleSystem and collect all Create blocks */ export class SPSSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; + private _sps: SolidParticleSystem | null = null; private _connectionObservers = new Map>(); private _disconnectionObservers = new Map>(); + private _onBeforeRenderObserver: Nullable> = null; + private _disposeHandlerAdded = false; @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { embedded: true, @@ -35,9 +40,7 @@ export class SPSSystemBlock extends NodeParticleBlock { public constructor(name: string) { super(name); - this._isSystem = true; - this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); this.registerOutput("system", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); @@ -66,8 +69,6 @@ export class SPSSystemBlock extends NodeParticleBlock { private _manageExtendedInputs(index: number) { const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { - console.log("connectionObserver solidParticle", index); - console.log(" connectionObserver this._entryCount", this._entryCount); if (this._entryCount - 1 > index) { return; } @@ -75,8 +76,6 @@ export class SPSSystemBlock extends NodeParticleBlock { }); const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { - console.log("solidParticle", index); - console.log("this._entryCount", this._entryCount); if (this._entryCount - 1 > index) { return; } @@ -113,72 +112,121 @@ export class SPSSystemBlock extends NodeParticleBlock { public createSystem(state: NodeParticleBuildState): SolidParticleSystem { state.capacity = this.capacity; - state.buildId = this._buildId++; + state.buildId = this._buildId ? this._buildId + 1 : 0; this.build(state); if (!state.scene) { throw new Error("Scene is not initialized in NodeParticleBuildState"); } + if (this._sps) { + // dispose is not working correctly + // this._sps.dispose(); + this._sps = null; + } - const sps = new SolidParticleSystem(this.name, state.scene); - sps.billboard = this.billboard; - sps.name = this.name; + if (this._onBeforeRenderObserver) { + state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } - // Collect data from all connected solidParticle inputs - const createBlocks: ISPSCreateData[] = []; + this._sps = new SolidParticleSystem(this.name, state.scene, { + useModelMaterial: true, + }); + this._sps.billboard = this.billboard; + this._sps.name = this.name; + + const createBlocks = new Map(); for (let i = 0; i < this._inputs.length; i++) { - if (this._inputs[i].isConnected && this._inputs[i]._storedValue) { - createBlocks.push(this._inputs[i]._storedValue); + const creatData = this._inputs[i].getConnectedValue(state) as ISPSCreateData; + if (this._inputs[i].isConnected && creatData) { + if (creatData.mesh && creatData.count) { + const shapeId = this._sps.addShape(creatData.mesh, creatData.count); + createBlocks.set(shapeId, creatData); + creatData.mesh.isVisible = false; + } } } - for (const createBlock of createBlocks) { - if (createBlock.mesh && createBlock.count) { - createBlock.shapeId = sps.addShape(createBlock.mesh, createBlock.count); - createBlock.mesh.isVisible = false; + this._sps.initParticles = () => { + if (!this._sps) { + return; } - } - - sps.initParticles = () => { - for (const createBlock of createBlocks) { - if (createBlock.initBlock && createBlock.shapeId !== undefined) { - const particles = sps.getParticlesByShapeId(createBlock.shapeId); - - particles.forEach((particle) => { - if (createBlock.initBlock) { - particle.position.copyFrom(createBlock.initBlock.position()); - particle.velocity.copyFrom(createBlock.initBlock.velocity()); - particle.color?.copyFrom(createBlock.initBlock.color()); - particle.scaling.copyFrom(createBlock.initBlock.scaling()); - particle.rotation.copyFrom(createBlock.initBlock.rotation()); - } - }); + for (let p = 0; p < this._sps.nbParticles; p++) { + const particle = this._sps.particles[p]; + const particleCreateData = createBlocks.get(particle.shapeId); + const initBlock = particleCreateData?.initBlock; + if (!initBlock) { + continue; + } + if (initBlock.position) { + particle.position.copyFrom(initBlock.position()); + } + if (initBlock.velocity) { + particle.velocity.copyFrom(initBlock.velocity()); + } + if (initBlock.color) { + particle.color?.copyFrom(initBlock.color()); + } + if (initBlock.scaling) { + particle.scaling.copyFrom(initBlock.scaling()); + } + if (initBlock.rotation) { + particle.rotation.copyFrom(initBlock.rotation()); } } }; - sps.updateParticle = (particle: SolidParticle) => { - const createBlock = createBlocks.find((createBlock) => createBlock.shapeId === particle.shapeId); - if (createBlock && createBlock.updateBlock) { - particle.position.copyFrom(createBlock.updateBlock.position()); - particle.velocity.copyFrom(createBlock.updateBlock.velocity()); - particle.color?.copyFrom(createBlock.updateBlock.color()); - particle.scaling.copyFrom(createBlock.updateBlock.scaling()); - particle.rotation.copyFrom(createBlock.updateBlock.rotation()); + this._sps.updateParticle = (particle: SolidParticle) => { + if (!this._sps) { + return particle; + } + const particleCreateData = createBlocks.get(particle.shapeId); + const updateBlock = particleCreateData?.updateBlock; + if (!updateBlock) { + return particle; + } + if (updateBlock.position) { + particle.position.copyFrom(updateBlock.position()); + } + if (updateBlock.velocity) { + particle.velocity.copyFrom(updateBlock.velocity()); + } + if (updateBlock.color) { + particle.color?.copyFrom(updateBlock.color()); + } + if (updateBlock.scaling) { + particle.scaling.copyFrom(updateBlock.scaling()); + } + if (updateBlock.rotation) { + particle.rotation.copyFrom(updateBlock.rotation()); } return particle; }; - sps.start(); + this._sps.buildMesh(); + this._sps.initParticles(); + this._sps.setParticles(); - this.system._storedValue = this; - - this.onDisposeObservable.addOnce(() => { - sps.dispose(); + this._onBeforeRenderObserver = state.scene.onBeforeRenderObservable.add(() => { + this._sps?.setParticles(); }); - return sps; + this.system._storedValue = this; + + if (!this._disposeHandlerAdded) { + this.onDisposeObservable.addOnce(() => { + this._sps?.dispose(); + this._sps = null; + if (this._onBeforeRenderObserver) { + state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + }); + this._disposeHandlerAdded = true; + } + console.log("SPSSystemBlock#createSystem", this._sps.mesh.getScene().meshes.length); + return this._sps; } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index 4183fed9c9d..193f57e4bf8 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -53,72 +53,67 @@ export class SPSUpdateBlock extends NodeParticleBlock { public override _build(state: NodeParticleBuildState) { const updateData: ISPSUpdateData = {} as ISPSUpdateData; - updateData.position = () => { - return this.getPosition(state); - }; - updateData.velocity = () => { - return this.getVelocity(state); - }; - updateData.color = () => { - return this.getColor(state); - }; - updateData.scaling = () => { - return this.getScaling(state); - }; - updateData.rotation = () => { - return this.getRotation(state); - }; + if (this.position.isConnected) { + updateData.position = () => { + return this.getPosition(state); + }; + } + if (this.velocity.isConnected) { + updateData.velocity = () => { + return this.getVelocity(state); + }; + } + if (this.color.isConnected) { + updateData.color = () => { + return this.getColor(state); + }; + } + if (this.scaling.isConnected) { + updateData.scaling = () => { + return this.getScaling(state); + }; + } + if (this.rotation.isConnected) { + updateData.rotation = () => { + return this.getRotation(state); + }; + } this.updateData._storedValue = updateData; } private getPosition(state: NodeParticleBuildState) { - if (this.position.isConnected) { - if (this.position._storedFunction) { - return this.position._storedFunction!(state); - } - return this.position.getConnectedValue(state); + if (this.position._storedFunction) { + return this.position._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.position.getConnectedValue(state); } private getVelocity(state: NodeParticleBuildState) { - if (this.velocity.isConnected) { - if (this.velocity._storedFunction) { - return this.velocity._storedFunction!(state); - } - return this.velocity.getConnectedValue(state); + if (this.velocity._storedFunction) { + return this.velocity._storedFunction(state); } - return new Vector3(0, 0, 0); + return this.velocity.getConnectedValue(state); } private getColor(state: NodeParticleBuildState) { - if (this.color.isConnected) { - if (this.color._storedFunction) { - return this.color._storedFunction!(state); - } - return this.color.getConnectedValue(state); + if (this.color._storedFunction) { + return this.color._storedFunction(state); } - return new Color4(1, 1, 1, 1); + return this.color.getConnectedValue(state); } private getScaling(state: NodeParticleBuildState) { - if (this.scaling.isConnected) { - if (this.scaling._storedFunction) { - return this.scaling._storedFunction!(state); - } - return this.scaling.getConnectedValue(state); + if (this.scaling._storedFunction) { + return this.scaling._storedFunction(state); } - return new Vector3(1, 1, 1); + return this.scaling.getConnectedValue(state); } private getRotation(state: NodeParticleBuildState) { - if (this.rotation.isConnected) { - if (this.rotation._storedFunction) { - return this.rotation._storedFunction!(state); - } - return this.rotation.getConnectedValue(state); + if (this.rotation._storedFunction) { + return this.rotation._storedFunction!(state); } - return new Vector3(0, 0, 0); + return this.rotation.getConnectedValue(state); } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index aa78047ffd5..fa9a0a8a10b 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -7,7 +7,6 @@ import { Mesh } from "../Meshes/mesh"; import { CreateDisc } from "../Meshes/Builders/discBuilder"; import { EngineStore } from "../Engines/engineStore"; import type { Scene, IDisposable } from "../scene"; -import type { Observer } from "../Misc/observable"; import { DepthSortedParticle, SolidParticle, ModelShape, SolidParticleVertex } from "./solidParticle"; import type { TargetCamera } from "../Cameras/targetCamera"; import { BoundingInfo } from "../Culling/boundingInfo"; @@ -93,11 +92,6 @@ export class SolidParticleSystem implements IDisposable { */ public depthSortedParticles: DepthSortedParticle[]; - /** - * If the SPS has been started. - */ - public isStarted: boolean = false; - /** * If the particle intersection must be computed only with the bounding sphere (no bounding box computation, so faster). (Internal use only) * @internal @@ -1540,40 +1534,11 @@ export class SolidParticleSystem implements IDisposable { return this; } - private _onBeforeRenderObserver: Nullable> = null; - - /** - * Starts the SPS by subscribing to the scene's before render observable - */ - public start(): void { - this.stop(); - this.buildMesh(); - this.initParticles(); - this.setParticles(); - this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { - this.setParticles(); - }); - this.isStarted = true; - } - - /** - * Stops the SPS by unsubscribing from the scene's before render observable - */ - public stop(): void { - if (!this.isStarted) { - return; - } - this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - this.mesh.dispose(); - this.isStarted = false; - } - /** * Disposes the SPS. */ public dispose(): void { - this.stop(); + this.mesh.dispose(); this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; From dfe540f575184ac6064ee2d2e9a75d89329a70bc Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Wed, 29 Oct 2025 10:59:10 +0300 Subject: [PATCH 36/68] Refactor SolidParticleSystem blocks to introduce SPSParticleConfigBlock and enhance particle configuration This commit introduces the new SPSParticleConfigBlock for configuring particle parameters such as mesh, count, material, and initialization/update blocks. The SPSCreateBlock has been refactored to utilize this new block, streamlining the creation of particles. Additionally, the ISPSData interface has been updated to reflect the new configuration structure, and various blocks have been adjusted to improve their connection logic and overall functionality within the particle system. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 2 +- .../Blocks/SolidParticle/SPSCreateBlock.ts | 211 +++++++++++++++--- .../SolidParticle/SPSParticleConfigBlock.ts | 72 ++++++ .../Blocks/SolidParticle/SPSSystemBlock.ts | 208 ++--------------- .../Blocks/SolidParticle/SPSUpdateBlock.ts | 2 - .../Node/Blocks/SolidParticle/index.ts | 3 +- .../Particles/Node/nodeParticleSystemSet.ts | 14 +- .../nodeParticleEditor/src/blockTools.ts | 4 +- .../components/nodeList/nodeListComponent.tsx | 7 +- .../graphSystem/registerToDisplayLedger.ts | 2 +- .../graphSystem/registerToPropertyLedger.ts | 2 +- 11 files changed, 285 insertions(+), 242 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index 0119aab9b89..8d0e2f0227f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -17,7 +17,7 @@ export interface ISPSUpdateData { /** * Interface for SPS create block data */ -export interface ISPSCreateData { +export interface ISPSParticleConfigData { mesh: Mesh; count: number; material?: Material; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 3e7c6c2a20f..25823dd45e5 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -3,69 +3,212 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISPSCreateData } from "./ISPSData"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { ISPSParticleConfigData } from "./ISPSData"; +import { SolidParticle } from "../../../solidParticle"; +import { Observer } from "../../../../Misc"; +import { Nullable } from "../../../../types"; +import { Scene } from "../../../.."; /** - * Block used to configure SPS parameters (mesh, count, initBlocks) + * Block used to create SolidParticleSystem and collect all Create blocks */ export class SPSCreateBlock extends NodeParticleBlock { + private _connectionObservers = new Map>(); + private _disconnectionObservers = new Map>(); + private _onBeforeRenderObserver: Nullable> = null; + private _disposeHandlerAdded = false; + public constructor(name: string) { super(name); + this.registerInput(`particleConfig-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); - this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); - this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); - this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); - this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); - this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); - - this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this._manageExtendedInputs(0); } public override getClassName() { return "SPSCreateBlock"; } - public get mesh(): NodeParticleConnectionPoint { - return this._inputs[0]; + private _entryCount = 1; + + private _extend() { + this._entryCount++; + this.registerInput(`particleConfig-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle, true); + this._manageExtendedInputs(this._entryCount - 1); } - public get count(): NodeParticleConnectionPoint { - return this._inputs[1]; + private _shrink() { + if (this._entryCount > 1) { + this._unmanageExtendedInputs(this._entryCount - 1); + this._entryCount--; + this.unregisterInput(`particleConfig-${this._entryCount}`); + } } - public get material(): NodeParticleConnectionPoint { - return this._inputs[2]; + private _manageExtendedInputs(index: number) { + const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { + if (this._entryCount - 1 > index) { + return; + } + this._extend(); + }); + + const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { + if (this._entryCount - 1 > index) { + return; + } + this._shrink(); + }); + + // Store observers for later removal + this._connectionObservers.set(index, connectionObserver); + this._disconnectionObservers.set(index, disconnectionObserver); } - public get initBlock(): NodeParticleConnectionPoint { - return this._inputs[3]; + private _unmanageExtendedInputs(index: number) { + const connectionObserver = this._connectionObservers.get(index); + const disconnectionObserver = this._disconnectionObservers.get(index); + + if (connectionObserver) { + this._inputs[index].onConnectionObservable.remove(connectionObserver); + this._connectionObservers.delete(index); + } + + if (disconnectionObserver) { + this._inputs[index].onDisconnectionObservable.remove(disconnectionObserver); + this._disconnectionObservers.delete(index); + } } - public get updateBlock(): NodeParticleConnectionPoint { - return this._inputs[4]; + public get particleConfig(): NodeParticleConnectionPoint { + return this._inputs[this._entryCount - 1]; } - public get solidParticle(): NodeParticleConnectionPoint { + public get solidParticleSystem(): NodeParticleConnectionPoint { return this._outputs[0]; } public override _build(state: NodeParticleBuildState) { - const mesh = this.mesh.getConnectedValue(state); - const count = (this.count.getConnectedValue(state) as number) || 1; - const material = this.material.getConnectedValue(state); - - const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; - const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; - - const solidParticle: ISPSCreateData = { - mesh, - count, - material, - initBlock, - updateBlock, + if (!state.scene) { + throw new Error("Scene is not initialized in NodeParticleBuildState"); + } + + if (this._onBeforeRenderObserver) { + state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + + const sps = new SolidParticleSystem(this.name, state.scene, { + useModelMaterial: true, + }); + + const createBlocks = new Map(); + for (let i = 0; i < this._inputs.length; i++) { + const creatData = this._inputs[i].getConnectedValue(state) as ISPSParticleConfigData; + if (this._inputs[i].isConnected && creatData) { + if (creatData.mesh && creatData.count) { + const shapeId = sps.addShape(creatData.mesh, creatData.count); + createBlocks.set(shapeId, creatData); + creatData.mesh.isVisible = false; + } + } + } + + sps.initParticles = () => { + if (!sps) { + return; + } + for (let p = 0; p < sps.nbParticles; p++) { + const particle = sps.particles[p]; + const particleCreateData = createBlocks.get(particle.shapeId); + const initBlock = particleCreateData?.initBlock; + if (!initBlock) { + continue; + } + if (initBlock.position) { + particle.position.copyFrom(initBlock.position()); + } + if (initBlock.velocity) { + particle.velocity.copyFrom(initBlock.velocity()); + } + if (initBlock.color) { + particle.color?.copyFrom(initBlock.color()); + } + if (initBlock.scaling) { + particle.scaling.copyFrom(initBlock.scaling()); + } + if (initBlock.rotation) { + particle.rotation.copyFrom(initBlock.rotation()); + } + } + }; + + sps.updateParticle = (particle: SolidParticle) => { + if (!sps) { + return particle; + } + const particleCreateData = createBlocks.get(particle.shapeId); + const updateBlock = particleCreateData?.updateBlock; + if (!updateBlock) { + return particle; + } + if (updateBlock.position) { + particle.position.copyFrom(updateBlock.position()); + } + if (updateBlock.velocity) { + particle.velocity.copyFrom(updateBlock.velocity()); + } + if (updateBlock.color) { + particle.color?.copyFrom(updateBlock.color()); + } + if (updateBlock.scaling) { + particle.scaling.copyFrom(updateBlock.scaling()); + } + if (updateBlock.rotation) { + particle.rotation.copyFrom(updateBlock.rotation()); + } + return particle; }; - this.solidParticle._storedValue = solidParticle; + sps.buildMesh(); + sps.initParticles(); + sps.setParticles(); + + this._onBeforeRenderObserver = state.scene.onBeforeRenderObservable.add(() => { + sps?.setParticles(); + }); + + // this.solidParticleSystem._storedValue?.dispose(); + this.solidParticleSystem._storedValue = sps; + + if (!this._disposeHandlerAdded) { + this.onDisposeObservable.addOnce(() => { + sps?.dispose(); + if (this._onBeforeRenderObserver) { + state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + }); + this._disposeHandlerAdded = true; + } + return sps; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject._entryCount = this._entryCount; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + if (serializationObject._entryCount && serializationObject._entryCount > 1) { + for (let i = 1; i < serializationObject._entryCount; i++) { + this._extend(); + } + } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts new file mode 100644 index 00000000000..d585cbd1258 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -0,0 +1,72 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISPSParticleConfigData } from "./ISPSData"; + +/** + * Block used to configure SPS particle parameters (mesh, count, material, initBlock, updateBlock) + */ +export class SPSParticleConfigBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); + this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); + this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); + this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); + + this.registerOutput("particleConfig", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "SPSParticleConfigBlock"; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get count(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get material(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get initBlock(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get updateBlock(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public get particleConfig(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const mesh = this.mesh.getConnectedValue(state); + const count = (this.count.getConnectedValue(state) as number) || 1; + const material = this.material.getConnectedValue(state); + + const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; + const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; + + const particleConfig: ISPSParticleConfigData = { + mesh, + count, + material, + initBlock, + updateBlock, + }; + + this.particleConfig._storedValue = particleConfig; + } +} + +RegisterClass("BABYLON.SPSParticleConfigBlock", SPSParticleConfigBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 31f738f45ef..80b89056897 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -5,30 +5,17 @@ import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnect import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; -import type { ISPSCreateData } from "./ISPSData"; -import { SolidParticle } from "../../../solidParticle"; -import { Observer } from "../../../../Misc"; -import { Nullable } from "../../../../types"; -import { Scene } from "../../../.."; /** * Block used to create SolidParticleSystem and collect all Create blocks */ export class SPSSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; - private _sps: SolidParticleSystem | null = null; - private _connectionObservers = new Map>(); - private _disconnectionObservers = new Map>(); - private _onBeforeRenderObserver: Nullable> = null; - private _disposeHandlerAdded = false; - - @editableInPropertyPage("Capacity", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - notifiers: { rebuild: true }, - min: 0, - max: 100000, - }) - public capacity = 1000; + // private _sps: SolidParticleSystem | null = null; + // private _connectionObservers = new Map>(); + // private _disconnectionObservers = new Map>(); + // private _onBeforeRenderObserver: Nullable> = null; + // private _disposeHandlerAdded = false; @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { embedded: true, @@ -41,69 +28,16 @@ export class SPSSystemBlock extends NodeParticleBlock { public constructor(name: string) { super(name); this._isSystem = true; - this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerOutput("system", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); - - this._manageExtendedInputs(0); + this.registerInput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + this.registerOutput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); } public override getClassName() { return "SPSSystemBlock"; } - private _entryCount = 1; - - private _extend() { - this._entryCount++; - this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle, true); - this._manageExtendedInputs(this._entryCount - 1); - } - - private _shrink() { - if (this._entryCount > 1) { - this._unmanageExtendedInputs(this._entryCount - 1); - this._entryCount--; - this.unregisterInput(`solidParticle-${this._entryCount}`); - } - } - - private _manageExtendedInputs(index: number) { - const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { - if (this._entryCount - 1 > index) { - return; - } - this._extend(); - }); - - const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { - if (this._entryCount - 1 > index) { - return; - } - this._shrink(); - }); - - // Store observers for later removal - this._connectionObservers.set(index, connectionObserver); - this._disconnectionObservers.set(index, disconnectionObserver); - } - - private _unmanageExtendedInputs(index: number) { - const connectionObserver = this._connectionObservers.get(index); - const disconnectionObserver = this._disconnectionObservers.get(index); - - if (connectionObserver) { - this._inputs[index].onConnectionObservable.remove(connectionObserver); - this._connectionObservers.delete(index); - } - - if (disconnectionObserver) { - this._inputs[index].onDisconnectionObservable.remove(disconnectionObserver); - this._disconnectionObservers.delete(index); - } - } - - public get solidParticle(): NodeParticleConnectionPoint { - return this._inputs[this._entryCount - 1]; + public get solidParticleSystem(): NodeParticleConnectionPoint { + return this._inputs[0]; } public get system(): NodeParticleConnectionPoint { @@ -111,142 +45,32 @@ export class SPSSystemBlock extends NodeParticleBlock { } public createSystem(state: NodeParticleBuildState): SolidParticleSystem { - state.capacity = this.capacity; - state.buildId = this._buildId ? this._buildId + 1 : 0; + state.buildId = ++this._buildId; this.build(state); - if (!state.scene) { - throw new Error("Scene is not initialized in NodeParticleBuildState"); - } - if (this._sps) { - // dispose is not working correctly - // this._sps.dispose(); - this._sps = null; - } + const sps = this.solidParticleSystem.getConnectedValue(state) as SolidParticleSystem; - if (this._onBeforeRenderObserver) { - state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; + if (!sps) { + throw new Error("No SolidParticleSystem connected to SPSSystemBlock"); } - this._sps = new SolidParticleSystem(this.name, state.scene, { - useModelMaterial: true, - }); - this._sps.billboard = this.billboard; - this._sps.name = this.name; - - const createBlocks = new Map(); - for (let i = 0; i < this._inputs.length; i++) { - const creatData = this._inputs[i].getConnectedValue(state) as ISPSCreateData; - if (this._inputs[i].isConnected && creatData) { - if (creatData.mesh && creatData.count) { - const shapeId = this._sps.addShape(creatData.mesh, creatData.count); - createBlocks.set(shapeId, creatData); - creatData.mesh.isVisible = false; - } - } - } - - this._sps.initParticles = () => { - if (!this._sps) { - return; - } - for (let p = 0; p < this._sps.nbParticles; p++) { - const particle = this._sps.particles[p]; - const particleCreateData = createBlocks.get(particle.shapeId); - const initBlock = particleCreateData?.initBlock; - if (!initBlock) { - continue; - } - if (initBlock.position) { - particle.position.copyFrom(initBlock.position()); - } - if (initBlock.velocity) { - particle.velocity.copyFrom(initBlock.velocity()); - } - if (initBlock.color) { - particle.color?.copyFrom(initBlock.color()); - } - if (initBlock.scaling) { - particle.scaling.copyFrom(initBlock.scaling()); - } - if (initBlock.rotation) { - particle.rotation.copyFrom(initBlock.rotation()); - } - } - }; - - this._sps.updateParticle = (particle: SolidParticle) => { - if (!this._sps) { - return particle; - } - const particleCreateData = createBlocks.get(particle.shapeId); - const updateBlock = particleCreateData?.updateBlock; - if (!updateBlock) { - return particle; - } - if (updateBlock.position) { - particle.position.copyFrom(updateBlock.position()); - } - if (updateBlock.velocity) { - particle.velocity.copyFrom(updateBlock.velocity()); - } - if (updateBlock.color) { - particle.color?.copyFrom(updateBlock.color()); - } - if (updateBlock.scaling) { - particle.scaling.copyFrom(updateBlock.scaling()); - } - if (updateBlock.rotation) { - particle.rotation.copyFrom(updateBlock.rotation()); - } - return particle; - }; - - this._sps.buildMesh(); - this._sps.initParticles(); - this._sps.setParticles(); - - this._onBeforeRenderObserver = state.scene.onBeforeRenderObservable.add(() => { - this._sps?.setParticles(); - }); + sps.billboard = this.billboard; + sps.name = this.name; this.system._storedValue = this; - - if (!this._disposeHandlerAdded) { - this.onDisposeObservable.addOnce(() => { - this._sps?.dispose(); - this._sps = null; - if (this._onBeforeRenderObserver) { - state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - } - }); - this._disposeHandlerAdded = true; - } - console.log("SPSSystemBlock#createSystem", this._sps.mesh.getScene().meshes.length); - return this._sps; + return sps; } public override serialize(): any { const serializationObject = super.serialize(); - serializationObject.capacity = this.capacity; serializationObject.billboard = this.billboard; - serializationObject._entryCount = this._entryCount; return serializationObject; } public override _deserialize(serializationObject: any) { super._deserialize(serializationObject); - this.capacity = serializationObject.capacity; this.billboard = !!serializationObject.billboard; - - if (serializationObject._entryCount && serializationObject._entryCount > 1) { - for (let i = 1; i < serializationObject._entryCount; i++) { - this._extend(); - } - } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index 193f57e4bf8..cadae7dea94 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -3,8 +3,6 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import { Vector3 } from "core/Maths/math.vector"; -import { Color4 } from "core/Maths/math.color"; import type { ISPSUpdateData } from "./ISPSData"; /** diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 2b66eabf628..070d7ebd205 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -1,7 +1,8 @@ export * from "./ISPSData"; export * from "./SPSMeshShapeType"; export * from "./SPSMeshSourceBlock"; -export * from "./SPSCreateBlock"; +export * from "./SPSParticleConfigBlock"; export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; export * from "./SPSInitBlock"; +export * from "./SPSCreateBlock"; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 6b629e0a297..4ac93e26363 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,7 +22,7 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; -import { SPSCreateBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock } from "./Blocks"; +import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock, SPSCreateBlock } from "./Blocks"; import { ParticleSystem } from ".."; import { Vector3 } from "../../Maths"; @@ -343,17 +343,19 @@ export class NodeParticleSystemSet { this.clear(); this.editorData = null; const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.capacity = 1000; spsSystem.billboard = false; - const spsCreateBox = new SPSCreateBlock("Create Box Particles"); - const spsCreateSphere = new SPSCreateBlock("Create Sphere Particles"); + const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + + const spsCreateBox = new SPSParticleConfigBlock("Create Box Particles"); + const spsCreateSphere = new SPSParticleConfigBlock("Create Sphere Particles"); spsCreateBox.count.value = 5; spsCreateSphere.count.value = 1; - spsCreateBox.solidParticle.connectTo(spsSystem.solidParticle); - spsCreateSphere.solidParticle.connectTo(spsSystem.solidParticle); + spsCreateBox.particleConfig.connectTo(spsCreateBlock.particleConfig); + spsCreateSphere.particleConfig.connectTo(spsCreateBlock.particleConfig); const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 60b0ec0f443..6325534a0b4 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -42,7 +42,7 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock, SPSUpdateBlock } from "core/Particles/Node/Blocks"; +import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock, SPSUpdateBlock, SPSParticleConfigBlock } from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -153,6 +153,8 @@ export class BlockTools { return new SystemBlock("System"); case "SPSMeshSourceBlock": return new SPSMeshSourceBlock("SPS Mesh Source"); + case "SPSParticleConfigBlock": + return new SPSParticleConfigBlock("SPS Particle Config"); case "SPSSystemBlock": return new SPSSystemBlock("SPS System"); case "SPSCreateBlock": diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 8afabb07a56..9491bfd406a 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -24,10 +24,11 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["UpdateFlowMapBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["SystemBlock"] = SystemDisplayManager; - // SPS Blocks DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSParticleConfigBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; DisplayLedger.RegisteredControls["SPSInitBlock"] = UpdateDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index cd5287e3d42..57075b01779 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -14,8 +14,8 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; - // SPS Blocks - use default GenericPropertyComponent PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSParticleConfigBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; From a4207e3fcd602b6b005af51a3b949c0ea32e0082 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 30 Oct 2025 17:15:48 +0300 Subject: [PATCH 37/68] Refactor SPSCreateBlock and SPSSystemBlock to improve lifecycle management and remove unused observers This commit refactors the SPSCreateBlock and SPSSystemBlock classes by removing unused observer properties and simplifying the lifecycle management of SolidParticleSystem instances. The changes enhance clarity and maintainability by eliminating redundant code related to rendering observers and disposal handlers, ensuring a more efficient handling of particle systems. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 26 ------------------- .../Blocks/SolidParticle/SPSSystemBlock.ts | 19 ++++++-------- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 25823dd45e5..41a398781b2 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -7,8 +7,6 @@ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISPSParticleConfigData } from "./ISPSData"; import { SolidParticle } from "../../../solidParticle"; import { Observer } from "../../../../Misc"; -import { Nullable } from "../../../../types"; -import { Scene } from "../../../.."; /** * Block used to create SolidParticleSystem and collect all Create blocks @@ -16,8 +14,6 @@ import { Scene } from "../../../.."; export class SPSCreateBlock extends NodeParticleBlock { private _connectionObservers = new Map>(); private _disconnectionObservers = new Map>(); - private _onBeforeRenderObserver: Nullable> = null; - private _disposeHandlerAdded = false; public constructor(name: string) { super(name); @@ -95,11 +91,6 @@ export class SPSCreateBlock extends NodeParticleBlock { throw new Error("Scene is not initialized in NodeParticleBuildState"); } - if (this._onBeforeRenderObserver) { - state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - } - const sps = new SolidParticleSystem(this.name, state.scene, { useModelMaterial: true, }); @@ -176,24 +167,7 @@ export class SPSCreateBlock extends NodeParticleBlock { sps.initParticles(); sps.setParticles(); - this._onBeforeRenderObserver = state.scene.onBeforeRenderObservable.add(() => { - sps?.setParticles(); - }); - - // this.solidParticleSystem._storedValue?.dispose(); this.solidParticleSystem._storedValue = sps; - - if (!this._disposeHandlerAdded) { - this.onDisposeObservable.addOnce(() => { - sps?.dispose(); - if (this._onBeforeRenderObserver) { - state.scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); - this._onBeforeRenderObserver = null; - } - }); - this._disposeHandlerAdded = true; - } - return sps; } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 80b89056897..4d0a8e4340d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -11,11 +11,6 @@ import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; */ export class SPSSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; - // private _sps: SolidParticleSystem | null = null; - // private _connectionObservers = new Map>(); - // private _disconnectionObservers = new Map>(); - // private _onBeforeRenderObserver: Nullable> = null; - // private _disposeHandlerAdded = false; @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { embedded: true, @@ -49,17 +44,19 @@ export class SPSSystemBlock extends NodeParticleBlock { this.build(state); - const sps = this.solidParticleSystem.getConnectedValue(state) as SolidParticleSystem; + const solidParticleSystem = this.solidParticleSystem.getConnectedValue(state) as SolidParticleSystem; - if (!sps) { + if (!solidParticleSystem) { throw new Error("No SolidParticleSystem connected to SPSSystemBlock"); } - sps.billboard = this.billboard; - sps.name = this.name; + solidParticleSystem.billboard = this.billboard; + solidParticleSystem.name = this.name; - this.system._storedValue = this; - return sps; + this.onDisposeObservable.addOnce(() => { + solidParticleSystem.dispose(); + }); + return solidParticleSystem; } public override serialize(): any { From 6ac1d1591f51318f444a30ecc65b00eaf6a8e0f4 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 31 Oct 2025 13:58:49 +0300 Subject: [PATCH 38/68] Enhance ParticleSystemSet to support SolidParticleSystem integration This commit updates the ParticleSystemSet class to accommodate SolidParticleSystem instances alongside traditional ParticleSystem objects. The systems array is modified to include both types, and conditional checks are added to ensure proper handling of emitters and serialization for SolidParticleSystems. These changes improve the flexibility and functionality of the particle system management. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 4 ---- .../Particles/Node/nodeParticleSystemSet.ts | 2 +- .../core/src/Particles/particleSystemSet.ts | 17 +++++++++++----- .../core/src/Particles/solidParticleSystem.ts | 20 +++++++++++++++++-- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 41a398781b2..6a5c475662d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -163,10 +163,6 @@ export class SPSCreateBlock extends NodeParticleBlock { return particle; }; - sps.buildMesh(); - sps.initParticles(); - sps.setParticles(); - this.solidParticleSystem._storedValue = sps; } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 4ac93e26363..56a3f0238b6 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -275,8 +275,8 @@ export class NodeParticleSystemSet { if (system instanceof ParticleSystem) { system._source = this; system._blockReference = block._internalId; - output.systems.push(system); } + output.systems.push(system); // Errors state.emitErrors(); } diff --git a/packages/dev/core/src/Particles/particleSystemSet.ts b/packages/dev/core/src/Particles/particleSystemSet.ts index 9537fdbbc51..75b52830ad7 100644 --- a/packages/dev/core/src/Particles/particleSystemSet.ts +++ b/packages/dev/core/src/Particles/particleSystemSet.ts @@ -9,6 +9,7 @@ import { ParticleSystem } from "../Particles/particleSystem"; import type { Scene, IDisposable } from "../scene"; import { StandardMaterial } from "../Materials/standardMaterial"; import type { Vector3 } from "../Maths/math.vector"; +import type { SolidParticleSystem } from "./solidParticleSystem"; /** Internal class used to store shapes for emitters */ class ParticleSystemSetEmitterCreationOptions { @@ -34,7 +35,7 @@ export class ParticleSystemSet implements IDisposable { /** * Gets the particle system list */ - public systems: IParticleSystem[] = []; + public systems: (IParticleSystem | SolidParticleSystem)[] = []; /** * Gets or sets the emitter node used with this set @@ -52,7 +53,9 @@ export class ParticleSystemSet implements IDisposable { } for (const system of this.systems) { - system.emitter = value; + if (system instanceof ParticleSystem) { + system.emitter = value; + } } this._emitterNode = value; @@ -90,7 +93,9 @@ export class ParticleSystemSet implements IDisposable { emitterMesh.material = material; for (const system of this.systems) { - system.emitter = emitterMesh; + if (system instanceof ParticleSystem) { + system.emitter = emitterMesh; + } } this._emitterNode = emitterMesh; @@ -103,7 +108,9 @@ export class ParticleSystemSet implements IDisposable { public start(emitter?: AbstractMesh): void { for (const system of this.systems) { if (emitter) { - system.emitter = emitter; + if (system instanceof ParticleSystem) { + system.emitter = emitter; + } } system.start(); } @@ -137,7 +144,7 @@ export class ParticleSystemSet implements IDisposable { result.systems = []; for (const system of this.systems) { - if (!system.doNotSerialize) { + if (system instanceof ParticleSystem && !system.doNotSerialize) { result.systems.push(system.serialize(serializeTexture)); } } diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index fa9a0a8a10b..0b002d34fbe 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -17,6 +17,7 @@ import { StandardMaterial } from "../Materials/standardMaterial"; import { MultiMaterial } from "../Materials/multiMaterial"; import type { PickingInfo } from "../Collisions/pickingInfo"; import type { PBRMaterial } from "../Materials/PBR/pbrMaterial"; +import type { Observer } from "../Misc/observable"; /** * The SPS is a single updatable mesh. The solid particles are simply separate parts or faces of this big mesh. @@ -152,7 +153,7 @@ export class SolidParticleSystem implements IDisposable { protected _autoUpdateSubMeshes: boolean = false; protected _tmpVertex: SolidParticleVertex; protected _recomputeInvisibles: boolean = false; - + protected _onBeforeRenderObserver: Nullable> = null; /** * Creates a SPS (Solid Particle System) object. * @param name (String) is the SPS name, this will be the underlying mesh name. @@ -1538,7 +1539,13 @@ export class SolidParticleSystem implements IDisposable { * Disposes the SPS. */ public dispose(): void { - this.mesh.dispose(); + if (this._onBeforeRenderObserver) { + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + if (this.mesh) { + this.mesh.dispose(); + } this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; @@ -1998,6 +2005,15 @@ export class SolidParticleSystem implements IDisposable { */ public initParticles(): void {} + public start(): void { + this.buildMesh(); + this.initParticles(); + this.setParticles(); + this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { + this.setParticles(); + }); + } + /** * This function does nothing. It may be overwritten to recycle a particle. * The SPS doesn't call this function, you may have to call it by your own. From cc9bbe0b6def40dae349d74c8f1508b8cbda4d31 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 10:26:33 +0300 Subject: [PATCH 39/68] Enhance NodeParticleBuildState and NodeParticleSystemSet for SolidParticle integration This commit updates the NodeParticleBuildState class to support both Particle and SolidParticle contexts, allowing for more flexible handling of particle data. The systemContext property is modified to accommodate both ThinParticleSystem and SolidParticleSystem. Additionally, the NodeParticleSystemSet class is enhanced with a new method, setToTetrahedronSPS, which sets up a SolidParticleSystem with a tetrahedron configuration, including random positioning, rotation, and color generation. These changes improve the overall functionality and versatility of the particle system management. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 98 +++++--- .../Particles/Node/nodeParticleBuildState.ts | 183 ++++++++++++-- .../Particles/Node/nodeParticleSystemSet.ts | 236 +++++++++++++++--- 3 files changed, 417 insertions(+), 100 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 6a5c475662d..d0798fe21c0 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -111,28 +111,41 @@ export class SPSCreateBlock extends NodeParticleBlock { if (!sps) { return; } - for (let p = 0; p < sps.nbParticles; p++) { - const particle = sps.particles[p]; - const particleCreateData = createBlocks.get(particle.shapeId); - const initBlock = particleCreateData?.initBlock; - if (!initBlock) { - continue; - } - if (initBlock.position) { - particle.position.copyFrom(initBlock.position()); - } - if (initBlock.velocity) { - particle.velocity.copyFrom(initBlock.velocity()); - } - if (initBlock.color) { - particle.color?.copyFrom(initBlock.color()); - } - if (initBlock.scaling) { - particle.scaling.copyFrom(initBlock.scaling()); - } - if (initBlock.rotation) { - particle.rotation.copyFrom(initBlock.rotation()); + + const originalContext = state.particleContext; + const originalSystemContext = state.systemContext; + + try { + for (let p = 0; p < sps.nbParticles; p++) { + const particle = sps.particles[p]; + const particleCreateData = createBlocks.get(particle.shapeId); + const initBlock = particleCreateData?.initBlock; + if (!initBlock) { + continue; + } + + state.particleContext = particle; + state.systemContext = sps; + + if (initBlock.position) { + particle.position.copyFrom(initBlock.position()); + } + if (initBlock.velocity) { + particle.velocity.copyFrom(initBlock.velocity()); + } + if (initBlock.color) { + particle.color?.copyFrom(initBlock.color()); + } + if (initBlock.scaling) { + particle.scaling.copyFrom(initBlock.scaling()); + } + if (initBlock.rotation) { + particle.rotation.copyFrom(initBlock.rotation()); + } } + } finally { + state.particleContext = originalContext; + state.systemContext = originalSystemContext; } }; @@ -140,25 +153,40 @@ export class SPSCreateBlock extends NodeParticleBlock { if (!sps) { return particle; } + const particleCreateData = createBlocks.get(particle.shapeId); const updateBlock = particleCreateData?.updateBlock; if (!updateBlock) { return particle; } - if (updateBlock.position) { - particle.position.copyFrom(updateBlock.position()); - } - if (updateBlock.velocity) { - particle.velocity.copyFrom(updateBlock.velocity()); - } - if (updateBlock.color) { - particle.color?.copyFrom(updateBlock.color()); - } - if (updateBlock.scaling) { - particle.scaling.copyFrom(updateBlock.scaling()); - } - if (updateBlock.rotation) { - particle.rotation.copyFrom(updateBlock.rotation()); + // Set particle context in state for PerParticle lock mode + const originalContext = state.particleContext; + const originalSystemContext = state.systemContext; + + // Temporarily set particle context for PerParticle lock mode + state.particleContext = particle; + state.systemContext = sps; + + try { + if (updateBlock.position) { + particle.position.copyFrom(updateBlock.position()); + } + if (updateBlock.velocity) { + particle.velocity.copyFrom(updateBlock.velocity()); + } + if (updateBlock.color) { + particle.color?.copyFrom(updateBlock.color()); + } + if (updateBlock.scaling) { + particle.scaling.copyFrom(updateBlock.scaling()); + } + if (updateBlock.rotation) { + particle.rotation.copyFrom(updateBlock.rotation()); + } + } finally { + // Restore original context + state.particleContext = originalContext; + state.systemContext = originalSystemContext; } return particle; }; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts index a86e14068c0..fbcc0b43375 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -1,15 +1,16 @@ import type { Scene } from "core/scene"; import type { NodeParticleConnectionPoint } from "./nodeParticleBlockConnectionPoint"; import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; -import type { Particle } from "../particle"; +import { Particle } from "../particle"; import type { Nullable } from "core/types"; import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; import { Vector2, Vector3 } from "core/Maths/math.vector"; -import type { ThinParticleSystem } from "../thinParticleSystem"; +import { SolidParticle } from "../solidParticle"; +import { ThinParticleSystem } from "../thinParticleSystem"; import { Color4 } from "core/Maths/math.color"; import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; import type { AbstractMesh } from "core/Meshes/abstractMesh"; -import type { SolidParticleSystem } from "../solidParticleSystem"; +import { SolidParticleSystem } from "../solidParticleSystem"; /** * Class used to store node based geometry build state @@ -37,17 +38,13 @@ export class NodeParticleBuildState { /** * Gets or sets the particle context for contextual data */ - public particleContext: Nullable = null; + public particleContext: Nullable = null; /** * Gets or sets the system context for contextual data + * Can be either ThinParticleSystem or SolidParticleSystem */ - public systemContext: Nullable = null; - - /** - * Gets or sets the SPS context for SPS blocks - */ - public spsContext: Nullable = null; + public systemContext: Nullable = null; /** * Gets or sets the delta time for physics calculations @@ -110,58 +107,164 @@ export class NodeParticleBuildState { return null; } + /** + * Type guard to check if particle context is a Particle and system context is ThinParticleSystem + */ + private isParticleWithThinSystem(): this is this & { particleContext: Particle; systemContext: ThinParticleSystem } { + return this.particleContext instanceof Particle && this.systemContext instanceof ThinParticleSystem; + } + + /** + * Type guard to check if particle context is a Particle + */ + private isParticle(): this is this & { particleContext: Particle } { + return this.particleContext instanceof Particle; + } + + /** + * Type guard to check if particle context is a SolidParticle + */ + private isSolidParticle(): this is this & { particleContext: SolidParticle } { + return this.particleContext instanceof SolidParticle; + } + + /** + * Type guard to check if system context is a ThinParticleSystem + */ + private isThinParticleSystem(): this is this & { systemContext: ThinParticleSystem } { + return this.systemContext instanceof ThinParticleSystem; + } + + /** + * Type guard to check if system context is a SolidParticleSystem + */ + private isSolidParticleSystem(): this is this & { systemContext: SolidParticleSystem } { + return this.systemContext instanceof SolidParticleSystem; + } + /** * Gets the value associated with a contextual source * @param source Source of the contextual value * @returns the value associated with the source */ public getContextualValue(source: NodeParticleContextualSources) { - if (!this.particleContext || !this.systemContext) { + if (!this.particleContext) { return null; } switch (source) { + // Common properties available on both Particle and SolidParticle case NodeParticleContextualSources.Position: return this.particleContext.position; + case NodeParticleContextualSources.Color: + return this.particleContext.color; + case NodeParticleContextualSources.Scale: + if (this.isParticle()) { + return this.particleContext.scale; + } + if (this.isSolidParticle()) { + // Convert Vector3 scaling to Vector2 for compatibility + const scaling = this.particleContext.scaling; + return new Vector2(scaling.x, scaling.y); + } + return null; + case NodeParticleContextualSources.Direction: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.direction; + case NodeParticleContextualSources.ScaledDirection: + if (!this.isParticleWithThinSystem()) { + return null; + } this.particleContext.direction.scaleToRef(this.systemContext._directionScale, this.systemContext._scaledDirection); return this.systemContext._scaledDirection; - case NodeParticleContextualSources.Color: - return this.particleContext.color; + case NodeParticleContextualSources.InitialColor: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.initialColor; + case NodeParticleContextualSources.ColorDead: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.colorDead; + case NodeParticleContextualSources.Age: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.age; + case NodeParticleContextualSources.Lifetime: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.lifeTime; + case NodeParticleContextualSources.Angle: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.angle; - case NodeParticleContextualSources.Scale: - return this.particleContext.scale; + case NodeParticleContextualSources.AgeGradient: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.age / this.particleContext.lifeTime; - case NodeParticleContextualSources.SpriteCellEnd: - return this.systemContext.endSpriteCellID; + case NodeParticleContextualSources.SpriteCellIndex: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.cellIndex; - case NodeParticleContextualSources.SpriteCellStart: - return this.systemContext.startSpriteCellID; + case NodeParticleContextualSources.InitialDirection: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext._initialDirection; + case NodeParticleContextualSources.ColorStep: + if (!this.isParticleWithThinSystem()) { + return null; + } return this.particleContext.colorStep; + case NodeParticleContextualSources.ScaledColorStep: + if (!this.isParticleWithThinSystem()) { + return null; + } this.particleContext.colorStep.scaleToRef(this.systemContext._scaledUpdateSpeed, this.systemContext._scaledColorStep); return this.systemContext._scaledColorStep; + case NodeParticleContextualSources.LocalPositionUpdated: + if (!this.isParticleWithThinSystem()) { + return null; + } this.particleContext.direction.scaleToRef(this.systemContext._directionScale, this.systemContext._scaledDirection); - this.particleContext._localPosition!.addInPlace(this.systemContext._scaledDirection); - Vector3.TransformCoordinatesToRef(this.particleContext._localPosition!, this.systemContext._emitterWorldMatrix, this.particleContext.position); + if (this.particleContext._localPosition) { + this.particleContext._localPosition.addInPlace(this.systemContext._scaledDirection); + Vector3.TransformCoordinatesToRef(this.particleContext._localPosition, this.systemContext._emitterWorldMatrix, this.particleContext.position); + } return this.particleContext.position; + + case NodeParticleContextualSources.SpriteCellEnd: + if (!this.isThinParticleSystem()) { + return null; + } + return this.systemContext.endSpriteCellID; + + case NodeParticleContextualSources.SpriteCellStart: + if (!this.isThinParticleSystem()) { + return null; + } + return this.systemContext.startSpriteCellID; } return null; @@ -171,7 +274,7 @@ export class NodeParticleBuildState { * Gets the emitter world matrix */ public get emitterWorldMatrix() { - if (!this.systemContext) { + if (!this.isThinParticleSystem()) { return null; } return this.systemContext._emitterWorldMatrix; @@ -181,7 +284,7 @@ export class NodeParticleBuildState { * Gets the emitter inverse world matrix */ public get emitterInverseWorldMatrix() { - if (!this.systemContext) { + if (!this.isThinParticleSystem()) { return null; } return this.systemContext._emitterInverseWorldMatrix; @@ -195,6 +298,14 @@ export class NodeParticleBuildState { return null; } + if (this.isSolidParticleSystem()) { + return this.systemContext.mesh?.absolutePosition || Vector3.Zero(); + } + + if (!this.isThinParticleSystem()) { + return null; + } + if (!this.systemContext.emitter) { return null; } @@ -203,7 +314,27 @@ export class NodeParticleBuildState { return this.systemContext.emitter; } - return (this.systemContext.emitter).absolutePosition; + return (this.systemContext.emitter as AbstractMesh).absolutePosition; + } + + /** + * Gets the actual frame number + */ + public get actualFrame() { + if (this.isThinParticleSystem()) { + return this.systemContext._actualFrame; + } + return this.scene.getFrameId() || 0; + } + + /** + * Gets the delta time + */ + public get delta() { + if (this.isThinParticleSystem()) { + return this.systemContext._scaledUpdateSpeed; + } + return this.scene.getEngine().getDeltaTime() || this.deltaTime; } /** @@ -218,9 +349,9 @@ export class NodeParticleBuildState { switch (source) { case NodeParticleSystemSources.Time: - return this.systemContext._actualFrame; + return this.actualFrame; case NodeParticleSystemSources.Delta: - return this.systemContext._scaledUpdateSpeed; + return this.delta; case NodeParticleSystemSources.Emitter: return this.emitterPosition; case NodeParticleSystemSources.CameraPosition: diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 56a3f0238b6..6e7f254da94 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,9 +22,11 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; -import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock, SPSCreateBlock } from "./Blocks"; +import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock } from "./Blocks"; import { ParticleSystem } from ".."; -import { Vector3 } from "../../Maths"; +import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; +import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; +import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -340,58 +342,214 @@ export class NodeParticleSystemSet { } public setToDefaultSPS() { - this.clear(); - this.editorData = null; - const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.billboard = false; + // this.clear(); + // this.editorData = null; + // const spsSystem = new SPSSystemBlock("SPS System"); + // spsSystem.billboard = false; - const spsCreateBlock = new SPSCreateBlock("Create Particles System"); - spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + // const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + // spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + + // const spsCreateBox = new SPSParticleConfigBlock("Create Box Particles"); + // const spsCreateSphere = new SPSParticleConfigBlock("Create Sphere Particles"); + + // spsCreateBox.count.value = 5; + // spsCreateSphere.count.value = 1; + + // spsCreateBox.particleConfig.connectTo(spsCreateBlock.particleConfig); + // spsCreateSphere.particleConfig.connectTo(spsCreateBlock.particleConfig); - const spsCreateBox = new SPSParticleConfigBlock("Create Box Particles"); - const spsCreateSphere = new SPSParticleConfigBlock("Create Sphere Particles"); + // const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); + // const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); - spsCreateBox.count.value = 5; - spsCreateSphere.count.value = 1; + // meshSourceBox.shapeType = SPSMeshShapeType.Box; + // meshSourceSphere.shapeType = SPSMeshShapeType.Sphere; - spsCreateBox.particleConfig.connectTo(spsCreateBlock.particleConfig); - spsCreateSphere.particleConfig.connectTo(spsCreateBlock.particleConfig); + // meshSourceBox.size = 1; + // meshSourceSphere.size = 1; - const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); - const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); + // meshSourceBox.mesh.connectTo(spsCreateBox.mesh); + // meshSourceSphere.mesh.connectTo(spsCreateSphere.mesh); - meshSourceBox.shapeType = SPSMeshShapeType.Box; - meshSourceSphere.shapeType = SPSMeshShapeType.Sphere; + // const spsInitBox = new SPSInitBlock("Initialize Box Particles"); + // const spsInitSphere = new SPSInitBlock("Initialize Sphere Particles"); - meshSourceBox.size = 1; - meshSourceSphere.size = 1; + // spsInitBox.initData.connectTo(spsCreateBox.initBlock); + // spsInitSphere.initData.connectTo(spsCreateSphere.initBlock); - meshSourceBox.mesh.connectTo(spsCreateBox.mesh); - meshSourceSphere.mesh.connectTo(spsCreateSphere.mesh); + // const positionBlockBox = new ParticleInputBlock("Position"); + // positionBlockBox.value = new Vector3(1, 1, 1); + // positionBlockBox.output.connectTo(spsInitBox.position); - const spsInitBox = new SPSInitBlock("Initialize Box Particles"); - const spsInitSphere = new SPSInitBlock("Initialize Sphere Particles"); + // const rotationBlockBox = new ParticleInputBlock("Rotation"); + // rotationBlockBox.value = new Vector3(3, 0, 0); + // rotationBlockBox.output.connectTo(spsInitBox.rotation); - spsInitBox.initData.connectTo(spsCreateBox.initBlock); - spsInitSphere.initData.connectTo(spsCreateSphere.initBlock); + // const positionBlockSphere = new ParticleInputBlock("Position"); + // positionBlockSphere.value = new Vector3(0, 0, 0); + // positionBlockSphere.output.connectTo(spsInitSphere.position); - const positionBlockBox = new ParticleInputBlock("Position"); - positionBlockBox.value = new Vector3(1, 1, 1); - positionBlockBox.output.connectTo(spsInitBox.position); + // const spsUpdateBox = new SPSUpdateBlock("Update Box Particles"); + // const spsUpdateSphere = new SPSUpdateBlock("Update Sphere Particles"); - const rotationBlockBox = new ParticleInputBlock("Rotation"); - rotationBlockBox.value = new Vector3(3, 0, 0); - rotationBlockBox.output.connectTo(spsInitBox.rotation); + // spsUpdateBox.updateData.connectTo(spsCreateBox.updateBlock); + // spsUpdateSphere.updateData.connectTo(spsCreateSphere.updateBlock); - const positionBlockSphere = new ParticleInputBlock("Position"); - positionBlockSphere.value = new Vector3(0, 0, 0); - positionBlockSphere.output.connectTo(spsInitSphere.position); + // this._systemBlocks.push(spsSystem); + this.setToTetrahedronSPS(); + } - const spsUpdateBox = new SPSUpdateBlock("Update Box Particles"); - const spsUpdateSphere = new SPSUpdateBlock("Update Sphere Particles"); + public setToTetrahedronSPS() { + this.clear(); + this.editorData = null; + + // STEP 1: Create basic SPS system + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.billboard = false; + + // STEP 2: Create SPS creation block + const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); - spsUpdateBox.updateData.connectTo(spsCreateBox.updateBlock); - spsUpdateSphere.updateData.connectTo(spsCreateSphere.updateBlock); + // STEP 3: Create particle config block with count + const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); + spsCreateTetra.count.value = 2000; // Start with small number for testing + spsCreateTetra.particleConfig.connectTo(spsCreateBlock.particleConfig); + + // STEP 4: Create mesh source (using Box first to test) + const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); + meshSourceTetra.shapeType = SPSMeshShapeType.Box; // Start with Box to test + meshSourceTetra.size = 0.3; + meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); + + // STEP 5: Create init block + const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); + spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); + + // STEP 6: Random X position (-10 to 10) + const randomXMin = new ParticleInputBlock("Random X Min"); + randomXMin.value = -10; + const randomXMax = new ParticleInputBlock("Random X Max"); + randomXMax.value = 10; + const randomX = new ParticleRandomBlock("Random X"); + randomX.lockMode = ParticleRandomBlockLocks.PerParticle; + randomXMin.output.connectTo(randomX.min); + randomXMax.output.connectTo(randomX.max); + + // STEP 7: Random Z position (-10 to 10) + const randomZMin = new ParticleInputBlock("Random Z Min"); + randomZMin.value = -10; + const randomZMax = new ParticleInputBlock("Random Z Max"); + randomZMax.value = 10; + const randomZ = new ParticleRandomBlock("Random Z"); + randomZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomZMin.output.connectTo(randomZ.min); + randomZMax.output.connectTo(randomZ.max); + + // STEP 8: Random angle (-PI to PI) + const randomAngleMin = new ParticleInputBlock("Random Angle Min"); + randomAngleMin.value = -Math.PI; + const randomAngleMax = new ParticleInputBlock("Random Angle Max"); + randomAngleMax.value = Math.PI; + const randomAngle = new ParticleRandomBlock("Random Angle"); + randomAngle.lockMode = ParticleRandomBlockLocks.PerParticle; + randomAngleMin.output.connectTo(randomAngle.min); + randomAngleMax.output.connectTo(randomAngle.max); + + // STEP 9: Random range (1 to 5) - smaller range for Y position + const randomRangeMin = new ParticleInputBlock("Random Range Min"); + randomRangeMin.value = 1; + const randomRangeMax = new ParticleInputBlock("Random Range Max"); + randomRangeMax.value = 5; + const randomRange = new ParticleRandomBlock("Random Range"); + randomRange.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRangeMin.output.connectTo(randomRange.min); + randomRangeMax.output.connectTo(randomRange.max); + + // STEP 10: Calculate Y position: range * (1 + cos(angle)) + const one = new ParticleInputBlock("One"); + one.value = 1; + const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); + cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; + randomAngle.output.connectTo(cosAngle.input); + const addOne = new ParticleMathBlock("Add One"); + addOne.operation = ParticleMathBlockOperations.Add; + one.output.connectTo(addOne.left); + cosAngle.output.connectTo(addOne.right); + const multiplyRange = new ParticleMathBlock("Multiply Range"); + multiplyRange.operation = ParticleMathBlockOperations.Multiply; + randomRange.output.connectTo(multiplyRange.left); + addOne.output.connectTo(multiplyRange.right); + + // STEP 11: Combine X, Y, Z into Vector3 using ConverterBlock + const positionConverter = new ParticleConverterBlock("Position Converter"); + randomX.output.connectTo(positionConverter.xIn); + multiplyRange.output.connectTo(positionConverter.yIn); + randomZ.output.connectTo(positionConverter.zIn); + positionConverter.xyzOut.connectTo(spsInitTetra.position); + + // STEP 12: Random rotation X (-PI to PI) + const randomRotXMin = new ParticleInputBlock("Random Rot X Min"); + randomRotXMin.value = -Math.PI; + const randomRotXMax = new ParticleInputBlock("Random Rot X Max"); + randomRotXMax.value = Math.PI; + const randomRotX = new ParticleRandomBlock("Random Rot X"); + randomRotX.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotXMin.output.connectTo(randomRotX.min); + randomRotXMax.output.connectTo(randomRotX.max); + + // STEP 13: Random rotation Y (-PI to PI) + const randomRotYMin = new ParticleInputBlock("Random Rot Y Min"); + randomRotYMin.value = -Math.PI; + const randomRotYMax = new ParticleInputBlock("Random Rot Y Max"); + randomRotYMax.value = Math.PI; + const randomRotY = new ParticleRandomBlock("Random Rot Y"); + randomRotY.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotYMin.output.connectTo(randomRotY.min); + randomRotYMax.output.connectTo(randomRotY.max); + + // STEP 14: Random rotation Z (-PI to PI) + const randomRotZMin = new ParticleInputBlock("Random Rot Z Min"); + randomRotZMin.value = -Math.PI; + const randomRotZMax = new ParticleInputBlock("Random Rot Z Max"); + randomRotZMax.value = Math.PI; + const randomRotZ = new ParticleRandomBlock("Random Rot Z"); + randomRotZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotZMin.output.connectTo(randomRotZ.min); + randomRotZMax.output.connectTo(randomRotZ.max); + + // STEP 15: Rotation Vector3 using ConverterBlock + const rotationConverter = new ParticleConverterBlock("Rotation Converter"); + randomRotX.output.connectTo(rotationConverter.xIn); + randomRotY.output.connectTo(rotationConverter.yIn); + randomRotZ.output.connectTo(rotationConverter.zIn); + rotationConverter.xyzOut.connectTo(spsInitTetra.rotation); + + // STEP 16: Random color using ConverterBlock + const randomColorMin = new ParticleInputBlock("Random Color Min"); + randomColorMin.value = 0; + const randomColorMax = new ParticleInputBlock("Random Color Max"); + randomColorMax.value = 1; + const randomColorR = new ParticleRandomBlock("Random Color R"); + randomColorR.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorR.min); + randomColorMax.output.connectTo(randomColorR.max); + const randomColorG = new ParticleRandomBlock("Random Color G"); + randomColorG.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorG.min); + randomColorMax.output.connectTo(randomColorG.max); + const randomColorB = new ParticleRandomBlock("Random Color B"); + randomColorB.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorB.min); + randomColorMax.output.connectTo(randomColorB.max); + const colorOne = new ParticleInputBlock("Color Alpha"); + colorOne.value = 1; + const colorConverter = new ParticleConverterBlock("Color Converter"); + randomColorR.output.connectTo(colorConverter.xIn); + randomColorG.output.connectTo(colorConverter.yIn); + randomColorB.output.connectTo(colorConverter.zIn); + colorOne.output.connectTo(colorConverter.wIn); + colorConverter.colorOut.connectTo(spsInitTetra.color); this._systemBlocks.push(spsSystem); } From 1eb3110809a763e50d951b6c1bdb8907c90c61f6 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 10:32:47 +0300 Subject: [PATCH 40/68] Refactor SPSInitBlock and SPSUpdateBlock to streamline value retrieval This commit refactors the SPSInitBlock and SPSUpdateBlock classes by removing redundant private methods for retrieving position, velocity, color, scaling, and rotation values. Instead, the blocks now directly use the getConnectedValue method for connected properties, optimizing performance and simplifying the code structure. These changes enhance clarity and maintainability within the SolidParticle system. --- .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 45 +++---------------- .../Blocks/SolidParticle/SPSUpdateBlock.ts | 45 +++---------------- 2 files changed, 10 insertions(+), 80 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts index df424f3d5b3..94fa69bb512 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -53,68 +53,33 @@ export class SPSInitBlock extends NodeParticleBlock { const initData = {} as ISPSUpdateData; if (this.position.isConnected) { initData.position = () => { - return this.getPosition(state); + return this.position.getConnectedValue(state); }; } if (this.velocity.isConnected) { initData.velocity = () => { - return this.getVelocity(state); + return this.velocity.getConnectedValue(state); }; } if (this.color.isConnected) { initData.color = () => { - return this.getColor(state); + return this.color.getConnectedValue(state); }; } if (this.scaling.isConnected) { initData.scaling = () => { - return this.getScaling(state); + return this.scaling.getConnectedValue(state); }; } if (this.rotation.isConnected) { initData.rotation = () => { - return this.getRotation(state); + return this.rotation.getConnectedValue(state); }; } this.initData._storedValue = initData; } - private getPosition(state: NodeParticleBuildState) { - if (this.position._storedFunction) { - return this.position._storedFunction(state); - } - return this.position.getConnectedValue(state); - } - - private getVelocity(state: NodeParticleBuildState) { - if (this.velocity._storedFunction) { - return this.velocity._storedFunction(state); - } - return this.velocity.getConnectedValue(state); - } - - private getColor(state: NodeParticleBuildState) { - if (this.color._storedFunction) { - return this.color._storedFunction(state); - } - return this.color.getConnectedValue(state); - } - - private getScaling(state: NodeParticleBuildState) { - if (this.scaling._storedFunction) { - return this.scaling._storedFunction(state); - } - return this.scaling.getConnectedValue(state); - } - - private getRotation(state: NodeParticleBuildState) { - if (this.rotation._storedFunction) { - return this.rotation._storedFunction(state); - } - return this.rotation.getConnectedValue(state); - } - public override serialize(): any { const serializationObject = super.serialize(); return serializationObject; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index cadae7dea94..4fac6e6af07 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -53,67 +53,32 @@ export class SPSUpdateBlock extends NodeParticleBlock { const updateData: ISPSUpdateData = {} as ISPSUpdateData; if (this.position.isConnected) { updateData.position = () => { - return this.getPosition(state); + return this.position.getConnectedValue(state); }; } if (this.velocity.isConnected) { updateData.velocity = () => { - return this.getVelocity(state); + return this.velocity.getConnectedValue(state); }; } if (this.color.isConnected) { updateData.color = () => { - return this.getColor(state); + return this.color.getConnectedValue(state); }; } if (this.scaling.isConnected) { updateData.scaling = () => { - return this.getScaling(state); + return this.scaling.getConnectedValue(state); }; } if (this.rotation.isConnected) { updateData.rotation = () => { - return this.getRotation(state); + return this.rotation.getConnectedValue(state); }; } this.updateData._storedValue = updateData; } - private getPosition(state: NodeParticleBuildState) { - if (this.position._storedFunction) { - return this.position._storedFunction(state); - } - return this.position.getConnectedValue(state); - } - - private getVelocity(state: NodeParticleBuildState) { - if (this.velocity._storedFunction) { - return this.velocity._storedFunction(state); - } - return this.velocity.getConnectedValue(state); - } - - private getColor(state: NodeParticleBuildState) { - if (this.color._storedFunction) { - return this.color._storedFunction(state); - } - return this.color.getConnectedValue(state); - } - - private getScaling(state: NodeParticleBuildState) { - if (this.scaling._storedFunction) { - return this.scaling._storedFunction(state); - } - return this.scaling.getConnectedValue(state); - } - - private getRotation(state: NodeParticleBuildState) { - if (this.rotation._storedFunction) { - return this.rotation._storedFunction!(state); - } - return this.rotation.getConnectedValue(state); - } - public override serialize(): any { const serializationObject = super.serialize(); return serializationObject; From b9a365b758f272456256f574fae7894074a6887c Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 11:39:51 +0300 Subject: [PATCH 41/68] Refactor NodeParticleSystemSet to streamline random position and rotation handling This commit updates the NodeParticleSystemSet class to simplify the random positioning and rotation of particles by utilizing Vector2 and Vector3 for combined input blocks. The previous individual random blocks for X, Y, and Z positions and rotations have been consolidated into more efficient structures, enhancing clarity and maintainability. Additionally, the random color generation has been updated to use Vector3, improving the overall functionality of the particle system. --- .../Particles/Node/nodeParticleSystemSet.ts | 125 ++++++------------ 1 file changed, 38 insertions(+), 87 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 6e7f254da94..689fac253e7 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,6 +22,7 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; +import { Vector2, Vector3 } from "core/Maths/math.vector"; import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock } from "./Blocks"; import { ParticleSystem } from ".."; import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; @@ -403,48 +404,33 @@ export class NodeParticleSystemSet { this.clear(); this.editorData = null; - // STEP 1: Create basic SPS system const spsSystem = new SPSSystemBlock("SPS System"); spsSystem.billboard = false; - // STEP 2: Create SPS creation block const spsCreateBlock = new SPSCreateBlock("Create Particles System"); spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); - // STEP 3: Create particle config block with count const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); - spsCreateTetra.count.value = 2000; // Start with small number for testing + spsCreateTetra.count.value = 2000; spsCreateTetra.particleConfig.connectTo(spsCreateBlock.particleConfig); - // STEP 4: Create mesh source (using Box first to test) const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); - meshSourceTetra.shapeType = SPSMeshShapeType.Box; // Start with Box to test + meshSourceTetra.shapeType = SPSMeshShapeType.Box; meshSourceTetra.size = 0.3; meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); - // STEP 5: Create init block const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); - // STEP 6: Random X position (-10 to 10) - const randomXMin = new ParticleInputBlock("Random X Min"); - randomXMin.value = -10; - const randomXMax = new ParticleInputBlock("Random X Max"); - randomXMax.value = 10; - const randomX = new ParticleRandomBlock("Random X"); - randomX.lockMode = ParticleRandomBlockLocks.PerParticle; - randomXMin.output.connectTo(randomX.min); - randomXMax.output.connectTo(randomX.max); - - // STEP 7: Random Z position (-10 to 10) - const randomZMin = new ParticleInputBlock("Random Z Min"); - randomZMin.value = -10; - const randomZMax = new ParticleInputBlock("Random Z Max"); - randomZMax.value = 10; - const randomZ = new ParticleRandomBlock("Random Z"); - randomZ.lockMode = ParticleRandomBlockLocks.PerParticle; - randomZMin.output.connectTo(randomZ.min); - randomZMax.output.connectTo(randomZ.max); + // STEP 6: Random X, Z position using Vector2 (-10 to 10) + const randomXZMin = new ParticleInputBlock("Random XZ Min"); + randomXZMin.value = new Vector2(-10, -10); + const randomXZMax = new ParticleInputBlock("Random XZ Max"); + randomXZMax.value = new Vector2(10, 10); + const randomXZ = new ParticleRandomBlock("Random XZ"); + randomXZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomXZMin.output.connectTo(randomXZ.min); + randomXZMax.output.connectTo(randomXZ.max); // STEP 8: Random angle (-PI to PI) const randomAngleMin = new ParticleInputBlock("Random Angle Min"); @@ -481,74 +467,39 @@ export class NodeParticleSystemSet { randomRange.output.connectTo(multiplyRange.left); addOne.output.connectTo(multiplyRange.right); - // STEP 11: Combine X, Y, Z into Vector3 using ConverterBlock + const extractXZ = new ParticleConverterBlock("Extract XZ"); + randomXZ.output.connectTo(extractXZ.xyIn); const positionConverter = new ParticleConverterBlock("Position Converter"); - randomX.output.connectTo(positionConverter.xIn); + extractXZ.xOut.connectTo(positionConverter.xIn); multiplyRange.output.connectTo(positionConverter.yIn); - randomZ.output.connectTo(positionConverter.zIn); + extractXZ.yOut.connectTo(positionConverter.zIn); positionConverter.xyzOut.connectTo(spsInitTetra.position); - // STEP 12: Random rotation X (-PI to PI) - const randomRotXMin = new ParticleInputBlock("Random Rot X Min"); - randomRotXMin.value = -Math.PI; - const randomRotXMax = new ParticleInputBlock("Random Rot X Max"); - randomRotXMax.value = Math.PI; - const randomRotX = new ParticleRandomBlock("Random Rot X"); - randomRotX.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotXMin.output.connectTo(randomRotX.min); - randomRotXMax.output.connectTo(randomRotX.max); - - // STEP 13: Random rotation Y (-PI to PI) - const randomRotYMin = new ParticleInputBlock("Random Rot Y Min"); - randomRotYMin.value = -Math.PI; - const randomRotYMax = new ParticleInputBlock("Random Rot Y Max"); - randomRotYMax.value = Math.PI; - const randomRotY = new ParticleRandomBlock("Random Rot Y"); - randomRotY.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotYMin.output.connectTo(randomRotY.min); - randomRotYMax.output.connectTo(randomRotY.max); - - // STEP 14: Random rotation Z (-PI to PI) - const randomRotZMin = new ParticleInputBlock("Random Rot Z Min"); - randomRotZMin.value = -Math.PI; - const randomRotZMax = new ParticleInputBlock("Random Rot Z Max"); - randomRotZMax.value = Math.PI; - const randomRotZ = new ParticleRandomBlock("Random Rot Z"); - randomRotZ.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotZMin.output.connectTo(randomRotZ.min); - randomRotZMax.output.connectTo(randomRotZ.max); - - // STEP 15: Rotation Vector3 using ConverterBlock - const rotationConverter = new ParticleConverterBlock("Rotation Converter"); - randomRotX.output.connectTo(rotationConverter.xIn); - randomRotY.output.connectTo(rotationConverter.yIn); - randomRotZ.output.connectTo(rotationConverter.zIn); - rotationConverter.xyzOut.connectTo(spsInitTetra.rotation); - - // STEP 16: Random color using ConverterBlock + // STEP 12: Random rotation using Vector3 (-PI to PI) + const randomRotMin = new ParticleInputBlock("Random Rot Min"); + randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); + const randomRotMax = new ParticleInputBlock("Random Rot Max"); + randomRotMax.value = new Vector3(Math.PI, Math.PI, Math.PI); + const randomRot = new ParticleRandomBlock("Random Rotation"); + randomRot.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotMin.output.connectTo(randomRot.min); + randomRotMax.output.connectTo(randomRot.max); + randomRot.output.connectTo(spsInitTetra.rotation); + + // STEP 16: Random color using Vector3 random and ConverterBlock const randomColorMin = new ParticleInputBlock("Random Color Min"); - randomColorMin.value = 0; + randomColorMin.value = new Vector3(0, 0, 0); const randomColorMax = new ParticleInputBlock("Random Color Max"); - randomColorMax.value = 1; - const randomColorR = new ParticleRandomBlock("Random Color R"); - randomColorR.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorR.min); - randomColorMax.output.connectTo(randomColorR.max); - const randomColorG = new ParticleRandomBlock("Random Color G"); - randomColorG.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorG.min); - randomColorMax.output.connectTo(randomColorG.max); - const randomColorB = new ParticleRandomBlock("Random Color B"); - randomColorB.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorB.min); - randomColorMax.output.connectTo(randomColorB.max); - const colorOne = new ParticleInputBlock("Color Alpha"); - colorOne.value = 1; + randomColorMax.value = new Vector3(1, 1, 1); + const randomColorRGB = new ParticleRandomBlock("Random Color RGB"); + randomColorRGB.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorRGB.min); + randomColorMax.output.connectTo(randomColorRGB.max); + const colorAlpha = new ParticleInputBlock("Color Alpha"); + colorAlpha.value = 1; const colorConverter = new ParticleConverterBlock("Color Converter"); - randomColorR.output.connectTo(colorConverter.xIn); - randomColorG.output.connectTo(colorConverter.yIn); - randomColorB.output.connectTo(colorConverter.zIn); - colorOne.output.connectTo(colorConverter.wIn); + randomColorRGB.output.connectTo(colorConverter.xyzIn); + colorAlpha.output.connectTo(colorConverter.wIn); colorConverter.colorOut.connectTo(spsInitTetra.color); this._systemBlocks.push(spsSystem); From bf82fdceb9c6486d0bdda61bca5835fab893a21a Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 11:40:02 +0300 Subject: [PATCH 42/68] Remove commented-out steps in NodeParticleSystemSet for improved code clarity This commit cleans up the NodeParticleSystemSet class by removing outdated comments related to random positioning, rotation, and color generation steps. The removal of these comments enhances the readability of the code and focuses on the current implementation without unnecessary distractions. --- .../dev/core/src/Particles/Node/nodeParticleSystemSet.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 689fac253e7..935bcb25161 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -422,7 +422,6 @@ export class NodeParticleSystemSet { const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); - // STEP 6: Random X, Z position using Vector2 (-10 to 10) const randomXZMin = new ParticleInputBlock("Random XZ Min"); randomXZMin.value = new Vector2(-10, -10); const randomXZMax = new ParticleInputBlock("Random XZ Max"); @@ -432,7 +431,6 @@ export class NodeParticleSystemSet { randomXZMin.output.connectTo(randomXZ.min); randomXZMax.output.connectTo(randomXZ.max); - // STEP 8: Random angle (-PI to PI) const randomAngleMin = new ParticleInputBlock("Random Angle Min"); randomAngleMin.value = -Math.PI; const randomAngleMax = new ParticleInputBlock("Random Angle Max"); @@ -442,7 +440,6 @@ export class NodeParticleSystemSet { randomAngleMin.output.connectTo(randomAngle.min); randomAngleMax.output.connectTo(randomAngle.max); - // STEP 9: Random range (1 to 5) - smaller range for Y position const randomRangeMin = new ParticleInputBlock("Random Range Min"); randomRangeMin.value = 1; const randomRangeMax = new ParticleInputBlock("Random Range Max"); @@ -452,7 +449,6 @@ export class NodeParticleSystemSet { randomRangeMin.output.connectTo(randomRange.min); randomRangeMax.output.connectTo(randomRange.max); - // STEP 10: Calculate Y position: range * (1 + cos(angle)) const one = new ParticleInputBlock("One"); one.value = 1; const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); @@ -475,7 +471,6 @@ export class NodeParticleSystemSet { extractXZ.yOut.connectTo(positionConverter.zIn); positionConverter.xyzOut.connectTo(spsInitTetra.position); - // STEP 12: Random rotation using Vector3 (-PI to PI) const randomRotMin = new ParticleInputBlock("Random Rot Min"); randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); const randomRotMax = new ParticleInputBlock("Random Rot Max"); @@ -486,7 +481,6 @@ export class NodeParticleSystemSet { randomRotMax.output.connectTo(randomRot.max); randomRot.output.connectTo(spsInitTetra.rotation); - // STEP 16: Random color using Vector3 random and ConverterBlock const randomColorMin = new ParticleInputBlock("Random Color Min"); randomColorMin.value = new Vector3(0, 0, 0); const randomColorMax = new ParticleInputBlock("Random Color Max"); From 2085aa6db6fdc65c449116d6c58306cb049962b5 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 12:58:17 +0300 Subject: [PATCH 43/68] Add ParticlePropsSetBlock and ParticlePropsGetBlock for dynamic property management in particles This commit introduces two new blocks, ParticlePropsSetBlock and ParticlePropsGetBlock, to facilitate the setting and retrieval of custom properties in particle.props. The ParticlePropsSetBlock allows for storing values, while the ParticlePropsGetBlock enables reading these values dynamically. Additionally, the NodeParticleSystemSet class is updated to utilize these new blocks, enhancing the flexibility and functionality of the particle system. The node list component is also updated to include descriptions for the new blocks, improving user understanding and accessibility. --- .../SolidParticle/ParticlePropsGetBlock.ts | 115 ++++++++++++ .../SolidParticle/ParticlePropsSetBlock.ts | 118 +++++++++++++ .../Node/Blocks/SolidParticle/index.ts | 2 + .../Particles/Node/nodeParticleSystemSet.ts | 164 +++++++++++------- .../components/nodeList/nodeListComponent.tsx | 13 +- 5 files changed, 350 insertions(+), 62 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts new file mode 100644 index 00000000000..9bf23b3d5f9 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts @@ -0,0 +1,115 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { SolidParticle } from "../../../solidParticle"; + +/** + * Block used to get custom properties from particle.props + * Works similar to contextual blocks but for dynamic property names + */ +export class ParticlePropsGetBlock extends NodeParticleBlock { + /** + * Gets or sets the property name to read from particle.props + */ + public propertyName: string = "value"; + + /** + * Gets or sets the connection point type (default float) + */ + private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; + + public constructor(name: string) { + super(name); + + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.AutoDetect); + // Set default type + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + public override getClassName() { + return "ParticlePropsGetBlock"; + } + + /** + * Gets or sets the connection point type + */ + public get type(): NodeParticleBlockConnectionPointTypes { + return this._type; + } + + public set type(value: NodeParticleBlockConnectionPointTypes) { + if (this._type !== value) { + this._type = value; + // Update output type + (this._outputs[0] as any)._type = value; + (this._outputs[0] as any)._defaultConnectionPointType = value; + } + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + // Validate property name + if (!this.propertyName || this.propertyName.trim() === "") { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + // Validate type + if (this._type === NodeParticleBlockConnectionPointTypes.Undefined || this._type === NodeParticleBlockConnectionPointTypes.AutoDetect) { + this._type = NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._type = this._type; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + const propertyName = this.propertyName; + + const func = (state: NodeParticleBuildState) => { + if (!state.particleContext) { + return null; + } + + const particle = state.particleContext as SolidParticle; + + if (!particle.props) { + return null; + } + + const value = particle.props[propertyName]; + + if (value === undefined) { + return null; + } + + return value; + }; + + if (this.output.isConnected) { + this.output._storedFunction = func; + } else { + this.output._storedValue = func(state); + } + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.propertyName = this.propertyName; + serializationObject.type = this._type; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.propertyName = serializationObject.propertyName || "value"; + this._type = serializationObject.type || NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._type = this._type; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } +} + +RegisterClass("BABYLON.ParticlePropsGetBlock", ParticlePropsGetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts new file mode 100644 index 00000000000..76b8e33e837 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts @@ -0,0 +1,118 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { SolidParticle } from "../../../solidParticle"; + +/** + * Block used to set custom properties in particle.props + * Works as a side-effect block that stores values and passes them through + */ +export class ParticlePropsSetBlock extends NodeParticleBlock { + /** + * Gets or sets the property name to store in particle.props + */ + public propertyName: string = "value"; + + /** + * Gets or sets the connection point type (default float) + */ + private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; + + public constructor(name: string) { + super(name); + + this.registerInput("value", NodeParticleBlockConnectionPointTypes.AutoDetect, true); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.BasedOnInput); + + // Link output type to input type + this._outputs[0]._typeConnectionSource = this._inputs[0]; + // Set default type for when input is not connected + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + public override getClassName() { + return "ParticlePropsSetBlock"; + } + + public get value(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * Gets or sets the connection point type + */ + public get type(): NodeParticleBlockConnectionPointTypes { + return this._type; + } + + public set type(value: NodeParticleBlockConnectionPointTypes) { + if (this._type !== value) { + this._type = value; + // Update default type (used when input is not connected) + (this._outputs[0] as any)._defaultConnectionPointType = value; + } + } + + public override _build(state: NodeParticleBuildState) { + // Validate property name + if (!this.propertyName || this.propertyName.trim() === "") { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + if (!this.value.isConnected) { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + const propertyName = this.propertyName; + + const func = (state: NodeParticleBuildState) => { + if (!state.particleContext) { + return null; + } + + const particle = state.particleContext as SolidParticle; + + const value = this.value.getConnectedValue(state); + + if (!particle.props) { + particle.props = {}; + } + + particle.props[propertyName] = value; + + return value; + }; + + if (this.output.isConnected) { + this.output._storedFunction = func; + } else { + this.output._storedValue = func(state); + } + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.propertyName = this.propertyName; + serializationObject.type = this._type; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.propertyName = serializationObject.propertyName || "value"; + this._type = serializationObject.type || NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } +} + +RegisterClass("BABYLON.ParticlePropsSetBlock", ParticlePropsSetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 070d7ebd205..66eb47cd817 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -6,3 +6,5 @@ export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; export * from "./SPSInitBlock"; export * from "./SPSCreateBlock"; +export * from "./ParticlePropsSetBlock"; +export * from "./ParticlePropsGetBlock"; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 935bcb25161..b5f648e25c7 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -23,11 +23,23 @@ import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "../../types"; import { Color4 } from "core/Maths/math.color"; import { Vector2, Vector3 } from "core/Maths/math.vector"; -import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock } from "./Blocks"; +import { + SPSParticleConfigBlock, + SPSInitBlock, + SPSMeshShapeType, + SPSMeshSourceBlock, + SPSSystemBlock, + SPSCreateBlock, + SPSUpdateBlock, + ParticlePropsSetBlock, + ParticlePropsGetBlock, +} from "./Blocks"; import { ParticleSystem } from ".."; import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; +import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -343,64 +355,6 @@ export class NodeParticleSystemSet { } public setToDefaultSPS() { - // this.clear(); - // this.editorData = null; - // const spsSystem = new SPSSystemBlock("SPS System"); - // spsSystem.billboard = false; - - // const spsCreateBlock = new SPSCreateBlock("Create Particles System"); - // spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); - - // const spsCreateBox = new SPSParticleConfigBlock("Create Box Particles"); - // const spsCreateSphere = new SPSParticleConfigBlock("Create Sphere Particles"); - - // spsCreateBox.count.value = 5; - // spsCreateSphere.count.value = 1; - - // spsCreateBox.particleConfig.connectTo(spsCreateBlock.particleConfig); - // spsCreateSphere.particleConfig.connectTo(spsCreateBlock.particleConfig); - - // const meshSourceBox = new SPSMeshSourceBlock("Box Mesh"); - // const meshSourceSphere = new SPSMeshSourceBlock("Sphere Mesh"); - - // meshSourceBox.shapeType = SPSMeshShapeType.Box; - // meshSourceSphere.shapeType = SPSMeshShapeType.Sphere; - - // meshSourceBox.size = 1; - // meshSourceSphere.size = 1; - - // meshSourceBox.mesh.connectTo(spsCreateBox.mesh); - // meshSourceSphere.mesh.connectTo(spsCreateSphere.mesh); - - // const spsInitBox = new SPSInitBlock("Initialize Box Particles"); - // const spsInitSphere = new SPSInitBlock("Initialize Sphere Particles"); - - // spsInitBox.initData.connectTo(spsCreateBox.initBlock); - // spsInitSphere.initData.connectTo(spsCreateSphere.initBlock); - - // const positionBlockBox = new ParticleInputBlock("Position"); - // positionBlockBox.value = new Vector3(1, 1, 1); - // positionBlockBox.output.connectTo(spsInitBox.position); - - // const rotationBlockBox = new ParticleInputBlock("Rotation"); - // rotationBlockBox.value = new Vector3(3, 0, 0); - // rotationBlockBox.output.connectTo(spsInitBox.rotation); - - // const positionBlockSphere = new ParticleInputBlock("Position"); - // positionBlockSphere.value = new Vector3(0, 0, 0); - // positionBlockSphere.output.connectTo(spsInitSphere.position); - - // const spsUpdateBox = new SPSUpdateBlock("Update Box Particles"); - // const spsUpdateSphere = new SPSUpdateBlock("Update Sphere Particles"); - - // spsUpdateBox.updateData.connectTo(spsCreateBox.updateBlock); - // spsUpdateSphere.updateData.connectTo(spsCreateSphere.updateBlock); - - // this._systemBlocks.push(spsSystem); - this.setToTetrahedronSPS(); - } - - public setToTetrahedronSPS() { this.clear(); this.editorData = null; @@ -453,14 +407,21 @@ export class NodeParticleSystemSet { one.value = 1; const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; - randomAngle.output.connectTo(cosAngle.input); + // Store angle in props so we can reuse during update + const setAnglePropInit = new ParticlePropsSetBlock("Set Angle Prop Init"); + setAnglePropInit.propertyName = "angle"; + randomAngle.output.connectTo(setAnglePropInit.value); + setAnglePropInit.output.connectTo(cosAngle.input); const addOne = new ParticleMathBlock("Add One"); addOne.operation = ParticleMathBlockOperations.Add; one.output.connectTo(addOne.left); cosAngle.output.connectTo(addOne.right); const multiplyRange = new ParticleMathBlock("Multiply Range"); multiplyRange.operation = ParticleMathBlockOperations.Multiply; - randomRange.output.connectTo(multiplyRange.left); + const setRangePropInit = new ParticlePropsSetBlock("Set Range Prop Init"); + setRangePropInit.propertyName = "range"; + randomRange.output.connectTo(setRangePropInit.value); + setRangePropInit.output.connectTo(multiplyRange.left); addOne.output.connectTo(multiplyRange.right); const extractXZ = new ParticleConverterBlock("Extract XZ"); @@ -496,6 +457,87 @@ export class NodeParticleSystemSet { colorAlpha.output.connectTo(colorConverter.wIn); colorConverter.colorOut.connectTo(spsInitTetra.color); + // Create update block + const spsUpdateTetra = new SPSUpdateBlock("Update Tetrahedron Particles"); + spsUpdateTetra.updateData.connectTo(spsCreateTetra.updateBlock); + + // Get current position (X, Z stay the same, Y updates) + const currentPosition = new ParticleInputBlock("Current Position"); + currentPosition.contextualValue = NodeParticleContextualSources.Position; + + // Extract X and Z from current position + const extractPosition = new ParticleConverterBlock("Extract Position"); + currentPosition.output.connectTo(extractPosition.xyzIn); + + // Retrieve stored properties + const getAngleProp = new ParticlePropsGetBlock("Get Angle Prop"); + getAngleProp.propertyName = "angle"; + getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRangeProp = new ParticlePropsGetBlock("Get Range Prop"); + getRangeProp.propertyName = "range"; + getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; + + // Accumulate angle using delta time to avoid relying on absolute frame id + const deltaBlock = new ParticleInputBlock("Delta Time"); + deltaBlock.systemSource = NodeParticleSystemSources.Delta; + + const milliToSecond = new ParticleInputBlock("Milli To Second"); + milliToSecond.value = 0.001; + + const deltaSeconds = new ParticleMathBlock("Delta Seconds"); + deltaSeconds.operation = ParticleMathBlockOperations.Multiply; + deltaBlock.output.connectTo(deltaSeconds.left); + milliToSecond.output.connectTo(deltaSeconds.right); + + const targetFps = new ParticleInputBlock("Target FPS"); + targetFps.value = 60; + + const normalizedDelta = new ParticleMathBlock("Normalized Delta"); + normalizedDelta.operation = ParticleMathBlockOperations.Multiply; + deltaSeconds.output.connectTo(normalizedDelta.left); + targetFps.output.connectTo(normalizedDelta.right); + + const speedPerFrame = new ParticleInputBlock("Speed Per Frame"); + speedPerFrame.value = Math.PI / 100; + + const scaledIncrement = new ParticleMathBlock("Scaled Increment"); + scaledIncrement.operation = ParticleMathBlockOperations.Multiply; + speedPerFrame.output.connectTo(scaledIncrement.left); + normalizedDelta.output.connectTo(scaledIncrement.right); + + const accumulateAngle = new ParticleMathBlock("Accumulate Angle"); + accumulateAngle.operation = ParticleMathBlockOperations.Add; + getAngleProp.output.connectTo(accumulateAngle.left); + scaledIncrement.output.connectTo(accumulateAngle.right); + + const setAnglePropUpdate = new ParticlePropsSetBlock("Set Angle Prop Update"); + setAnglePropUpdate.propertyName = "angle"; + setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + accumulateAngle.output.connectTo(setAnglePropUpdate.value); + + // Calculate new Y position: range * (1 + cos(angle)) + const oneUpdate = new ParticleInputBlock("One Update"); + oneUpdate.value = 1; + const cosUpdatedAngle = new ParticleTrigonometryBlock("Cos Updated Angle"); + cosUpdatedAngle.operation = ParticleTrigonometryBlockOperations.Cos; + setAnglePropUpdate.output.connectTo(cosUpdatedAngle.input); + const addOneUpdate = new ParticleMathBlock("Add One Update"); + addOneUpdate.operation = ParticleMathBlockOperations.Add; + oneUpdate.output.connectTo(addOneUpdate.left); + cosUpdatedAngle.output.connectTo(addOneUpdate.right); + const multiplyRangeUpdate = new ParticleMathBlock("Multiply Range Update"); + multiplyRangeUpdate.operation = ParticleMathBlockOperations.Multiply; + getRangeProp.output.connectTo(multiplyRangeUpdate.left); + addOneUpdate.output.connectTo(multiplyRangeUpdate.right); + + // Combine X (from current position), Y (new), Z (from current position) + const updatePositionConverter = new ParticleConverterBlock("Update Position Converter"); + extractPosition.xOut.connectTo(updatePositionConverter.xIn); + multiplyRangeUpdate.output.connectTo(updatePositionConverter.yIn); + extractPosition.zOut.connectTo(updatePositionConverter.zIn); + updatePositionConverter.xyzOut.connectTo(spsUpdateTetra.position); + this._systemBlocks.push(spsSystem); } diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 9491bfd406a..f17df445a13 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -27,6 +27,8 @@ export class NodeListComponent extends React.Component Date: Fri, 14 Nov 2025 13:35:13 +0300 Subject: [PATCH 44/68] Enhance particle property management with String type support and display value retrieval This commit adds support for String properties in the ParticlePropsGetBlock and ParticlePropsSetBlock, allowing for dynamic management of string-type properties in particle systems. The displayValue method is introduced to provide a user-friendly representation of the property name. Additionally, the InputDisplayManager is updated to handle string values appropriately, improving the overall functionality and user experience in the particle editor. The GenericPropertyTabComponent is also enhanced to include a text input for string properties, further streamlining property management. --- .../dev/core/src/Decorators/nodeDecorator.ts | 2 ++ .../SolidParticle/ParticlePropsGetBlock.ts | 14 ++++++++ .../SolidParticle/ParticlePropsSetBlock.ts | 15 ++++++++ .../Particles/Node/nodeParticleSystemSet.ts | 2 +- .../display/inputDisplayManager.ts | 35 ++++++++++++++----- .../genericNodePropertyComponent.tsx | 14 ++++++++ .../graphSystem/registerToDisplayLedger.ts | 2 ++ 7 files changed, 75 insertions(+), 9 deletions(-) diff --git a/packages/dev/core/src/Decorators/nodeDecorator.ts b/packages/dev/core/src/Decorators/nodeDecorator.ts index b0ad690caca..44754fe852d 100644 --- a/packages/dev/core/src/Decorators/nodeDecorator.ts +++ b/packages/dev/core/src/Decorators/nodeDecorator.ts @@ -21,6 +21,8 @@ export const enum PropertyTypeForEdition { Color3, /** property is a Color4 */ Color4, + /** property is a string */ + String, /** property (int) should be edited as a combo box with a list of sampling modes */ SamplingMode, /** property (int) should be edited as a combo box with a list of texture formats */ diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts index 9bf23b3d5f9..c0fc904c3de 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts @@ -4,6 +4,8 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { SolidParticle } from "../../../solidParticle"; +import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; +import { serialize } from "../../../../Misc/decorators"; /** * Block used to get custom properties from particle.props @@ -13,6 +15,11 @@ export class ParticlePropsGetBlock extends NodeParticleBlock { /** * Gets or sets the property name to read from particle.props */ + @serialize("propertyName") + @editableInPropertyPage("Property Name", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + notifiers: { rebuild: true }, + }) public propertyName: string = "value"; /** @@ -20,6 +27,13 @@ export class ParticlePropsGetBlock extends NodeParticleBlock { */ private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; + /** + * Gets the value to display (returns propertyName as string) + */ + public get displayValue(): string { + return this.propertyName || "value"; + } + public constructor(name: string) { super(name); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts index 76b8e33e837..6313d5a7c58 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts @@ -4,6 +4,8 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { SolidParticle } from "../../../solidParticle"; +import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; +import { serialize } from "../../../../Misc/decorators"; /** * Block used to set custom properties in particle.props @@ -13,6 +15,11 @@ export class ParticlePropsSetBlock extends NodeParticleBlock { /** * Gets or sets the property name to store in particle.props */ + @serialize("propertyName") + @editableInPropertyPage("Property Name", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + notifiers: { rebuild: true }, + }) public propertyName: string = "value"; /** @@ -40,6 +47,14 @@ export class ParticlePropsSetBlock extends NodeParticleBlock { return this._inputs[0]; } + /** + * Gets the value to display (returns propertyName as string) + * This shadows the connection point name for display purposes + */ + public get displayValue(): string { + return this.propertyName || "value"; + } + public get output(): NodeParticleConnectionPoint { return this._outputs[0]; } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index b5f648e25c7..5ae8dcfd847 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -370,7 +370,7 @@ export class NodeParticleSystemSet { const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); meshSourceTetra.shapeType = SPSMeshShapeType.Box; - meshSourceTetra.size = 0.3; + meshSourceTetra.size = 0.1; meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts b/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts index 9050c1eb679..aa3522bb2b5 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts @@ -130,28 +130,47 @@ export class InputDisplayManager implements IDisplayManager { break; } } else { + const block = nodeData.data as any; + const blockValue = block.displayValue !== undefined ? block.displayValue : (inputBlock as any).value; + const isStringValue = typeof blockValue === "string"; + switch (inputBlock.type) { case NodeParticleBlockConnectionPointTypes.Int: - value = inputBlock.value.toFixed(0); + value = isStringValue ? blockValue : inputBlock.value.toFixed(0); break; case NodeParticleBlockConnectionPointTypes.Float: - value = inputBlock.value.toFixed(4); + value = isStringValue ? blockValue : inputBlock.value.toFixed(4); break; case NodeParticleBlockConnectionPointTypes.Vector2: { - const vec2Value = inputBlock.value as Vector2; - value = `(${vec2Value.x.toFixed(2)}, ${vec2Value.y.toFixed(2)})`; + if (isStringValue) { + value = blockValue; + } else { + const vec2Value = inputBlock.value as Vector2; + value = `(${vec2Value.x.toFixed(2)}, ${vec2Value.y.toFixed(2)})`; + } break; } case NodeParticleBlockConnectionPointTypes.Vector3: { - const vec3Value = inputBlock.value as Vector3; - value = `(${vec3Value.x.toFixed(2)}, ${vec3Value.y.toFixed(2)}, ${vec3Value.z.toFixed(2)})`; + if (isStringValue) { + value = blockValue; + } else { + const vec3Value = inputBlock.value as Vector3; + value = `(${vec3Value.x.toFixed(2)}, ${vec3Value.y.toFixed(2)}, ${vec3Value.z.toFixed(2)})`; + } break; } case NodeParticleBlockConnectionPointTypes.Color4: { - const col4Value = inputBlock.value as Color4; - value = `(${col4Value.r.toFixed(2)}, ${col4Value.g.toFixed(2)}, ${col4Value.b.toFixed(2)}, ${col4Value.a.toFixed(2)})`; + if (isStringValue) { + value = blockValue; + } else { + const col4Value = inputBlock.value as Color4; + value = `(${col4Value.r.toFixed(2)}, ${col4Value.g.toFixed(2)}, ${col4Value.b.toFixed(2)}, ${col4Value.a.toFixed(2)})`; + } break; } + default: + value = isStringValue ? blockValue : String(blockValue); + break; } } contentArea.innerHTML = value; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/genericNodePropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/genericNodePropertyComponent.tsx index 343dff680f5..edee94902df 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/properties/genericNodePropertyComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/genericNodePropertyComponent.tsx @@ -186,6 +186,20 @@ export class GenericPropertyTabComponent extends React.Component ForceRebuild(block, this.props.stateManager, propertyName, options.notifiers)} + throttlePropertyChangedNotification={true} + /> + ); + break; + } case PropertyTypeForEdition.Float: { const cantDisplaySlider = isNaN(options.min as number) || isNaN(options.max as number) || options.min === options.max; if (cantDisplaySlider) { diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts index 822aa48cc46..ad51f51f8f9 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts @@ -50,4 +50,6 @@ export const RegisterToDisplayManagers = () => { DisplayLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutDisplayManager; DisplayLedger.RegisteredControls["BasicConditionBlock"] = ConditionDisplayManager; DisplayLedger.RegisteredControls["ParticleTriggerBlock"] = TriggerDisplayManager; + DisplayLedger.RegisteredControls["ParticlePropsGetBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["ParticlePropsSetBlock"] = InputDisplayManager; }; From e03fa4b828dd8c0a3f2bf7591e2e724000a10a5d Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 14 Nov 2025 19:23:33 +0300 Subject: [PATCH 45/68] Remove String property from PropertyTypeForEdition enum and refactor particleLocalVariableBlock to use state.scene for frame ID retrieval --- packages/dev/core/src/Decorators/nodeDecorator.ts | 2 -- .../src/Particles/Node/Blocks/particleLocalVariableBlock.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dev/core/src/Decorators/nodeDecorator.ts b/packages/dev/core/src/Decorators/nodeDecorator.ts index 44754fe852d..293140903a4 100644 --- a/packages/dev/core/src/Decorators/nodeDecorator.ts +++ b/packages/dev/core/src/Decorators/nodeDecorator.ts @@ -29,8 +29,6 @@ export const enum PropertyTypeForEdition { TextureFormat, /** property (int) should be edited as a combo box with a list of texture types */ TextureType, - /** property is a string */ - String, /** property is a matrix */ Matrix, /** property is a viewport */ diff --git a/packages/dev/core/src/Particles/Node/Blocks/particleLocalVariableBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/particleLocalVariableBlock.ts index 52eedf2d5d1..3e0b46a418d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/particleLocalVariableBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/particleLocalVariableBlock.ts @@ -88,7 +88,7 @@ export class ParticleLocalVariableBlock extends NodeParticleBlock { return null; } - const id = (this.scope === ParticleLocalVariableBlockScope.Particle ? state.particleContext?.id : state.systemContext?.getScene()!.getFrameId()) || -1; + const id = (this.scope === ParticleLocalVariableBlockScope.Particle ? state.particleContext?.id : state.scene.getFrameId()) || -1; if (localId !== id) { localId = id; From 499386de5c90694e7662f798245f00593ca4ca0e Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 17 Nov 2025 10:21:01 +0300 Subject: [PATCH 46/68] Refactor particle property blocks to enhance naming consistency and type management This commit renames several methods and classes in the particle system to improve clarity and consistency, specifically changing `is*` methods to `_is*` in the NodeParticleBuildState class. Additionally, it updates the ParticlePropsSetBlock and ParticlePropsGetBlock to SpsParticlePropsSetBlock and SpsParticlePropsGetBlock, respectively, ensuring a unified naming convention across the particle system. Type imports are also adjusted for better clarity, enhancing overall code maintainability. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 12 ++-- .../SolidParticle/ParticlePropsGetBlock.ts | 8 +-- .../SolidParticle/ParticlePropsSetBlock.ts | 10 +-- .../Blocks/SolidParticle/SPSCreateBlock.ts | 12 ++-- .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 6 +- .../Blocks/SolidParticle/SPSMeshShapeType.ts | 2 + .../SolidParticle/SPSMeshSourceBlock.ts | 4 +- .../SolidParticle/SPSParticleConfigBlock.ts | 6 +- .../Blocks/SolidParticle/SPSSystemBlock.ts | 4 +- .../Blocks/SolidParticle/SPSUpdateBlock.ts | 6 +- .../Particles/Node/nodeParticleBuildState.ts | 61 ++++++++++--------- .../Particles/Node/nodeParticleSystemSet.ts | 22 +++---- .../src/components/preview/previewManager.ts | 50 +++++++++------ .../components/nodeList/nodeListComponent.tsx | 8 +-- .../propertyTab/propertyTabComponent.tsx | 2 +- .../graphSystem/registerToDisplayLedger.ts | 4 +- .../graphSystem/registerToPropertyLedger.ts | 2 + 17 files changed, 125 insertions(+), 94 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index 8d0e2f0227f..0285cbfe61e 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -1,12 +1,12 @@ -import { Vector3 } from "core/Maths/math.vector"; -import { Color4 } from "core/Maths/math.color"; +import type { Vector3 } from "core/Maths/math.vector"; +import type { Color4 } from "core/Maths/math.color"; import type { Mesh } from "core/Meshes/mesh"; import type { Material } from "core/Materials/material"; /** * Interface for SPS update block data */ -export interface ISPSUpdateData { +export interface ISpsUpdateData { position?: () => Vector3; velocity?: () => Vector3; color?: () => Color4; @@ -17,10 +17,10 @@ export interface ISPSUpdateData { /** * Interface for SPS create block data */ -export interface ISPSParticleConfigData { +export interface ISpsParticleConfigData { mesh: Mesh; count: number; material?: Material; - initBlock?: ISPSUpdateData; - updateBlock?: ISPSUpdateData; + initBlock?: ISpsUpdateData; + updateBlock?: ISpsUpdateData; } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts index c0fc904c3de..6798d44ab4a 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts @@ -3,7 +3,7 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import { SolidParticle } from "../../../solidParticle"; +import type { SolidParticle } from "../../../solidParticle"; import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; import { serialize } from "../../../../Misc/decorators"; @@ -11,7 +11,7 @@ import { serialize } from "../../../../Misc/decorators"; * Block used to get custom properties from particle.props * Works similar to contextual blocks but for dynamic property names */ -export class ParticlePropsGetBlock extends NodeParticleBlock { +export class SpsParticlePropsGetBlock extends NodeParticleBlock { /** * Gets or sets the property name to read from particle.props */ @@ -43,7 +43,7 @@ export class ParticlePropsGetBlock extends NodeParticleBlock { } public override getClassName() { - return "ParticlePropsGetBlock"; + return "SpsParticlePropsGetBlock"; } /** @@ -126,4 +126,4 @@ export class ParticlePropsGetBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.ParticlePropsGetBlock", ParticlePropsGetBlock); +RegisterClass("BABYLON.SpsParticlePropsGetBlock", SpsParticlePropsGetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts index 6313d5a7c58..4d48cbb71a9 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts @@ -3,7 +3,7 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import { SolidParticle } from "../../../solidParticle"; +import type { SolidParticle } from "../../../solidParticle"; import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; import { serialize } from "../../../../Misc/decorators"; @@ -11,7 +11,7 @@ import { serialize } from "../../../../Misc/decorators"; * Block used to set custom properties in particle.props * Works as a side-effect block that stores values and passes them through */ -export class ParticlePropsSetBlock extends NodeParticleBlock { +export class SpsParticlePropsSetBlock extends NodeParticleBlock { /** * Gets or sets the property name to store in particle.props */ @@ -32,7 +32,7 @@ export class ParticlePropsSetBlock extends NodeParticleBlock { this.registerInput("value", NodeParticleBlockConnectionPointTypes.AutoDetect, true); this.registerOutput("output", NodeParticleBlockConnectionPointTypes.BasedOnInput); - + // Link output type to input type this._outputs[0]._typeConnectionSource = this._inputs[0]; // Set default type for when input is not connected @@ -40,7 +40,7 @@ export class ParticlePropsSetBlock extends NodeParticleBlock { } public override getClassName() { - return "ParticlePropsSetBlock"; + return "SpsParticlePropsSetBlock"; } public get value(): NodeParticleConnectionPoint { @@ -130,4 +130,4 @@ export class ParticlePropsSetBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.ParticlePropsSetBlock", ParticlePropsSetBlock); +RegisterClass("BABYLON.SpsParticlePropsSetBlock", SpsParticlePropsSetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index d0798fe21c0..b35ca889773 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -1,12 +1,14 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; -import type { ISPSParticleConfigData } from "./ISPSData"; -import { SolidParticle } from "../../../solidParticle"; -import { Observer } from "../../../../Misc"; +import type { ISpsParticleConfigData } from "./ISPSData"; +import type { SolidParticle } from "../../../solidParticle"; +import type { Observer } from "core/Misc/observable"; /** * Block used to create SolidParticleSystem and collect all Create blocks @@ -95,9 +97,9 @@ export class SPSCreateBlock extends NodeParticleBlock { useModelMaterial: true, }); - const createBlocks = new Map(); + const createBlocks = new Map(); for (let i = 0; i < this._inputs.length; i++) { - const creatData = this._inputs[i].getConnectedValue(state) as ISPSParticleConfigData; + const creatData = this._inputs[i].getConnectedValue(state) as ISpsParticleConfigData; if (this._inputs[i].isConnected && creatData) { if (creatData.mesh && creatData.count) { const shapeId = sps.addShape(creatData.mesh, creatData.count); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts index 94fa69bb512..d0cf1de3f19 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISPSUpdateData } from "./ISPSData"; +import type { ISpsUpdateData } from "./ISPSData"; /** * Block used to generate initialization function for SPS particles @@ -50,7 +52,7 @@ export class SPSInitBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - const initData = {} as ISPSUpdateData; + const initData = {} as ISpsUpdateData; if (this.position.isConnected) { initData.position = () => { return this.position.getConnectedValue(state); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts index 242d6e22f2e..f52c709d7dd 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + /** * Mesh shape types for SPS */ diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts index fd4e56d9a5e..ad87a5107f8 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; @@ -9,7 +11,7 @@ import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; import { SPSMeshShapeType } from "./SPSMeshShapeType"; -import { Mesh } from "../../../../Meshes"; +import type { Mesh } from "core/Meshes/mesh"; /** * Block used to provide mesh source for SPS diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts index d585cbd1258..20ebcc0ed90 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISPSParticleConfigData } from "./ISPSData"; +import type { ISpsParticleConfigData } from "./ISPSData"; /** * Block used to configure SPS particle parameters (mesh, count, material, initBlock, updateBlock) @@ -57,7 +59,7 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; - const particleConfig: ISPSParticleConfigData = { + const particleConfig: ISpsParticleConfigData = { mesh, count, material, diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 4d0a8e4340d..0cf9af64e91 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -1,10 +1,12 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; -import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { SolidParticleSystem } from "core/Particles/solidParticleSystem"; /** * Block used to create SolidParticleSystem and collect all Create blocks diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index 4fac6e6af07..f3cdafc4a77 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -1,9 +1,11 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISPSUpdateData } from "./ISPSData"; +import type { ISpsUpdateData } from "./ISPSData"; /** * Block used to generate update function for SPS particles @@ -50,7 +52,7 @@ export class SPSUpdateBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - const updateData: ISPSUpdateData = {} as ISPSUpdateData; + const updateData: ISpsUpdateData = {} as ISpsUpdateData; if (this.position.isConnected) { updateData.position = () => { return this.position.getConnectedValue(state); diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts index fbcc0b43375..ece0d389666 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -109,36 +109,41 @@ export class NodeParticleBuildState { /** * Type guard to check if particle context is a Particle and system context is ThinParticleSystem + * @returns true when the contexts are Particle + ThinParticleSystem */ - private isParticleWithThinSystem(): this is this & { particleContext: Particle; systemContext: ThinParticleSystem } { + private _isParticleWithThinSystem(): this is this & { particleContext: Particle; systemContext: ThinParticleSystem } { return this.particleContext instanceof Particle && this.systemContext instanceof ThinParticleSystem; } /** * Type guard to check if particle context is a Particle + * @returns true when the particle context is a Particle */ - private isParticle(): this is this & { particleContext: Particle } { + private _isParticle(): this is this & { particleContext: Particle } { return this.particleContext instanceof Particle; } /** * Type guard to check if particle context is a SolidParticle + * @returns true when the particle context is a SolidParticle */ - private isSolidParticle(): this is this & { particleContext: SolidParticle } { + private _isSolidParticle(): this is this & { particleContext: SolidParticle } { return this.particleContext instanceof SolidParticle; } /** * Type guard to check if system context is a ThinParticleSystem + * @returns true when the system context is a ThinParticleSystem */ - private isThinParticleSystem(): this is this & { systemContext: ThinParticleSystem } { + private _isThinParticleSystem(): this is this & { systemContext: ThinParticleSystem } { return this.systemContext instanceof ThinParticleSystem; } /** * Type guard to check if system context is a SolidParticleSystem + * @returns true when the system context is a SolidParticleSystem */ - private isSolidParticleSystem(): this is this & { systemContext: SolidParticleSystem } { + private _isSolidParticleSystem(): this is this & { systemContext: SolidParticleSystem } { return this.systemContext instanceof SolidParticleSystem; } @@ -159,10 +164,10 @@ export class NodeParticleBuildState { case NodeParticleContextualSources.Color: return this.particleContext.color; case NodeParticleContextualSources.Scale: - if (this.isParticle()) { + if (this._isParticle()) { return this.particleContext.scale; } - if (this.isSolidParticle()) { + if (this._isSolidParticle()) { // Convert Vector3 scaling to Vector2 for compatibility const scaling = this.particleContext.scaling; return new Vector2(scaling.x, scaling.y); @@ -170,81 +175,81 @@ export class NodeParticleBuildState { return null; case NodeParticleContextualSources.Direction: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.direction; case NodeParticleContextualSources.ScaledDirection: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } this.particleContext.direction.scaleToRef(this.systemContext._directionScale, this.systemContext._scaledDirection); return this.systemContext._scaledDirection; case NodeParticleContextualSources.InitialColor: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.initialColor; case NodeParticleContextualSources.ColorDead: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.colorDead; case NodeParticleContextualSources.Age: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.age; case NodeParticleContextualSources.Lifetime: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.lifeTime; case NodeParticleContextualSources.Angle: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.angle; case NodeParticleContextualSources.AgeGradient: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.age / this.particleContext.lifeTime; case NodeParticleContextualSources.SpriteCellIndex: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.cellIndex; case NodeParticleContextualSources.InitialDirection: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext._initialDirection; case NodeParticleContextualSources.ColorStep: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } return this.particleContext.colorStep; case NodeParticleContextualSources.ScaledColorStep: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } this.particleContext.colorStep.scaleToRef(this.systemContext._scaledUpdateSpeed, this.systemContext._scaledColorStep); return this.systemContext._scaledColorStep; case NodeParticleContextualSources.LocalPositionUpdated: - if (!this.isParticleWithThinSystem()) { + if (!this._isParticleWithThinSystem()) { return null; } this.particleContext.direction.scaleToRef(this.systemContext._directionScale, this.systemContext._scaledDirection); @@ -255,13 +260,13 @@ export class NodeParticleBuildState { return this.particleContext.position; case NodeParticleContextualSources.SpriteCellEnd: - if (!this.isThinParticleSystem()) { + if (!this._isThinParticleSystem()) { return null; } return this.systemContext.endSpriteCellID; case NodeParticleContextualSources.SpriteCellStart: - if (!this.isThinParticleSystem()) { + if (!this._isThinParticleSystem()) { return null; } return this.systemContext.startSpriteCellID; @@ -274,7 +279,7 @@ export class NodeParticleBuildState { * Gets the emitter world matrix */ public get emitterWorldMatrix() { - if (!this.isThinParticleSystem()) { + if (!this._isThinParticleSystem()) { return null; } return this.systemContext._emitterWorldMatrix; @@ -284,7 +289,7 @@ export class NodeParticleBuildState { * Gets the emitter inverse world matrix */ public get emitterInverseWorldMatrix() { - if (!this.isThinParticleSystem()) { + if (!this._isThinParticleSystem()) { return null; } return this.systemContext._emitterInverseWorldMatrix; @@ -298,11 +303,11 @@ export class NodeParticleBuildState { return null; } - if (this.isSolidParticleSystem()) { + if (this._isSolidParticleSystem()) { return this.systemContext.mesh?.absolutePosition || Vector3.Zero(); } - if (!this.isThinParticleSystem()) { + if (!this._isThinParticleSystem()) { return null; } @@ -321,7 +326,7 @@ export class NodeParticleBuildState { * Gets the actual frame number */ public get actualFrame() { - if (this.isThinParticleSystem()) { + if (this._isThinParticleSystem()) { return this.systemContext._actualFrame; } return this.scene.getFrameId() || 0; @@ -331,7 +336,7 @@ export class NodeParticleBuildState { * Gets the delta time */ public get delta() { - if (this.isThinParticleSystem()) { + if (this._isThinParticleSystem()) { return this.systemContext._scaledUpdateSpeed; } return this.scene.getEngine().getDeltaTime() || this.deltaTime; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 5ae8dcfd847..88556937e10 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -20,8 +20,8 @@ import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTelepor import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; -import type { Nullable } from "../../types"; -import { Color4 } from "core/Maths/math.color"; +import type { Nullable } from "core/types"; +import type { Color4 } from "core/Maths/math.color"; import { Vector2, Vector3 } from "core/Maths/math.vector"; import { SPSParticleConfigBlock, @@ -31,10 +31,10 @@ import { SPSSystemBlock, SPSCreateBlock, SPSUpdateBlock, - ParticlePropsSetBlock, - ParticlePropsGetBlock, + SpsParticlePropsSetBlock, + SpsParticlePropsGetBlock, } from "./Blocks"; -import { ParticleSystem } from ".."; +import { ParticleSystem } from "core/Particles/particleSystem"; import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; @@ -354,7 +354,7 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } - public setToDefaultSPS() { + public setToDefaultSps() { this.clear(); this.editorData = null; @@ -408,7 +408,7 @@ export class NodeParticleSystemSet { const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; // Store angle in props so we can reuse during update - const setAnglePropInit = new ParticlePropsSetBlock("Set Angle Prop Init"); + const setAnglePropInit = new SpsParticlePropsSetBlock("Set Angle Prop Init"); setAnglePropInit.propertyName = "angle"; randomAngle.output.connectTo(setAnglePropInit.value); setAnglePropInit.output.connectTo(cosAngle.input); @@ -418,7 +418,7 @@ export class NodeParticleSystemSet { cosAngle.output.connectTo(addOne.right); const multiplyRange = new ParticleMathBlock("Multiply Range"); multiplyRange.operation = ParticleMathBlockOperations.Multiply; - const setRangePropInit = new ParticlePropsSetBlock("Set Range Prop Init"); + const setRangePropInit = new SpsParticlePropsSetBlock("Set Range Prop Init"); setRangePropInit.propertyName = "range"; randomRange.output.connectTo(setRangePropInit.value); setRangePropInit.output.connectTo(multiplyRange.left); @@ -470,11 +470,11 @@ export class NodeParticleSystemSet { currentPosition.output.connectTo(extractPosition.xyzIn); // Retrieve stored properties - const getAngleProp = new ParticlePropsGetBlock("Get Angle Prop"); + const getAngleProp = new SpsParticlePropsGetBlock("Get Angle Prop"); getAngleProp.propertyName = "angle"; getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; - const getRangeProp = new ParticlePropsGetBlock("Get Range Prop"); + const getRangeProp = new SpsParticlePropsGetBlock("Get Range Prop"); getRangeProp.propertyName = "range"; getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; @@ -511,7 +511,7 @@ export class NodeParticleSystemSet { getAngleProp.output.connectTo(accumulateAngle.left); scaledIncrement.output.connectTo(accumulateAngle.right); - const setAnglePropUpdate = new ParticlePropsSetBlock("Set Angle Prop Update"); + const setAnglePropUpdate = new SpsParticlePropsSetBlock("Set Angle Prop Update"); setAnglePropUpdate.propertyName = "angle"; setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; accumulateAngle.output.connectTo(setAnglePropUpdate.value); diff --git a/packages/tools/nodeEditor/src/components/preview/previewManager.ts b/packages/tools/nodeEditor/src/components/preview/previewManager.ts index 08da0764f7f..fbf4fba746d 100644 --- a/packages/tools/nodeEditor/src/components/preview/previewManager.ts +++ b/packages/tools/nodeEditor/src/components/preview/previewManager.ts @@ -45,6 +45,7 @@ import { Engine } from "core/Engines/engine"; import { Animation } from "core/Animations/animation"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { SceneLoaderFlags } from "core/Loading/sceneLoaderFlags"; +import type { SolidParticleSystem } from "core/Particles/solidParticleSystem"; const DontSerializeTextureContent = true; export class PreviewManager { @@ -69,7 +70,7 @@ export class PreviewManager { private _lightParent: TransformNode; private _postprocess: Nullable; private _proceduralTexture: Nullable; - private _particleSystem: Nullable; + private _particleSystem: Nullable; private _layer: Nullable; private _hdrSkyBox: Mesh; private _hdrTexture: CubeTexture; @@ -389,7 +390,8 @@ export class PreviewManager { case NodeMaterialModes.Particle: { this._camera.radius = this._globalState.previewType === PreviewType.Explosion ? 50 : this._globalState.previewType === PreviewType.DefaultParticleSystem ? 6 : 20; this._camera.upperRadiusLimit = 5000; - this._globalState.particleSystemBlendMode = this._particleSystem?.blendMode ?? ParticleSystem.BLENDMODE_STANDARD; + this._globalState.particleSystemBlendMode = + (this._particleSystem instanceof ParticleSystem ? this._particleSystem.blendMode : null) ?? ParticleSystem.BLENDMODE_STANDARD; break; } case NodeMaterialModes.GaussianSplatting: { @@ -454,9 +456,11 @@ export class PreviewManager { this._engine.releaseEffects(); if (this._particleSystem) { - this._particleSystem.onBeforeDrawParticlesObservable.clear(); - this._particleSystem.onDisposeObservable.clear(); - this._particleSystem.stop(); + if (this._particleSystem instanceof ParticleSystem) { + this._particleSystem.onBeforeDrawParticlesObservable.clear(); + this._particleSystem.onDisposeObservable.clear(); + this._particleSystem.stop(); + } this._particleSystem.dispose(); this._particleSystem = null; } @@ -631,12 +635,16 @@ export class PreviewManager { ParticleHelper.CreateAsync(name, this._scene).then((set) => { for (let i = 0; i < set.systems.length; ++i) { if (i == systemIndex) { - this._particleSystem = set.systems[i]; - this._particleSystem.disposeOnStop = true; - this._particleSystem.onDisposeObservable.add(() => { - this._loadParticleSystem(particleNumber, systemIndex, false); - }); - this._particleSystem.start(); + const system = set.systems[i]; + // Only handle ParticleSystem (which implements IParticleSystem), skip SolidParticleSystem + if (system instanceof ParticleSystem) { + this._particleSystem = system; + this._particleSystem.disposeOnStop = true; + this._particleSystem.onDisposeObservable.add(() => { + this._loadParticleSystem(particleNumber, systemIndex, false); + }); + this._particleSystem.start(); + } } else { set.systems[i].dispose(); } @@ -720,17 +728,19 @@ export class PreviewManager { case NodeMaterialModes.Particle: { this._globalState.onIsLoadingChanged.notifyObservers(false); - this._particleSystem!.onBeforeDrawParticlesObservable.clear(); + if (this._particleSystem instanceof ParticleSystem) { + this._particleSystem.onBeforeDrawParticlesObservable.clear(); - this._particleSystem!.onBeforeDrawParticlesObservable.add((effect) => { - const textureBlock = tempMaterial.getBlockByPredicate((block) => block instanceof ParticleTextureBlock); - if (textureBlock && (textureBlock as ParticleTextureBlock).texture && effect) { - effect.setTexture("diffuseSampler", (textureBlock as ParticleTextureBlock).texture); - } - }); - tempMaterial.createEffectForParticles(this._particleSystem!); + this._particleSystem.onBeforeDrawParticlesObservable.add((effect) => { + const textureBlock = tempMaterial.getBlockByPredicate((block) => block instanceof ParticleTextureBlock); + if (textureBlock && (textureBlock as ParticleTextureBlock).texture && effect) { + effect.setTexture("diffuseSampler", (textureBlock as ParticleTextureBlock).texture); + } + }); + tempMaterial.createEffectForParticles(this._particleSystem); - this._particleSystem!.blendMode = this._globalState.particleSystemBlendMode; + this._particleSystem.blendMode = this._globalState.particleSystemBlendMode; + } if (this._material) { this._material.dispose(); diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index f17df445a13..74167ce44a3 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -27,8 +27,8 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutDisplayManager; DisplayLedger.RegisteredControls["BasicConditionBlock"] = ConditionDisplayManager; DisplayLedger.RegisteredControls["ParticleTriggerBlock"] = TriggerDisplayManager; - DisplayLedger.RegisteredControls["ParticlePropsGetBlock"] = InputDisplayManager; - DisplayLedger.RegisteredControls["ParticlePropsSetBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["SpsParticlePropsGetBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["SpsParticlePropsSetBlock"] = InputDisplayManager; }; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 57075b01779..32828aee82f 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -20,4 +20,6 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSUpdateBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SpsParticlePropsSetBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SpsParticlePropsGetBlock"] = GenericPropertyComponent; }; From 28912f4e1ca85d5fb03994ade6d14f022d78da67 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 17 Nov 2025 17:45:28 +0300 Subject: [PATCH 47/68] Implement SPSParticlePropsSetBlock and SPSParticlePropsGetBlock for dynamic property management in particles This commit introduces two new blocks, SPSParticlePropsSetBlock and SPSParticlePropsGetBlock, to facilitate the setting and getting of custom properties in particle systems. These blocks enhance the flexibility of property management by allowing dynamic property names and types, improving the overall functionality of the particle editor. The existing ParticlePropsSetBlock and ParticlePropsGetBlock have been renamed to maintain consistency in naming conventions. --- .../{ParticlePropsGetBlock.ts => SPSParticlePropsGetBlock.ts} | 0 .../{ParticlePropsSetBlock.ts => SPSParticlePropsSetBlock.ts} | 0 .../dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{ParticlePropsGetBlock.ts => SPSParticlePropsGetBlock.ts} (100%) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{ParticlePropsSetBlock.ts => SPSParticlePropsSetBlock.ts} (100%) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts similarity index 100% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsGetBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts similarity index 100% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ParticlePropsSetBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 66eb47cd817..c20bce3114f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -6,5 +6,5 @@ export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; export * from "./SPSInitBlock"; export * from "./SPSCreateBlock"; -export * from "./ParticlePropsSetBlock"; -export * from "./ParticlePropsGetBlock"; +export * from "./SPSParticlePropsSetBlock"; +export * from "./SPSParticlePropsGetBlock"; From 98d1decba55039d2deff27e315d0c1bc56f95630 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Thu, 20 Nov 2025 16:47:51 +0300 Subject: [PATCH 48/68] Add lifetime and disposeOnEnd properties to SPSSystemBlock and SolidParticleSystem This commit introduces lifetime and disposeOnEnd properties to the SPSSystemBlock and SolidParticleSystem classes. The lifetime property allows for the specification of a duration in milliseconds for the solid particle system, while disposeOnEnd determines if the system should automatically dispose itself once the lifetime ends. These enhancements improve the control over particle system behavior and resource management, aligning with the recent updates to dynamic property management in particle systems. --- .../Blocks/SolidParticle/SPSSystemBlock.ts | 17 + .../Particles/Node/nodeParticleSystemSet.ts | 2017 ++++++++++------- .../core/src/Particles/solidParticleSystem.ts | 65 +- 3 files changed, 1244 insertions(+), 855 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 0cf9af64e91..b5727b5f852 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -20,6 +20,17 @@ export class SPSSystemBlock extends NodeParticleBlock { }) public billboard = false; + @editableInPropertyPage("Lifetime (ms)", PropertyTypeForEdition.Float, "ADVANCED", { + embedded: true, + min: 0, + }) + public lifetime = 0; + + @editableInPropertyPage("Dispose on end", PropertyTypeForEdition.Boolean, "ADVANCED", { + embedded: true, + }) + public disposeOnEnd = false; + public _internalId = SPSSystemBlock._IdCounter++; public constructor(name: string) { @@ -54,6 +65,8 @@ export class SPSSystemBlock extends NodeParticleBlock { solidParticleSystem.billboard = this.billboard; solidParticleSystem.name = this.name; + solidParticleSystem.lifetime = this.lifetime; + solidParticleSystem.disposeOnEnd = this.disposeOnEnd; this.onDisposeObservable.addOnce(() => { solidParticleSystem.dispose(); @@ -64,12 +77,16 @@ export class SPSSystemBlock extends NodeParticleBlock { public override serialize(): any { const serializationObject = super.serialize(); serializationObject.billboard = this.billboard; + serializationObject.lifetime = this.lifetime; + serializationObject.disposeOnEnd = this.disposeOnEnd; return serializationObject; } public override _deserialize(serializationObject: any) { super._deserialize(serializationObject); this.billboard = !!serializationObject.billboard; + this.lifetime = serializationObject.lifetime ?? 0; + this.disposeOnEnd = !!serializationObject.disposeOnEnd; } } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 88556937e10..41053b26537 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -1,854 +1,1163 @@ -import { serialize } from "core/Misc/decorators"; -import { ParticleSystemSet } from "../particleSystemSet"; -import { SystemBlock } from "./Blocks/systemBlock"; -import type { Scene } from "core/scene"; -import { NodeParticleBuildState } from "./nodeParticleBuildState"; -import type { NodeParticleBlock } from "./nodeParticleBlock"; -import { SerializationHelper } from "core/Misc/decorators.serialization"; -import { Observable } from "core/Misc/observable"; -import { GetClass } from "core/Misc/typeStore"; -import { WebRequest } from "core/Misc/webRequest"; -import { Constants } from "core/Engines/constants"; -import { Tools } from "core/Misc/tools"; -import { AbstractEngine } from "core/Engines/abstractEngine"; -import { ParticleInputBlock } from "./Blocks/particleInputBlock"; -import { ParticleTextureSourceBlock } from "./Blocks/particleSourceTextureBlock"; -import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; -import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock"; -import { ParticleMathBlock, ParticleMathBlockOperations } from "./Blocks/particleMathBlock"; -import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTeleportOutBlock"; -import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; -import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; -import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; -import type { Nullable } from "core/types"; -import type { Color4 } from "core/Maths/math.color"; -import { Vector2, Vector3 } from "core/Maths/math.vector"; -import { - SPSParticleConfigBlock, - SPSInitBlock, - SPSMeshShapeType, - SPSMeshSourceBlock, - SPSSystemBlock, - SPSCreateBlock, - SPSUpdateBlock, - SpsParticlePropsSetBlock, - SpsParticlePropsGetBlock, -} from "./Blocks"; -import { ParticleSystem } from "core/Particles/particleSystem"; -import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; -import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; -import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; -import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; -import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; - -// declare NODEPARTICLEEDITOR namespace for compilation issue -declare let NODEPARTICLEEDITOR: any; -declare let BABYLON: any; - -/** - * Interface used to configure the node particle editor - */ -export interface INodeParticleEditorOptions { - /** Define the URL to load node editor script from */ - editorURL?: string; - /** Additional configuration for the NPE */ - nodeEditorConfig?: { - backgroundColor?: Color4; - }; -} - -/** - * Defines a set of particle systems defined as a node graph. - * NPE: #K6F1ZB#1 - * PG: #ZT509U#1 - */ -export class NodeParticleSystemSet { - private _systemBlocks: (SystemBlock | SPSSystemBlock)[] = []; - private _buildId: number = 0; - - /** Define the Url to load node editor script */ - public static EditorURL = `${Tools._DefaultCdnUrl}/v${AbstractEngine.Version}/nodeParticleEditor/babylon.nodeParticleEditor.js`; - - /** Define the Url to load snippets */ - public static SnippetUrl = Constants.SnippetUrl; - - /** - * Snippet ID if the material was created from the snippet server - */ - public snippetId: string; - - /** - * Gets an array of blocks that needs to be serialized even if they are not yet connected - */ - public attachedBlocks: NodeParticleBlock[] = []; - - /** - * Gets or sets data used by visual editor - * @see https://npe.babylonjs.com - */ - public editorData: any = null; - - /** - * Observable raised when the particle set is built - */ - public onBuildObservable = new Observable(); - - /** - * The name of the set - */ - @serialize() - public name: string; - - /** - * A free comment about the set - */ - @serialize("comment") - public comment: string; - - /** - * Gets the system blocks - */ - public get systemBlocks(): (SystemBlock | SPSSystemBlock)[] { - return this._systemBlocks; - } - - /** - * Gets the list of input blocks attached to this material - * @returns an array of InputBlocks - */ - public get inputBlocks() { - const blocks: ParticleInputBlock[] = []; - for (const block of this.attachedBlocks) { - if (block.isInput) { - blocks.push(block as ParticleInputBlock); - } - } - - return blocks; - } - - /** - * Get a block by its name - * @param name defines the name of the block to retrieve - * @returns the required block or null if not found - */ - public getBlockByName(name: string) { - let result = null; - for (const block of this.attachedBlocks) { - if (block.name === name) { - if (!result) { - result = block; - } else { - Tools.Warn("More than one block was found with the name `" + name + "`"); - return result; - } - } - } - - return result; - } - - /** - * Get a block using a predicate - * @param predicate defines the predicate used to find the good candidate - * @returns the required block or null if not found - */ - public getBlockByPredicate(predicate: (block: NodeParticleBlock) => boolean) { - for (const block of this.attachedBlocks) { - if (predicate(block)) { - return block; - } - } - - return null; - } - - /** - * Get an input block using a predicate - * @param predicate defines the predicate used to find the good candidate - * @returns the required input block or null if not found - */ - public getInputBlockByPredicate(predicate: (block: ParticleInputBlock) => boolean): Nullable { - for (const block of this.attachedBlocks) { - if (block.isInput && predicate(block as ParticleInputBlock)) { - return block as ParticleInputBlock; - } - } - - return null; - } - /** - * Creates a new set - * @param name defines the name of the set - */ - public constructor(name: string) { - this.name = name; - } - - /** - * Gets the current class name of the node particle set e.g. "NodeParticleSystemSet" - * @returns the class name - */ - public getClassName(): string { - return "NodeParticleSystemSet"; - } - - private _initializeBlock(node: NodeParticleBlock, autoConfigure = true) { - if (this.attachedBlocks.indexOf(node) === -1) { - this.attachedBlocks.push(node); - } - - for (const input of node.inputs) { - const connectedPoint = input.connectedPoint; - if (connectedPoint) { - const block = connectedPoint.ownerBlock; - if (block !== node) { - this._initializeBlock(block, autoConfigure); - } - } - } - } - - private BJSNODEPARTICLEEDITOR = this._getGlobalNodeParticleEditor(); - - /** Get the editor from bundle or global - * @returns the global NPE - */ - private _getGlobalNodeParticleEditor(): any { - // UMD Global name detection from Webpack Bundle UMD Name. - if (typeof NODEPARTICLEEDITOR !== "undefined") { - return NODEPARTICLEEDITOR; - } - - // In case of module let's check the global emitted from the editor entry point. - if (typeof BABYLON !== "undefined" && typeof BABYLON.NodeParticleEditor !== "undefined") { - return BABYLON; - } - - return undefined; - } - - /** Creates the node editor window. - * @param additionalConfig Define the configuration of the editor - */ - private _createNodeParticleEditor(additionalConfig?: any) { - const nodeEditorConfig: any = { - nodeParticleSet: this, - ...additionalConfig, - }; - this.BJSNODEPARTICLEEDITOR.NodeParticleEditor.Show(nodeEditorConfig); - } - - /** - * Launch the node particle editor - * @param config Define the configuration of the editor - * @returns a promise fulfilled when the node editor is visible - */ - public async editAsync(config?: INodeParticleEditorOptions): Promise { - return await new Promise((resolve) => { - this.BJSNODEPARTICLEEDITOR = this.BJSNODEPARTICLEEDITOR || this._getGlobalNodeParticleEditor(); - if (typeof this.BJSNODEPARTICLEEDITOR == "undefined") { - const editorUrl = config && config.editorURL ? config.editorURL : NodeParticleSystemSet.EditorURL; - - // Load editor and add it to the DOM - Tools.LoadBabylonScript(editorUrl, () => { - this.BJSNODEPARTICLEEDITOR = this.BJSNODEPARTICLEEDITOR || this._getGlobalNodeParticleEditor(); - this._createNodeParticleEditor(config?.nodeEditorConfig); - resolve(); - }); - } else { - // Otherwise creates the editor - this._createNodeParticleEditor(config?.nodeEditorConfig); - resolve(); - } - }); - } - - /** - * Builds the particle system set from the defined blocks. - * @param scene defines the hosting scene - * @param verbose defines whether to log detailed information during the build process (false by default) - * @returns a promise that resolves to the built particle system set - */ - public async buildAsync(scene: Scene, verbose = false): Promise { - return await new Promise((resolve) => { - const output = new ParticleSystemSet(); - - // Initialize all blocks - for (const block of this._systemBlocks) { - this._initializeBlock(block); - } - - // Build the blocks - for (const block of this.systemBlocks) { - const state = new NodeParticleBuildState(); - state.buildId = this._buildId++; - state.scene = scene; - state.verbose = verbose; - - const system = block.createSystem(state); - if (system instanceof ParticleSystem) { - system._source = this; - system._blockReference = block._internalId; - } - output.systems.push(system); - // Errors - state.emitErrors(); - } - - this.onBuildObservable.notifyObservers(this); - - resolve(output); - }); - } - - /** - * Clear the current node particle set - */ - public clear() { - this.attachedBlocks.length = 0; - this._systemBlocks.length = 0; - } - - /** - * Clear the current set and restore it to a default state - */ - public setToDefault() { - this.clear(); - - this.editorData = null; - - // Main system - const system = new SystemBlock("Particle system"); - - // Update position - const updatePositionBlock = new UpdatePositionBlock("Update position"); - updatePositionBlock.output.connectTo(system.particle); - - // Contextual inputs - const positionBlock = new ParticleInputBlock("Position"); - positionBlock.contextualValue = NodeParticleContextualSources.Position; - const directionBlock = new ParticleInputBlock("Scaled direction"); - directionBlock.contextualValue = NodeParticleContextualSources.ScaledDirection; - - // Add - const addBlock = new ParticleMathBlock("Add"); - addBlock.operation = ParticleMathBlockOperations.Add; - positionBlock.output.connectTo(addBlock.left); - directionBlock.output.connectTo(addBlock.right); - addBlock.output.connectTo(updatePositionBlock.position); - - // Create particle - const createParticleBlock = new CreateParticleBlock("Create particle"); - - // Shape - const emitterShape = new BoxShapeBlock("Box shape"); - createParticleBlock.particle.connectTo(emitterShape.particle); - emitterShape.output.connectTo(updatePositionBlock.particle); - - // Texture - const textureBlock = new ParticleTextureSourceBlock("Texture"); - textureBlock.texture.connectTo(system.texture); - textureBlock.url = Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"); - - this._systemBlocks.push(system); - } - - public setToDefaultSps() { - this.clear(); - this.editorData = null; - - const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.billboard = false; - - const spsCreateBlock = new SPSCreateBlock("Create Particles System"); - spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); - - const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); - spsCreateTetra.count.value = 2000; - spsCreateTetra.particleConfig.connectTo(spsCreateBlock.particleConfig); - - const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); - meshSourceTetra.shapeType = SPSMeshShapeType.Box; - meshSourceTetra.size = 0.1; - meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); - - const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); - spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); - - const randomXZMin = new ParticleInputBlock("Random XZ Min"); - randomXZMin.value = new Vector2(-10, -10); - const randomXZMax = new ParticleInputBlock("Random XZ Max"); - randomXZMax.value = new Vector2(10, 10); - const randomXZ = new ParticleRandomBlock("Random XZ"); - randomXZ.lockMode = ParticleRandomBlockLocks.PerParticle; - randomXZMin.output.connectTo(randomXZ.min); - randomXZMax.output.connectTo(randomXZ.max); - - const randomAngleMin = new ParticleInputBlock("Random Angle Min"); - randomAngleMin.value = -Math.PI; - const randomAngleMax = new ParticleInputBlock("Random Angle Max"); - randomAngleMax.value = Math.PI; - const randomAngle = new ParticleRandomBlock("Random Angle"); - randomAngle.lockMode = ParticleRandomBlockLocks.PerParticle; - randomAngleMin.output.connectTo(randomAngle.min); - randomAngleMax.output.connectTo(randomAngle.max); - - const randomRangeMin = new ParticleInputBlock("Random Range Min"); - randomRangeMin.value = 1; - const randomRangeMax = new ParticleInputBlock("Random Range Max"); - randomRangeMax.value = 5; - const randomRange = new ParticleRandomBlock("Random Range"); - randomRange.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRangeMin.output.connectTo(randomRange.min); - randomRangeMax.output.connectTo(randomRange.max); - - const one = new ParticleInputBlock("One"); - one.value = 1; - const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); - cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; - // Store angle in props so we can reuse during update - const setAnglePropInit = new SpsParticlePropsSetBlock("Set Angle Prop Init"); - setAnglePropInit.propertyName = "angle"; - randomAngle.output.connectTo(setAnglePropInit.value); - setAnglePropInit.output.connectTo(cosAngle.input); - const addOne = new ParticleMathBlock("Add One"); - addOne.operation = ParticleMathBlockOperations.Add; - one.output.connectTo(addOne.left); - cosAngle.output.connectTo(addOne.right); - const multiplyRange = new ParticleMathBlock("Multiply Range"); - multiplyRange.operation = ParticleMathBlockOperations.Multiply; - const setRangePropInit = new SpsParticlePropsSetBlock("Set Range Prop Init"); - setRangePropInit.propertyName = "range"; - randomRange.output.connectTo(setRangePropInit.value); - setRangePropInit.output.connectTo(multiplyRange.left); - addOne.output.connectTo(multiplyRange.right); - - const extractXZ = new ParticleConverterBlock("Extract XZ"); - randomXZ.output.connectTo(extractXZ.xyIn); - const positionConverter = new ParticleConverterBlock("Position Converter"); - extractXZ.xOut.connectTo(positionConverter.xIn); - multiplyRange.output.connectTo(positionConverter.yIn); - extractXZ.yOut.connectTo(positionConverter.zIn); - positionConverter.xyzOut.connectTo(spsInitTetra.position); - - const randomRotMin = new ParticleInputBlock("Random Rot Min"); - randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); - const randomRotMax = new ParticleInputBlock("Random Rot Max"); - randomRotMax.value = new Vector3(Math.PI, Math.PI, Math.PI); - const randomRot = new ParticleRandomBlock("Random Rotation"); - randomRot.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotMin.output.connectTo(randomRot.min); - randomRotMax.output.connectTo(randomRot.max); - randomRot.output.connectTo(spsInitTetra.rotation); - - const randomColorMin = new ParticleInputBlock("Random Color Min"); - randomColorMin.value = new Vector3(0, 0, 0); - const randomColorMax = new ParticleInputBlock("Random Color Max"); - randomColorMax.value = new Vector3(1, 1, 1); - const randomColorRGB = new ParticleRandomBlock("Random Color RGB"); - randomColorRGB.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorRGB.min); - randomColorMax.output.connectTo(randomColorRGB.max); - const colorAlpha = new ParticleInputBlock("Color Alpha"); - colorAlpha.value = 1; - const colorConverter = new ParticleConverterBlock("Color Converter"); - randomColorRGB.output.connectTo(colorConverter.xyzIn); - colorAlpha.output.connectTo(colorConverter.wIn); - colorConverter.colorOut.connectTo(spsInitTetra.color); - - // Create update block - const spsUpdateTetra = new SPSUpdateBlock("Update Tetrahedron Particles"); - spsUpdateTetra.updateData.connectTo(spsCreateTetra.updateBlock); - - // Get current position (X, Z stay the same, Y updates) - const currentPosition = new ParticleInputBlock("Current Position"); - currentPosition.contextualValue = NodeParticleContextualSources.Position; - - // Extract X and Z from current position - const extractPosition = new ParticleConverterBlock("Extract Position"); - currentPosition.output.connectTo(extractPosition.xyzIn); - - // Retrieve stored properties - const getAngleProp = new SpsParticlePropsGetBlock("Get Angle Prop"); - getAngleProp.propertyName = "angle"; - getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getRangeProp = new SpsParticlePropsGetBlock("Get Range Prop"); - getRangeProp.propertyName = "range"; - getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; - - // Accumulate angle using delta time to avoid relying on absolute frame id - const deltaBlock = new ParticleInputBlock("Delta Time"); - deltaBlock.systemSource = NodeParticleSystemSources.Delta; - - const milliToSecond = new ParticleInputBlock("Milli To Second"); - milliToSecond.value = 0.001; - - const deltaSeconds = new ParticleMathBlock("Delta Seconds"); - deltaSeconds.operation = ParticleMathBlockOperations.Multiply; - deltaBlock.output.connectTo(deltaSeconds.left); - milliToSecond.output.connectTo(deltaSeconds.right); - - const targetFps = new ParticleInputBlock("Target FPS"); - targetFps.value = 60; - - const normalizedDelta = new ParticleMathBlock("Normalized Delta"); - normalizedDelta.operation = ParticleMathBlockOperations.Multiply; - deltaSeconds.output.connectTo(normalizedDelta.left); - targetFps.output.connectTo(normalizedDelta.right); - - const speedPerFrame = new ParticleInputBlock("Speed Per Frame"); - speedPerFrame.value = Math.PI / 100; - - const scaledIncrement = new ParticleMathBlock("Scaled Increment"); - scaledIncrement.operation = ParticleMathBlockOperations.Multiply; - speedPerFrame.output.connectTo(scaledIncrement.left); - normalizedDelta.output.connectTo(scaledIncrement.right); - - const accumulateAngle = new ParticleMathBlock("Accumulate Angle"); - accumulateAngle.operation = ParticleMathBlockOperations.Add; - getAngleProp.output.connectTo(accumulateAngle.left); - scaledIncrement.output.connectTo(accumulateAngle.right); - - const setAnglePropUpdate = new SpsParticlePropsSetBlock("Set Angle Prop Update"); - setAnglePropUpdate.propertyName = "angle"; - setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; - accumulateAngle.output.connectTo(setAnglePropUpdate.value); - - // Calculate new Y position: range * (1 + cos(angle)) - const oneUpdate = new ParticleInputBlock("One Update"); - oneUpdate.value = 1; - const cosUpdatedAngle = new ParticleTrigonometryBlock("Cos Updated Angle"); - cosUpdatedAngle.operation = ParticleTrigonometryBlockOperations.Cos; - setAnglePropUpdate.output.connectTo(cosUpdatedAngle.input); - const addOneUpdate = new ParticleMathBlock("Add One Update"); - addOneUpdate.operation = ParticleMathBlockOperations.Add; - oneUpdate.output.connectTo(addOneUpdate.left); - cosUpdatedAngle.output.connectTo(addOneUpdate.right); - const multiplyRangeUpdate = new ParticleMathBlock("Multiply Range Update"); - multiplyRangeUpdate.operation = ParticleMathBlockOperations.Multiply; - getRangeProp.output.connectTo(multiplyRangeUpdate.left); - addOneUpdate.output.connectTo(multiplyRangeUpdate.right); - - // Combine X (from current position), Y (new), Z (from current position) - const updatePositionConverter = new ParticleConverterBlock("Update Position Converter"); - extractPosition.xOut.connectTo(updatePositionConverter.xIn); - multiplyRangeUpdate.output.connectTo(updatePositionConverter.yIn); - extractPosition.zOut.connectTo(updatePositionConverter.zIn); - updatePositionConverter.xyzOut.connectTo(spsUpdateTetra.position); - - this._systemBlocks.push(spsSystem); - } - - /** - * Remove a block from the current system set - * @param block defines the block to remove - */ - public removeBlock(block: NodeParticleBlock) { - const attachedBlockIndex = this.attachedBlocks.indexOf(block); - if (attachedBlockIndex > -1) { - this.attachedBlocks.splice(attachedBlockIndex, 1); - } - - if (block.isSystem) { - const index = this._systemBlocks.indexOf(block as SystemBlock); - if (index > -1) { - this._systemBlocks.splice(index, 1); - } - } - } - - /** - * Clear the current graph and load a new one from a serialization object - * @param source defines the JSON representation of the particle set - * @param merge defines whether or not the source must be merged or replace the current content - */ - public parseSerializedObject(source: any, merge = false) { - if (!merge) { - this.clear(); - } - - const map: { [key: number]: NodeParticleBlock } = {}; - - // Create blocks - for (const parsedBlock of source.blocks) { - const blockType = GetClass(parsedBlock.customType); - if (blockType) { - const block: NodeParticleBlock = new blockType(); - block._deserialize(parsedBlock); - map[parsedBlock.id] = block; - - this.attachedBlocks.push(block); - - if (block.isSystem) { - this._systemBlocks.push(block as SystemBlock); - } - } - } - - // Reconnect teleportation - for (const block of this.attachedBlocks) { - if (block.isTeleportOut) { - const teleportOut = block as ParticleTeleportOutBlock; - const id = teleportOut._tempEntryPointUniqueId; - if (id) { - const source = map[id] as ParticleTeleportInBlock; - if (source) { - source.attachToEndpoint(teleportOut); - } - } - } - } - - // Connections - Starts with input blocks only (except if in "merge" mode where we scan all blocks) - for (let blockIndex = 0; blockIndex < source.blocks.length; blockIndex++) { - const parsedBlock = source.blocks[blockIndex]; - const block = map[parsedBlock.id]; - - if (!block) { - continue; - } - - if (block.inputs.length && parsedBlock.inputs.some((i: any) => i.targetConnectionName) && !merge) { - continue; - } - this._restoreConnections(block, source, map); - } - - // UI related info - if (source.locations || (source.editorData && source.editorData.locations)) { - const locations: { - blockId: number; - x: number; - y: number; - isCollapsed: boolean; - }[] = source.locations || source.editorData.locations; - - for (const location of locations) { - if (map[location.blockId]) { - location.blockId = map[location.blockId].uniqueId; - } - } - - if (merge && this.editorData && this.editorData.locations) { - locations.concat(this.editorData.locations); - } - - if (source.locations) { - this.editorData = { - locations: locations, - }; - } else { - this.editorData = source.editorData; - this.editorData.locations = locations; - } - - const blockMap: { [key: number]: number } = {}; - - for (const key in map) { - blockMap[key] = map[key].uniqueId; - } - - this.editorData.map = blockMap; - } - - this.comment = source.comment; - } - - private _restoreConnections(block: NodeParticleBlock, source: any, map: { [key: number]: NodeParticleBlock }) { - for (const outputPoint of block.outputs) { - for (const candidate of source.blocks) { - const target = map[candidate.id]; - - if (!target) { - continue; - } - - for (const input of candidate.inputs) { - if (map[input.targetBlockId] === block && input.targetConnectionName === outputPoint.name) { - const inputPoint = target.getInputByName(input.inputName); - if (!inputPoint || inputPoint.isConnected) { - continue; - } - - outputPoint.connectTo(inputPoint, true); - this._restoreConnections(target, source, map); - continue; - } - } - } - } - } - - /** - * Serializes this node particle set in a JSON representation - * @param selectedBlocks defines the list of blocks to save (if null the whole node particle set will be saved) - * @returns the serialized particle system set object - */ - public serialize(selectedBlocks?: NodeParticleBlock[]): any { - const serializationObject = selectedBlocks ? {} : SerializationHelper.Serialize(this); - serializationObject.editorData = JSON.parse(JSON.stringify(this.editorData)); // Copy - - let blocks: NodeParticleBlock[] = []; - - if (selectedBlocks) { - blocks = selectedBlocks; - } else { - serializationObject.customType = "BABYLON.NodeParticleSystemSet"; - } - - // Blocks - serializationObject.blocks = []; - - for (const block of blocks) { - serializationObject.blocks.push(block.serialize()); - } - - if (!selectedBlocks) { - for (const block of this.attachedBlocks) { - if (blocks.indexOf(block) !== -1) { - continue; - } - serializationObject.blocks.push(block.serialize()); - } - } - - return serializationObject; - } - - /** - * Makes a duplicate of the current particle system set. - * @param name defines the name to use for the new particle system set - * @returns the cloned particle system set - */ - public clone(name: string): NodeParticleSystemSet { - const serializationObject = this.serialize(); - - const clone = SerializationHelper.Clone(() => new NodeParticleSystemSet(name), this); - clone.name = name; - clone.snippetId = this.snippetId; - - clone.parseSerializedObject(serializationObject); - clone._buildId = this._buildId; - - return clone; - } - - /** - * Disposes the resources - */ - public dispose(): void { - for (const block of this.attachedBlocks) { - block.dispose(); - } - - this.attachedBlocks.length = 0; - this.onBuildObservable.clear(); - } - - /** - * Creates a new node particle set set to default basic configuration - * @param name defines the name of the particle set - * @returns a new NodeParticleSystemSet - */ - public static CreateDefault(name: string) { - const nodeParticleSet = new NodeParticleSystemSet(name); - - nodeParticleSet.setToDefault(); - - return nodeParticleSet; - } - - /** - * Creates a node particle set from parsed data - * @param source defines the JSON representation of the particle set - * @returns a new node particle set - */ - public static Parse(source: any): NodeParticleSystemSet { - const nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(source.name), source, null); - - nodeParticleSet.parseSerializedObject(source); - - return nodeParticleSet; - } - - /** - * Creates a node particle set from a snippet saved in a remote file - * @param name defines the name of the node particle set to create - * @param url defines the url to load from - * @param nodeParticleSet defines a node particle set to update (instead of creating a new one) - * @returns a promise that will resolve to the new node particle set - */ - // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax - public static ParseFromFileAsync(name: string, url: string, nodeParticleSet?: NodeParticleSystemSet): Promise { - return new Promise((resolve, reject) => { - const request = new WebRequest(); - request.addEventListener("readystatechange", () => { - if (request.readyState == 4) { - if (request.status == 200) { - const serializationObject = JSON.parse(request.responseText); - if (!nodeParticleSet) { - nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(name), serializationObject, null); - } - - nodeParticleSet.parseSerializedObject(serializationObject); - - resolve(nodeParticleSet); - } else { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject("Unable to load the node particle system set"); - } - } - }); - - request.open("GET", url); - request.send(); - }); - } - - /** - * Creates a node particle set from a snippet saved by the node particle editor - * @param snippetId defines the snippet to load - * @param nodeParticleSet defines a node particle set to update (instead of creating a new one) - * @returns a promise that will resolve to the new node particle set - */ - // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax - public static ParseFromSnippetAsync(snippetId: string, nodeParticleSet?: NodeParticleSystemSet): Promise { - if (snippetId === "_BLANK") { - return Promise.resolve(NodeParticleSystemSet.CreateDefault("blank")); - } - - return new Promise((resolve, reject) => { - const request = new WebRequest(); - request.addEventListener("readystatechange", () => { - if (request.readyState == 4) { - if (request.status == 200) { - const snippet = JSON.parse(JSON.parse(request.responseText).jsonPayload); - const serializationObject = JSON.parse(snippet.nodeParticle); - - if (!nodeParticleSet) { - nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(snippetId), serializationObject, null); - } - - nodeParticleSet.parseSerializedObject(serializationObject); - nodeParticleSet.snippetId = snippetId; - - try { - resolve(nodeParticleSet); - } catch (err) { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(err); - } - } else { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject("Unable to load the snippet " + snippetId); - } - } - }); - - request.open("GET", this.SnippetUrl + "/" + snippetId.replace(/#/g, "/")); - request.send(); - }); - } -} +import { serialize } from "core/Misc/decorators"; +import { ParticleSystemSet } from "../particleSystemSet"; +import { SystemBlock } from "./Blocks/systemBlock"; +import type { Scene } from "core/scene"; +import { NodeParticleBuildState } from "./nodeParticleBuildState"; +import type { NodeParticleBlock } from "./nodeParticleBlock"; +import { SerializationHelper } from "core/Misc/decorators.serialization"; +import { Observable } from "core/Misc/observable"; +import { GetClass } from "core/Misc/typeStore"; +import { WebRequest } from "core/Misc/webRequest"; +import { Constants } from "core/Engines/constants"; +import { Tools } from "core/Misc/tools"; +import { AbstractEngine } from "core/Engines/abstractEngine"; +import { ParticleInputBlock } from "./Blocks/particleInputBlock"; +import { ParticleTextureSourceBlock } from "./Blocks/particleSourceTextureBlock"; +import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; +import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock"; +import { ParticleMathBlock, ParticleMathBlockOperations } from "./Blocks/particleMathBlock"; +import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTeleportOutBlock"; +import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; +import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; +import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; +import type { Nullable } from "core/types"; +import { Color4 } from "core/Maths/math.color"; +import { Vector2, Vector3 } from "core/Maths/math.vector"; +import { + SPSParticleConfigBlock, + SPSInitBlock, + SPSMeshShapeType, + SPSMeshSourceBlock, + SPSSystemBlock, + SPSCreateBlock, + SPSUpdateBlock, + SpsParticlePropsSetBlock, + SpsParticlePropsGetBlock, +} from "./Blocks"; +import { ParticleSystem } from "core/Particles/particleSystem"; +import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; +import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; +import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; +import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; + +// declare NODEPARTICLEEDITOR namespace for compilation issue +declare let NODEPARTICLEEDITOR: any; +declare let BABYLON: any; + +/** + * Interface used to configure the node particle editor + */ +export interface INodeParticleEditorOptions { + /** Define the URL to load node editor script from */ + editorURL?: string; + /** Additional configuration for the NPE */ + nodeEditorConfig?: { + backgroundColor?: Color4; + }; +} + +/** + * Defines a set of particle systems defined as a node graph. + * NPE: #K6F1ZB#1 + * PG: #ZT509U#1 + */ +export class NodeParticleSystemSet { + private _systemBlocks: (SystemBlock | SPSSystemBlock)[] = []; + private _buildId: number = 0; + + /** Define the Url to load node editor script */ + public static EditorURL = `${Tools._DefaultCdnUrl}/v${AbstractEngine.Version}/nodeParticleEditor/babylon.nodeParticleEditor.js`; + + /** Define the Url to load snippets */ + public static SnippetUrl = Constants.SnippetUrl; + + /** + * Snippet ID if the material was created from the snippet server + */ + public snippetId: string; + + /** + * Gets an array of blocks that needs to be serialized even if they are not yet connected + */ + public attachedBlocks: NodeParticleBlock[] = []; + + /** + * Gets or sets data used by visual editor + * @see https://npe.babylonjs.com + */ + public editorData: any = null; + + /** + * Observable raised when the particle set is built + */ + public onBuildObservable = new Observable(); + + /** + * The name of the set + */ + @serialize() + public name: string; + + /** + * A free comment about the set + */ + @serialize("comment") + public comment: string; + + /** + * Gets the system blocks + */ + public get systemBlocks(): (SystemBlock | SPSSystemBlock)[] { + return this._systemBlocks; + } + + /** + * Gets the list of input blocks attached to this material + * @returns an array of InputBlocks + */ + public get inputBlocks() { + const blocks: ParticleInputBlock[] = []; + for (const block of this.attachedBlocks) { + if (block.isInput) { + blocks.push(block as ParticleInputBlock); + } + } + + return blocks; + } + + /** + * Get a block by its name + * @param name defines the name of the block to retrieve + * @returns the required block or null if not found + */ + public getBlockByName(name: string) { + let result = null; + for (const block of this.attachedBlocks) { + if (block.name === name) { + if (!result) { + result = block; + } else { + Tools.Warn("More than one block was found with the name `" + name + "`"); + return result; + } + } + } + + return result; + } + + /** + * Get a block using a predicate + * @param predicate defines the predicate used to find the good candidate + * @returns the required block or null if not found + */ + public getBlockByPredicate(predicate: (block: NodeParticleBlock) => boolean) { + for (const block of this.attachedBlocks) { + if (predicate(block)) { + return block; + } + } + + return null; + } + + /** + * Get an input block using a predicate + * @param predicate defines the predicate used to find the good candidate + * @returns the required input block or null if not found + */ + public getInputBlockByPredicate(predicate: (block: ParticleInputBlock) => boolean): Nullable { + for (const block of this.attachedBlocks) { + if (block.isInput && predicate(block as ParticleInputBlock)) { + return block as ParticleInputBlock; + } + } + + return null; + } + /** + * Creates a new set + * @param name defines the name of the set + */ + public constructor(name: string) { + this.name = name; + } + + /** + * Gets the current class name of the node particle set e.g. "NodeParticleSystemSet" + * @returns the class name + */ + public getClassName(): string { + return "NodeParticleSystemSet"; + } + + private _initializeBlock(node: NodeParticleBlock, autoConfigure = true) { + if (this.attachedBlocks.indexOf(node) === -1) { + this.attachedBlocks.push(node); + } + + for (const input of node.inputs) { + const connectedPoint = input.connectedPoint; + if (connectedPoint) { + const block = connectedPoint.ownerBlock; + if (block !== node) { + this._initializeBlock(block, autoConfigure); + } + } + } + } + + private BJSNODEPARTICLEEDITOR = this._getGlobalNodeParticleEditor(); + + /** Get the editor from bundle or global + * @returns the global NPE + */ + private _getGlobalNodeParticleEditor(): any { + // UMD Global name detection from Webpack Bundle UMD Name. + if (typeof NODEPARTICLEEDITOR !== "undefined") { + return NODEPARTICLEEDITOR; + } + + // In case of module let's check the global emitted from the editor entry point. + if (typeof BABYLON !== "undefined" && typeof BABYLON.NodeParticleEditor !== "undefined") { + return BABYLON; + } + + return undefined; + } + + /** Creates the node editor window. + * @param additionalConfig Define the configuration of the editor + */ + private _createNodeParticleEditor(additionalConfig?: any) { + const nodeEditorConfig: any = { + nodeParticleSet: this, + ...additionalConfig, + }; + this.BJSNODEPARTICLEEDITOR.NodeParticleEditor.Show(nodeEditorConfig); + } + + /** + * Launch the node particle editor + * @param config Define the configuration of the editor + * @returns a promise fulfilled when the node editor is visible + */ + public async editAsync(config?: INodeParticleEditorOptions): Promise { + return await new Promise((resolve) => { + this.BJSNODEPARTICLEEDITOR = this.BJSNODEPARTICLEEDITOR || this._getGlobalNodeParticleEditor(); + if (typeof this.BJSNODEPARTICLEEDITOR == "undefined") { + const editorUrl = config && config.editorURL ? config.editorURL : NodeParticleSystemSet.EditorURL; + + // Load editor and add it to the DOM + Tools.LoadBabylonScript(editorUrl, () => { + this.BJSNODEPARTICLEEDITOR = this.BJSNODEPARTICLEEDITOR || this._getGlobalNodeParticleEditor(); + this._createNodeParticleEditor(config?.nodeEditorConfig); + resolve(); + }); + } else { + // Otherwise creates the editor + this._createNodeParticleEditor(config?.nodeEditorConfig); + resolve(); + } + }); + } + + /** + * Builds the particle system set from the defined blocks. + * @param scene defines the hosting scene + * @param verbose defines whether to log detailed information during the build process (false by default) + * @returns a promise that resolves to the built particle system set + */ + public async buildAsync(scene: Scene, verbose = false): Promise { + return await new Promise((resolve) => { + const output = new ParticleSystemSet(); + + // Initialize all blocks + for (const block of this._systemBlocks) { + this._initializeBlock(block); + } + + // Build the blocks + for (const block of this.systemBlocks) { + const state = new NodeParticleBuildState(); + state.buildId = this._buildId++; + state.scene = scene; + state.verbose = verbose; + + const system = block.createSystem(state); + if (system instanceof ParticleSystem) { + system._source = this; + system._blockReference = block._internalId; + } + output.systems.push(system); + // Errors + state.emitErrors(); + } + + this.onBuildObservable.notifyObservers(this); + + resolve(output); + }); + } + + /** + * Clear the current node particle set + */ + public clear() { + this.attachedBlocks.length = 0; + this._systemBlocks.length = 0; + } + + /** + * Clear the current set and restore it to a default state + */ + public setToDefault() { + this.clear(); + + this.editorData = null; + + // Main system + const system = new SystemBlock("Particle system"); + + // Update position + const updatePositionBlock = new UpdatePositionBlock("Update position"); + updatePositionBlock.output.connectTo(system.particle); + + // Contextual inputs + const positionBlock = new ParticleInputBlock("Position"); + positionBlock.contextualValue = NodeParticleContextualSources.Position; + const directionBlock = new ParticleInputBlock("Scaled direction"); + directionBlock.contextualValue = NodeParticleContextualSources.ScaledDirection; + + // Add + const addBlock = new ParticleMathBlock("Add"); + addBlock.operation = ParticleMathBlockOperations.Add; + positionBlock.output.connectTo(addBlock.left); + directionBlock.output.connectTo(addBlock.right); + addBlock.output.connectTo(updatePositionBlock.position); + + // Create particle + const createParticleBlock = new CreateParticleBlock("Create particle"); + + // Shape + const emitterShape = new BoxShapeBlock("Box shape"); + createParticleBlock.particle.connectTo(emitterShape.particle); + emitterShape.output.connectTo(updatePositionBlock.particle); + + // Texture + const textureBlock = new ParticleTextureSourceBlock("Texture"); + textureBlock.texture.connectTo(system.texture); + textureBlock.url = Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"); + + this._systemBlocks.push(system); + } + + public setToDefaultSps() { + this.createShockwaveSps(); + } + + public createDefaultSps() { + this.clear(); + this.editorData = null; + + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.billboard = false; + + const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + + const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); + spsCreateTetra.count.value = 2000; + spsCreateTetra.particleConfig.connectTo(spsCreateBlock.particleConfig); + + const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); + meshSourceTetra.shapeType = SPSMeshShapeType.Box; + meshSourceTetra.size = 0.1; + meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); + + const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); + spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); + + const randomXZMin = new ParticleInputBlock("Random XZ Min"); + randomXZMin.value = new Vector2(-10, -10); + const randomXZMax = new ParticleInputBlock("Random XZ Max"); + randomXZMax.value = new Vector2(10, 10); + const randomXZ = new ParticleRandomBlock("Random XZ"); + randomXZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomXZMin.output.connectTo(randomXZ.min); + randomXZMax.output.connectTo(randomXZ.max); + + const randomAngleMin = new ParticleInputBlock("Random Angle Min"); + randomAngleMin.value = -Math.PI; + const randomAngleMax = new ParticleInputBlock("Random Angle Max"); + randomAngleMax.value = Math.PI; + const randomAngle = new ParticleRandomBlock("Random Angle"); + randomAngle.lockMode = ParticleRandomBlockLocks.PerParticle; + randomAngleMin.output.connectTo(randomAngle.min); + randomAngleMax.output.connectTo(randomAngle.max); + + const randomRangeMin = new ParticleInputBlock("Random Range Min"); + randomRangeMin.value = 1; + const randomRangeMax = new ParticleInputBlock("Random Range Max"); + randomRangeMax.value = 5; + const randomRange = new ParticleRandomBlock("Random Range"); + randomRange.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRangeMin.output.connectTo(randomRange.min); + randomRangeMax.output.connectTo(randomRange.max); + + const one = new ParticleInputBlock("One"); + one.value = 1; + const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); + cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; + // Store angle in props so we can reuse during update + const setAnglePropInit = new SpsParticlePropsSetBlock("Set Angle Prop Init"); + setAnglePropInit.propertyName = "angle"; + randomAngle.output.connectTo(setAnglePropInit.value); + setAnglePropInit.output.connectTo(cosAngle.input); + const addOne = new ParticleMathBlock("Add One"); + addOne.operation = ParticleMathBlockOperations.Add; + one.output.connectTo(addOne.left); + cosAngle.output.connectTo(addOne.right); + const multiplyRange = new ParticleMathBlock("Multiply Range"); + multiplyRange.operation = ParticleMathBlockOperations.Multiply; + const setRangePropInit = new SpsParticlePropsSetBlock("Set Range Prop Init"); + setRangePropInit.propertyName = "range"; + randomRange.output.connectTo(setRangePropInit.value); + setRangePropInit.output.connectTo(multiplyRange.left); + addOne.output.connectTo(multiplyRange.right); + + const extractXZ = new ParticleConverterBlock("Extract XZ"); + randomXZ.output.connectTo(extractXZ.xyIn); + const positionConverter = new ParticleConverterBlock("Position Converter"); + extractXZ.xOut.connectTo(positionConverter.xIn); + multiplyRange.output.connectTo(positionConverter.yIn); + extractXZ.yOut.connectTo(positionConverter.zIn); + positionConverter.xyzOut.connectTo(spsInitTetra.position); + + const randomRotMin = new ParticleInputBlock("Random Rot Min"); + randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); + const randomRotMax = new ParticleInputBlock("Random Rot Max"); + randomRotMax.value = new Vector3(Math.PI, Math.PI, Math.PI); + const randomRot = new ParticleRandomBlock("Random Rotation"); + randomRot.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotMin.output.connectTo(randomRot.min); + randomRotMax.output.connectTo(randomRot.max); + randomRot.output.connectTo(spsInitTetra.rotation); + + const randomColorMin = new ParticleInputBlock("Random Color Min"); + randomColorMin.value = new Vector3(0, 0, 0); + const randomColorMax = new ParticleInputBlock("Random Color Max"); + randomColorMax.value = new Vector3(1, 1, 1); + const randomColorRGB = new ParticleRandomBlock("Random Color RGB"); + randomColorRGB.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorRGB.min); + randomColorMax.output.connectTo(randomColorRGB.max); + const colorAlpha = new ParticleInputBlock("Color Alpha"); + colorAlpha.value = 1; + const colorConverter = new ParticleConverterBlock("Color Converter"); + randomColorRGB.output.connectTo(colorConverter.xyzIn); + colorAlpha.output.connectTo(colorConverter.wIn); + colorConverter.colorOut.connectTo(spsInitTetra.color); + + // Create update block + const spsUpdateTetra = new SPSUpdateBlock("Update Tetrahedron Particles"); + spsUpdateTetra.updateData.connectTo(spsCreateTetra.updateBlock); + + // Get current position (X, Z stay the same, Y updates) + const currentPosition = new ParticleInputBlock("Current Position"); + currentPosition.contextualValue = NodeParticleContextualSources.Position; + + // Extract X and Z from current position + const extractPosition = new ParticleConverterBlock("Extract Position"); + currentPosition.output.connectTo(extractPosition.xyzIn); + + // Retrieve stored properties + const getAngleProp = new SpsParticlePropsGetBlock("Get Angle Prop"); + getAngleProp.propertyName = "angle"; + getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRangeProp = new SpsParticlePropsGetBlock("Get Range Prop"); + getRangeProp.propertyName = "range"; + getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; + + // Accumulate angle using delta time to avoid relying on absolute frame id + const deltaBlock = new ParticleInputBlock("Delta Time"); + deltaBlock.systemSource = NodeParticleSystemSources.Delta; + + const milliToSecond = new ParticleInputBlock("Milli To Second"); + milliToSecond.value = 0.001; + + const deltaSeconds = new ParticleMathBlock("Delta Seconds"); + deltaSeconds.operation = ParticleMathBlockOperations.Multiply; + deltaBlock.output.connectTo(deltaSeconds.left); + milliToSecond.output.connectTo(deltaSeconds.right); + + const targetFps = new ParticleInputBlock("Target FPS"); + targetFps.value = 60; + + const normalizedDelta = new ParticleMathBlock("Normalized Delta"); + normalizedDelta.operation = ParticleMathBlockOperations.Multiply; + deltaSeconds.output.connectTo(normalizedDelta.left); + targetFps.output.connectTo(normalizedDelta.right); + + const speedPerFrame = new ParticleInputBlock("Speed Per Frame"); + speedPerFrame.value = Math.PI / 100; + + const scaledIncrement = new ParticleMathBlock("Scaled Increment"); + scaledIncrement.operation = ParticleMathBlockOperations.Multiply; + speedPerFrame.output.connectTo(scaledIncrement.left); + normalizedDelta.output.connectTo(scaledIncrement.right); + + const accumulateAngle = new ParticleMathBlock("Accumulate Angle"); + accumulateAngle.operation = ParticleMathBlockOperations.Add; + getAngleProp.output.connectTo(accumulateAngle.left); + scaledIncrement.output.connectTo(accumulateAngle.right); + + const setAnglePropUpdate = new SpsParticlePropsSetBlock("Set Angle Prop Update"); + setAnglePropUpdate.propertyName = "angle"; + setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + accumulateAngle.output.connectTo(setAnglePropUpdate.value); + + // Calculate new Y position: range * (1 + cos(angle)) + const oneUpdate = new ParticleInputBlock("One Update"); + oneUpdate.value = 1; + const cosUpdatedAngle = new ParticleTrigonometryBlock("Cos Updated Angle"); + cosUpdatedAngle.operation = ParticleTrigonometryBlockOperations.Cos; + setAnglePropUpdate.output.connectTo(cosUpdatedAngle.input); + const addOneUpdate = new ParticleMathBlock("Add One Update"); + addOneUpdate.operation = ParticleMathBlockOperations.Add; + oneUpdate.output.connectTo(addOneUpdate.left); + cosUpdatedAngle.output.connectTo(addOneUpdate.right); + const multiplyRangeUpdate = new ParticleMathBlock("Multiply Range Update"); + multiplyRangeUpdate.operation = ParticleMathBlockOperations.Multiply; + getRangeProp.output.connectTo(multiplyRangeUpdate.left); + addOneUpdate.output.connectTo(multiplyRangeUpdate.right); + + // Combine X (from current position), Y (new), Z (from current position) + const updatePositionConverter = new ParticleConverterBlock("Update Position Converter"); + extractPosition.xOut.connectTo(updatePositionConverter.xIn); + multiplyRangeUpdate.output.connectTo(updatePositionConverter.yIn); + extractPosition.zOut.connectTo(updatePositionConverter.zIn); + updatePositionConverter.xyzOut.connectTo(spsUpdateTetra.position); + + this._systemBlocks.push(spsSystem); + } + + /** + * Sets the current set to an SPS shockwave preset inspired by Patrick Ryan's createShockwave sample + */ + public createShockwaveSps() { + this.clear(); + this.editorData = null; + + const spsSystem = new SPSSystemBlock("Shockwave SPS System"); + spsSystem.billboard = false; + + const lifetimeMs = new ParticleInputBlock("Shockwave Lifetime (ms)"); + lifetimeMs.value = 2500; + spsSystem.lifetime = lifetimeMs.value; + spsSystem.disposeOnEnd = true; + + const minLifetimeMs = new ParticleInputBlock("Shockwave Min Lifetime (ms)"); + minLifetimeMs.value = 1; + const lifetimeSafe = new ParticleMathBlock("Shockwave Lifetime Safe"); + lifetimeSafe.operation = ParticleMathBlockOperations.Max; + lifetimeMs.output.connectTo(lifetimeSafe.left); + minLifetimeMs.output.connectTo(lifetimeSafe.right); + + const spsCreateBlock = new SPSCreateBlock("Create Shockwave SPS"); + spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + + const shockwaveConfig = new SPSParticleConfigBlock("Shockwave Particle Config"); + shockwaveConfig.count.value = 7; + shockwaveConfig.particleConfig.connectTo(spsCreateBlock.particleConfig); + + const shockwaveMesh = new SPSMeshSourceBlock("Shockwave Mesh Source"); + shockwaveMesh.shapeType = SPSMeshShapeType.Plane; + shockwaveMesh.size = 0.75; + shockwaveMesh.segments = 32; + shockwaveMesh.mesh.connectTo(shockwaveConfig.mesh); + + const shockwaveInit = new SPSInitBlock("Initialize Shockwave Particles"); + shockwaveInit.initData.connectTo(shockwaveConfig.initBlock); + + const shockwaveUpdate = new SPSUpdateBlock("Update Shockwave Particles"); + shockwaveUpdate.updateData.connectTo(shockwaveConfig.updateBlock); + + const deltaBlock = new ParticleInputBlock("Shockwave Delta Time"); + deltaBlock.systemSource = NodeParticleSystemSources.Delta; + const milliToSecond = new ParticleInputBlock("Shockwave Milli To Second"); + milliToSecond.value = 0.001; + const deltaSeconds = new ParticleMathBlock("Shockwave Delta Seconds"); + deltaSeconds.operation = ParticleMathBlockOperations.Multiply; + deltaBlock.output.connectTo(deltaSeconds.left); + milliToSecond.output.connectTo(deltaSeconds.right); + const targetFps = new ParticleInputBlock("Shockwave Target FPS"); + targetFps.value = 60; + const normalizedDelta = new ParticleMathBlock("Shockwave Normalized Delta"); + normalizedDelta.operation = ParticleMathBlockOperations.Multiply; + deltaSeconds.output.connectTo(normalizedDelta.left); + targetFps.output.connectTo(normalizedDelta.right); + + const lifetimeSeconds = new ParticleMathBlock("Shockwave Lifetime Seconds"); + lifetimeSeconds.operation = ParticleMathBlockOperations.Multiply; + lifetimeSafe.output.connectTo(lifetimeSeconds.left); + milliToSecond.output.connectTo(lifetimeSeconds.right); + const framesPerLifetime = new ParticleMathBlock("Shockwave Frames Per Lifetime"); + framesPerLifetime.operation = ParticleMathBlockOperations.Multiply; + lifetimeSeconds.output.connectTo(framesPerLifetime.left); + targetFps.output.connectTo(framesPerLifetime.right); + + const origin = new ParticleInputBlock("Shockwave Origin"); + origin.value = new Vector3(0, 0.05, 0); + origin.output.connectTo(shockwaveInit.position); + + const shockwaveColor = new ParticleInputBlock("Shockwave Base Color"); + shockwaveColor.value = new Color4(0.33, 0.49, 0.88, 0.9); + shockwaveColor.output.connectTo(shockwaveInit.color); + + const zeroValue = new ParticleInputBlock("Shockwave Zero"); + zeroValue.value = 0; + + const radiusStart = new ParticleInputBlock("Shockwave Radius Start"); + radiusStart.value = 1; + const storeRadiusInit = new SpsParticlePropsSetBlock("Store Radius Init"); + storeRadiusInit.propertyName = "radius"; + storeRadiusInit.type = NodeParticleBlockConnectionPointTypes.Float; + radiusStart.output.connectTo(storeRadiusInit.value); + + const maxRadius = new ParticleInputBlock("Shockwave Max Radius"); + maxRadius.value = 4; + + const radiusRangeBlock = new ParticleMathBlock("Shockwave Radius Range"); + radiusRangeBlock.operation = ParticleMathBlockOperations.Subtract; + maxRadius.output.connectTo(radiusRangeBlock.left); + radiusStart.output.connectTo(radiusRangeBlock.right); + + const growthMultiplierMin = new ParticleInputBlock("Shockwave Growth Multiplier Min"); + growthMultiplierMin.value = 0.85; + const growthMultiplierMax = new ParticleInputBlock("Shockwave Growth Multiplier Max"); + growthMultiplierMax.value = 1.15; + const growthMultiplier = new ParticleRandomBlock("Shockwave Growth Multiplier"); + growthMultiplier.lockMode = ParticleRandomBlockLocks.OncePerParticle; + growthMultiplierMin.output.connectTo(growthMultiplier.min); + growthMultiplierMax.output.connectTo(growthMultiplier.max); + + const baseGrowthPerFrame = new ParticleMathBlock("Shockwave Base Growth Per Frame"); + baseGrowthPerFrame.operation = ParticleMathBlockOperations.Divide; + radiusRangeBlock.output.connectTo(baseGrowthPerFrame.left); + framesPerLifetime.output.connectTo(baseGrowthPerFrame.right); + + const growthPerFrame = new ParticleMathBlock("Shockwave Growth Per Frame"); + growthPerFrame.operation = ParticleMathBlockOperations.Multiply; + baseGrowthPerFrame.output.connectTo(growthPerFrame.left); + growthMultiplier.output.connectTo(growthPerFrame.right); + + const storeScaleStepInit = new SpsParticlePropsSetBlock("Store Scale Step Init"); + storeScaleStepInit.propertyName = "scaleStep"; + storeScaleStepInit.type = NodeParticleBlockConnectionPointTypes.Float; + growthPerFrame.output.connectTo(storeScaleStepInit.value); + + const initScaleConverter = new ParticleConverterBlock("Shockwave Init Scale Converter"); + storeRadiusInit.output.connectTo(initScaleConverter.xIn); + storeScaleStepInit.output.connectTo(initScaleConverter.yIn); + storeRadiusInit.output.connectTo(initScaleConverter.zIn); + initScaleConverter.xyzOut.connectTo(shockwaveInit.scaling); + + const rotationMin = new ParticleInputBlock("Shockwave Rotation Min"); + rotationMin.value = new Vector3(0, -Math.PI, 0); + const rotationMax = new ParticleInputBlock("Shockwave Rotation Max"); + rotationMax.value = new Vector3(0, Math.PI, 0); + const initialRotation = new ParticleRandomBlock("Shockwave Initial Rotation"); + initialRotation.lockMode = ParticleRandomBlockLocks.OncePerParticle; + rotationMin.output.connectTo(initialRotation.min); + rotationMax.output.connectTo(initialRotation.max); + + const rotationConverter = new ParticleConverterBlock("Shockwave Rotation Converter"); + initialRotation.output.connectTo(rotationConverter.xyzIn); + const storeRotationAngleInit = new SpsParticlePropsSetBlock("Store Rotation Angle Init"); + storeRotationAngleInit.propertyName = "rotationAngle"; + storeRotationAngleInit.type = NodeParticleBlockConnectionPointTypes.Float; + rotationConverter.yOut.connectTo(storeRotationAngleInit.value); + + const rotationCompose = new ParticleConverterBlock("Shockwave Rotation Compose"); + rotationConverter.xOut.connectTo(rotationCompose.xIn); + storeRotationAngleInit.output.connectTo(rotationCompose.yIn); + rotationConverter.zOut.connectTo(rotationCompose.zIn); + rotationCompose.xyzOut.connectTo(shockwaveInit.rotation); + + const rotationSpeedMin = new ParticleInputBlock("Shockwave Rotation Speed Min"); + rotationSpeedMin.value = -0.06; + const rotationSpeedMax = new ParticleInputBlock("Shockwave Rotation Speed Max"); + rotationSpeedMax.value = 0.06; + const rotationSpeedRandom = new ParticleRandomBlock("Shockwave Rotation Speed Random"); + rotationSpeedRandom.lockMode = ParticleRandomBlockLocks.OncePerParticle; + rotationSpeedMin.output.connectTo(rotationSpeedRandom.min); + rotationSpeedMax.output.connectTo(rotationSpeedRandom.max); + const storeRotationSpeed = new SpsParticlePropsSetBlock("Store Rotation Speed"); + storeRotationSpeed.propertyName = "rotationSpeed"; + storeRotationSpeed.type = NodeParticleBlockConnectionPointTypes.Float; + rotationSpeedRandom.output.connectTo(storeRotationSpeed.value); + + const rotationSpeedSink = new ParticleMathBlock("Shockwave Rotation Speed Sink"); + rotationSpeedSink.operation = ParticleMathBlockOperations.Multiply; + storeRotationSpeed.output.connectTo(rotationSpeedSink.left); + zeroValue.output.connectTo(rotationSpeedSink.right); + const rotationSpeedVelocity = new ParticleConverterBlock("Shockwave Rotation Speed Velocity"); + rotationSpeedSink.output.connectTo(rotationSpeedVelocity.xIn); + zeroValue.output.connectTo(rotationSpeedVelocity.yIn); + zeroValue.output.connectTo(rotationSpeedVelocity.zIn); + rotationSpeedVelocity.xyzOut.connectTo(shockwaveInit.velocity); + + const getRadiusProp = new SpsParticlePropsGetBlock("Get Radius Prop"); + getRadiusProp.propertyName = "radius"; + getRadiusProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getScaleStepProp = new SpsParticlePropsGetBlock("Get Scale Step Prop"); + getScaleStepProp.propertyName = "scaleStep"; + getScaleStepProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRotationSpeedProp = new SpsParticlePropsGetBlock("Get Rotation Speed Prop"); + getRotationSpeedProp.propertyName = "rotationSpeed"; + getRotationSpeedProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRotationAngleProp = new SpsParticlePropsGetBlock("Get Rotation Angle Prop"); + getRotationAngleProp.propertyName = "rotationAngle"; + getRotationAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const scaleStepDelta = new ParticleMathBlock("Shockwave Radius Delta"); + scaleStepDelta.operation = ParticleMathBlockOperations.Multiply; + getScaleStepProp.output.connectTo(scaleStepDelta.left); + normalizedDelta.output.connectTo(scaleStepDelta.right); + + const radiusIncrement = new ParticleMathBlock("Shockwave Radius Increment"); + radiusIncrement.operation = ParticleMathBlockOperations.Add; + getRadiusProp.output.connectTo(radiusIncrement.left); + scaleStepDelta.output.connectTo(radiusIncrement.right); + + const setRadiusPropUpdate = new SpsParticlePropsSetBlock("Set Radius Prop Update"); + setRadiusPropUpdate.propertyName = "radius"; + setRadiusPropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + radiusIncrement.output.connectTo(setRadiusPropUpdate.value); + + const clampRadius = new ParticleMathBlock("Shockwave Clamp Radius"); + clampRadius.operation = ParticleMathBlockOperations.Min; + setRadiusPropUpdate.output.connectTo(clampRadius.left); + maxRadius.output.connectTo(clampRadius.right); + + const normalizedRadius = new ParticleMathBlock("Shockwave Normalized Radius"); + normalizedRadius.operation = ParticleMathBlockOperations.Divide; + clampRadius.output.connectTo(normalizedRadius.left); + maxRadius.output.connectTo(normalizedRadius.right); + + const normalizedMin = new ParticleMathBlock("Shockwave Normalized Min"); + normalizedMin.operation = ParticleMathBlockOperations.Max; + zeroValue.output.connectTo(normalizedMin.left); + normalizedRadius.output.connectTo(normalizedMin.right); + + const oneValue = new ParticleInputBlock("Shockwave One"); + oneValue.value = 1; + const normalizedClamp = new ParticleMathBlock("Shockwave Normalized Clamp"); + normalizedClamp.operation = ParticleMathBlockOperations.Min; + normalizedMin.output.connectTo(normalizedClamp.left); + oneValue.output.connectTo(normalizedClamp.right); + + const minThickness = new ParticleInputBlock("Shockwave Min Thickness"); + minThickness.value = 0.25; + const maxThickness = new ParticleInputBlock("Shockwave Max Thickness"); + maxThickness.value = 4; + const thicknessRange = new ParticleMathBlock("Shockwave Thickness Range"); + thicknessRange.operation = ParticleMathBlockOperations.Subtract; + maxThickness.output.connectTo(thicknessRange.left); + minThickness.output.connectTo(thicknessRange.right); + const thicknessScale = new ParticleMathBlock("Shockwave Thickness Scale"); + thicknessScale.operation = ParticleMathBlockOperations.Multiply; + thicknessRange.output.connectTo(thicknessScale.left); + normalizedClamp.output.connectTo(thicknessScale.right); + const thicknessValue = new ParticleMathBlock("Shockwave Thickness Value"); + thicknessValue.operation = ParticleMathBlockOperations.Add; + minThickness.output.connectTo(thicknessValue.left); + thicknessScale.output.connectTo(thicknessValue.right); + + const minHeight = new ParticleInputBlock("Shockwave Min Height"); + minHeight.value = 0.05; + const maxHeight = new ParticleInputBlock("Shockwave Max Height"); + maxHeight.value = 0.25; + const heightRange = new ParticleMathBlock("Shockwave Height Range"); + heightRange.operation = ParticleMathBlockOperations.Subtract; + maxHeight.output.connectTo(heightRange.left); + minHeight.output.connectTo(heightRange.right); + const heightScale = new ParticleMathBlock("Shockwave Height Scale"); + heightScale.operation = ParticleMathBlockOperations.Multiply; + heightRange.output.connectTo(heightScale.left); + normalizedClamp.output.connectTo(heightScale.right); + const heightValue = new ParticleMathBlock("Shockwave Height Value"); + heightValue.operation = ParticleMathBlockOperations.Add; + minHeight.output.connectTo(heightValue.left); + heightScale.output.connectTo(heightValue.right); + + const scalingConverter = new ParticleConverterBlock("Shockwave Scaling Converter"); + clampRadius.output.connectTo(scalingConverter.xIn); + thicknessValue.output.connectTo(scalingConverter.yIn); + clampRadius.output.connectTo(scalingConverter.zIn); + scalingConverter.xyzOut.connectTo(shockwaveUpdate.scaling); + + const positionConverter = new ParticleConverterBlock("Shockwave Position Converter"); + zeroValue.output.connectTo(positionConverter.xIn); + heightValue.output.connectTo(positionConverter.yIn); + zeroValue.output.connectTo(positionConverter.zIn); + positionConverter.xyzOut.connectTo(shockwaveUpdate.position); + + const rotationIncrement = new ParticleMathBlock("Shockwave Rotation Increment"); + rotationIncrement.operation = ParticleMathBlockOperations.Multiply; + getRotationSpeedProp.output.connectTo(rotationIncrement.left); + normalizedDelta.output.connectTo(rotationIncrement.right); + + const updatedRotationAngle = new ParticleMathBlock("Shockwave Updated Rotation Angle"); + updatedRotationAngle.operation = ParticleMathBlockOperations.Add; + getRotationAngleProp.output.connectTo(updatedRotationAngle.left); + rotationIncrement.output.connectTo(updatedRotationAngle.right); + + const setRotationAngleUpdate = new SpsParticlePropsSetBlock("Set Rotation Angle Update"); + setRotationAngleUpdate.propertyName = "rotationAngle"; + setRotationAngleUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + updatedRotationAngle.output.connectTo(setRotationAngleUpdate.value); + + const rotationUpdateConverter = new ParticleConverterBlock("Shockwave Rotation Update Converter"); + zeroValue.output.connectTo(rotationUpdateConverter.xIn); + setRotationAngleUpdate.output.connectTo(rotationUpdateConverter.yIn); + zeroValue.output.connectTo(rotationUpdateConverter.zIn); + rotationUpdateConverter.xyzOut.connectTo(shockwaveUpdate.rotation); + + const colorEnd = new ParticleInputBlock("Shockwave Color End"); + colorEnd.value = new Color4(0, 0, 0, 0); + const colorRange = new ParticleMathBlock("Shockwave Color Range"); + colorRange.operation = ParticleMathBlockOperations.Subtract; + colorEnd.output.connectTo(colorRange.left); + shockwaveColor.output.connectTo(colorRange.right); + const colorScale = new ParticleMathBlock("Shockwave Color Scale"); + colorScale.operation = ParticleMathBlockOperations.Multiply; + colorRange.output.connectTo(colorScale.left); + normalizedClamp.output.connectTo(colorScale.right); + const colorValue = new ParticleMathBlock("Shockwave Color Value"); + colorValue.operation = ParticleMathBlockOperations.Add; + shockwaveColor.output.connectTo(colorValue.left); + colorScale.output.connectTo(colorValue.right); + colorValue.output.connectTo(shockwaveUpdate.color); + + this._systemBlocks.push(spsSystem); + } + + /** + * Remove a block from the current system set + * @param block defines the block to remove + */ + public removeBlock(block: NodeParticleBlock) { + const attachedBlockIndex = this.attachedBlocks.indexOf(block); + if (attachedBlockIndex > -1) { + this.attachedBlocks.splice(attachedBlockIndex, 1); + } + + if (block.isSystem) { + const index = this._systemBlocks.indexOf(block as SystemBlock); + if (index > -1) { + this._systemBlocks.splice(index, 1); + } + } + } + + /** + * Clear the current graph and load a new one from a serialization object + * @param source defines the JSON representation of the particle set + * @param merge defines whether or not the source must be merged or replace the current content + */ + public parseSerializedObject(source: any, merge = false) { + if (!merge) { + this.clear(); + } + + const map: { [key: number]: NodeParticleBlock } = {}; + + // Create blocks + for (const parsedBlock of source.blocks) { + const blockType = GetClass(parsedBlock.customType); + if (blockType) { + const block: NodeParticleBlock = new blockType(); + block._deserialize(parsedBlock); + map[parsedBlock.id] = block; + + this.attachedBlocks.push(block); + + if (block.isSystem) { + this._systemBlocks.push(block as SystemBlock); + } + } + } + + // Reconnect teleportation + for (const block of this.attachedBlocks) { + if (block.isTeleportOut) { + const teleportOut = block as ParticleTeleportOutBlock; + const id = teleportOut._tempEntryPointUniqueId; + if (id) { + const source = map[id] as ParticleTeleportInBlock; + if (source) { + source.attachToEndpoint(teleportOut); + } + } + } + } + + // Connections - Starts with input blocks only (except if in "merge" mode where we scan all blocks) + for (let blockIndex = 0; blockIndex < source.blocks.length; blockIndex++) { + const parsedBlock = source.blocks[blockIndex]; + const block = map[parsedBlock.id]; + + if (!block) { + continue; + } + + if (block.inputs.length && parsedBlock.inputs.some((i: any) => i.targetConnectionName) && !merge) { + continue; + } + this._restoreConnections(block, source, map); + } + + // UI related info + if (source.locations || (source.editorData && source.editorData.locations)) { + const locations: { + blockId: number; + x: number; + y: number; + isCollapsed: boolean; + }[] = source.locations || source.editorData.locations; + + for (const location of locations) { + if (map[location.blockId]) { + location.blockId = map[location.blockId].uniqueId; + } + } + + if (merge && this.editorData && this.editorData.locations) { + locations.concat(this.editorData.locations); + } + + if (source.locations) { + this.editorData = { + locations: locations, + }; + } else { + this.editorData = source.editorData; + this.editorData.locations = locations; + } + + const blockMap: { [key: number]: number } = {}; + + for (const key in map) { + blockMap[key] = map[key].uniqueId; + } + + this.editorData.map = blockMap; + } + + this.comment = source.comment; + } + + private _restoreConnections(block: NodeParticleBlock, source: any, map: { [key: number]: NodeParticleBlock }) { + for (const outputPoint of block.outputs) { + for (const candidate of source.blocks) { + const target = map[candidate.id]; + + if (!target) { + continue; + } + + for (const input of candidate.inputs) { + if (map[input.targetBlockId] === block && input.targetConnectionName === outputPoint.name) { + const inputPoint = target.getInputByName(input.inputName); + if (!inputPoint || inputPoint.isConnected) { + continue; + } + + outputPoint.connectTo(inputPoint, true); + this._restoreConnections(target, source, map); + continue; + } + } + } + } + } + + /** + * Serializes this node particle set in a JSON representation + * @param selectedBlocks defines the list of blocks to save (if null the whole node particle set will be saved) + * @returns the serialized particle system set object + */ + public serialize(selectedBlocks?: NodeParticleBlock[]): any { + const serializationObject = selectedBlocks ? {} : SerializationHelper.Serialize(this); + serializationObject.editorData = JSON.parse(JSON.stringify(this.editorData)); // Copy + + let blocks: NodeParticleBlock[] = []; + + if (selectedBlocks) { + blocks = selectedBlocks; + } else { + serializationObject.customType = "BABYLON.NodeParticleSystemSet"; + } + + // Blocks + serializationObject.blocks = []; + + for (const block of blocks) { + serializationObject.blocks.push(block.serialize()); + } + + if (!selectedBlocks) { + for (const block of this.attachedBlocks) { + if (blocks.indexOf(block) !== -1) { + continue; + } + serializationObject.blocks.push(block.serialize()); + } + } + + return serializationObject; + } + + /** + * Makes a duplicate of the current particle system set. + * @param name defines the name to use for the new particle system set + * @returns the cloned particle system set + */ + public clone(name: string): NodeParticleSystemSet { + const serializationObject = this.serialize(); + + const clone = SerializationHelper.Clone(() => new NodeParticleSystemSet(name), this); + clone.name = name; + clone.snippetId = this.snippetId; + + clone.parseSerializedObject(serializationObject); + clone._buildId = this._buildId; + + return clone; + } + + /** + * Disposes the resources + */ + public dispose(): void { + for (const block of this.attachedBlocks) { + block.dispose(); + } + + this.attachedBlocks.length = 0; + this.onBuildObservable.clear(); + } + + /** + * Creates a new node particle set set to default basic configuration + * @param name defines the name of the particle set + * @returns a new NodeParticleSystemSet + */ + public static CreateDefault(name: string) { + const nodeParticleSet = new NodeParticleSystemSet(name); + + nodeParticleSet.setToDefault(); + + return nodeParticleSet; + } + + /** + * Creates a node particle set from parsed data + * @param source defines the JSON representation of the particle set + * @returns a new node particle set + */ + public static Parse(source: any): NodeParticleSystemSet { + const nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(source.name), source, null); + + nodeParticleSet.parseSerializedObject(source); + + return nodeParticleSet; + } + + /** + * Creates a node particle set from a snippet saved in a remote file + * @param name defines the name of the node particle set to create + * @param url defines the url to load from + * @param nodeParticleSet defines a node particle set to update (instead of creating a new one) + * @returns a promise that will resolve to the new node particle set + */ + // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax + public static ParseFromFileAsync(name: string, url: string, nodeParticleSet?: NodeParticleSystemSet): Promise { + return new Promise((resolve, reject) => { + const request = new WebRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState == 4) { + if (request.status == 200) { + const serializationObject = JSON.parse(request.responseText); + if (!nodeParticleSet) { + nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(name), serializationObject, null); + } + + nodeParticleSet.parseSerializedObject(serializationObject); + + resolve(nodeParticleSet); + } else { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject("Unable to load the node particle system set"); + } + } + }); + + request.open("GET", url); + request.send(); + }); + } + + /** + * Creates a node particle set from a snippet saved by the node particle editor + * @param snippetId defines the snippet to load + * @param nodeParticleSet defines a node particle set to update (instead of creating a new one) + * @returns a promise that will resolve to the new node particle set + */ + // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax + public static ParseFromSnippetAsync(snippetId: string, nodeParticleSet?: NodeParticleSystemSet): Promise { + if (snippetId === "_BLANK") { + return Promise.resolve(NodeParticleSystemSet.CreateDefault("blank")); + } + + return new Promise((resolve, reject) => { + const request = new WebRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState == 4) { + if (request.status == 200) { + const snippet = JSON.parse(JSON.parse(request.responseText).jsonPayload); + const serializationObject = JSON.parse(snippet.nodeParticle); + + if (!nodeParticleSet) { + nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(snippetId), serializationObject, null); + } + + nodeParticleSet.parseSerializedObject(serializationObject); + nodeParticleSet.snippetId = snippetId; + + try { + resolve(nodeParticleSet); + } catch (err) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(err); + } + } else { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject("Unable to load the snippet " + snippetId); + } + } + }); + + request.open("GET", this.SnippetUrl + "/" + snippetId.replace(/#/g, "/")); + request.send(); + }); + } +} diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index 0b002d34fbe..9ed80e81a5a 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -18,6 +18,7 @@ import { MultiMaterial } from "../Materials/multiMaterial"; import type { PickingInfo } from "../Collisions/pickingInfo"; import type { PBRMaterial } from "../Materials/PBR/pbrMaterial"; import type { Observer } from "../Misc/observable"; +import { Observable } from "../Misc/observable"; /** * The SPS is a single updatable mesh. The solid particles are simply separate parts or faces of this big mesh. @@ -63,6 +64,18 @@ export class SolidParticleSystem implements IDisposable { * Please read : https://doc.babylonjs.com/features/featuresDeepDive/particles/solid_particle_system/optimize_sps#limit-garbage-collection */ public vars: any = {}; + /** + * Lifetime duration in milliseconds (0 means infinite) + */ + public lifetime = 0; + /** + * Defines if the SPS should dispose itself automatically once the lifetime ends + */ + public disposeOnEnd = false; + /** + * Observable raised when the SPS lifetime is reached + */ + public onLifeTimeEndedObservable = new Observable(); /** * This array is populated when the SPS is set as 'pickable'. * Each key of this array is a `faceId` value that you can get from a pickResult object. @@ -154,6 +167,8 @@ export class SolidParticleSystem implements IDisposable { protected _tmpVertex: SolidParticleVertex; protected _recomputeInvisibles: boolean = false; protected _onBeforeRenderObserver: Nullable> = null; + protected _elapsedLife: number = 0; + protected _lifeEnded: boolean = false; /** * Creates a SPS (Solid Particle System) object. * @param name (String) is the SPS name, this will be the underlying mesh name. @@ -1543,9 +1558,12 @@ export class SolidParticleSystem implements IDisposable { this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); this._onBeforeRenderObserver = null; } + this._lifeEnded = true; + this._elapsedLife = 0; if (this.mesh) { this.mesh.dispose(); } + this.onLifeTimeEndedObservable.clear(); this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; @@ -2005,15 +2023,60 @@ export class SolidParticleSystem implements IDisposable { */ public initParticles(): void {} - public start(): void { + /** + * Starts the SPS update loop + * @param lifetime optional lifetime override in milliseconds (0 means infinite) + * @param disposeOnEnd optional flag indicating if the SPS should dispose itself when the lifetime ends + */ + public start(lifetime?: number, disposeOnEnd?: boolean): void { this.buildMesh(); this.initParticles(); this.setParticles(); + + if (lifetime !== undefined) { + this.lifetime = lifetime; + } + if (disposeOnEnd !== undefined) { + this.disposeOnEnd = disposeOnEnd; + } + + this._elapsedLife = 0; + this._lifeEnded = false; + + if (this._onBeforeRenderObserver) { + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { this.setParticles(); + + if (this.lifetime > 0 && !this._lifeEnded) { + this._elapsedLife += this._scene.getEngine().getDeltaTime(); + if (this._elapsedLife >= this.lifetime) { + this._lifeEnded = true; + this.stop(this.disposeOnEnd); + this.onLifeTimeEndedObservable.notifyObservers(this); + } + } }); } + /** + * Stops the SPS update loop + * @param dispose if true, the SPS will be disposed after stopping + */ + public stop(dispose = false): void { + if (this._onBeforeRenderObserver) { + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + + if (dispose) { + this.dispose(); + } + } + /** * This function does nothing. It may be overwritten to recycle a particle. * The SPS doesn't call this function, you may have to call it by your own. From a0c36ab27e9b1d7e76208770fd0514e8422c9e65 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 21 Nov 2025 13:11:12 +0300 Subject: [PATCH 49/68] Enhance particle system functionality with new mesh and material blocks This commit introduces two new blocks, SPSMeshFileBlock and SPSNodeMaterialBlock, to facilitate the loading of external mesh assets and node materials in the particle system. Additionally, it refactors existing blocks to improve naming consistency, updating input and output connections for better clarity. The changes enhance the flexibility and usability of the particle system, allowing for more dynamic and visually rich particle effects. --- .../Blocks/SolidParticle/SPSCreateBlock.ts | 14 +-- .../Blocks/SolidParticle/SPSMeshFileBlock.ts | 102 +++++++++++++++ .../SolidParticle/SPSNodeMaterialBlock.ts | 116 ++++++++++++++++++ .../SolidParticle/SPSParticleConfigBlock.ts | 9 +- .../Blocks/SolidParticle/SPSSystemBlock.ts | 44 +++---- .../Node/Blocks/SolidParticle/index.ts | 2 + .../nodeParticleBlockConnectionPointTypes.ts | 8 +- .../Particles/Node/nodeParticleSystemSet.ts | 29 +++-- .../nodeParticleEditor/src/blockTools.ts | 7 -- 9 files changed, 276 insertions(+), 55 deletions(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshFileBlock.ts create mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index b35ca889773..21e6055e8ac 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -19,8 +19,8 @@ export class SPSCreateBlock extends NodeParticleBlock { public constructor(name: string) { super(name); - this.registerInput(`particleConfig-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); - this.registerOutput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + this.registerInput(`config-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticleConfig); + this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); this._manageExtendedInputs(0); } @@ -33,7 +33,7 @@ export class SPSCreateBlock extends NodeParticleBlock { private _extend() { this._entryCount++; - this.registerInput(`particleConfig-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle, true); + this.registerInput(`config-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticleConfig, true); this._manageExtendedInputs(this._entryCount - 1); } @@ -41,7 +41,7 @@ export class SPSCreateBlock extends NodeParticleBlock { if (this._entryCount > 1) { this._unmanageExtendedInputs(this._entryCount - 1); this._entryCount--; - this.unregisterInput(`particleConfig-${this._entryCount}`); + this.unregisterInput(`config-${this._entryCount}`); } } @@ -80,11 +80,11 @@ export class SPSCreateBlock extends NodeParticleBlock { } } - public get particleConfig(): NodeParticleConnectionPoint { + public get config(): NodeParticleConnectionPoint { return this._inputs[this._entryCount - 1]; } - public get solidParticleSystem(): NodeParticleConnectionPoint { + public get solidParticle(): NodeParticleConnectionPoint { return this._outputs[0]; } @@ -193,7 +193,7 @@ export class SPSCreateBlock extends NodeParticleBlock { return particle; }; - this.solidParticleSystem._storedValue = sps; + this.solidParticle._storedValue = sps; } public override serialize(): any { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshFileBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshFileBlock.ts new file mode 100644 index 00000000000..e237010f9e3 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshFileBlock.ts @@ -0,0 +1,102 @@ +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import type { Nullable } from "core/types"; +import type { Mesh } from "core/Meshes/mesh"; +import { ImportMeshAsync, SceneLoader } from "core/Loading/sceneLoader"; +import { Tools } from "core/Misc/tools"; +import { RegisterClass } from "../../../../Misc/typeStore"; + +/** + * Block used to load a mesh asset for SPS + */ +export class SPSMeshFileBlock extends NodeParticleBlock { + @editableInPropertyPage("Mesh URL", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + }) + public meshUrl = ""; + + @editableInPropertyPage("Mesh name (optional)", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + }) + public meshName = ""; + + private _loadedMesh: Nullable = null; + private _isLoading = false; + + public constructor(name: string) { + super(name); + this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + } + + public override getClassName() { + return "SPSMeshFileBlock"; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + if (this._loadedMesh && !this._loadedMesh.isDisposed()) { + this.mesh._storedValue = this._loadedMesh; + return; + } + + this.mesh._storedValue = null; + + if (!state.scene || this._isLoading || !this.meshUrl) { + return; + } + + this._isLoading = true; + + const fileName = Tools.GetFilename(this.meshUrl); + const rootUrl = this.meshUrl.substring(0, this.meshUrl.length - fileName.length); + + // TO-DO import mesh + + // ImportMeshAsync(this.meshUrl, state.scene) + // .then((result) => { + // let mesh = result.meshes.find((m) => (this.meshName ? m.name === this.meshName : m.name !== "__root__")); + // if (!mesh && result.meshes.length) { + // mesh = result.meshes[0]; + // } + // if (mesh) { + // mesh.isVisible = false; + // this._loadedMesh = mesh as Mesh; + // this.mesh._storedValue = this._loadedMesh; + // this.onValueChangedObservable.notifyObservers(this); + // } + // }) + // .catch(() => { + // // silently fail + // }) + // .finally(() => { + // this._isLoading = false; + // }); + } + + public override dispose() { + this._loadedMesh?.dispose(); + this._loadedMesh = null; + super.dispose(); + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.meshUrl = this.meshUrl; + serializationObject.meshName = this.meshName; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.meshUrl = serializationObject.meshUrl || ""; + this.meshName = serializationObject.meshName || ""; + } +} + +RegisterClass("BABYLON.SPSMeshFileBlock", SPSMeshFileBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts new file mode 100644 index 00000000000..ec7399ad59b --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts @@ -0,0 +1,116 @@ +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { Tools } from "core/Misc/tools"; +import { NodeMaterial } from "core/Materials/Node/nodeMaterial"; +import type { Nullable } from "core/types"; +import { Texture } from "core/Materials/Textures/texture"; +import { RegisterClass } from "../../../../Misc/typeStore"; + +/** + * Block used to load a node material for SPS + */ +export class SPSNodeMaterialBlock extends NodeParticleBlock { + @editableInPropertyPage("Shader URL", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + }) + public shaderUrl = ""; + + @editableInPropertyPage("Texture URL", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + }) + public textureUrl = ""; + + @editableInPropertyPage("Texture Block Name", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + }) + public textureBlockName = ""; + + private _nodeMaterial: Nullable = null; + private _isLoading = false; + + public constructor(name: string) { + super(name); + this.registerOutput("material", NodeParticleBlockConnectionPointTypes.Material); + } + + public override getClassName() { + return "SPSNodeMaterialBlock"; + } + + public get material(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + if (this._nodeMaterial) { + this.material._storedValue = this._nodeMaterial; + return; + } + + this.material._storedValue = null; + + if (!this.shaderUrl || this._isLoading) { + return; + } + + // TO-DO load node material + + // this._isLoading = true; + + // Tools.LoadFile( + // this.shaderUrl, + // (data) => { + // try { + // const json = JSON.parse(data as string); + // const nodeMaterial = NodeMaterial.Parse(json, state.scene); + // nodeMaterial.build(false); + // if (this.textureUrl && this.textureBlockName) { + // const block = nodeMaterial.getBlockByName(this.textureBlockName) as any; + // if (block && block.texture !== undefined) { + // block.texture = new Texture(this.textureUrl, state.scene); + // } + // } + // this._nodeMaterial = nodeMaterial; + // this.material._storedValue = nodeMaterial; + // this.onValueChangedObservable.notifyObservers(this); + // } catch (e) { + // // ignore parse errors + // } finally { + // this._isLoading = false; + // } + // }, + // undefined, + // undefined, + // false, + // () => { + // this._isLoading = false; + // } + // ); + } + + public override dispose() { + this._nodeMaterial?.dispose(); + this._nodeMaterial = null; + super.dispose(); + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.shaderUrl = this.shaderUrl; + serializationObject.textureUrl = this.textureUrl; + serializationObject.textureBlockName = this.textureBlockName; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.shaderUrl = serializationObject.shaderUrl || ""; + this.textureUrl = serializationObject.textureUrl || ""; + this.textureBlockName = serializationObject.textureBlockName || ""; + } +} + +RegisterClass("BABYLON.SPSNodeMaterialBlock", SPSNodeMaterialBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts index 20ebcc0ed90..94f3428ab92 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -13,14 +13,13 @@ import type { ISpsParticleConfigData } from "./ISPSData"; export class SPSParticleConfigBlock extends NodeParticleBlock { public constructor(name: string) { super(name); - - this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); + this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); - this.registerOutput("particleConfig", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("config", NodeParticleBlockConnectionPointTypes.SolidParticleConfig); } public override getClassName() { @@ -47,7 +46,7 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { return this._inputs[4]; } - public get particleConfig(): NodeParticleConnectionPoint { + public get config(): NodeParticleConnectionPoint { return this._outputs[0]; } @@ -67,7 +66,7 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { updateBlock, }; - this.particleConfig._storedValue = particleConfig; + this.config._storedValue = particleConfig; } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index b5727b5f852..6f2437a5ce0 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -20,12 +20,6 @@ export class SPSSystemBlock extends NodeParticleBlock { }) public billboard = false; - @editableInPropertyPage("Lifetime (ms)", PropertyTypeForEdition.Float, "ADVANCED", { - embedded: true, - min: 0, - }) - public lifetime = 0; - @editableInPropertyPage("Dispose on end", PropertyTypeForEdition.Boolean, "ADVANCED", { embedded: true, }) @@ -36,18 +30,23 @@ export class SPSSystemBlock extends NodeParticleBlock { public constructor(name: string) { super(name); this._isSystem = true; - this.registerInput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); - this.registerOutput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + this.registerInput("lifeTime", NodeParticleBlockConnectionPointTypes.Float, true, 0); + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System); } public override getClassName() { return "SPSSystemBlock"; } - public get solidParticleSystem(): NodeParticleConnectionPoint { + public get lifeTime(): NodeParticleConnectionPoint { return this._inputs[0]; } + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + public get system(): NodeParticleConnectionPoint { return this._outputs[0]; } @@ -57,27 +56,28 @@ export class SPSSystemBlock extends NodeParticleBlock { this.build(state); - const solidParticleSystem = this.solidParticleSystem.getConnectedValue(state) as SolidParticleSystem; + const solidParticle = this.solidParticle.getConnectedValue(state) as SolidParticleSystem; - if (!solidParticleSystem) { + if (!solidParticle) { throw new Error("No SolidParticleSystem connected to SPSSystemBlock"); } - solidParticleSystem.billboard = this.billboard; - solidParticleSystem.name = this.name; - solidParticleSystem.lifetime = this.lifetime; - solidParticleSystem.disposeOnEnd = this.disposeOnEnd; - - this.onDisposeObservable.addOnce(() => { - solidParticleSystem.dispose(); - }); - return solidParticleSystem; + solidParticle.billboard = this.billboard; + solidParticle.name = this.name; + if (this.lifeTime.isConnected) { + const connectedLifetime = this.lifeTime.getConnectedValue(state) as number; + solidParticle.lifetime = connectedLifetime ?? 0; + } else { + solidParticle.lifetime = this.lifeTime.value; + } + solidParticle.disposeOnEnd = this.disposeOnEnd; + return solidParticle; } public override serialize(): any { const serializationObject = super.serialize(); serializationObject.billboard = this.billboard; - serializationObject.lifetime = this.lifetime; + serializationObject.lifeTime = this.lifeTime.value; serializationObject.disposeOnEnd = this.disposeOnEnd; return serializationObject; } @@ -85,7 +85,7 @@ export class SPSSystemBlock extends NodeParticleBlock { public override _deserialize(serializationObject: any) { super._deserialize(serializationObject); this.billboard = !!serializationObject.billboard; - this.lifetime = serializationObject.lifetime ?? 0; + this.lifeTime.value = serializationObject.lifeTime ?? 0; this.disposeOnEnd = !!serializationObject.disposeOnEnd; } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index c20bce3114f..72e16177396 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -1,6 +1,7 @@ export * from "./ISPSData"; export * from "./SPSMeshShapeType"; export * from "./SPSMeshSourceBlock"; +export * from "./SPSMeshFileBlock"; export * from "./SPSParticleConfigBlock"; export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; @@ -8,3 +9,4 @@ export * from "./SPSInitBlock"; export * from "./SPSCreateBlock"; export * from "./SPSParticlePropsSetBlock"; export * from "./SPSParticlePropsGetBlock"; +export * from "./SPSNodeMaterialBlock"; diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts index 88d31930cd9..ef737a5a955 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts @@ -28,10 +28,10 @@ export enum NodeParticleBlockConnectionPointTypes { Color4Gradient = 0x0800, /** System */ System = 0x1000, - /** SPS - Solid Particle System */ - SolidParticleSystem = 0x2000, - /** SolidParticle */ - SolidParticle = 0x4000, + /** Solid Particle */ + SolidParticle = 0x2000, + /** Solid Particle Config */ + SolidParticleConfig = 0x4000, /** Mesh */ Mesh = 0x8000, /** Material */ diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 41053b26537..9e64f9f7f1e 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -33,6 +33,8 @@ import { SPSUpdateBlock, SpsParticlePropsSetBlock, SpsParticlePropsGetBlock, + SPSMeshFileBlock, + SPSNodeMaterialBlock, } from "./Blocks"; import { ParticleSystem } from "core/Particles/particleSystem"; import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; @@ -366,11 +368,11 @@ export class NodeParticleSystemSet { spsSystem.billboard = false; const spsCreateBlock = new SPSCreateBlock("Create Particles System"); - spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); spsCreateTetra.count.value = 2000; - spsCreateTetra.particleConfig.connectTo(spsCreateBlock.particleConfig); + spsCreateTetra.config.connectTo(spsCreateBlock.config); const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); meshSourceTetra.shapeType = SPSMeshShapeType.Box; @@ -557,29 +559,36 @@ export class NodeParticleSystemSet { const lifetimeMs = new ParticleInputBlock("Shockwave Lifetime (ms)"); lifetimeMs.value = 2500; - spsSystem.lifetime = lifetimeMs.value; - spsSystem.disposeOnEnd = true; - const minLifetimeMs = new ParticleInputBlock("Shockwave Min Lifetime (ms)"); minLifetimeMs.value = 1; const lifetimeSafe = new ParticleMathBlock("Shockwave Lifetime Safe"); lifetimeSafe.operation = ParticleMathBlockOperations.Max; lifetimeMs.output.connectTo(lifetimeSafe.left); minLifetimeMs.output.connectTo(lifetimeSafe.right); + lifetimeSafe.output.connectTo(spsSystem.lifeTime); + spsSystem.disposeOnEnd = true; const spsCreateBlock = new SPSCreateBlock("Create Shockwave SPS"); - spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); const shockwaveConfig = new SPSParticleConfigBlock("Shockwave Particle Config"); shockwaveConfig.count.value = 7; - shockwaveConfig.particleConfig.connectTo(spsCreateBlock.particleConfig); + shockwaveConfig.config.connectTo(spsCreateBlock.config); const shockwaveMesh = new SPSMeshSourceBlock("Shockwave Mesh Source"); - shockwaveMesh.shapeType = SPSMeshShapeType.Plane; - shockwaveMesh.size = 0.75; - shockwaveMesh.segments = 32; + shockwaveMesh.shapeType = SPSMeshShapeType.Custom; + const shockwaveMeshFile = new SPSMeshFileBlock("Shockwave Mesh File"); + shockwaveMeshFile.meshUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/gltf/shockwaveMesh.glb"; + shockwaveMeshFile.meshName = "shockwaveMesh"; + shockwaveMeshFile.mesh.connectTo(shockwaveMesh.customMesh); shockwaveMesh.mesh.connectTo(shockwaveConfig.mesh); + const shockwaveMaterial = new SPSNodeMaterialBlock("Shockwave Material"); + shockwaveMaterial.shaderUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/shaders/shockwaveParticleShader.json"; + shockwaveMaterial.textureUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/textures/electricityRing.png"; + shockwaveMaterial.textureBlockName = "particleTex"; + shockwaveMaterial.material.connectTo(shockwaveConfig.material); + const shockwaveInit = new SPSInitBlock("Initialize Shockwave Particles"); shockwaveInit.initData.connectTo(shockwaveConfig.initBlock); diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index e65718dc8e4..8ae55845a8d 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -474,9 +474,6 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.System: color = "#f20a2e"; break; - case NodeParticleBlockConnectionPointTypes.SolidParticleSystem: - color = "#8b4513"; - break; case NodeParticleBlockConnectionPointTypes.SolidParticle: color = "#2e8b57"; break; @@ -511,8 +508,6 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Color4; case "Matrix": return NodeParticleBlockConnectionPointTypes.Matrix; - case "SolidParticleSystem": - return NodeParticleBlockConnectionPointTypes.SolidParticleSystem; case "SolidParticle": return NodeParticleBlockConnectionPointTypes.SolidParticle; case "Mesh": @@ -542,8 +537,6 @@ export class BlockTools { return "Color4"; case NodeParticleBlockConnectionPointTypes.Matrix: return "Matrix"; - case NodeParticleBlockConnectionPointTypes.SolidParticleSystem: - return "SolidParticleSystem"; case NodeParticleBlockConnectionPointTypes.SolidParticle: return "SolidParticle"; case NodeParticleBlockConnectionPointTypes.Mesh: From e1d549e0bb01483e3c47ff199ffa5233e15096b7 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 21 Nov 2025 14:10:50 +0300 Subject: [PATCH 50/68] Refactor and enhance Solid Particle System blocks for improved functionality This commit removes deprecated blocks and introduces new features in the Solid Particle System. The SPSMeshFileBlock and SPSMeshShapeType have been removed, while the SPSMeshSourceBlock has been updated to support custom vertex data and remote mesh loading. Additionally, the SPSNodeMaterialBlock has been enhanced to manage custom materials more effectively. These changes streamline the particle system's architecture, improve usability, and provide greater flexibility for users in managing particle geometries and materials. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 9 +- .../Blocks/SolidParticle/SPSCreateBlock.ts | 24 +- .../Blocks/SolidParticle/SPSMeshFileBlock.ts | 102 - .../Blocks/SolidParticle/SPSMeshShapeType.ts | 12 - .../SolidParticle/SPSMeshSourceBlock.ts | 224 +- .../SolidParticle/SPSNodeMaterialBlock.ts | 159 +- .../SolidParticle/SPSParticleConfigBlock.ts | 6 +- .../Node/Blocks/SolidParticle/index.ts | 2 - .../Particles/Node/nodeParticleSystemSet.ts | 2342 ++++++++--------- .../components/nodeList/nodeListComponent.tsx | 2 +- .../spsMeshSourceNodePropertyComponent.tsx | 150 ++ .../spsNodeMaterialPropertyComponent.tsx | 73 + .../graphSystem/registerToPropertyLedger.ts | 5 +- 13 files changed, 1687 insertions(+), 1423 deletions(-) delete mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshFileBlock.ts delete mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts create mode 100644 packages/tools/nodeParticleEditor/src/graphSystem/properties/spsMeshSourceNodePropertyComponent.tsx create mode 100644 packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index 0285cbfe61e..825ee68c170 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -1,7 +1,12 @@ import type { Vector3 } from "core/Maths/math.vector"; import type { Color4 } from "core/Maths/math.color"; -import type { Mesh } from "core/Meshes/mesh"; import type { Material } from "core/Materials/material"; +import type { VertexData } from "core/Meshes/mesh.vertexData"; + +export interface ISpsMeshSourceData { + customMeshName?: string; + vertexData?: VertexData; +} /** * Interface for SPS update block data @@ -18,7 +23,7 @@ export interface ISpsUpdateData { * Interface for SPS create block data */ export interface ISpsParticleConfigData { - mesh: Mesh; + meshData: ISpsMeshSourceData | null; count: number; material?: Material; initBlock?: ISpsUpdateData; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index 21e6055e8ac..f1e32f67d25 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -7,6 +7,7 @@ import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnect import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; import type { ISpsParticleConfigData } from "./ISPSData"; +import { Mesh } from "core/Meshes/mesh"; import type { SolidParticle } from "../../../solidParticle"; import type { Observer } from "core/Misc/observable"; @@ -100,13 +101,24 @@ export class SPSCreateBlock extends NodeParticleBlock { const createBlocks = new Map(); for (let i = 0; i < this._inputs.length; i++) { const creatData = this._inputs[i].getConnectedValue(state) as ISpsParticleConfigData; - if (this._inputs[i].isConnected && creatData) { - if (creatData.mesh && creatData.count) { - const shapeId = sps.addShape(creatData.mesh, creatData.count); - createBlocks.set(shapeId, creatData); - creatData.mesh.isVisible = false; - } + if (!this._inputs[i].isConnected || !creatData || !creatData.meshData || !creatData.count) { + continue; + } + + if (!creatData.meshData.vertexData) { + continue; } + + const mesh = new Mesh(`${this.name}_shape_${i}`, state.scene); + creatData.meshData.vertexData.applyToMesh(mesh, true); + mesh.isVisible = false; + if (creatData.material) { + mesh.material = creatData.material; + } + + const shapeId = sps.addShape(mesh, creatData.count); + createBlocks.set(shapeId, creatData); + mesh.dispose(); } sps.initParticles = () => { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshFileBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshFileBlock.ts deleted file mode 100644 index e237010f9e3..00000000000 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshFileBlock.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { NodeParticleBlock } from "../../nodeParticleBlock"; -import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; -import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; -import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; -import type { Nullable } from "core/types"; -import type { Mesh } from "core/Meshes/mesh"; -import { ImportMeshAsync, SceneLoader } from "core/Loading/sceneLoader"; -import { Tools } from "core/Misc/tools"; -import { RegisterClass } from "../../../../Misc/typeStore"; - -/** - * Block used to load a mesh asset for SPS - */ -export class SPSMeshFileBlock extends NodeParticleBlock { - @editableInPropertyPage("Mesh URL", PropertyTypeForEdition.String, "PROPERTIES", { - embedded: false, - }) - public meshUrl = ""; - - @editableInPropertyPage("Mesh name (optional)", PropertyTypeForEdition.String, "PROPERTIES", { - embedded: false, - }) - public meshName = ""; - - private _loadedMesh: Nullable = null; - private _isLoading = false; - - public constructor(name: string) { - super(name); - this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); - } - - public override getClassName() { - return "SPSMeshFileBlock"; - } - - public get mesh(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - if (this._loadedMesh && !this._loadedMesh.isDisposed()) { - this.mesh._storedValue = this._loadedMesh; - return; - } - - this.mesh._storedValue = null; - - if (!state.scene || this._isLoading || !this.meshUrl) { - return; - } - - this._isLoading = true; - - const fileName = Tools.GetFilename(this.meshUrl); - const rootUrl = this.meshUrl.substring(0, this.meshUrl.length - fileName.length); - - // TO-DO import mesh - - // ImportMeshAsync(this.meshUrl, state.scene) - // .then((result) => { - // let mesh = result.meshes.find((m) => (this.meshName ? m.name === this.meshName : m.name !== "__root__")); - // if (!mesh && result.meshes.length) { - // mesh = result.meshes[0]; - // } - // if (mesh) { - // mesh.isVisible = false; - // this._loadedMesh = mesh as Mesh; - // this.mesh._storedValue = this._loadedMesh; - // this.onValueChangedObservable.notifyObservers(this); - // } - // }) - // .catch(() => { - // // silently fail - // }) - // .finally(() => { - // this._isLoading = false; - // }); - } - - public override dispose() { - this._loadedMesh?.dispose(); - this._loadedMesh = null; - super.dispose(); - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.meshUrl = this.meshUrl; - serializationObject.meshName = this.meshName; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.meshUrl = serializationObject.meshUrl || ""; - this.meshName = serializationObject.meshName || ""; - } -} - -RegisterClass("BABYLON.SPSMeshFileBlock", SPSMeshFileBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts deleted file mode 100644 index f52c709d7dd..00000000000 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshShapeType.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - -/** - * Mesh shape types for SPS - */ -export enum SPSMeshShapeType { - Box = 0, - Sphere = 1, - Cylinder = 2, - Plane = 3, - Custom = 4, -} diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts index ad87a5107f8..30a88f6fa43 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts @@ -5,50 +5,33 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; -import { CreateBox } from "core/Meshes/Builders/boxBuilder"; -import { CreateSphere } from "core/Meshes/Builders/sphereBuilder"; -import { CreateCylinder } from "core/Meshes/Builders/cylinderBuilder"; -import { CreatePlane } from "core/Meshes/Builders/planeBuilder"; -import { SPSMeshShapeType } from "./SPSMeshShapeType"; -import type { Mesh } from "core/Meshes/mesh"; +import { Mesh } from "core/Meshes/mesh"; +import { VertexData } from "core/Meshes/mesh.vertexData"; +import { Observable } from "core/Misc/observable"; +import type { Nullable } from "core/types"; +import { Tools } from "core/Misc/tools"; +import { ImportMeshAsync } from "core/Loading/sceneLoader"; +import type { ISpsMeshSourceData } from "./ISPSData"; /** * Block used to provide mesh source for SPS */ export class SPSMeshSourceBlock extends NodeParticleBlock { - private _mesh: Mesh | null = null; - private _disposeHandlerAdded = false; - - @editableInPropertyPage("Shape Type", PropertyTypeForEdition.List, "ADVANCED", { - notifiers: { rebuild: true }, - embedded: true, - options: [ - { label: "Box", value: SPSMeshShapeType.Box }, - { label: "Sphere", value: SPSMeshShapeType.Sphere }, - { label: "Cylinder", value: SPSMeshShapeType.Cylinder }, - { label: "Plane", value: SPSMeshShapeType.Plane }, - { label: "Custom", value: SPSMeshShapeType.Custom }, - ], - }) - public shapeType = SPSMeshShapeType.Box; - - @editableInPropertyPage("Size", PropertyTypeForEdition.Float, "ADVANCED", { - embedded: true, - min: 0.01, - }) - public size = 1; - - @editableInPropertyPage("Segments", PropertyTypeForEdition.Int, "ADVANCED", { - embedded: true, - min: 1, - }) - public segments = 16; + private _customVertexData: Nullable = null; + private _customMeshName = ""; + private _isRemoteMeshLoading = false; + + /** Gets an observable raised when the block data changes */ + public onValueChangedObservable = new Observable(); + + /** Optional remote mesh URL used to auto load geometry */ + public remoteMeshUrl = ""; + /** Optional mesh name filter when loading remote geometry */ + public remoteMeshName = ""; public constructor(name: string) { super(name); - this.registerInput("customMesh", NodeParticleBlockConnectionPointTypes.Mesh, true); this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); } @@ -56,76 +39,143 @@ export class SPSMeshSourceBlock extends NodeParticleBlock { return "SPSMeshSourceBlock"; } - public get customMesh(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - public get mesh(): NodeParticleConnectionPoint { return this._outputs[0]; } - public override _build(state: NodeParticleBuildState) { - if (this._mesh) { - this._mesh.dispose(); - this._mesh = null; - } - if (this.shapeType === SPSMeshShapeType.Custom) { - if (this.customMesh.isConnected) { - const customMesh = this.customMesh.getConnectedValue(state); - if (customMesh) { - this._mesh = customMesh; - } else { - this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - } - } else { - this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - } - } else { - switch (this.shapeType) { - case SPSMeshShapeType.Box: - this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - break; - case SPSMeshShapeType.Sphere: - this._mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); - break; - case SPSMeshShapeType.Cylinder: - this._mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); - break; - case SPSMeshShapeType.Plane: - this._mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); - break; - default: - this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); - break; - } + /** + * Gets whether a custom mesh is currently assigned + */ + public get hasCustomMesh(): boolean { + return !!this._customVertexData; + } + + /** + * Gets the friendly name of the assigned custom mesh + */ + public get customMeshName(): string { + return this._customMeshName; + } + + /** + * Assigns a mesh as custom geometry source + * @param mesh mesh providing geometry + */ + public setCustomMesh(mesh: Nullable) { + if (!mesh) { + this.clearCustomMesh(); + return; } - if (this._mesh) { - this._mesh.isVisible = false; + + this._customVertexData = VertexData.ExtractFromMesh(mesh, true, true); + this._customMeshName = mesh.name || ""; + this.remoteMeshUrl = ""; + this.remoteMeshName = ""; + this.onValueChangedObservable.notifyObservers(this); + } + + /** + * Assigns vertex data directly + * @param vertexData vertex data + * @param name friendly name + */ + public setCustomVertexData(vertexData: VertexData, name = "") { + this._customVertexData = vertexData; + this._customMeshName = name; + this.remoteMeshUrl = ""; + this.remoteMeshName = ""; + this.onValueChangedObservable.notifyObservers(this); + } + + /** + * Clears any assigned custom mesh data + */ + public clearCustomMesh() { + this._customVertexData = null; + this._customMeshName = ""; + this.remoteMeshUrl = ""; + this.remoteMeshName = ""; + this.onValueChangedObservable.notifyObservers(this); + } + + private _tryLoadRemoteMesh(state: NodeParticleBuildState) { + if (this._customVertexData || !this.remoteMeshUrl || this._isRemoteMeshLoading) { + return; } - if (!this._disposeHandlerAdded) { - this.onDisposeObservable.addOnce(() => { - this._mesh?.dispose(); - this._mesh = null; + this._isRemoteMeshLoading = true; + const fileName = Tools.GetFilename(this.remoteMeshUrl); + const rootUrl = this.remoteMeshUrl.substring(0, this.remoteMeshUrl.length - fileName.length); + + ImportMeshAsync(fileName, state.scene, { meshNames: "", rootUrl }) + .then((result) => { + let mesh = result.meshes.find((m) => (this.remoteMeshName ? m.name === this.remoteMeshName : !!m && m.name !== "__root__")); + if (!mesh && result.meshes.length) { + mesh = result.meshes[0]; + } + + if (mesh) { + this.setCustomMesh(mesh as Mesh); + this.onValueChangedObservable.notifyObservers(this); + } + + for (const loadedMesh of result.meshes) { + loadedMesh.dispose(); + } + for (const skeleton of result.skeletons) { + skeleton.dispose(); + } + for (const animationGroup of result.animationGroups) { + animationGroup.dispose(); + } + for (const particleSystem of result.particleSystems) { + particleSystem.dispose(); + } + }) + .catch(() => { + // Ignore load errors + }) + .finally(() => { + this._isRemoteMeshLoading = false; }); - this._disposeHandlerAdded = true; + } + + public override _build(state: NodeParticleBuildState) { + this._tryLoadRemoteMesh(state); + + if (!this._customVertexData) { + this.mesh._storedValue = null; + return; } - this.mesh._storedValue = this._mesh; + + const meshData: ISpsMeshSourceData = { + vertexData: this._customVertexData, + customMeshName: this._customMeshName, + }; + + this.mesh._storedValue = meshData; } public override serialize(): any { const serializationObject = super.serialize(); - serializationObject.shapeType = this.shapeType; - serializationObject.size = this.size; - serializationObject.segments = this.segments; + serializationObject.remoteMeshUrl = this.remoteMeshUrl; + serializationObject.remoteMeshName = this.remoteMeshName; + serializationObject.customMeshName = this._customMeshName; + if (this._customVertexData) { + serializationObject.customVertexData = this._customVertexData.serialize(); + } return serializationObject; } public override _deserialize(serializationObject: any) { super._deserialize(serializationObject); - this.shapeType = serializationObject.shapeType || SPSMeshShapeType.Box; - this.size = serializationObject.size || 1; - this.segments = serializationObject.segments || 16; + this.remoteMeshUrl = serializationObject.remoteMeshUrl ?? ""; + this.remoteMeshName = serializationObject.remoteMeshName ?? ""; + + if (serializationObject.customVertexData) { + this._customVertexData = VertexData.Parse(serializationObject.customVertexData); + this._customMeshName = serializationObject.customMeshName || ""; + } } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts index ec7399ad59b..66852628b99 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts @@ -8,6 +8,8 @@ import { NodeMaterial } from "core/Materials/Node/nodeMaterial"; import type { Nullable } from "core/types"; import { Texture } from "core/Materials/Textures/texture"; import { RegisterClass } from "../../../../Misc/typeStore"; +import { Observable } from "core/Misc/observable"; +import type { Scene } from "core/scene"; /** * Block used to load a node material for SPS @@ -29,10 +31,14 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { public textureBlockName = ""; private _nodeMaterial: Nullable = null; + private _serializedMaterial: Nullable = null; + private _customMaterialName = ""; + private _textureInstance: Nullable = null; private _isLoading = false; public constructor(name: string) { super(name); + this.registerInput("texture", NodeParticleBlockConnectionPointTypes.Texture, true); this.registerOutput("material", NodeParticleBlockConnectionPointTypes.Material); } @@ -40,60 +46,88 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { return "SPSNodeMaterialBlock"; } + /** Raised when material data changes */ + public onValueChangedObservable = new Observable(); + + public get texture(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + public get material(): NodeParticleConnectionPoint { return this._outputs[0]; } + public get hasCustomMaterial(): boolean { + return !!this._serializedMaterial; + } + + public get customMaterialName(): string { + return this._customMaterialName; + } + + public setSerializedMaterial(serializedData: string, name?: string) { + this._serializedMaterial = serializedData; + this._customMaterialName = name || "Custom Node Material"; + this.shaderUrl = ""; + this._disposeMaterial(); + this.onValueChangedObservable.notifyObservers(this); + } + + public clearMaterial() { + this._serializedMaterial = null; + this._customMaterialName = ""; + this._disposeMaterial(); + this.onValueChangedObservable.notifyObservers(this); + } + public override _build(state: NodeParticleBuildState) { if (this._nodeMaterial) { + this._applyTexture(state, this._nodeMaterial); this.material._storedValue = this._nodeMaterial; return; } this.material._storedValue = null; + if (this._serializedMaterial) { + if (this._instantiateMaterial(this._serializedMaterial, state.scene)) { + this._applyTexture(state, this._nodeMaterial!); + this.material._storedValue = this._nodeMaterial; + } + return; + } + if (!this.shaderUrl || this._isLoading) { return; } - // TO-DO load node material - - // this._isLoading = true; - - // Tools.LoadFile( - // this.shaderUrl, - // (data) => { - // try { - // const json = JSON.parse(data as string); - // const nodeMaterial = NodeMaterial.Parse(json, state.scene); - // nodeMaterial.build(false); - // if (this.textureUrl && this.textureBlockName) { - // const block = nodeMaterial.getBlockByName(this.textureBlockName) as any; - // if (block && block.texture !== undefined) { - // block.texture = new Texture(this.textureUrl, state.scene); - // } - // } - // this._nodeMaterial = nodeMaterial; - // this.material._storedValue = nodeMaterial; - // this.onValueChangedObservable.notifyObservers(this); - // } catch (e) { - // // ignore parse errors - // } finally { - // this._isLoading = false; - // } - // }, - // undefined, - // undefined, - // false, - // () => { - // this._isLoading = false; - // } - // ); + this._isLoading = true; + const scene = state.scene; + Tools.LoadFile( + this.shaderUrl, + (data) => { + try { + this._serializedMaterial = data as string; + this._customMaterialName = this.shaderUrl; + this._instantiateMaterial(this._serializedMaterial, scene); + this.onValueChangedObservable.notifyObservers(this); + } finally { + this._isLoading = false; + } + }, + undefined, + undefined, + false, + () => { + this._isLoading = false; + } + ); } public override dispose() { - this._nodeMaterial?.dispose(); - this._nodeMaterial = null; + this._disposeMaterial(); + this._textureInstance?.dispose(); + this._textureInstance = null; super.dispose(); } @@ -102,6 +136,8 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { serializationObject.shaderUrl = this.shaderUrl; serializationObject.textureUrl = this.textureUrl; serializationObject.textureBlockName = this.textureBlockName; + serializationObject.serializedMaterial = this._serializedMaterial; + serializationObject.customMaterialName = this._customMaterialName; return serializationObject; } @@ -110,6 +146,59 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { this.shaderUrl = serializationObject.shaderUrl || ""; this.textureUrl = serializationObject.textureUrl || ""; this.textureBlockName = serializationObject.textureBlockName || ""; + this._serializedMaterial = serializationObject.serializedMaterial || null; + this._customMaterialName = serializationObject.customMaterialName || ""; + this._disposeMaterial(); + } + + private _disposeMaterial() { + this._nodeMaterial?.dispose(); + this._nodeMaterial = null; + } + + private _instantiateMaterial(serializedData: string, scene: Scene): boolean { + try { + const json = JSON.parse(serializedData); + const nodeMaterial = NodeMaterial.Parse(json, scene); + nodeMaterial.build(false); + this._disposeMaterial(); + this._nodeMaterial = nodeMaterial; + return true; + } catch { + this._nodeMaterial = null; + } + return false; + } + + private _applyTexture(state: NodeParticleBuildState, nodeMaterial: NodeMaterial) { + if (!this.textureBlockName) { + return; + } + + const block = nodeMaterial.getBlockByName(this.textureBlockName) as { texture?: Texture | null }; + if (!block || block.texture === undefined) { + return; + } + + if (this.texture.isConnected) { + const connectedTexture = this.texture.getConnectedValue(state) as Texture; + if (connectedTexture) { + block.texture = connectedTexture; + return; + } + } + + if (!this.textureUrl) { + block.texture = null; + return; + } + + if (!this._textureInstance || this._textureInstance.url !== this.textureUrl) { + this._textureInstance?.dispose(); + this._textureInstance = new Texture(this.textureUrl, state.scene); + } + + block.texture = this._textureInstance; } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts index 94f3428ab92..ae513debca0 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -13,8 +13,8 @@ import type { ISpsParticleConfigData } from "./ISPSData"; export class SPSParticleConfigBlock extends NodeParticleBlock { public constructor(name: string) { super(name); - this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); @@ -51,7 +51,7 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - const mesh = this.mesh.getConnectedValue(state); + const meshData = this.mesh.getConnectedValue(state); const count = (this.count.getConnectedValue(state) as number) || 1; const material = this.material.getConnectedValue(state); @@ -59,7 +59,7 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; const particleConfig: ISpsParticleConfigData = { - mesh, + meshData, count, material, initBlock, diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 72e16177396..a45e9499321 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -1,7 +1,5 @@ export * from "./ISPSData"; -export * from "./SPSMeshShapeType"; export * from "./SPSMeshSourceBlock"; -export * from "./SPSMeshFileBlock"; export * from "./SPSParticleConfigBlock"; export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 9e64f9f7f1e..6fafe5e89b2 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -1,1172 +1,1170 @@ -import { serialize } from "core/Misc/decorators"; -import { ParticleSystemSet } from "../particleSystemSet"; -import { SystemBlock } from "./Blocks/systemBlock"; -import type { Scene } from "core/scene"; -import { NodeParticleBuildState } from "./nodeParticleBuildState"; -import type { NodeParticleBlock } from "./nodeParticleBlock"; -import { SerializationHelper } from "core/Misc/decorators.serialization"; -import { Observable } from "core/Misc/observable"; -import { GetClass } from "core/Misc/typeStore"; -import { WebRequest } from "core/Misc/webRequest"; -import { Constants } from "core/Engines/constants"; -import { Tools } from "core/Misc/tools"; -import { AbstractEngine } from "core/Engines/abstractEngine"; -import { ParticleInputBlock } from "./Blocks/particleInputBlock"; -import { ParticleTextureSourceBlock } from "./Blocks/particleSourceTextureBlock"; -import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; -import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock"; -import { ParticleMathBlock, ParticleMathBlockOperations } from "./Blocks/particleMathBlock"; -import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTeleportOutBlock"; -import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; -import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; -import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; -import type { Nullable } from "core/types"; -import { Color4 } from "core/Maths/math.color"; -import { Vector2, Vector3 } from "core/Maths/math.vector"; -import { - SPSParticleConfigBlock, - SPSInitBlock, - SPSMeshShapeType, - SPSMeshSourceBlock, - SPSSystemBlock, - SPSCreateBlock, - SPSUpdateBlock, - SpsParticlePropsSetBlock, - SpsParticlePropsGetBlock, - SPSMeshFileBlock, - SPSNodeMaterialBlock, -} from "./Blocks"; -import { ParticleSystem } from "core/Particles/particleSystem"; -import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; -import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; -import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; -import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; -import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; - -// declare NODEPARTICLEEDITOR namespace for compilation issue -declare let NODEPARTICLEEDITOR: any; -declare let BABYLON: any; - -/** - * Interface used to configure the node particle editor - */ -export interface INodeParticleEditorOptions { - /** Define the URL to load node editor script from */ - editorURL?: string; - /** Additional configuration for the NPE */ - nodeEditorConfig?: { - backgroundColor?: Color4; - }; -} - -/** - * Defines a set of particle systems defined as a node graph. - * NPE: #K6F1ZB#1 - * PG: #ZT509U#1 - */ -export class NodeParticleSystemSet { - private _systemBlocks: (SystemBlock | SPSSystemBlock)[] = []; - private _buildId: number = 0; - - /** Define the Url to load node editor script */ - public static EditorURL = `${Tools._DefaultCdnUrl}/v${AbstractEngine.Version}/nodeParticleEditor/babylon.nodeParticleEditor.js`; - - /** Define the Url to load snippets */ - public static SnippetUrl = Constants.SnippetUrl; - - /** - * Snippet ID if the material was created from the snippet server - */ - public snippetId: string; - - /** - * Gets an array of blocks that needs to be serialized even if they are not yet connected - */ - public attachedBlocks: NodeParticleBlock[] = []; - - /** - * Gets or sets data used by visual editor - * @see https://npe.babylonjs.com - */ - public editorData: any = null; - - /** - * Observable raised when the particle set is built - */ - public onBuildObservable = new Observable(); - - /** - * The name of the set - */ - @serialize() - public name: string; - - /** - * A free comment about the set - */ - @serialize("comment") - public comment: string; - - /** - * Gets the system blocks - */ - public get systemBlocks(): (SystemBlock | SPSSystemBlock)[] { - return this._systemBlocks; - } - - /** - * Gets the list of input blocks attached to this material - * @returns an array of InputBlocks - */ - public get inputBlocks() { - const blocks: ParticleInputBlock[] = []; - for (const block of this.attachedBlocks) { - if (block.isInput) { - blocks.push(block as ParticleInputBlock); - } - } - - return blocks; - } - - /** - * Get a block by its name - * @param name defines the name of the block to retrieve - * @returns the required block or null if not found - */ - public getBlockByName(name: string) { - let result = null; - for (const block of this.attachedBlocks) { - if (block.name === name) { - if (!result) { - result = block; - } else { - Tools.Warn("More than one block was found with the name `" + name + "`"); - return result; - } - } - } - - return result; - } - - /** - * Get a block using a predicate - * @param predicate defines the predicate used to find the good candidate - * @returns the required block or null if not found - */ - public getBlockByPredicate(predicate: (block: NodeParticleBlock) => boolean) { - for (const block of this.attachedBlocks) { - if (predicate(block)) { - return block; - } - } - - return null; - } - - /** - * Get an input block using a predicate - * @param predicate defines the predicate used to find the good candidate - * @returns the required input block or null if not found - */ - public getInputBlockByPredicate(predicate: (block: ParticleInputBlock) => boolean): Nullable { - for (const block of this.attachedBlocks) { - if (block.isInput && predicate(block as ParticleInputBlock)) { - return block as ParticleInputBlock; - } - } - - return null; - } - /** - * Creates a new set - * @param name defines the name of the set - */ - public constructor(name: string) { - this.name = name; - } - - /** - * Gets the current class name of the node particle set e.g. "NodeParticleSystemSet" - * @returns the class name - */ - public getClassName(): string { - return "NodeParticleSystemSet"; - } - - private _initializeBlock(node: NodeParticleBlock, autoConfigure = true) { - if (this.attachedBlocks.indexOf(node) === -1) { - this.attachedBlocks.push(node); - } - - for (const input of node.inputs) { - const connectedPoint = input.connectedPoint; - if (connectedPoint) { - const block = connectedPoint.ownerBlock; - if (block !== node) { - this._initializeBlock(block, autoConfigure); - } - } - } - } - - private BJSNODEPARTICLEEDITOR = this._getGlobalNodeParticleEditor(); - - /** Get the editor from bundle or global - * @returns the global NPE - */ - private _getGlobalNodeParticleEditor(): any { - // UMD Global name detection from Webpack Bundle UMD Name. - if (typeof NODEPARTICLEEDITOR !== "undefined") { - return NODEPARTICLEEDITOR; - } - - // In case of module let's check the global emitted from the editor entry point. - if (typeof BABYLON !== "undefined" && typeof BABYLON.NodeParticleEditor !== "undefined") { - return BABYLON; - } - - return undefined; - } - - /** Creates the node editor window. - * @param additionalConfig Define the configuration of the editor - */ - private _createNodeParticleEditor(additionalConfig?: any) { - const nodeEditorConfig: any = { - nodeParticleSet: this, - ...additionalConfig, - }; - this.BJSNODEPARTICLEEDITOR.NodeParticleEditor.Show(nodeEditorConfig); - } - - /** - * Launch the node particle editor - * @param config Define the configuration of the editor - * @returns a promise fulfilled when the node editor is visible - */ - public async editAsync(config?: INodeParticleEditorOptions): Promise { - return await new Promise((resolve) => { - this.BJSNODEPARTICLEEDITOR = this.BJSNODEPARTICLEEDITOR || this._getGlobalNodeParticleEditor(); - if (typeof this.BJSNODEPARTICLEEDITOR == "undefined") { - const editorUrl = config && config.editorURL ? config.editorURL : NodeParticleSystemSet.EditorURL; - - // Load editor and add it to the DOM - Tools.LoadBabylonScript(editorUrl, () => { - this.BJSNODEPARTICLEEDITOR = this.BJSNODEPARTICLEEDITOR || this._getGlobalNodeParticleEditor(); - this._createNodeParticleEditor(config?.nodeEditorConfig); - resolve(); - }); - } else { - // Otherwise creates the editor - this._createNodeParticleEditor(config?.nodeEditorConfig); - resolve(); - } - }); - } - - /** - * Builds the particle system set from the defined blocks. - * @param scene defines the hosting scene - * @param verbose defines whether to log detailed information during the build process (false by default) - * @returns a promise that resolves to the built particle system set - */ - public async buildAsync(scene: Scene, verbose = false): Promise { - return await new Promise((resolve) => { - const output = new ParticleSystemSet(); - - // Initialize all blocks - for (const block of this._systemBlocks) { - this._initializeBlock(block); - } - - // Build the blocks - for (const block of this.systemBlocks) { - const state = new NodeParticleBuildState(); - state.buildId = this._buildId++; - state.scene = scene; - state.verbose = verbose; - - const system = block.createSystem(state); - if (system instanceof ParticleSystem) { - system._source = this; - system._blockReference = block._internalId; - } - output.systems.push(system); - // Errors - state.emitErrors(); - } - - this.onBuildObservable.notifyObservers(this); - - resolve(output); - }); - } - - /** - * Clear the current node particle set - */ - public clear() { - this.attachedBlocks.length = 0; - this._systemBlocks.length = 0; - } - - /** - * Clear the current set and restore it to a default state - */ - public setToDefault() { - this.clear(); - - this.editorData = null; - - // Main system - const system = new SystemBlock("Particle system"); - - // Update position - const updatePositionBlock = new UpdatePositionBlock("Update position"); - updatePositionBlock.output.connectTo(system.particle); - - // Contextual inputs - const positionBlock = new ParticleInputBlock("Position"); - positionBlock.contextualValue = NodeParticleContextualSources.Position; - const directionBlock = new ParticleInputBlock("Scaled direction"); - directionBlock.contextualValue = NodeParticleContextualSources.ScaledDirection; - - // Add - const addBlock = new ParticleMathBlock("Add"); - addBlock.operation = ParticleMathBlockOperations.Add; - positionBlock.output.connectTo(addBlock.left); - directionBlock.output.connectTo(addBlock.right); - addBlock.output.connectTo(updatePositionBlock.position); - - // Create particle - const createParticleBlock = new CreateParticleBlock("Create particle"); - - // Shape - const emitterShape = new BoxShapeBlock("Box shape"); - createParticleBlock.particle.connectTo(emitterShape.particle); - emitterShape.output.connectTo(updatePositionBlock.particle); - - // Texture - const textureBlock = new ParticleTextureSourceBlock("Texture"); - textureBlock.texture.connectTo(system.texture); - textureBlock.url = Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"); - - this._systemBlocks.push(system); - } - - public setToDefaultSps() { - this.createShockwaveSps(); - } - - public createDefaultSps() { - this.clear(); - this.editorData = null; - - const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.billboard = false; - - const spsCreateBlock = new SPSCreateBlock("Create Particles System"); - spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); - - const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); - spsCreateTetra.count.value = 2000; - spsCreateTetra.config.connectTo(spsCreateBlock.config); - - const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); - meshSourceTetra.shapeType = SPSMeshShapeType.Box; - meshSourceTetra.size = 0.1; - meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); - - const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); - spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); - - const randomXZMin = new ParticleInputBlock("Random XZ Min"); - randomXZMin.value = new Vector2(-10, -10); - const randomXZMax = new ParticleInputBlock("Random XZ Max"); - randomXZMax.value = new Vector2(10, 10); - const randomXZ = new ParticleRandomBlock("Random XZ"); - randomXZ.lockMode = ParticleRandomBlockLocks.PerParticle; - randomXZMin.output.connectTo(randomXZ.min); - randomXZMax.output.connectTo(randomXZ.max); - - const randomAngleMin = new ParticleInputBlock("Random Angle Min"); - randomAngleMin.value = -Math.PI; - const randomAngleMax = new ParticleInputBlock("Random Angle Max"); - randomAngleMax.value = Math.PI; - const randomAngle = new ParticleRandomBlock("Random Angle"); - randomAngle.lockMode = ParticleRandomBlockLocks.PerParticle; - randomAngleMin.output.connectTo(randomAngle.min); - randomAngleMax.output.connectTo(randomAngle.max); - - const randomRangeMin = new ParticleInputBlock("Random Range Min"); - randomRangeMin.value = 1; - const randomRangeMax = new ParticleInputBlock("Random Range Max"); - randomRangeMax.value = 5; - const randomRange = new ParticleRandomBlock("Random Range"); - randomRange.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRangeMin.output.connectTo(randomRange.min); - randomRangeMax.output.connectTo(randomRange.max); - - const one = new ParticleInputBlock("One"); - one.value = 1; - const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); - cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; - // Store angle in props so we can reuse during update - const setAnglePropInit = new SpsParticlePropsSetBlock("Set Angle Prop Init"); - setAnglePropInit.propertyName = "angle"; - randomAngle.output.connectTo(setAnglePropInit.value); - setAnglePropInit.output.connectTo(cosAngle.input); - const addOne = new ParticleMathBlock("Add One"); - addOne.operation = ParticleMathBlockOperations.Add; - one.output.connectTo(addOne.left); - cosAngle.output.connectTo(addOne.right); - const multiplyRange = new ParticleMathBlock("Multiply Range"); - multiplyRange.operation = ParticleMathBlockOperations.Multiply; - const setRangePropInit = new SpsParticlePropsSetBlock("Set Range Prop Init"); - setRangePropInit.propertyName = "range"; - randomRange.output.connectTo(setRangePropInit.value); - setRangePropInit.output.connectTo(multiplyRange.left); - addOne.output.connectTo(multiplyRange.right); - - const extractXZ = new ParticleConverterBlock("Extract XZ"); - randomXZ.output.connectTo(extractXZ.xyIn); - const positionConverter = new ParticleConverterBlock("Position Converter"); - extractXZ.xOut.connectTo(positionConverter.xIn); - multiplyRange.output.connectTo(positionConverter.yIn); - extractXZ.yOut.connectTo(positionConverter.zIn); - positionConverter.xyzOut.connectTo(spsInitTetra.position); - - const randomRotMin = new ParticleInputBlock("Random Rot Min"); - randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); - const randomRotMax = new ParticleInputBlock("Random Rot Max"); - randomRotMax.value = new Vector3(Math.PI, Math.PI, Math.PI); - const randomRot = new ParticleRandomBlock("Random Rotation"); - randomRot.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotMin.output.connectTo(randomRot.min); - randomRotMax.output.connectTo(randomRot.max); - randomRot.output.connectTo(spsInitTetra.rotation); - - const randomColorMin = new ParticleInputBlock("Random Color Min"); - randomColorMin.value = new Vector3(0, 0, 0); - const randomColorMax = new ParticleInputBlock("Random Color Max"); - randomColorMax.value = new Vector3(1, 1, 1); - const randomColorRGB = new ParticleRandomBlock("Random Color RGB"); - randomColorRGB.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorRGB.min); - randomColorMax.output.connectTo(randomColorRGB.max); - const colorAlpha = new ParticleInputBlock("Color Alpha"); - colorAlpha.value = 1; - const colorConverter = new ParticleConverterBlock("Color Converter"); - randomColorRGB.output.connectTo(colorConverter.xyzIn); - colorAlpha.output.connectTo(colorConverter.wIn); - colorConverter.colorOut.connectTo(spsInitTetra.color); - - // Create update block - const spsUpdateTetra = new SPSUpdateBlock("Update Tetrahedron Particles"); - spsUpdateTetra.updateData.connectTo(spsCreateTetra.updateBlock); - - // Get current position (X, Z stay the same, Y updates) - const currentPosition = new ParticleInputBlock("Current Position"); - currentPosition.contextualValue = NodeParticleContextualSources.Position; - - // Extract X and Z from current position - const extractPosition = new ParticleConverterBlock("Extract Position"); - currentPosition.output.connectTo(extractPosition.xyzIn); - - // Retrieve stored properties - const getAngleProp = new SpsParticlePropsGetBlock("Get Angle Prop"); - getAngleProp.propertyName = "angle"; - getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getRangeProp = new SpsParticlePropsGetBlock("Get Range Prop"); - getRangeProp.propertyName = "range"; - getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; - - // Accumulate angle using delta time to avoid relying on absolute frame id - const deltaBlock = new ParticleInputBlock("Delta Time"); - deltaBlock.systemSource = NodeParticleSystemSources.Delta; - - const milliToSecond = new ParticleInputBlock("Milli To Second"); - milliToSecond.value = 0.001; - - const deltaSeconds = new ParticleMathBlock("Delta Seconds"); - deltaSeconds.operation = ParticleMathBlockOperations.Multiply; - deltaBlock.output.connectTo(deltaSeconds.left); - milliToSecond.output.connectTo(deltaSeconds.right); - - const targetFps = new ParticleInputBlock("Target FPS"); - targetFps.value = 60; - - const normalizedDelta = new ParticleMathBlock("Normalized Delta"); - normalizedDelta.operation = ParticleMathBlockOperations.Multiply; - deltaSeconds.output.connectTo(normalizedDelta.left); - targetFps.output.connectTo(normalizedDelta.right); - - const speedPerFrame = new ParticleInputBlock("Speed Per Frame"); - speedPerFrame.value = Math.PI / 100; - - const scaledIncrement = new ParticleMathBlock("Scaled Increment"); - scaledIncrement.operation = ParticleMathBlockOperations.Multiply; - speedPerFrame.output.connectTo(scaledIncrement.left); - normalizedDelta.output.connectTo(scaledIncrement.right); - - const accumulateAngle = new ParticleMathBlock("Accumulate Angle"); - accumulateAngle.operation = ParticleMathBlockOperations.Add; - getAngleProp.output.connectTo(accumulateAngle.left); - scaledIncrement.output.connectTo(accumulateAngle.right); - - const setAnglePropUpdate = new SpsParticlePropsSetBlock("Set Angle Prop Update"); - setAnglePropUpdate.propertyName = "angle"; - setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; - accumulateAngle.output.connectTo(setAnglePropUpdate.value); - - // Calculate new Y position: range * (1 + cos(angle)) - const oneUpdate = new ParticleInputBlock("One Update"); - oneUpdate.value = 1; - const cosUpdatedAngle = new ParticleTrigonometryBlock("Cos Updated Angle"); - cosUpdatedAngle.operation = ParticleTrigonometryBlockOperations.Cos; - setAnglePropUpdate.output.connectTo(cosUpdatedAngle.input); - const addOneUpdate = new ParticleMathBlock("Add One Update"); - addOneUpdate.operation = ParticleMathBlockOperations.Add; - oneUpdate.output.connectTo(addOneUpdate.left); - cosUpdatedAngle.output.connectTo(addOneUpdate.right); - const multiplyRangeUpdate = new ParticleMathBlock("Multiply Range Update"); - multiplyRangeUpdate.operation = ParticleMathBlockOperations.Multiply; - getRangeProp.output.connectTo(multiplyRangeUpdate.left); - addOneUpdate.output.connectTo(multiplyRangeUpdate.right); - - // Combine X (from current position), Y (new), Z (from current position) - const updatePositionConverter = new ParticleConverterBlock("Update Position Converter"); - extractPosition.xOut.connectTo(updatePositionConverter.xIn); - multiplyRangeUpdate.output.connectTo(updatePositionConverter.yIn); - extractPosition.zOut.connectTo(updatePositionConverter.zIn); - updatePositionConverter.xyzOut.connectTo(spsUpdateTetra.position); - - this._systemBlocks.push(spsSystem); - } - - /** - * Sets the current set to an SPS shockwave preset inspired by Patrick Ryan's createShockwave sample - */ - public createShockwaveSps() { - this.clear(); - this.editorData = null; - - const spsSystem = new SPSSystemBlock("Shockwave SPS System"); - spsSystem.billboard = false; - - const lifetimeMs = new ParticleInputBlock("Shockwave Lifetime (ms)"); - lifetimeMs.value = 2500; - const minLifetimeMs = new ParticleInputBlock("Shockwave Min Lifetime (ms)"); - minLifetimeMs.value = 1; - const lifetimeSafe = new ParticleMathBlock("Shockwave Lifetime Safe"); - lifetimeSafe.operation = ParticleMathBlockOperations.Max; - lifetimeMs.output.connectTo(lifetimeSafe.left); - minLifetimeMs.output.connectTo(lifetimeSafe.right); - lifetimeSafe.output.connectTo(spsSystem.lifeTime); - spsSystem.disposeOnEnd = true; - - const spsCreateBlock = new SPSCreateBlock("Create Shockwave SPS"); - spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); - - const shockwaveConfig = new SPSParticleConfigBlock("Shockwave Particle Config"); - shockwaveConfig.count.value = 7; - shockwaveConfig.config.connectTo(spsCreateBlock.config); - - const shockwaveMesh = new SPSMeshSourceBlock("Shockwave Mesh Source"); - shockwaveMesh.shapeType = SPSMeshShapeType.Custom; - const shockwaveMeshFile = new SPSMeshFileBlock("Shockwave Mesh File"); - shockwaveMeshFile.meshUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/gltf/shockwaveMesh.glb"; - shockwaveMeshFile.meshName = "shockwaveMesh"; - shockwaveMeshFile.mesh.connectTo(shockwaveMesh.customMesh); - shockwaveMesh.mesh.connectTo(shockwaveConfig.mesh); - - const shockwaveMaterial = new SPSNodeMaterialBlock("Shockwave Material"); - shockwaveMaterial.shaderUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/shaders/shockwaveParticleShader.json"; - shockwaveMaterial.textureUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/textures/electricityRing.png"; - shockwaveMaterial.textureBlockName = "particleTex"; - shockwaveMaterial.material.connectTo(shockwaveConfig.material); - - const shockwaveInit = new SPSInitBlock("Initialize Shockwave Particles"); - shockwaveInit.initData.connectTo(shockwaveConfig.initBlock); - - const shockwaveUpdate = new SPSUpdateBlock("Update Shockwave Particles"); - shockwaveUpdate.updateData.connectTo(shockwaveConfig.updateBlock); - - const deltaBlock = new ParticleInputBlock("Shockwave Delta Time"); - deltaBlock.systemSource = NodeParticleSystemSources.Delta; - const milliToSecond = new ParticleInputBlock("Shockwave Milli To Second"); - milliToSecond.value = 0.001; - const deltaSeconds = new ParticleMathBlock("Shockwave Delta Seconds"); - deltaSeconds.operation = ParticleMathBlockOperations.Multiply; - deltaBlock.output.connectTo(deltaSeconds.left); - milliToSecond.output.connectTo(deltaSeconds.right); - const targetFps = new ParticleInputBlock("Shockwave Target FPS"); - targetFps.value = 60; - const normalizedDelta = new ParticleMathBlock("Shockwave Normalized Delta"); - normalizedDelta.operation = ParticleMathBlockOperations.Multiply; - deltaSeconds.output.connectTo(normalizedDelta.left); - targetFps.output.connectTo(normalizedDelta.right); - - const lifetimeSeconds = new ParticleMathBlock("Shockwave Lifetime Seconds"); - lifetimeSeconds.operation = ParticleMathBlockOperations.Multiply; - lifetimeSafe.output.connectTo(lifetimeSeconds.left); - milliToSecond.output.connectTo(lifetimeSeconds.right); - const framesPerLifetime = new ParticleMathBlock("Shockwave Frames Per Lifetime"); - framesPerLifetime.operation = ParticleMathBlockOperations.Multiply; - lifetimeSeconds.output.connectTo(framesPerLifetime.left); - targetFps.output.connectTo(framesPerLifetime.right); - - const origin = new ParticleInputBlock("Shockwave Origin"); - origin.value = new Vector3(0, 0.05, 0); - origin.output.connectTo(shockwaveInit.position); - - const shockwaveColor = new ParticleInputBlock("Shockwave Base Color"); - shockwaveColor.value = new Color4(0.33, 0.49, 0.88, 0.9); - shockwaveColor.output.connectTo(shockwaveInit.color); - - const zeroValue = new ParticleInputBlock("Shockwave Zero"); - zeroValue.value = 0; - - const radiusStart = new ParticleInputBlock("Shockwave Radius Start"); - radiusStart.value = 1; - const storeRadiusInit = new SpsParticlePropsSetBlock("Store Radius Init"); - storeRadiusInit.propertyName = "radius"; - storeRadiusInit.type = NodeParticleBlockConnectionPointTypes.Float; - radiusStart.output.connectTo(storeRadiusInit.value); - - const maxRadius = new ParticleInputBlock("Shockwave Max Radius"); - maxRadius.value = 4; - - const radiusRangeBlock = new ParticleMathBlock("Shockwave Radius Range"); - radiusRangeBlock.operation = ParticleMathBlockOperations.Subtract; - maxRadius.output.connectTo(radiusRangeBlock.left); - radiusStart.output.connectTo(radiusRangeBlock.right); - - const growthMultiplierMin = new ParticleInputBlock("Shockwave Growth Multiplier Min"); - growthMultiplierMin.value = 0.85; - const growthMultiplierMax = new ParticleInputBlock("Shockwave Growth Multiplier Max"); - growthMultiplierMax.value = 1.15; - const growthMultiplier = new ParticleRandomBlock("Shockwave Growth Multiplier"); - growthMultiplier.lockMode = ParticleRandomBlockLocks.OncePerParticle; - growthMultiplierMin.output.connectTo(growthMultiplier.min); - growthMultiplierMax.output.connectTo(growthMultiplier.max); - - const baseGrowthPerFrame = new ParticleMathBlock("Shockwave Base Growth Per Frame"); - baseGrowthPerFrame.operation = ParticleMathBlockOperations.Divide; - radiusRangeBlock.output.connectTo(baseGrowthPerFrame.left); - framesPerLifetime.output.connectTo(baseGrowthPerFrame.right); - - const growthPerFrame = new ParticleMathBlock("Shockwave Growth Per Frame"); - growthPerFrame.operation = ParticleMathBlockOperations.Multiply; - baseGrowthPerFrame.output.connectTo(growthPerFrame.left); - growthMultiplier.output.connectTo(growthPerFrame.right); - - const storeScaleStepInit = new SpsParticlePropsSetBlock("Store Scale Step Init"); - storeScaleStepInit.propertyName = "scaleStep"; - storeScaleStepInit.type = NodeParticleBlockConnectionPointTypes.Float; - growthPerFrame.output.connectTo(storeScaleStepInit.value); - - const initScaleConverter = new ParticleConverterBlock("Shockwave Init Scale Converter"); - storeRadiusInit.output.connectTo(initScaleConverter.xIn); - storeScaleStepInit.output.connectTo(initScaleConverter.yIn); - storeRadiusInit.output.connectTo(initScaleConverter.zIn); - initScaleConverter.xyzOut.connectTo(shockwaveInit.scaling); - - const rotationMin = new ParticleInputBlock("Shockwave Rotation Min"); - rotationMin.value = new Vector3(0, -Math.PI, 0); - const rotationMax = new ParticleInputBlock("Shockwave Rotation Max"); - rotationMax.value = new Vector3(0, Math.PI, 0); - const initialRotation = new ParticleRandomBlock("Shockwave Initial Rotation"); - initialRotation.lockMode = ParticleRandomBlockLocks.OncePerParticle; - rotationMin.output.connectTo(initialRotation.min); - rotationMax.output.connectTo(initialRotation.max); - - const rotationConverter = new ParticleConverterBlock("Shockwave Rotation Converter"); - initialRotation.output.connectTo(rotationConverter.xyzIn); - const storeRotationAngleInit = new SpsParticlePropsSetBlock("Store Rotation Angle Init"); - storeRotationAngleInit.propertyName = "rotationAngle"; - storeRotationAngleInit.type = NodeParticleBlockConnectionPointTypes.Float; - rotationConverter.yOut.connectTo(storeRotationAngleInit.value); - - const rotationCompose = new ParticleConverterBlock("Shockwave Rotation Compose"); - rotationConverter.xOut.connectTo(rotationCompose.xIn); - storeRotationAngleInit.output.connectTo(rotationCompose.yIn); - rotationConverter.zOut.connectTo(rotationCompose.zIn); - rotationCompose.xyzOut.connectTo(shockwaveInit.rotation); - - const rotationSpeedMin = new ParticleInputBlock("Shockwave Rotation Speed Min"); - rotationSpeedMin.value = -0.06; - const rotationSpeedMax = new ParticleInputBlock("Shockwave Rotation Speed Max"); - rotationSpeedMax.value = 0.06; - const rotationSpeedRandom = new ParticleRandomBlock("Shockwave Rotation Speed Random"); - rotationSpeedRandom.lockMode = ParticleRandomBlockLocks.OncePerParticle; - rotationSpeedMin.output.connectTo(rotationSpeedRandom.min); - rotationSpeedMax.output.connectTo(rotationSpeedRandom.max); - const storeRotationSpeed = new SpsParticlePropsSetBlock("Store Rotation Speed"); - storeRotationSpeed.propertyName = "rotationSpeed"; - storeRotationSpeed.type = NodeParticleBlockConnectionPointTypes.Float; - rotationSpeedRandom.output.connectTo(storeRotationSpeed.value); - - const rotationSpeedSink = new ParticleMathBlock("Shockwave Rotation Speed Sink"); - rotationSpeedSink.operation = ParticleMathBlockOperations.Multiply; - storeRotationSpeed.output.connectTo(rotationSpeedSink.left); - zeroValue.output.connectTo(rotationSpeedSink.right); - const rotationSpeedVelocity = new ParticleConverterBlock("Shockwave Rotation Speed Velocity"); - rotationSpeedSink.output.connectTo(rotationSpeedVelocity.xIn); - zeroValue.output.connectTo(rotationSpeedVelocity.yIn); - zeroValue.output.connectTo(rotationSpeedVelocity.zIn); - rotationSpeedVelocity.xyzOut.connectTo(shockwaveInit.velocity); - - const getRadiusProp = new SpsParticlePropsGetBlock("Get Radius Prop"); - getRadiusProp.propertyName = "radius"; - getRadiusProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getScaleStepProp = new SpsParticlePropsGetBlock("Get Scale Step Prop"); - getScaleStepProp.propertyName = "scaleStep"; - getScaleStepProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getRotationSpeedProp = new SpsParticlePropsGetBlock("Get Rotation Speed Prop"); - getRotationSpeedProp.propertyName = "rotationSpeed"; - getRotationSpeedProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getRotationAngleProp = new SpsParticlePropsGetBlock("Get Rotation Angle Prop"); - getRotationAngleProp.propertyName = "rotationAngle"; - getRotationAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const scaleStepDelta = new ParticleMathBlock("Shockwave Radius Delta"); - scaleStepDelta.operation = ParticleMathBlockOperations.Multiply; - getScaleStepProp.output.connectTo(scaleStepDelta.left); - normalizedDelta.output.connectTo(scaleStepDelta.right); - - const radiusIncrement = new ParticleMathBlock("Shockwave Radius Increment"); - radiusIncrement.operation = ParticleMathBlockOperations.Add; - getRadiusProp.output.connectTo(radiusIncrement.left); - scaleStepDelta.output.connectTo(radiusIncrement.right); - - const setRadiusPropUpdate = new SpsParticlePropsSetBlock("Set Radius Prop Update"); - setRadiusPropUpdate.propertyName = "radius"; - setRadiusPropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; - radiusIncrement.output.connectTo(setRadiusPropUpdate.value); - - const clampRadius = new ParticleMathBlock("Shockwave Clamp Radius"); - clampRadius.operation = ParticleMathBlockOperations.Min; - setRadiusPropUpdate.output.connectTo(clampRadius.left); - maxRadius.output.connectTo(clampRadius.right); - - const normalizedRadius = new ParticleMathBlock("Shockwave Normalized Radius"); - normalizedRadius.operation = ParticleMathBlockOperations.Divide; - clampRadius.output.connectTo(normalizedRadius.left); - maxRadius.output.connectTo(normalizedRadius.right); - - const normalizedMin = new ParticleMathBlock("Shockwave Normalized Min"); - normalizedMin.operation = ParticleMathBlockOperations.Max; - zeroValue.output.connectTo(normalizedMin.left); - normalizedRadius.output.connectTo(normalizedMin.right); - - const oneValue = new ParticleInputBlock("Shockwave One"); - oneValue.value = 1; - const normalizedClamp = new ParticleMathBlock("Shockwave Normalized Clamp"); - normalizedClamp.operation = ParticleMathBlockOperations.Min; - normalizedMin.output.connectTo(normalizedClamp.left); - oneValue.output.connectTo(normalizedClamp.right); - - const minThickness = new ParticleInputBlock("Shockwave Min Thickness"); - minThickness.value = 0.25; - const maxThickness = new ParticleInputBlock("Shockwave Max Thickness"); - maxThickness.value = 4; - const thicknessRange = new ParticleMathBlock("Shockwave Thickness Range"); - thicknessRange.operation = ParticleMathBlockOperations.Subtract; - maxThickness.output.connectTo(thicknessRange.left); - minThickness.output.connectTo(thicknessRange.right); - const thicknessScale = new ParticleMathBlock("Shockwave Thickness Scale"); - thicknessScale.operation = ParticleMathBlockOperations.Multiply; - thicknessRange.output.connectTo(thicknessScale.left); - normalizedClamp.output.connectTo(thicknessScale.right); - const thicknessValue = new ParticleMathBlock("Shockwave Thickness Value"); - thicknessValue.operation = ParticleMathBlockOperations.Add; - minThickness.output.connectTo(thicknessValue.left); - thicknessScale.output.connectTo(thicknessValue.right); - - const minHeight = new ParticleInputBlock("Shockwave Min Height"); - minHeight.value = 0.05; - const maxHeight = new ParticleInputBlock("Shockwave Max Height"); - maxHeight.value = 0.25; - const heightRange = new ParticleMathBlock("Shockwave Height Range"); - heightRange.operation = ParticleMathBlockOperations.Subtract; - maxHeight.output.connectTo(heightRange.left); - minHeight.output.connectTo(heightRange.right); - const heightScale = new ParticleMathBlock("Shockwave Height Scale"); - heightScale.operation = ParticleMathBlockOperations.Multiply; - heightRange.output.connectTo(heightScale.left); - normalizedClamp.output.connectTo(heightScale.right); - const heightValue = new ParticleMathBlock("Shockwave Height Value"); - heightValue.operation = ParticleMathBlockOperations.Add; - minHeight.output.connectTo(heightValue.left); - heightScale.output.connectTo(heightValue.right); - - const scalingConverter = new ParticleConverterBlock("Shockwave Scaling Converter"); - clampRadius.output.connectTo(scalingConverter.xIn); - thicknessValue.output.connectTo(scalingConverter.yIn); - clampRadius.output.connectTo(scalingConverter.zIn); - scalingConverter.xyzOut.connectTo(shockwaveUpdate.scaling); - - const positionConverter = new ParticleConverterBlock("Shockwave Position Converter"); - zeroValue.output.connectTo(positionConverter.xIn); - heightValue.output.connectTo(positionConverter.yIn); - zeroValue.output.connectTo(positionConverter.zIn); - positionConverter.xyzOut.connectTo(shockwaveUpdate.position); - - const rotationIncrement = new ParticleMathBlock("Shockwave Rotation Increment"); - rotationIncrement.operation = ParticleMathBlockOperations.Multiply; - getRotationSpeedProp.output.connectTo(rotationIncrement.left); - normalizedDelta.output.connectTo(rotationIncrement.right); - - const updatedRotationAngle = new ParticleMathBlock("Shockwave Updated Rotation Angle"); - updatedRotationAngle.operation = ParticleMathBlockOperations.Add; - getRotationAngleProp.output.connectTo(updatedRotationAngle.left); - rotationIncrement.output.connectTo(updatedRotationAngle.right); - - const setRotationAngleUpdate = new SpsParticlePropsSetBlock("Set Rotation Angle Update"); - setRotationAngleUpdate.propertyName = "rotationAngle"; - setRotationAngleUpdate.type = NodeParticleBlockConnectionPointTypes.Float; - updatedRotationAngle.output.connectTo(setRotationAngleUpdate.value); - - const rotationUpdateConverter = new ParticleConverterBlock("Shockwave Rotation Update Converter"); - zeroValue.output.connectTo(rotationUpdateConverter.xIn); - setRotationAngleUpdate.output.connectTo(rotationUpdateConverter.yIn); - zeroValue.output.connectTo(rotationUpdateConverter.zIn); - rotationUpdateConverter.xyzOut.connectTo(shockwaveUpdate.rotation); - - const colorEnd = new ParticleInputBlock("Shockwave Color End"); - colorEnd.value = new Color4(0, 0, 0, 0); - const colorRange = new ParticleMathBlock("Shockwave Color Range"); - colorRange.operation = ParticleMathBlockOperations.Subtract; - colorEnd.output.connectTo(colorRange.left); - shockwaveColor.output.connectTo(colorRange.right); - const colorScale = new ParticleMathBlock("Shockwave Color Scale"); - colorScale.operation = ParticleMathBlockOperations.Multiply; - colorRange.output.connectTo(colorScale.left); - normalizedClamp.output.connectTo(colorScale.right); - const colorValue = new ParticleMathBlock("Shockwave Color Value"); - colorValue.operation = ParticleMathBlockOperations.Add; - shockwaveColor.output.connectTo(colorValue.left); - colorScale.output.connectTo(colorValue.right); - colorValue.output.connectTo(shockwaveUpdate.color); - - this._systemBlocks.push(spsSystem); - } - - /** - * Remove a block from the current system set - * @param block defines the block to remove - */ - public removeBlock(block: NodeParticleBlock) { - const attachedBlockIndex = this.attachedBlocks.indexOf(block); - if (attachedBlockIndex > -1) { - this.attachedBlocks.splice(attachedBlockIndex, 1); - } - - if (block.isSystem) { - const index = this._systemBlocks.indexOf(block as SystemBlock); - if (index > -1) { - this._systemBlocks.splice(index, 1); - } - } - } - - /** - * Clear the current graph and load a new one from a serialization object - * @param source defines the JSON representation of the particle set - * @param merge defines whether or not the source must be merged or replace the current content - */ - public parseSerializedObject(source: any, merge = false) { - if (!merge) { - this.clear(); - } - - const map: { [key: number]: NodeParticleBlock } = {}; - - // Create blocks - for (const parsedBlock of source.blocks) { - const blockType = GetClass(parsedBlock.customType); - if (blockType) { - const block: NodeParticleBlock = new blockType(); - block._deserialize(parsedBlock); - map[parsedBlock.id] = block; - - this.attachedBlocks.push(block); - - if (block.isSystem) { - this._systemBlocks.push(block as SystemBlock); - } - } - } - - // Reconnect teleportation - for (const block of this.attachedBlocks) { - if (block.isTeleportOut) { - const teleportOut = block as ParticleTeleportOutBlock; - const id = teleportOut._tempEntryPointUniqueId; - if (id) { - const source = map[id] as ParticleTeleportInBlock; - if (source) { - source.attachToEndpoint(teleportOut); - } - } - } - } - - // Connections - Starts with input blocks only (except if in "merge" mode where we scan all blocks) - for (let blockIndex = 0; blockIndex < source.blocks.length; blockIndex++) { - const parsedBlock = source.blocks[blockIndex]; - const block = map[parsedBlock.id]; - - if (!block) { - continue; - } - - if (block.inputs.length && parsedBlock.inputs.some((i: any) => i.targetConnectionName) && !merge) { - continue; - } - this._restoreConnections(block, source, map); - } - - // UI related info - if (source.locations || (source.editorData && source.editorData.locations)) { - const locations: { - blockId: number; - x: number; - y: number; - isCollapsed: boolean; - }[] = source.locations || source.editorData.locations; - - for (const location of locations) { - if (map[location.blockId]) { - location.blockId = map[location.blockId].uniqueId; - } - } - - if (merge && this.editorData && this.editorData.locations) { - locations.concat(this.editorData.locations); - } - - if (source.locations) { - this.editorData = { - locations: locations, - }; - } else { - this.editorData = source.editorData; - this.editorData.locations = locations; - } - - const blockMap: { [key: number]: number } = {}; - - for (const key in map) { - blockMap[key] = map[key].uniqueId; - } - - this.editorData.map = blockMap; - } - - this.comment = source.comment; - } - - private _restoreConnections(block: NodeParticleBlock, source: any, map: { [key: number]: NodeParticleBlock }) { - for (const outputPoint of block.outputs) { - for (const candidate of source.blocks) { - const target = map[candidate.id]; - - if (!target) { - continue; - } - - for (const input of candidate.inputs) { - if (map[input.targetBlockId] === block && input.targetConnectionName === outputPoint.name) { - const inputPoint = target.getInputByName(input.inputName); - if (!inputPoint || inputPoint.isConnected) { - continue; - } - - outputPoint.connectTo(inputPoint, true); - this._restoreConnections(target, source, map); - continue; - } - } - } - } - } - - /** - * Serializes this node particle set in a JSON representation - * @param selectedBlocks defines the list of blocks to save (if null the whole node particle set will be saved) - * @returns the serialized particle system set object - */ - public serialize(selectedBlocks?: NodeParticleBlock[]): any { - const serializationObject = selectedBlocks ? {} : SerializationHelper.Serialize(this); - serializationObject.editorData = JSON.parse(JSON.stringify(this.editorData)); // Copy - - let blocks: NodeParticleBlock[] = []; - - if (selectedBlocks) { - blocks = selectedBlocks; - } else { - serializationObject.customType = "BABYLON.NodeParticleSystemSet"; - } - - // Blocks - serializationObject.blocks = []; - - for (const block of blocks) { - serializationObject.blocks.push(block.serialize()); - } - - if (!selectedBlocks) { - for (const block of this.attachedBlocks) { - if (blocks.indexOf(block) !== -1) { - continue; - } - serializationObject.blocks.push(block.serialize()); - } - } - - return serializationObject; - } - - /** - * Makes a duplicate of the current particle system set. - * @param name defines the name to use for the new particle system set - * @returns the cloned particle system set - */ - public clone(name: string): NodeParticleSystemSet { - const serializationObject = this.serialize(); - - const clone = SerializationHelper.Clone(() => new NodeParticleSystemSet(name), this); - clone.name = name; - clone.snippetId = this.snippetId; - - clone.parseSerializedObject(serializationObject); - clone._buildId = this._buildId; - - return clone; - } - - /** - * Disposes the resources - */ - public dispose(): void { - for (const block of this.attachedBlocks) { - block.dispose(); - } - - this.attachedBlocks.length = 0; - this.onBuildObservable.clear(); - } - - /** - * Creates a new node particle set set to default basic configuration - * @param name defines the name of the particle set - * @returns a new NodeParticleSystemSet - */ - public static CreateDefault(name: string) { - const nodeParticleSet = new NodeParticleSystemSet(name); - - nodeParticleSet.setToDefault(); - - return nodeParticleSet; - } - - /** - * Creates a node particle set from parsed data - * @param source defines the JSON representation of the particle set - * @returns a new node particle set - */ - public static Parse(source: any): NodeParticleSystemSet { - const nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(source.name), source, null); - - nodeParticleSet.parseSerializedObject(source); - - return nodeParticleSet; - } - - /** - * Creates a node particle set from a snippet saved in a remote file - * @param name defines the name of the node particle set to create - * @param url defines the url to load from - * @param nodeParticleSet defines a node particle set to update (instead of creating a new one) - * @returns a promise that will resolve to the new node particle set - */ - // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax - public static ParseFromFileAsync(name: string, url: string, nodeParticleSet?: NodeParticleSystemSet): Promise { - return new Promise((resolve, reject) => { - const request = new WebRequest(); - request.addEventListener("readystatechange", () => { - if (request.readyState == 4) { - if (request.status == 200) { - const serializationObject = JSON.parse(request.responseText); - if (!nodeParticleSet) { - nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(name), serializationObject, null); - } - - nodeParticleSet.parseSerializedObject(serializationObject); - - resolve(nodeParticleSet); - } else { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject("Unable to load the node particle system set"); - } - } - }); - - request.open("GET", url); - request.send(); - }); - } - - /** - * Creates a node particle set from a snippet saved by the node particle editor - * @param snippetId defines the snippet to load - * @param nodeParticleSet defines a node particle set to update (instead of creating a new one) - * @returns a promise that will resolve to the new node particle set - */ - // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax - public static ParseFromSnippetAsync(snippetId: string, nodeParticleSet?: NodeParticleSystemSet): Promise { - if (snippetId === "_BLANK") { - return Promise.resolve(NodeParticleSystemSet.CreateDefault("blank")); - } - - return new Promise((resolve, reject) => { - const request = new WebRequest(); - request.addEventListener("readystatechange", () => { - if (request.readyState == 4) { - if (request.status == 200) { - const snippet = JSON.parse(JSON.parse(request.responseText).jsonPayload); - const serializationObject = JSON.parse(snippet.nodeParticle); - - if (!nodeParticleSet) { - nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(snippetId), serializationObject, null); - } - - nodeParticleSet.parseSerializedObject(serializationObject); - nodeParticleSet.snippetId = snippetId; - - try { - resolve(nodeParticleSet); - } catch (err) { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject(err); - } - } else { - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - reject("Unable to load the snippet " + snippetId); - } - } - }); - - request.open("GET", this.SnippetUrl + "/" + snippetId.replace(/#/g, "/")); - request.send(); - }); - } -} +import { serialize } from "core/Misc/decorators"; +import { ParticleSystemSet } from "../particleSystemSet"; +import { SystemBlock } from "./Blocks/systemBlock"; +import type { Scene } from "core/scene"; +import { NodeParticleBuildState } from "./nodeParticleBuildState"; +import type { NodeParticleBlock } from "./nodeParticleBlock"; +import { SerializationHelper } from "core/Misc/decorators.serialization"; +import { Observable } from "core/Misc/observable"; +import { GetClass } from "core/Misc/typeStore"; +import { WebRequest } from "core/Misc/webRequest"; +import { Constants } from "core/Engines/constants"; +import { Tools } from "core/Misc/tools"; +import { AbstractEngine } from "core/Engines/abstractEngine"; +import { ParticleInputBlock } from "./Blocks/particleInputBlock"; +import { ParticleTextureSourceBlock } from "./Blocks/particleSourceTextureBlock"; +import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; +import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock"; +import { ParticleMathBlock, ParticleMathBlockOperations } from "./Blocks/particleMathBlock"; +import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTeleportOutBlock"; +import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; +import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; +import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; +import type { Nullable } from "core/types"; +import { Color4 } from "core/Maths/math.color"; +import { Vector2, Vector3 } from "core/Maths/math.vector"; +import { VertexData } from "core/Meshes/mesh.vertexData"; +import { + SPSParticleConfigBlock, + SPSInitBlock, + SPSMeshSourceBlock, + SPSSystemBlock, + SPSCreateBlock, + SPSUpdateBlock, + SpsParticlePropsSetBlock, + SpsParticlePropsGetBlock, + SPSNodeMaterialBlock, +} from "./Blocks"; +import { ParticleSystem } from "core/Particles/particleSystem"; +import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; +import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; +import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; +import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; + +// declare NODEPARTICLEEDITOR namespace for compilation issue +declare let NODEPARTICLEEDITOR: any; +declare let BABYLON: any; + +/** + * Interface used to configure the node particle editor + */ +export interface INodeParticleEditorOptions { + /** Define the URL to load node editor script from */ + editorURL?: string; + /** Additional configuration for the NPE */ + nodeEditorConfig?: { + backgroundColor?: Color4; + }; +} + +/** + * Defines a set of particle systems defined as a node graph. + * NPE: #K6F1ZB#1 + * PG: #ZT509U#1 + */ +export class NodeParticleSystemSet { + private _systemBlocks: (SystemBlock | SPSSystemBlock)[] = []; + private _buildId: number = 0; + + /** Define the Url to load node editor script */ + public static EditorURL = `${Tools._DefaultCdnUrl}/v${AbstractEngine.Version}/nodeParticleEditor/babylon.nodeParticleEditor.js`; + + /** Define the Url to load snippets */ + public static SnippetUrl = Constants.SnippetUrl; + + /** + * Snippet ID if the material was created from the snippet server + */ + public snippetId: string; + + /** + * Gets an array of blocks that needs to be serialized even if they are not yet connected + */ + public attachedBlocks: NodeParticleBlock[] = []; + + /** + * Gets or sets data used by visual editor + * @see https://npe.babylonjs.com + */ + public editorData: any = null; + + /** + * Observable raised when the particle set is built + */ + public onBuildObservable = new Observable(); + + /** + * The name of the set + */ + @serialize() + public name: string; + + /** + * A free comment about the set + */ + @serialize("comment") + public comment: string; + + /** + * Gets the system blocks + */ + public get systemBlocks(): (SystemBlock | SPSSystemBlock)[] { + return this._systemBlocks; + } + + /** + * Gets the list of input blocks attached to this material + * @returns an array of InputBlocks + */ + public get inputBlocks() { + const blocks: ParticleInputBlock[] = []; + for (const block of this.attachedBlocks) { + if (block.isInput) { + blocks.push(block as ParticleInputBlock); + } + } + + return blocks; + } + + /** + * Get a block by its name + * @param name defines the name of the block to retrieve + * @returns the required block or null if not found + */ + public getBlockByName(name: string) { + let result = null; + for (const block of this.attachedBlocks) { + if (block.name === name) { + if (!result) { + result = block; + } else { + Tools.Warn("More than one block was found with the name `" + name + "`"); + return result; + } + } + } + + return result; + } + + /** + * Get a block using a predicate + * @param predicate defines the predicate used to find the good candidate + * @returns the required block or null if not found + */ + public getBlockByPredicate(predicate: (block: NodeParticleBlock) => boolean) { + for (const block of this.attachedBlocks) { + if (predicate(block)) { + return block; + } + } + + return null; + } + + /** + * Get an input block using a predicate + * @param predicate defines the predicate used to find the good candidate + * @returns the required input block or null if not found + */ + public getInputBlockByPredicate(predicate: (block: ParticleInputBlock) => boolean): Nullable { + for (const block of this.attachedBlocks) { + if (block.isInput && predicate(block as ParticleInputBlock)) { + return block as ParticleInputBlock; + } + } + + return null; + } + /** + * Creates a new set + * @param name defines the name of the set + */ + public constructor(name: string) { + this.name = name; + } + + /** + * Gets the current class name of the node particle set e.g. "NodeParticleSystemSet" + * @returns the class name + */ + public getClassName(): string { + return "NodeParticleSystemSet"; + } + + private _initializeBlock(node: NodeParticleBlock, autoConfigure = true) { + if (this.attachedBlocks.indexOf(node) === -1) { + this.attachedBlocks.push(node); + } + + for (const input of node.inputs) { + const connectedPoint = input.connectedPoint; + if (connectedPoint) { + const block = connectedPoint.ownerBlock; + if (block !== node) { + this._initializeBlock(block, autoConfigure); + } + } + } + } + + private BJSNODEPARTICLEEDITOR = this._getGlobalNodeParticleEditor(); + + /** Get the editor from bundle or global + * @returns the global NPE + */ + private _getGlobalNodeParticleEditor(): any { + // UMD Global name detection from Webpack Bundle UMD Name. + if (typeof NODEPARTICLEEDITOR !== "undefined") { + return NODEPARTICLEEDITOR; + } + + // In case of module let's check the global emitted from the editor entry point. + if (typeof BABYLON !== "undefined" && typeof BABYLON.NodeParticleEditor !== "undefined") { + return BABYLON; + } + + return undefined; + } + + /** Creates the node editor window. + * @param additionalConfig Define the configuration of the editor + */ + private _createNodeParticleEditor(additionalConfig?: any) { + const nodeEditorConfig: any = { + nodeParticleSet: this, + ...additionalConfig, + }; + this.BJSNODEPARTICLEEDITOR.NodeParticleEditor.Show(nodeEditorConfig); + } + + /** + * Launch the node particle editor + * @param config Define the configuration of the editor + * @returns a promise fulfilled when the node editor is visible + */ + public async editAsync(config?: INodeParticleEditorOptions): Promise { + return await new Promise((resolve) => { + this.BJSNODEPARTICLEEDITOR = this.BJSNODEPARTICLEEDITOR || this._getGlobalNodeParticleEditor(); + if (typeof this.BJSNODEPARTICLEEDITOR == "undefined") { + const editorUrl = config && config.editorURL ? config.editorURL : NodeParticleSystemSet.EditorURL; + + // Load editor and add it to the DOM + Tools.LoadBabylonScript(editorUrl, () => { + this.BJSNODEPARTICLEEDITOR = this.BJSNODEPARTICLEEDITOR || this._getGlobalNodeParticleEditor(); + this._createNodeParticleEditor(config?.nodeEditorConfig); + resolve(); + }); + } else { + // Otherwise creates the editor + this._createNodeParticleEditor(config?.nodeEditorConfig); + resolve(); + } + }); + } + + /** + * Builds the particle system set from the defined blocks. + * @param scene defines the hosting scene + * @param verbose defines whether to log detailed information during the build process (false by default) + * @returns a promise that resolves to the built particle system set + */ + public async buildAsync(scene: Scene, verbose = false): Promise { + return await new Promise((resolve) => { + const output = new ParticleSystemSet(); + + // Initialize all blocks + for (const block of this._systemBlocks) { + this._initializeBlock(block); + } + + // Build the blocks + for (const block of this.systemBlocks) { + const state = new NodeParticleBuildState(); + state.buildId = this._buildId++; + state.scene = scene; + state.verbose = verbose; + + const system = block.createSystem(state); + if (system instanceof ParticleSystem) { + system._source = this; + system._blockReference = block._internalId; + } + output.systems.push(system); + // Errors + state.emitErrors(); + } + + this.onBuildObservable.notifyObservers(this); + + resolve(output); + }); + } + + /** + * Clear the current node particle set + */ + public clear() { + this.attachedBlocks.length = 0; + this._systemBlocks.length = 0; + } + + /** + * Clear the current set and restore it to a default state + */ + public setToDefault() { + this.clear(); + + this.editorData = null; + + // Main system + const system = new SystemBlock("Particle system"); + + // Update position + const updatePositionBlock = new UpdatePositionBlock("Update position"); + updatePositionBlock.output.connectTo(system.particle); + + // Contextual inputs + const positionBlock = new ParticleInputBlock("Position"); + positionBlock.contextualValue = NodeParticleContextualSources.Position; + const directionBlock = new ParticleInputBlock("Scaled direction"); + directionBlock.contextualValue = NodeParticleContextualSources.ScaledDirection; + + // Add + const addBlock = new ParticleMathBlock("Add"); + addBlock.operation = ParticleMathBlockOperations.Add; + positionBlock.output.connectTo(addBlock.left); + directionBlock.output.connectTo(addBlock.right); + addBlock.output.connectTo(updatePositionBlock.position); + + // Create particle + const createParticleBlock = new CreateParticleBlock("Create particle"); + + // Shape + const emitterShape = new BoxShapeBlock("Box shape"); + createParticleBlock.particle.connectTo(emitterShape.particle); + emitterShape.output.connectTo(updatePositionBlock.particle); + + // Texture + const textureBlock = new ParticleTextureSourceBlock("Texture"); + textureBlock.texture.connectTo(system.texture); + textureBlock.url = Tools.GetAssetUrl("https://assets.babylonjs.com/core/textures/flare.png"); + + this._systemBlocks.push(system); + } + + public setToDefaultSps() { + this.createShockwaveSps(); + } + + public createDefaultSps() { + this.clear(); + this.editorData = null; + + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.billboard = false; + + const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); + + const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); + spsCreateTetra.count.value = 2000; + spsCreateTetra.config.connectTo(spsCreateBlock.config); + + const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); + const tetraVertexData = VertexData.CreateBox({ size: 0.1 }); + meshSourceTetra.setCustomVertexData(tetraVertexData, "Default Box"); + meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); + + const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); + spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); + + const randomXZMin = new ParticleInputBlock("Random XZ Min"); + randomXZMin.value = new Vector2(-10, -10); + const randomXZMax = new ParticleInputBlock("Random XZ Max"); + randomXZMax.value = new Vector2(10, 10); + const randomXZ = new ParticleRandomBlock("Random XZ"); + randomXZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomXZMin.output.connectTo(randomXZ.min); + randomXZMax.output.connectTo(randomXZ.max); + + const randomAngleMin = new ParticleInputBlock("Random Angle Min"); + randomAngleMin.value = -Math.PI; + const randomAngleMax = new ParticleInputBlock("Random Angle Max"); + randomAngleMax.value = Math.PI; + const randomAngle = new ParticleRandomBlock("Random Angle"); + randomAngle.lockMode = ParticleRandomBlockLocks.PerParticle; + randomAngleMin.output.connectTo(randomAngle.min); + randomAngleMax.output.connectTo(randomAngle.max); + + const randomRangeMin = new ParticleInputBlock("Random Range Min"); + randomRangeMin.value = 1; + const randomRangeMax = new ParticleInputBlock("Random Range Max"); + randomRangeMax.value = 5; + const randomRange = new ParticleRandomBlock("Random Range"); + randomRange.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRangeMin.output.connectTo(randomRange.min); + randomRangeMax.output.connectTo(randomRange.max); + + const one = new ParticleInputBlock("One"); + one.value = 1; + const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); + cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; + // Store angle in props so we can reuse during update + const setAnglePropInit = new SpsParticlePropsSetBlock("Set Angle Prop Init"); + setAnglePropInit.propertyName = "angle"; + randomAngle.output.connectTo(setAnglePropInit.value); + setAnglePropInit.output.connectTo(cosAngle.input); + const addOne = new ParticleMathBlock("Add One"); + addOne.operation = ParticleMathBlockOperations.Add; + one.output.connectTo(addOne.left); + cosAngle.output.connectTo(addOne.right); + const multiplyRange = new ParticleMathBlock("Multiply Range"); + multiplyRange.operation = ParticleMathBlockOperations.Multiply; + const setRangePropInit = new SpsParticlePropsSetBlock("Set Range Prop Init"); + setRangePropInit.propertyName = "range"; + randomRange.output.connectTo(setRangePropInit.value); + setRangePropInit.output.connectTo(multiplyRange.left); + addOne.output.connectTo(multiplyRange.right); + + const extractXZ = new ParticleConverterBlock("Extract XZ"); + randomXZ.output.connectTo(extractXZ.xyIn); + const positionConverter = new ParticleConverterBlock("Position Converter"); + extractXZ.xOut.connectTo(positionConverter.xIn); + multiplyRange.output.connectTo(positionConverter.yIn); + extractXZ.yOut.connectTo(positionConverter.zIn); + positionConverter.xyzOut.connectTo(spsInitTetra.position); + + const randomRotMin = new ParticleInputBlock("Random Rot Min"); + randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); + const randomRotMax = new ParticleInputBlock("Random Rot Max"); + randomRotMax.value = new Vector3(Math.PI, Math.PI, Math.PI); + const randomRot = new ParticleRandomBlock("Random Rotation"); + randomRot.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotMin.output.connectTo(randomRot.min); + randomRotMax.output.connectTo(randomRot.max); + randomRot.output.connectTo(spsInitTetra.rotation); + + const randomColorMin = new ParticleInputBlock("Random Color Min"); + randomColorMin.value = new Vector3(0, 0, 0); + const randomColorMax = new ParticleInputBlock("Random Color Max"); + randomColorMax.value = new Vector3(1, 1, 1); + const randomColorRGB = new ParticleRandomBlock("Random Color RGB"); + randomColorRGB.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorRGB.min); + randomColorMax.output.connectTo(randomColorRGB.max); + const colorAlpha = new ParticleInputBlock("Color Alpha"); + colorAlpha.value = 1; + const colorConverter = new ParticleConverterBlock("Color Converter"); + randomColorRGB.output.connectTo(colorConverter.xyzIn); + colorAlpha.output.connectTo(colorConverter.wIn); + colorConverter.colorOut.connectTo(spsInitTetra.color); + + // Create update block + const spsUpdateTetra = new SPSUpdateBlock("Update Tetrahedron Particles"); + spsUpdateTetra.updateData.connectTo(spsCreateTetra.updateBlock); + + // Get current position (X, Z stay the same, Y updates) + const currentPosition = new ParticleInputBlock("Current Position"); + currentPosition.contextualValue = NodeParticleContextualSources.Position; + + // Extract X and Z from current position + const extractPosition = new ParticleConverterBlock("Extract Position"); + currentPosition.output.connectTo(extractPosition.xyzIn); + + // Retrieve stored properties + const getAngleProp = new SpsParticlePropsGetBlock("Get Angle Prop"); + getAngleProp.propertyName = "angle"; + getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRangeProp = new SpsParticlePropsGetBlock("Get Range Prop"); + getRangeProp.propertyName = "range"; + getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; + + // Accumulate angle using delta time to avoid relying on absolute frame id + const deltaBlock = new ParticleInputBlock("Delta Time"); + deltaBlock.systemSource = NodeParticleSystemSources.Delta; + + const milliToSecond = new ParticleInputBlock("Milli To Second"); + milliToSecond.value = 0.001; + + const deltaSeconds = new ParticleMathBlock("Delta Seconds"); + deltaSeconds.operation = ParticleMathBlockOperations.Multiply; + deltaBlock.output.connectTo(deltaSeconds.left); + milliToSecond.output.connectTo(deltaSeconds.right); + + const targetFps = new ParticleInputBlock("Target FPS"); + targetFps.value = 60; + + const normalizedDelta = new ParticleMathBlock("Normalized Delta"); + normalizedDelta.operation = ParticleMathBlockOperations.Multiply; + deltaSeconds.output.connectTo(normalizedDelta.left); + targetFps.output.connectTo(normalizedDelta.right); + + const speedPerFrame = new ParticleInputBlock("Speed Per Frame"); + speedPerFrame.value = Math.PI / 100; + + const scaledIncrement = new ParticleMathBlock("Scaled Increment"); + scaledIncrement.operation = ParticleMathBlockOperations.Multiply; + speedPerFrame.output.connectTo(scaledIncrement.left); + normalizedDelta.output.connectTo(scaledIncrement.right); + + const accumulateAngle = new ParticleMathBlock("Accumulate Angle"); + accumulateAngle.operation = ParticleMathBlockOperations.Add; + getAngleProp.output.connectTo(accumulateAngle.left); + scaledIncrement.output.connectTo(accumulateAngle.right); + + const setAnglePropUpdate = new SpsParticlePropsSetBlock("Set Angle Prop Update"); + setAnglePropUpdate.propertyName = "angle"; + setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + accumulateAngle.output.connectTo(setAnglePropUpdate.value); + + // Calculate new Y position: range * (1 + cos(angle)) + const oneUpdate = new ParticleInputBlock("One Update"); + oneUpdate.value = 1; + const cosUpdatedAngle = new ParticleTrigonometryBlock("Cos Updated Angle"); + cosUpdatedAngle.operation = ParticleTrigonometryBlockOperations.Cos; + setAnglePropUpdate.output.connectTo(cosUpdatedAngle.input); + const addOneUpdate = new ParticleMathBlock("Add One Update"); + addOneUpdate.operation = ParticleMathBlockOperations.Add; + oneUpdate.output.connectTo(addOneUpdate.left); + cosUpdatedAngle.output.connectTo(addOneUpdate.right); + const multiplyRangeUpdate = new ParticleMathBlock("Multiply Range Update"); + multiplyRangeUpdate.operation = ParticleMathBlockOperations.Multiply; + getRangeProp.output.connectTo(multiplyRangeUpdate.left); + addOneUpdate.output.connectTo(multiplyRangeUpdate.right); + + // Combine X (from current position), Y (new), Z (from current position) + const updatePositionConverter = new ParticleConverterBlock("Update Position Converter"); + extractPosition.xOut.connectTo(updatePositionConverter.xIn); + multiplyRangeUpdate.output.connectTo(updatePositionConverter.yIn); + extractPosition.zOut.connectTo(updatePositionConverter.zIn); + updatePositionConverter.xyzOut.connectTo(spsUpdateTetra.position); + + this._systemBlocks.push(spsSystem); + } + + /** + * Sets the current set to an SPS shockwave preset inspired by Patrick Ryan's createShockwave sample + */ + public createShockwaveSps() { + this.clear(); + this.editorData = null; + + const spsSystem = new SPSSystemBlock("Shockwave SPS System"); + spsSystem.billboard = false; + + const lifetimeMs = new ParticleInputBlock("Shockwave Lifetime (ms)"); + lifetimeMs.value = 2500; + const minLifetimeMs = new ParticleInputBlock("Shockwave Min Lifetime (ms)"); + minLifetimeMs.value = 1; + const lifetimeSafe = new ParticleMathBlock("Shockwave Lifetime Safe"); + lifetimeSafe.operation = ParticleMathBlockOperations.Max; + lifetimeMs.output.connectTo(lifetimeSafe.left); + minLifetimeMs.output.connectTo(lifetimeSafe.right); + lifetimeSafe.output.connectTo(spsSystem.lifeTime); + spsSystem.disposeOnEnd = true; + + const spsCreateBlock = new SPSCreateBlock("Create Shockwave SPS"); + spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); + + const shockwaveConfig = new SPSParticleConfigBlock("Shockwave Particle Config"); + shockwaveConfig.count.value = 7; + shockwaveConfig.config.connectTo(spsCreateBlock.config); + + const shockwaveMesh = new SPSMeshSourceBlock("Shockwave Mesh Source"); + shockwaveMesh.remoteMeshUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/gltf/shockwaveMesh.glb"; + shockwaveMesh.remoteMeshName = "shockwaveMesh"; + shockwaveMesh.mesh.connectTo(shockwaveConfig.mesh); + + const shockwaveMaterial = new SPSNodeMaterialBlock("Shockwave Material"); + shockwaveMaterial.shaderUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/shaders/shockwaveParticleShader.json"; + shockwaveMaterial.textureBlockName = "particleTex"; + const shockwaveTexture = new ParticleTextureSourceBlock("Shockwave Texture"); + shockwaveTexture.url = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/textures/electricityRing.png"; + shockwaveTexture.texture.connectTo(shockwaveMaterial.texture); + shockwaveMaterial.material.connectTo(shockwaveConfig.material); + + const shockwaveInit = new SPSInitBlock("Initialize Shockwave Particles"); + shockwaveInit.initData.connectTo(shockwaveConfig.initBlock); + + const shockwaveUpdate = new SPSUpdateBlock("Update Shockwave Particles"); + shockwaveUpdate.updateData.connectTo(shockwaveConfig.updateBlock); + + const deltaBlock = new ParticleInputBlock("Shockwave Delta Time"); + deltaBlock.systemSource = NodeParticleSystemSources.Delta; + const milliToSecond = new ParticleInputBlock("Shockwave Milli To Second"); + milliToSecond.value = 0.001; + const deltaSeconds = new ParticleMathBlock("Shockwave Delta Seconds"); + deltaSeconds.operation = ParticleMathBlockOperations.Multiply; + deltaBlock.output.connectTo(deltaSeconds.left); + milliToSecond.output.connectTo(deltaSeconds.right); + const targetFps = new ParticleInputBlock("Shockwave Target FPS"); + targetFps.value = 60; + const normalizedDelta = new ParticleMathBlock("Shockwave Normalized Delta"); + normalizedDelta.operation = ParticleMathBlockOperations.Multiply; + deltaSeconds.output.connectTo(normalizedDelta.left); + targetFps.output.connectTo(normalizedDelta.right); + + const lifetimeSeconds = new ParticleMathBlock("Shockwave Lifetime Seconds"); + lifetimeSeconds.operation = ParticleMathBlockOperations.Multiply; + lifetimeSafe.output.connectTo(lifetimeSeconds.left); + milliToSecond.output.connectTo(lifetimeSeconds.right); + const framesPerLifetime = new ParticleMathBlock("Shockwave Frames Per Lifetime"); + framesPerLifetime.operation = ParticleMathBlockOperations.Multiply; + lifetimeSeconds.output.connectTo(framesPerLifetime.left); + targetFps.output.connectTo(framesPerLifetime.right); + + const origin = new ParticleInputBlock("Shockwave Origin"); + origin.value = new Vector3(0, 0.05, 0); + origin.output.connectTo(shockwaveInit.position); + + const shockwaveColor = new ParticleInputBlock("Shockwave Base Color"); + shockwaveColor.value = new Color4(0.33, 0.49, 0.88, 0.9); + shockwaveColor.output.connectTo(shockwaveInit.color); + + const zeroValue = new ParticleInputBlock("Shockwave Zero"); + zeroValue.value = 0; + + const radiusStart = new ParticleInputBlock("Shockwave Radius Start"); + radiusStart.value = 1; + const storeRadiusInit = new SpsParticlePropsSetBlock("Store Radius Init"); + storeRadiusInit.propertyName = "radius"; + storeRadiusInit.type = NodeParticleBlockConnectionPointTypes.Float; + radiusStart.output.connectTo(storeRadiusInit.value); + + const maxRadius = new ParticleInputBlock("Shockwave Max Radius"); + maxRadius.value = 4; + + const radiusRangeBlock = new ParticleMathBlock("Shockwave Radius Range"); + radiusRangeBlock.operation = ParticleMathBlockOperations.Subtract; + maxRadius.output.connectTo(radiusRangeBlock.left); + radiusStart.output.connectTo(radiusRangeBlock.right); + + const growthMultiplierMin = new ParticleInputBlock("Shockwave Growth Multiplier Min"); + growthMultiplierMin.value = 0.85; + const growthMultiplierMax = new ParticleInputBlock("Shockwave Growth Multiplier Max"); + growthMultiplierMax.value = 1.15; + const growthMultiplier = new ParticleRandomBlock("Shockwave Growth Multiplier"); + growthMultiplier.lockMode = ParticleRandomBlockLocks.OncePerParticle; + growthMultiplierMin.output.connectTo(growthMultiplier.min); + growthMultiplierMax.output.connectTo(growthMultiplier.max); + + const baseGrowthPerFrame = new ParticleMathBlock("Shockwave Base Growth Per Frame"); + baseGrowthPerFrame.operation = ParticleMathBlockOperations.Divide; + radiusRangeBlock.output.connectTo(baseGrowthPerFrame.left); + framesPerLifetime.output.connectTo(baseGrowthPerFrame.right); + + const growthPerFrame = new ParticleMathBlock("Shockwave Growth Per Frame"); + growthPerFrame.operation = ParticleMathBlockOperations.Multiply; + baseGrowthPerFrame.output.connectTo(growthPerFrame.left); + growthMultiplier.output.connectTo(growthPerFrame.right); + + const storeScaleStepInit = new SpsParticlePropsSetBlock("Store Scale Step Init"); + storeScaleStepInit.propertyName = "scaleStep"; + storeScaleStepInit.type = NodeParticleBlockConnectionPointTypes.Float; + growthPerFrame.output.connectTo(storeScaleStepInit.value); + + const initScaleConverter = new ParticleConverterBlock("Shockwave Init Scale Converter"); + storeRadiusInit.output.connectTo(initScaleConverter.xIn); + storeScaleStepInit.output.connectTo(initScaleConverter.yIn); + storeRadiusInit.output.connectTo(initScaleConverter.zIn); + initScaleConverter.xyzOut.connectTo(shockwaveInit.scaling); + + const rotationMin = new ParticleInputBlock("Shockwave Rotation Min"); + rotationMin.value = new Vector3(0, -Math.PI, 0); + const rotationMax = new ParticleInputBlock("Shockwave Rotation Max"); + rotationMax.value = new Vector3(0, Math.PI, 0); + const initialRotation = new ParticleRandomBlock("Shockwave Initial Rotation"); + initialRotation.lockMode = ParticleRandomBlockLocks.OncePerParticle; + rotationMin.output.connectTo(initialRotation.min); + rotationMax.output.connectTo(initialRotation.max); + + const rotationConverter = new ParticleConverterBlock("Shockwave Rotation Converter"); + initialRotation.output.connectTo(rotationConverter.xyzIn); + const storeRotationAngleInit = new SpsParticlePropsSetBlock("Store Rotation Angle Init"); + storeRotationAngleInit.propertyName = "rotationAngle"; + storeRotationAngleInit.type = NodeParticleBlockConnectionPointTypes.Float; + rotationConverter.yOut.connectTo(storeRotationAngleInit.value); + + const rotationCompose = new ParticleConverterBlock("Shockwave Rotation Compose"); + rotationConverter.xOut.connectTo(rotationCompose.xIn); + storeRotationAngleInit.output.connectTo(rotationCompose.yIn); + rotationConverter.zOut.connectTo(rotationCompose.zIn); + rotationCompose.xyzOut.connectTo(shockwaveInit.rotation); + + const rotationSpeedMin = new ParticleInputBlock("Shockwave Rotation Speed Min"); + rotationSpeedMin.value = -0.06; + const rotationSpeedMax = new ParticleInputBlock("Shockwave Rotation Speed Max"); + rotationSpeedMax.value = 0.06; + const rotationSpeedRandom = new ParticleRandomBlock("Shockwave Rotation Speed Random"); + rotationSpeedRandom.lockMode = ParticleRandomBlockLocks.OncePerParticle; + rotationSpeedMin.output.connectTo(rotationSpeedRandom.min); + rotationSpeedMax.output.connectTo(rotationSpeedRandom.max); + const storeRotationSpeed = new SpsParticlePropsSetBlock("Store Rotation Speed"); + storeRotationSpeed.propertyName = "rotationSpeed"; + storeRotationSpeed.type = NodeParticleBlockConnectionPointTypes.Float; + rotationSpeedRandom.output.connectTo(storeRotationSpeed.value); + + const rotationSpeedSink = new ParticleMathBlock("Shockwave Rotation Speed Sink"); + rotationSpeedSink.operation = ParticleMathBlockOperations.Multiply; + storeRotationSpeed.output.connectTo(rotationSpeedSink.left); + zeroValue.output.connectTo(rotationSpeedSink.right); + const rotationSpeedVelocity = new ParticleConverterBlock("Shockwave Rotation Speed Velocity"); + rotationSpeedSink.output.connectTo(rotationSpeedVelocity.xIn); + zeroValue.output.connectTo(rotationSpeedVelocity.yIn); + zeroValue.output.connectTo(rotationSpeedVelocity.zIn); + rotationSpeedVelocity.xyzOut.connectTo(shockwaveInit.velocity); + + const getRadiusProp = new SpsParticlePropsGetBlock("Get Radius Prop"); + getRadiusProp.propertyName = "radius"; + getRadiusProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getScaleStepProp = new SpsParticlePropsGetBlock("Get Scale Step Prop"); + getScaleStepProp.propertyName = "scaleStep"; + getScaleStepProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRotationSpeedProp = new SpsParticlePropsGetBlock("Get Rotation Speed Prop"); + getRotationSpeedProp.propertyName = "rotationSpeed"; + getRotationSpeedProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRotationAngleProp = new SpsParticlePropsGetBlock("Get Rotation Angle Prop"); + getRotationAngleProp.propertyName = "rotationAngle"; + getRotationAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const scaleStepDelta = new ParticleMathBlock("Shockwave Radius Delta"); + scaleStepDelta.operation = ParticleMathBlockOperations.Multiply; + getScaleStepProp.output.connectTo(scaleStepDelta.left); + normalizedDelta.output.connectTo(scaleStepDelta.right); + + const radiusIncrement = new ParticleMathBlock("Shockwave Radius Increment"); + radiusIncrement.operation = ParticleMathBlockOperations.Add; + getRadiusProp.output.connectTo(radiusIncrement.left); + scaleStepDelta.output.connectTo(radiusIncrement.right); + + const setRadiusPropUpdate = new SpsParticlePropsSetBlock("Set Radius Prop Update"); + setRadiusPropUpdate.propertyName = "radius"; + setRadiusPropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + radiusIncrement.output.connectTo(setRadiusPropUpdate.value); + + const clampRadius = new ParticleMathBlock("Shockwave Clamp Radius"); + clampRadius.operation = ParticleMathBlockOperations.Min; + setRadiusPropUpdate.output.connectTo(clampRadius.left); + maxRadius.output.connectTo(clampRadius.right); + + const normalizedRadius = new ParticleMathBlock("Shockwave Normalized Radius"); + normalizedRadius.operation = ParticleMathBlockOperations.Divide; + clampRadius.output.connectTo(normalizedRadius.left); + maxRadius.output.connectTo(normalizedRadius.right); + + const normalizedMin = new ParticleMathBlock("Shockwave Normalized Min"); + normalizedMin.operation = ParticleMathBlockOperations.Max; + zeroValue.output.connectTo(normalizedMin.left); + normalizedRadius.output.connectTo(normalizedMin.right); + + const oneValue = new ParticleInputBlock("Shockwave One"); + oneValue.value = 1; + const normalizedClamp = new ParticleMathBlock("Shockwave Normalized Clamp"); + normalizedClamp.operation = ParticleMathBlockOperations.Min; + normalizedMin.output.connectTo(normalizedClamp.left); + oneValue.output.connectTo(normalizedClamp.right); + + const minThickness = new ParticleInputBlock("Shockwave Min Thickness"); + minThickness.value = 0.25; + const maxThickness = new ParticleInputBlock("Shockwave Max Thickness"); + maxThickness.value = 4; + const thicknessRange = new ParticleMathBlock("Shockwave Thickness Range"); + thicknessRange.operation = ParticleMathBlockOperations.Subtract; + maxThickness.output.connectTo(thicknessRange.left); + minThickness.output.connectTo(thicknessRange.right); + const thicknessScale = new ParticleMathBlock("Shockwave Thickness Scale"); + thicknessScale.operation = ParticleMathBlockOperations.Multiply; + thicknessRange.output.connectTo(thicknessScale.left); + normalizedClamp.output.connectTo(thicknessScale.right); + const thicknessValue = new ParticleMathBlock("Shockwave Thickness Value"); + thicknessValue.operation = ParticleMathBlockOperations.Add; + minThickness.output.connectTo(thicknessValue.left); + thicknessScale.output.connectTo(thicknessValue.right); + + const minHeight = new ParticleInputBlock("Shockwave Min Height"); + minHeight.value = 0.05; + const maxHeight = new ParticleInputBlock("Shockwave Max Height"); + maxHeight.value = 0.25; + const heightRange = new ParticleMathBlock("Shockwave Height Range"); + heightRange.operation = ParticleMathBlockOperations.Subtract; + maxHeight.output.connectTo(heightRange.left); + minHeight.output.connectTo(heightRange.right); + const heightScale = new ParticleMathBlock("Shockwave Height Scale"); + heightScale.operation = ParticleMathBlockOperations.Multiply; + heightRange.output.connectTo(heightScale.left); + normalizedClamp.output.connectTo(heightScale.right); + const heightValue = new ParticleMathBlock("Shockwave Height Value"); + heightValue.operation = ParticleMathBlockOperations.Add; + minHeight.output.connectTo(heightValue.left); + heightScale.output.connectTo(heightValue.right); + + const scalingConverter = new ParticleConverterBlock("Shockwave Scaling Converter"); + clampRadius.output.connectTo(scalingConverter.xIn); + thicknessValue.output.connectTo(scalingConverter.yIn); + clampRadius.output.connectTo(scalingConverter.zIn); + scalingConverter.xyzOut.connectTo(shockwaveUpdate.scaling); + + const positionConverter = new ParticleConverterBlock("Shockwave Position Converter"); + zeroValue.output.connectTo(positionConverter.xIn); + heightValue.output.connectTo(positionConverter.yIn); + zeroValue.output.connectTo(positionConverter.zIn); + positionConverter.xyzOut.connectTo(shockwaveUpdate.position); + + const rotationIncrement = new ParticleMathBlock("Shockwave Rotation Increment"); + rotationIncrement.operation = ParticleMathBlockOperations.Multiply; + getRotationSpeedProp.output.connectTo(rotationIncrement.left); + normalizedDelta.output.connectTo(rotationIncrement.right); + + const updatedRotationAngle = new ParticleMathBlock("Shockwave Updated Rotation Angle"); + updatedRotationAngle.operation = ParticleMathBlockOperations.Add; + getRotationAngleProp.output.connectTo(updatedRotationAngle.left); + rotationIncrement.output.connectTo(updatedRotationAngle.right); + + const setRotationAngleUpdate = new SpsParticlePropsSetBlock("Set Rotation Angle Update"); + setRotationAngleUpdate.propertyName = "rotationAngle"; + setRotationAngleUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + updatedRotationAngle.output.connectTo(setRotationAngleUpdate.value); + + const rotationUpdateConverter = new ParticleConverterBlock("Shockwave Rotation Update Converter"); + zeroValue.output.connectTo(rotationUpdateConverter.xIn); + setRotationAngleUpdate.output.connectTo(rotationUpdateConverter.yIn); + zeroValue.output.connectTo(rotationUpdateConverter.zIn); + rotationUpdateConverter.xyzOut.connectTo(shockwaveUpdate.rotation); + + const colorEnd = new ParticleInputBlock("Shockwave Color End"); + colorEnd.value = new Color4(0, 0, 0, 0); + const colorRange = new ParticleMathBlock("Shockwave Color Range"); + colorRange.operation = ParticleMathBlockOperations.Subtract; + colorEnd.output.connectTo(colorRange.left); + shockwaveColor.output.connectTo(colorRange.right); + const colorScale = new ParticleMathBlock("Shockwave Color Scale"); + colorScale.operation = ParticleMathBlockOperations.Multiply; + colorRange.output.connectTo(colorScale.left); + normalizedClamp.output.connectTo(colorScale.right); + const colorValue = new ParticleMathBlock("Shockwave Color Value"); + colorValue.operation = ParticleMathBlockOperations.Add; + shockwaveColor.output.connectTo(colorValue.left); + colorScale.output.connectTo(colorValue.right); + colorValue.output.connectTo(shockwaveUpdate.color); + + this._systemBlocks.push(spsSystem); + } + + /** + * Remove a block from the current system set + * @param block defines the block to remove + */ + public removeBlock(block: NodeParticleBlock) { + const attachedBlockIndex = this.attachedBlocks.indexOf(block); + if (attachedBlockIndex > -1) { + this.attachedBlocks.splice(attachedBlockIndex, 1); + } + + if (block.isSystem) { + const index = this._systemBlocks.indexOf(block as SystemBlock); + if (index > -1) { + this._systemBlocks.splice(index, 1); + } + } + } + + /** + * Clear the current graph and load a new one from a serialization object + * @param source defines the JSON representation of the particle set + * @param merge defines whether or not the source must be merged or replace the current content + */ + public parseSerializedObject(source: any, merge = false) { + if (!merge) { + this.clear(); + } + + const map: { [key: number]: NodeParticleBlock } = {}; + + // Create blocks + for (const parsedBlock of source.blocks) { + const blockType = GetClass(parsedBlock.customType); + if (blockType) { + const block: NodeParticleBlock = new blockType(); + block._deserialize(parsedBlock); + map[parsedBlock.id] = block; + + this.attachedBlocks.push(block); + + if (block.isSystem) { + this._systemBlocks.push(block as SystemBlock); + } + } + } + + // Reconnect teleportation + for (const block of this.attachedBlocks) { + if (block.isTeleportOut) { + const teleportOut = block as ParticleTeleportOutBlock; + const id = teleportOut._tempEntryPointUniqueId; + if (id) { + const source = map[id] as ParticleTeleportInBlock; + if (source) { + source.attachToEndpoint(teleportOut); + } + } + } + } + + // Connections - Starts with input blocks only (except if in "merge" mode where we scan all blocks) + for (let blockIndex = 0; blockIndex < source.blocks.length; blockIndex++) { + const parsedBlock = source.blocks[blockIndex]; + const block = map[parsedBlock.id]; + + if (!block) { + continue; + } + + if (block.inputs.length && parsedBlock.inputs.some((i: any) => i.targetConnectionName) && !merge) { + continue; + } + this._restoreConnections(block, source, map); + } + + // UI related info + if (source.locations || (source.editorData && source.editorData.locations)) { + const locations: { + blockId: number; + x: number; + y: number; + isCollapsed: boolean; + }[] = source.locations || source.editorData.locations; + + for (const location of locations) { + if (map[location.blockId]) { + location.blockId = map[location.blockId].uniqueId; + } + } + + if (merge && this.editorData && this.editorData.locations) { + locations.concat(this.editorData.locations); + } + + if (source.locations) { + this.editorData = { + locations: locations, + }; + } else { + this.editorData = source.editorData; + this.editorData.locations = locations; + } + + const blockMap: { [key: number]: number } = {}; + + for (const key in map) { + blockMap[key] = map[key].uniqueId; + } + + this.editorData.map = blockMap; + } + + this.comment = source.comment; + } + + private _restoreConnections(block: NodeParticleBlock, source: any, map: { [key: number]: NodeParticleBlock }) { + for (const outputPoint of block.outputs) { + for (const candidate of source.blocks) { + const target = map[candidate.id]; + + if (!target) { + continue; + } + + for (const input of candidate.inputs) { + if (map[input.targetBlockId] === block && input.targetConnectionName === outputPoint.name) { + const inputPoint = target.getInputByName(input.inputName); + if (!inputPoint || inputPoint.isConnected) { + continue; + } + + outputPoint.connectTo(inputPoint, true); + this._restoreConnections(target, source, map); + continue; + } + } + } + } + } + + /** + * Serializes this node particle set in a JSON representation + * @param selectedBlocks defines the list of blocks to save (if null the whole node particle set will be saved) + * @returns the serialized particle system set object + */ + public serialize(selectedBlocks?: NodeParticleBlock[]): any { + const serializationObject = selectedBlocks ? {} : SerializationHelper.Serialize(this); + serializationObject.editorData = JSON.parse(JSON.stringify(this.editorData)); // Copy + + let blocks: NodeParticleBlock[] = []; + + if (selectedBlocks) { + blocks = selectedBlocks; + } else { + serializationObject.customType = "BABYLON.NodeParticleSystemSet"; + } + + // Blocks + serializationObject.blocks = []; + + for (const block of blocks) { + serializationObject.blocks.push(block.serialize()); + } + + if (!selectedBlocks) { + for (const block of this.attachedBlocks) { + if (blocks.indexOf(block) !== -1) { + continue; + } + serializationObject.blocks.push(block.serialize()); + } + } + + return serializationObject; + } + + /** + * Makes a duplicate of the current particle system set. + * @param name defines the name to use for the new particle system set + * @returns the cloned particle system set + */ + public clone(name: string): NodeParticleSystemSet { + const serializationObject = this.serialize(); + + const clone = SerializationHelper.Clone(() => new NodeParticleSystemSet(name), this); + clone.name = name; + clone.snippetId = this.snippetId; + + clone.parseSerializedObject(serializationObject); + clone._buildId = this._buildId; + + return clone; + } + + /** + * Disposes the resources + */ + public dispose(): void { + for (const block of this.attachedBlocks) { + block.dispose(); + } + + this.attachedBlocks.length = 0; + this.onBuildObservable.clear(); + } + + /** + * Creates a new node particle set set to default basic configuration + * @param name defines the name of the particle set + * @returns a new NodeParticleSystemSet + */ + public static CreateDefault(name: string) { + const nodeParticleSet = new NodeParticleSystemSet(name); + + nodeParticleSet.setToDefault(); + + return nodeParticleSet; + } + + /** + * Creates a node particle set from parsed data + * @param source defines the JSON representation of the particle set + * @returns a new node particle set + */ + public static Parse(source: any): NodeParticleSystemSet { + const nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(source.name), source, null); + + nodeParticleSet.parseSerializedObject(source); + + return nodeParticleSet; + } + + /** + * Creates a node particle set from a snippet saved in a remote file + * @param name defines the name of the node particle set to create + * @param url defines the url to load from + * @param nodeParticleSet defines a node particle set to update (instead of creating a new one) + * @returns a promise that will resolve to the new node particle set + */ + // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax + public static ParseFromFileAsync(name: string, url: string, nodeParticleSet?: NodeParticleSystemSet): Promise { + return new Promise((resolve, reject) => { + const request = new WebRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState == 4) { + if (request.status == 200) { + const serializationObject = JSON.parse(request.responseText); + if (!nodeParticleSet) { + nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(name), serializationObject, null); + } + + nodeParticleSet.parseSerializedObject(serializationObject); + + resolve(nodeParticleSet); + } else { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject("Unable to load the node particle system set"); + } + } + }); + + request.open("GET", url); + request.send(); + }); + } + + /** + * Creates a node particle set from a snippet saved by the node particle editor + * @param snippetId defines the snippet to load + * @param nodeParticleSet defines a node particle set to update (instead of creating a new one) + * @returns a promise that will resolve to the new node particle set + */ + // eslint-disable-next-line @typescript-eslint/promise-function-async, no-restricted-syntax + public static ParseFromSnippetAsync(snippetId: string, nodeParticleSet?: NodeParticleSystemSet): Promise { + if (snippetId === "_BLANK") { + return Promise.resolve(NodeParticleSystemSet.CreateDefault("blank")); + } + + return new Promise((resolve, reject) => { + const request = new WebRequest(); + request.addEventListener("readystatechange", () => { + if (request.readyState == 4) { + if (request.status == 200) { + const snippet = JSON.parse(JSON.parse(request.responseText).jsonPayload); + const serializationObject = JSON.parse(snippet.nodeParticle); + + if (!nodeParticleSet) { + nodeParticleSet = SerializationHelper.Parse(() => new NodeParticleSystemSet(snippetId), serializationObject, null); + } + + nodeParticleSet.parseSerializedObject(serializationObject); + nodeParticleSet.snippetId = snippetId; + + try { + resolve(nodeParticleSet); + } catch (err) { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject(err); + } + } else { + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + reject("Unable to load the snippet " + snippetId); + } + } + }); + + request.open("GET", this.SnippetUrl + "/" + snippetId.replace(/#/g, "/")); + request.send(); + }); + } +} diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 9e29bc41207..b9484affcd1 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -23,7 +23,7 @@ export class NodeListComponent extends React.Component { + private _onValueChangedObserver: Nullable> = null; + + constructor(props: IPropertyComponentProps) { + super(props); + this.state = { isLoading: false }; + } + + override componentDidMount(): void { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + this._onValueChangedObserver = block.onValueChangedObservable.add(() => { + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + }); + } + + private _getNodeScene(): Nullable { + return (this.props.nodeData as any).__spsMeshScene || null; + } + + private _setNodeScene(scene: Nullable) { + const nodeData = this.props.nodeData as any; + if (nodeData.__spsMeshScene) { + nodeData.__spsMeshScene.dispose(); + } + nodeData.__spsMeshScene = scene || null; + } + + async loadMesh(file: File) { + if (!EngineStore.LastCreatedEngine) { + return; + } + this.setState({ isLoading: true }); + const scene = await LoadSceneAsync(file, EngineStore.LastCreatedEngine); + this.setState({ isLoading: false }); + + if (!scene) { + return; + } + + this._setNodeScene(scene); + + const meshes = scene.meshes.filter((m) => !!m.name && m.getTotalVertices() > 0) as Mesh[]; + if (meshes.length) { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + block.setCustomMesh(meshes[0]); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + } + + this.forceUpdate(); + } + + removeData() { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + block.clearCustomMesh(); + this._setNodeScene(null); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + } + + applyMesh(mesh: Nullable) { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + block.setCustomMesh(mesh ?? null); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + } + + override componentWillUnmount(): void { + const scene = this._getNodeScene(); + if (scene) { + scene.dispose(); + } + (this.props.nodeData as any).__spsMeshScene = null; + const block = this.props.nodeData.data as SPSMeshSourceBlock; + if (this._onValueChangedObserver) { + block.onValueChangedObservable.remove(this._onValueChangedObserver); + this._onValueChangedObserver = null; + } + } + + override render() { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + const scene = this._getNodeScene(); + + const meshes = scene ? (scene.meshes.filter((m) => !!m.name && m.getTotalVertices() > 0) as Mesh[]) : []; + const meshOptions = [{ label: "None", value: -1 }]; + meshOptions.push( + ...meshes.map((mesh, index) => ({ + label: mesh.name, + value: index, + })) + ); + + const selectedMeshIndex = block.hasCustomMesh ? meshes.findIndex((m) => m.name === block.customMeshName) : -1; + + return ( +
+ + + {block.hasCustomMesh ? ( + + ) : ( + + )} + {this.state.isLoading && } + {!this.state.isLoading && await this.loadMesh(file)} />} + {scene && meshOptions.length > 1 && ( + selectedMeshIndex} + onSelect={(value) => { + const index = value as number; + if (index === -1) { + this.applyMesh(null); + return; + } + this.applyMesh(meshes[index]); + this.forceUpdate(); + }} + /> + )} + {block.hasCustomMesh && ( + this.removeData()} /> + )} + +
+ ); + } +} + diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx new file mode 100644 index 00000000000..51de414aa97 --- /dev/null +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import { LineContainerComponent } from "shared-ui-components/lines/lineContainerComponent"; +import { GeneralPropertyTabComponent, GenericPropertyTabComponent } from "./genericNodePropertyComponent"; +import type { IPropertyComponentProps } from "shared-ui-components/nodeGraphSystem/interfaces/propertyComponentProps"; +import { FileButtonLine } from "shared-ui-components/lines/fileButtonLineComponent"; +import { TextLineComponent } from "shared-ui-components/lines/textLineComponent"; +import { ButtonLineComponent } from "shared-ui-components/lines/buttonLineComponent"; +import type { Nullable } from "core/types"; +import type { Observer } from "core/Misc/observable"; +import { SPSNodeMaterialBlock } from "core/Particles/Node/Blocks"; + +export class SPSNodeMaterialPropertyTabComponent extends React.Component { + private _onValueChangedObserver: Nullable> = null; + + constructor(props: IPropertyComponentProps) { + super(props); + this.state = { isLoading: false }; + } + + override componentDidMount(): void { + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + this._onValueChangedObserver = block.onValueChangedObservable.add(() => { + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + }); + } + + override componentWillUnmount(): void { + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + if (this._onValueChangedObserver) { + block.onValueChangedObservable.remove(this._onValueChangedObserver); + this._onValueChangedObserver = null; + } + } + + async loadMaterial(file: File) { + this.setState({ isLoading: true }); + const text = await file.text(); + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + block.setSerializedMaterial(text, file.name); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.props.stateManager.onUpdateRequiredObservable.notifyObservers(block); + this.setState({ isLoading: false }); + this.forceUpdate(); + } + + removeMaterial() { + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + block.clearMaterial(); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.props.stateManager.onUpdateRequiredObservable.notifyObservers(block); + this.forceUpdate(); + } + + override render() { + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + + return ( +
+ + + + + {block.hasCustomMaterial && } + {this.state.isLoading && } + {!this.state.isLoading && await this.loadMaterial(file)} accept=".json" />} + {block.hasCustomMaterial && this.removeMaterial()} />} + +
+ ); + } +} + diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 32828aee82f..4ce78a4ac1e 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -5,6 +5,8 @@ import { TextureSourcePropertyTabComponent } from "./properties/textureSourceNod import { DebugPropertyTabComponent } from "./properties/debugNodePropertyComponent"; import { TeleportOutPropertyTabComponent } from "./properties/teleportOutNodePropertyComponent"; import { MeshShapePropertyTabComponent } from "./properties/meshShapeNodePropertyComponent"; +import { SPSMeshSourcePropertyTabComponent } from "./properties/spsMeshSourceNodePropertyComponent"; +import { SPSNodeMaterialPropertyTabComponent } from "./properties/spsNodeMaterialPropertyComponent"; export const RegisterToPropertyTabManagers = () => { PropertyLedger.DefaultControl = GenericPropertyComponent; @@ -14,7 +16,7 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; - PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = SPSMeshSourcePropertyTabComponent; PropertyLedger.RegisteredControls["SPSParticleConfigBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; @@ -22,4 +24,5 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["SPSUpdateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SpsParticlePropsSetBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SpsParticlePropsGetBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSNodeMaterialBlock"] = SPSNodeMaterialPropertyTabComponent; }; From 7e2ce2084a9c071283da34c3842df9600e5cc5d5 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Sun, 23 Nov 2025 13:05:09 +0300 Subject: [PATCH 51/68] Enhance SolidParticleSystem and NodeParticleBuildState for improved context handling - Made `_scaledUpdateSpeed` public in `SolidParticleSystem` and added `_actualFrame` for frame tracking. - Updated `NodeParticleBuildState` to support `SolidParticle` and `SolidParticleSystem` in `particleContext` and `systemContext`. - Added checks in various methods to handle `SolidParticle` and `SolidParticleSystem` instances, ensuring proper context management and avoiding null returns for specific properties. These changes improve the integration of solid particles within the node particle system architecture. --- .../Particles/Node/nodeParticleBuildState.ts | 68 ++++++++++++++++++- .../core/src/Particles/solidParticleSystem.ts | 6 +- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts index f025c4cae7b..8a1acfe970e 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -6,10 +6,12 @@ import type { ThinParticleSystem } from "core/Particles/thinParticleSystem"; import type { NodeParticleConnectionPoint } from "core/Particles/Node/nodeParticleBlockConnectionPoint"; import { Color4 } from "core/Maths/math.color"; -import { Vector2, Vector3 } from "core/Maths/math.vector"; +import { Matrix, Vector2, Vector3 } from "core/Maths/math.vector"; import { NodeParticleBlockConnectionPointTypes } from "core/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleContextualSources } from "core/Particles/Node/Enums/nodeParticleContextualSources"; import { NodeParticleSystemSources } from "core/Particles/Node/Enums/nodeParticleSystemSources"; +import { SolidParticle } from "core/Particles/solidParticle"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; /** * Class used to store node based geometry build state @@ -37,12 +39,12 @@ export class NodeParticleBuildState { /** * Gets or sets the particle context for contextual data */ - public particleContext: Nullable = null; + public particleContext: Nullable = null; /** * Gets or sets the system context for contextual data */ - public systemContext: Nullable = null; + public systemContext: Nullable = null; /** * Gets or sets the index of the gradient to use @@ -114,44 +116,86 @@ export class NodeParticleBuildState { case NodeParticleContextualSources.Position: return this.particleContext.position; case NodeParticleContextualSources.Direction: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext.direction; case NodeParticleContextualSources.DirectionScale: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext._directionScale; case NodeParticleContextualSources.ScaledDirection: + if (this.particleContext instanceof SolidParticle) { + return null; + } this.particleContext.direction.scaleToRef(this.particleContext._directionScale, this.particleContext._scaledDirection); return this.particleContext._scaledDirection; case NodeParticleContextualSources.Color: return this.particleContext.color; case NodeParticleContextualSources.InitialColor: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext.initialColor; case NodeParticleContextualSources.ColorDead: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext.colorDead; case NodeParticleContextualSources.Age: return this.particleContext.age; case NodeParticleContextualSources.Lifetime: return this.particleContext.lifeTime; case NodeParticleContextualSources.Angle: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext.angle; case NodeParticleContextualSources.Scale: return this.particleContext.scale; case NodeParticleContextualSources.Size: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext.size; case NodeParticleContextualSources.AgeGradient: return this.particleContext.age / this.particleContext.lifeTime; case NodeParticleContextualSources.SpriteCellEnd: + if (this.systemContext instanceof SolidParticleSystem) { + return null; + } return this.systemContext.endSpriteCellID; case NodeParticleContextualSources.SpriteCellIndex: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext.cellIndex; case NodeParticleContextualSources.SpriteCellStart: + if (this.systemContext instanceof SolidParticleSystem) { + return null; + } return this.systemContext.startSpriteCellID; case NodeParticleContextualSources.InitialDirection: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext._initialDirection; case NodeParticleContextualSources.ColorStep: + if (this.particleContext instanceof SolidParticle) { + return null; + } return this.particleContext.colorStep; case NodeParticleContextualSources.ScaledColorStep: + if (this.particleContext instanceof SolidParticle || this.systemContext instanceof SolidParticleSystem) { + return null; + } this.particleContext.colorStep.scaleToRef(this.systemContext._scaledUpdateSpeed, this.systemContext._scaledColorStep); return this.systemContext._scaledColorStep; case NodeParticleContextualSources.LocalPositionUpdated: + if (this.particleContext instanceof SolidParticle || this.systemContext instanceof SolidParticleSystem) { + return this.particleContext.position; + } this.particleContext.direction.scaleToRef(this.particleContext._directionScale, this.particleContext._scaledDirection); this.particleContext._localPosition!.addInPlace(this.particleContext._scaledDirection); Vector3.TransformCoordinatesToRef(this.particleContext._localPosition!, this.systemContext._emitterWorldMatrix, this.particleContext.position); @@ -168,6 +212,13 @@ export class NodeParticleBuildState { if (!this.systemContext) { return null; } + if (this.systemContext instanceof SolidParticleSystem) { + const worldMatrix = this.systemContext.mesh?.getWorldMatrix(); + if (!worldMatrix) { + return Matrix.Identity(); + } + return worldMatrix; + } return this.systemContext._emitterWorldMatrix; } @@ -178,6 +229,13 @@ export class NodeParticleBuildState { if (!this.systemContext) { return null; } + if (this.systemContext instanceof SolidParticleSystem) { + const worldMatrix = this.systemContext.mesh?.getWorldMatrix(); + if (!worldMatrix) { + return Matrix.Identity(); + } + return worldMatrix.invert(); + } return this.systemContext._emitterInverseWorldMatrix; } @@ -189,6 +247,10 @@ export class NodeParticleBuildState { return null; } + if (this.systemContext instanceof SolidParticleSystem) { + return this.systemContext.mesh?.absolutePosition || Vector3.Zero(); + } + if (!this.systemContext.emitter) { return null; } diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index e58d170e507..adfe9a175d3 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -161,7 +161,9 @@ export class SolidParticleSystem implements IDisposable { */ public updateSpeed = 0.01; /** @internal */ - protected _scaledUpdateSpeed: number; + public _scaledUpdateSpeed: number; + /** @internal */ + public _actualFrame = 0; /** * Creates a SPS (Solid Particle System) object. @@ -1184,6 +1186,7 @@ export class SolidParticleSystem implements IDisposable { // Calculate scaled update speed based on animation ratio (for FPS independence) if (this._started && !this._stopped) { this._scaledUpdateSpeed = this.updateSpeed * (this._scene?.getAnimationRatio() || 1); + this._actualFrame += this._scaledUpdateSpeed; } for (let p = start; p <= end; p++) { @@ -2104,6 +2107,7 @@ export class SolidParticleSystem implements IDisposable { this._started = true; this._stopped = false; + this._actualFrame = 0; // Register update loop if (this._scene) { From 36546958214c687d562615333921c5d2a79194cc Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 24 Nov 2025 10:41:32 +0300 Subject: [PATCH 52/68] Refactor SPSSystemBlock to streamline properties and improve serialization - Removed the `disposeOnEnd` property and its associated logic from `SPSSystemBlock`. - Adjusted the `solidParticle` getter to return the correct input index. - Updated serialization and deserialization methods to reflect the removal of `disposeOnEnd` and `lifeTime` properties, ensuring backward compatibility. These changes enhance the clarity and efficiency of the `SPSSystemBlock` implementation. --- .../Blocks/SolidParticle/SPSSystemBlock.ts | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 6f2437a5ce0..6e2f5a7eb48 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -20,11 +20,6 @@ export class SPSSystemBlock extends NodeParticleBlock { }) public billboard = false; - @editableInPropertyPage("Dispose on end", PropertyTypeForEdition.Boolean, "ADVANCED", { - embedded: true, - }) - public disposeOnEnd = false; - public _internalId = SPSSystemBlock._IdCounter++; public constructor(name: string) { @@ -39,12 +34,8 @@ export class SPSSystemBlock extends NodeParticleBlock { return "SPSSystemBlock"; } - public get lifeTime(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - public get solidParticle(): NodeParticleConnectionPoint { - return this._inputs[1]; + return this._inputs[0]; } public get system(): NodeParticleConnectionPoint { @@ -64,29 +55,19 @@ export class SPSSystemBlock extends NodeParticleBlock { solidParticle.billboard = this.billboard; solidParticle.name = this.name; - if (this.lifeTime.isConnected) { - const connectedLifetime = this.lifeTime.getConnectedValue(state) as number; - solidParticle.lifetime = connectedLifetime ?? 0; - } else { - solidParticle.lifetime = this.lifeTime.value; - } - solidParticle.disposeOnEnd = this.disposeOnEnd; + return solidParticle; } public override serialize(): any { const serializationObject = super.serialize(); serializationObject.billboard = this.billboard; - serializationObject.lifeTime = this.lifeTime.value; - serializationObject.disposeOnEnd = this.disposeOnEnd; return serializationObject; } public override _deserialize(serializationObject: any) { super._deserialize(serializationObject); this.billboard = !!serializationObject.billboard; - this.lifeTime.value = serializationObject.lifeTime ?? 0; - this.disposeOnEnd = !!serializationObject.disposeOnEnd; } } From 7375fbcc9c6e99cad90cd26134bc4dba5aca2d3f Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 24 Nov 2025 13:43:27 +0300 Subject: [PATCH 53/68] Refactor SolidParticleSystem and NodeParticle architecture for improved lifecycle management - Added null checks in `SolidParticleSystem.dispose()` to prevent errors when `mesh` is not defined. - Removed the `SPSInitBlock` and integrated its functionality into `SPSParticleConfigBlock`, enhancing the configuration process for solid particles. - Updated `SPSCreateBlock` to utilize new properties from `SPSParticleConfigBlock`, streamlining particle initialization. - Enhanced `ISpsParticleConfigData` to include additional properties such as `position`, `velocity`, `color`, `scaling`, and `rotation`, allowing for more flexible particle configurations. These changes improve the robustness and flexibility of the solid particle system, aligning it more closely with standard particle system behaviors. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 9 +- .../Blocks/SolidParticle/SPSCreateBlock.ts | 31 +- .../Node/Blocks/SolidParticle/SPSInitBlock.ts | 95 ---- .../SolidParticle/SPSNodeMaterialBlock.ts | 113 +--- .../SolidParticle/SPSParticleConfigBlock.ts | 59 +- .../Blocks/SolidParticle/SPSSystemBlock.ts | 1 - .../Node/Blocks/SolidParticle/index.ts | 1 - .../Particles/Node/nodeParticleSystemSet.ts | 516 +----------------- .../core/src/Particles/solidParticleSystem.ts | 4 +- .../nodeParticleEditor/src/blockTools.ts | 6 +- .../components/nodeList/nodeListComponent.tsx | 4 +- .../graphSystem/registerToDisplayLedger.ts | 2 +- .../graphSystem/registerToPropertyLedger.ts | 1 - 13 files changed, 99 insertions(+), 743 deletions(-) delete mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index 825ee68c170..df766ccaa1f 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -26,6 +26,11 @@ export interface ISpsParticleConfigData { meshData: ISpsMeshSourceData | null; count: number; material?: Material; - initBlock?: ISpsUpdateData; - updateBlock?: ISpsUpdateData; + position?: Vector3; + velocity?: Vector3; + color?: Color4; + scaling?: Vector3; + rotation?: Vector3; + lifeTime?: number; + updateBlock?: ISpsUpdateData | null; } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index f1e32f67d25..e1f94328795 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -111,7 +111,6 @@ export class SPSCreateBlock extends NodeParticleBlock { const mesh = new Mesh(`${this.name}_shape_${i}`, state.scene); creatData.meshData.vertexData.applyToMesh(mesh, true); - mesh.isVisible = false; if (creatData.material) { mesh.material = creatData.material; } @@ -133,28 +132,34 @@ export class SPSCreateBlock extends NodeParticleBlock { for (let p = 0; p < sps.nbParticles; p++) { const particle = sps.particles[p]; const particleCreateData = createBlocks.get(particle.shapeId); - const initBlock = particleCreateData?.initBlock; - if (!initBlock) { + if (!particleCreateData) { continue; } + const { lifeTime, position, velocity, color, scaling, rotation } = particleCreateData; state.particleContext = particle; state.systemContext = sps; - if (initBlock.position) { - particle.position.copyFrom(initBlock.position()); + if (lifeTime) { + particle.lifeTime = lifeTime; + particle.age = 0; + particle.alive = true; } - if (initBlock.velocity) { - particle.velocity.copyFrom(initBlock.velocity()); + + if (position) { + particle.position.copyFrom(position); + } + if (velocity) { + particle.velocity.copyFrom(velocity); } - if (initBlock.color) { - particle.color?.copyFrom(initBlock.color()); + if (color) { + particle.color?.copyFrom(color); } - if (initBlock.scaling) { - particle.scaling.copyFrom(initBlock.scaling()); + if (scaling) { + particle.scaling.copyFrom(scaling); } - if (initBlock.rotation) { - particle.rotation.copyFrom(initBlock.rotation()); + if (rotation) { + particle.rotation.copyFrom(rotation); } } } finally { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts deleted file mode 100644 index d0cf1de3f19..00000000000 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - -import { RegisterClass } from "../../../../Misc/typeStore"; -import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; -import { NodeParticleBlock } from "../../nodeParticleBlock"; -import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; -import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISpsUpdateData } from "./ISPSData"; - -/** - * Block used to generate initialization function for SPS particles - */ -export class SPSInitBlock extends NodeParticleBlock { - public constructor(name: string) { - super(name); - - this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); - this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); - this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); - this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); - this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); - - this.registerOutput("initData", NodeParticleBlockConnectionPointTypes.System); - } - - public override getClassName() { - return "SPSInitBlock"; - } - - public get initData(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public get position(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - public get velocity(): NodeParticleConnectionPoint { - return this._inputs[1]; - } - - public get color(): NodeParticleConnectionPoint { - return this._inputs[2]; - } - - public get scaling(): NodeParticleConnectionPoint { - return this._inputs[3]; - } - - public get rotation(): NodeParticleConnectionPoint { - return this._inputs[4]; - } - - public override _build(state: NodeParticleBuildState) { - const initData = {} as ISpsUpdateData; - if (this.position.isConnected) { - initData.position = () => { - return this.position.getConnectedValue(state); - }; - } - if (this.velocity.isConnected) { - initData.velocity = () => { - return this.velocity.getConnectedValue(state); - }; - } - if (this.color.isConnected) { - initData.color = () => { - return this.color.getConnectedValue(state); - }; - } - if (this.scaling.isConnected) { - initData.scaling = () => { - return this.scaling.getConnectedValue(state); - }; - } - if (this.rotation.isConnected) { - initData.rotation = () => { - return this.rotation.getConnectedValue(state); - }; - } - - this.initData._storedValue = initData; - } - - public override serialize(): any { - const serializationObject = super.serialize(); - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - } -} - -RegisterClass("BABYLON.SPSInitBlock", SPSInitBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts index 66852628b99..ae43af82e7e 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts @@ -2,39 +2,19 @@ import { NodeParticleBlock } from "../../nodeParticleBlock"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; -import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; -import { Tools } from "core/Misc/tools"; import { NodeMaterial } from "core/Materials/Node/nodeMaterial"; import type { Nullable } from "core/types"; -import { Texture } from "core/Materials/Textures/texture"; import { RegisterClass } from "../../../../Misc/typeStore"; import { Observable } from "core/Misc/observable"; import type { Scene } from "core/scene"; +import type { Texture } from "core/Materials/Textures/texture"; /** * Block used to load a node material for SPS */ export class SPSNodeMaterialBlock extends NodeParticleBlock { - @editableInPropertyPage("Shader URL", PropertyTypeForEdition.String, "PROPERTIES", { - embedded: false, - }) - public shaderUrl = ""; - - @editableInPropertyPage("Texture URL", PropertyTypeForEdition.String, "PROPERTIES", { - embedded: false, - }) - public textureUrl = ""; - - @editableInPropertyPage("Texture Block Name", PropertyTypeForEdition.String, "PROPERTIES", { - embedded: false, - }) - public textureBlockName = ""; - - private _nodeMaterial: Nullable = null; private _serializedMaterial: Nullable = null; private _customMaterialName = ""; - private _textureInstance: Nullable = null; - private _isLoading = false; public constructor(name: string) { super(name); @@ -56,7 +36,6 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { public get material(): NodeParticleConnectionPoint { return this._outputs[0]; } - public get hasCustomMaterial(): boolean { return !!this._serializedMaterial; } @@ -68,74 +47,37 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { public setSerializedMaterial(serializedData: string, name?: string) { this._serializedMaterial = serializedData; this._customMaterialName = name || "Custom Node Material"; - this.shaderUrl = ""; - this._disposeMaterial(); this.onValueChangedObservable.notifyObservers(this); } public clearMaterial() { this._serializedMaterial = null; this._customMaterialName = ""; - this._disposeMaterial(); this.onValueChangedObservable.notifyObservers(this); } public override _build(state: NodeParticleBuildState) { - if (this._nodeMaterial) { - this._applyTexture(state, this._nodeMaterial); - this.material._storedValue = this._nodeMaterial; - return; - } - this.material._storedValue = null; - if (this._serializedMaterial) { - if (this._instantiateMaterial(this._serializedMaterial, state.scene)) { - this._applyTexture(state, this._nodeMaterial!); - this.material._storedValue = this._nodeMaterial; - } + if (!this._serializedMaterial) { return; } - if (!this.shaderUrl || this._isLoading) { + const nodeMaterial = this._instantiateMaterial(this._serializedMaterial, state.scene); + if (!nodeMaterial) { return; } - this._isLoading = true; - const scene = state.scene; - Tools.LoadFile( - this.shaderUrl, - (data) => { - try { - this._serializedMaterial = data as string; - this._customMaterialName = this.shaderUrl; - this._instantiateMaterial(this._serializedMaterial, scene); - this.onValueChangedObservable.notifyObservers(this); - } finally { - this._isLoading = false; - } - }, - undefined, - undefined, - false, - () => { - this._isLoading = false; - } - ); + this._applyTexture(state, nodeMaterial); + this.material._storedValue = nodeMaterial; } public override dispose() { - this._disposeMaterial(); - this._textureInstance?.dispose(); - this._textureInstance = null; super.dispose(); } public override serialize(): any { const serializationObject = super.serialize(); - serializationObject.shaderUrl = this.shaderUrl; - serializationObject.textureUrl = this.textureUrl; - serializationObject.textureBlockName = this.textureBlockName; serializationObject.serializedMaterial = this._serializedMaterial; serializationObject.customMaterialName = this._customMaterialName; return serializationObject; @@ -143,62 +85,39 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { public override _deserialize(serializationObject: any) { super._deserialize(serializationObject); - this.shaderUrl = serializationObject.shaderUrl || ""; - this.textureUrl = serializationObject.textureUrl || ""; - this.textureBlockName = serializationObject.textureBlockName || ""; this._serializedMaterial = serializationObject.serializedMaterial || null; this._customMaterialName = serializationObject.customMaterialName || ""; - this._disposeMaterial(); } - private _disposeMaterial() { - this._nodeMaterial?.dispose(); - this._nodeMaterial = null; - } - - private _instantiateMaterial(serializedData: string, scene: Scene): boolean { + private _instantiateMaterial(serializedData: string, scene: Scene): Nullable { try { const json = JSON.parse(serializedData); const nodeMaterial = NodeMaterial.Parse(json, scene); nodeMaterial.build(false); - this._disposeMaterial(); - this._nodeMaterial = nodeMaterial; - return true; + return nodeMaterial; } catch { - this._nodeMaterial = null; + return null; } - return false; } private _applyTexture(state: NodeParticleBuildState, nodeMaterial: NodeMaterial) { - if (!this.textureBlockName) { + if (!this.texture.isConnected) { return; } - const block = nodeMaterial.getBlockByName(this.textureBlockName) as { texture?: Texture | null }; - if (!block || block.texture === undefined) { + const connectedTexture = this.texture.getConnectedValue(state) as Texture; + if (!connectedTexture) { return; } - if (this.texture.isConnected) { - const connectedTexture = this.texture.getConnectedValue(state) as Texture; - if (connectedTexture) { - block.texture = connectedTexture; - return; - } - } - - if (!this.textureUrl) { - block.texture = null; + const textureBlocks = nodeMaterial.getTextureBlocks(); + if (!textureBlocks.length) { return; } - if (!this._textureInstance || this._textureInstance.url !== this.textureUrl) { - this._textureInstance?.dispose(); - this._textureInstance = new Texture(this.textureUrl, state.scene); + for (const textureBlock of textureBlocks) { + textureBlock.texture = connectedTexture; } - - block.texture = this._textureInstance; } } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts index ae513debca0..18d71314978 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -1,11 +1,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import { Color4, Vector3 } from "../../../../Maths"; import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISpsParticleConfigData } from "./ISPSData"; +import type { ISpsParticleConfigData, ISpsUpdateData } from "./ISPSData"; /** * Block used to configure SPS particle parameters (mesh, count, material, initBlock, updateBlock) @@ -15,8 +16,13 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { super(name); this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); + this.registerInput("lifeTime", NodeParticleBlockConnectionPointTypes.Float, true); + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); - this.registerInput("initBlock", NodeParticleBlockConnectionPointTypes.System, true); this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); this.registerOutput("config", NodeParticleBlockConnectionPointTypes.SolidParticleConfig); @@ -26,43 +32,74 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { return "SPSParticleConfigBlock"; } - public get mesh(): NodeParticleConnectionPoint { + public get count(): NodeParticleConnectionPoint { return this._inputs[0]; } - public get count(): NodeParticleConnectionPoint { + public get lifeTime(): NodeParticleConnectionPoint { return this._inputs[1]; } - public get material(): NodeParticleConnectionPoint { + public get position(): NodeParticleConnectionPoint { return this._inputs[2]; } - public get initBlock(): NodeParticleConnectionPoint { + public get velocity(): NodeParticleConnectionPoint { return this._inputs[3]; } - public get updateBlock(): NodeParticleConnectionPoint { + public get color(): NodeParticleConnectionPoint { return this._inputs[4]; } + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[5]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[6]; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._inputs[7]; + } + + public get material(): NodeParticleConnectionPoint { + return this._inputs[8]; + } + + public get updateBlock(): NodeParticleConnectionPoint { + return this._inputs[9]; + } + public get config(): NodeParticleConnectionPoint { return this._outputs[0]; } public override _build(state: NodeParticleBuildState) { const meshData = this.mesh.getConnectedValue(state); - const count = (this.count.getConnectedValue(state) as number) || 1; + const count = (this.count.getConnectedValue(state) as number) ?? 1; + const lifeTime = (this.lifeTime.getConnectedValue(state) as number) ?? Infinity; const material = this.material.getConnectedValue(state); - const initBlock = this.initBlock.isConnected ? this.initBlock.getConnectedValue(state) : null; - const updateBlock = this.updateBlock.isConnected ? this.updateBlock.getConnectedValue(state) : null; + const position = (this.position.getConnectedValue(state) as Vector3) ?? new Vector3(0, 0, 0); + const velocity = (this.velocity.getConnectedValue(state) as Vector3) ?? new Vector3(0, 0, 0); + const color = (this.color.getConnectedValue(state) as Color4) ?? new Color4(1, 1, 1, 1); + const scaling = (this.scaling.getConnectedValue(state) as Vector3) ?? new Vector3(1, 1, 1); + const rotation = (this.rotation.getConnectedValue(state) as Vector3) ?? new Vector3(0, 0, 0); + + const updateBlock = this.updateBlock.isConnected ? (this.updateBlock.getConnectedValue(state) as ISpsUpdateData) : null; const particleConfig: ISpsParticleConfigData = { meshData, count, material, - initBlock, + lifeTime, + position, + velocity, + color, + scaling, + rotation, updateBlock, }; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts index 6e2f5a7eb48..44417fe074b 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -25,7 +25,6 @@ export class SPSSystemBlock extends NodeParticleBlock { public constructor(name: string) { super(name); this._isSystem = true; - this.registerInput("lifeTime", NodeParticleBlockConnectionPointTypes.Float, true, 0); this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System); } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index a45e9499321..29dcae10c7d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -3,7 +3,6 @@ export * from "./SPSMeshSourceBlock"; export * from "./SPSParticleConfigBlock"; export * from "./SPSSystemBlock"; export * from "./SPSUpdateBlock"; -export * from "./SPSInitBlock"; export * from "./SPSCreateBlock"; export * from "./SPSParticlePropsSetBlock"; export * from "./SPSParticlePropsGetBlock"; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 6fafe5e89b2..c9297d16e74 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,25 +22,8 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "core/types"; import { Color4 } from "core/Maths/math.color"; -import { Vector2, Vector3 } from "core/Maths/math.vector"; -import { VertexData } from "core/Meshes/mesh.vertexData"; -import { - SPSParticleConfigBlock, - SPSInitBlock, - SPSMeshSourceBlock, - SPSSystemBlock, - SPSCreateBlock, - SPSUpdateBlock, - SpsParticlePropsSetBlock, - SpsParticlePropsGetBlock, - SPSNodeMaterialBlock, -} from "./Blocks"; +import { SPSSystemBlock } from "./Blocks"; import { ParticleSystem } from "core/Particles/particleSystem"; -import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; -import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; -import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; -import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; -import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -356,505 +339,8 @@ export class NodeParticleSystemSet { } public setToDefaultSps() { - this.createShockwaveSps(); - } - - public createDefaultSps() { - this.clear(); - this.editorData = null; - - const spsSystem = new SPSSystemBlock("SPS System"); - spsSystem.billboard = false; - - const spsCreateBlock = new SPSCreateBlock("Create Particles System"); - spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); - - const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); - spsCreateTetra.count.value = 2000; - spsCreateTetra.config.connectTo(spsCreateBlock.config); - - const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); - const tetraVertexData = VertexData.CreateBox({ size: 0.1 }); - meshSourceTetra.setCustomVertexData(tetraVertexData, "Default Box"); - meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); - - const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); - spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); - - const randomXZMin = new ParticleInputBlock("Random XZ Min"); - randomXZMin.value = new Vector2(-10, -10); - const randomXZMax = new ParticleInputBlock("Random XZ Max"); - randomXZMax.value = new Vector2(10, 10); - const randomXZ = new ParticleRandomBlock("Random XZ"); - randomXZ.lockMode = ParticleRandomBlockLocks.PerParticle; - randomXZMin.output.connectTo(randomXZ.min); - randomXZMax.output.connectTo(randomXZ.max); - - const randomAngleMin = new ParticleInputBlock("Random Angle Min"); - randomAngleMin.value = -Math.PI; - const randomAngleMax = new ParticleInputBlock("Random Angle Max"); - randomAngleMax.value = Math.PI; - const randomAngle = new ParticleRandomBlock("Random Angle"); - randomAngle.lockMode = ParticleRandomBlockLocks.PerParticle; - randomAngleMin.output.connectTo(randomAngle.min); - randomAngleMax.output.connectTo(randomAngle.max); - - const randomRangeMin = new ParticleInputBlock("Random Range Min"); - randomRangeMin.value = 1; - const randomRangeMax = new ParticleInputBlock("Random Range Max"); - randomRangeMax.value = 5; - const randomRange = new ParticleRandomBlock("Random Range"); - randomRange.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRangeMin.output.connectTo(randomRange.min); - randomRangeMax.output.connectTo(randomRange.max); - - const one = new ParticleInputBlock("One"); - one.value = 1; - const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); - cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; - // Store angle in props so we can reuse during update - const setAnglePropInit = new SpsParticlePropsSetBlock("Set Angle Prop Init"); - setAnglePropInit.propertyName = "angle"; - randomAngle.output.connectTo(setAnglePropInit.value); - setAnglePropInit.output.connectTo(cosAngle.input); - const addOne = new ParticleMathBlock("Add One"); - addOne.operation = ParticleMathBlockOperations.Add; - one.output.connectTo(addOne.left); - cosAngle.output.connectTo(addOne.right); - const multiplyRange = new ParticleMathBlock("Multiply Range"); - multiplyRange.operation = ParticleMathBlockOperations.Multiply; - const setRangePropInit = new SpsParticlePropsSetBlock("Set Range Prop Init"); - setRangePropInit.propertyName = "range"; - randomRange.output.connectTo(setRangePropInit.value); - setRangePropInit.output.connectTo(multiplyRange.left); - addOne.output.connectTo(multiplyRange.right); - - const extractXZ = new ParticleConverterBlock("Extract XZ"); - randomXZ.output.connectTo(extractXZ.xyIn); - const positionConverter = new ParticleConverterBlock("Position Converter"); - extractXZ.xOut.connectTo(positionConverter.xIn); - multiplyRange.output.connectTo(positionConverter.yIn); - extractXZ.yOut.connectTo(positionConverter.zIn); - positionConverter.xyzOut.connectTo(spsInitTetra.position); - - const randomRotMin = new ParticleInputBlock("Random Rot Min"); - randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); - const randomRotMax = new ParticleInputBlock("Random Rot Max"); - randomRotMax.value = new Vector3(Math.PI, Math.PI, Math.PI); - const randomRot = new ParticleRandomBlock("Random Rotation"); - randomRot.lockMode = ParticleRandomBlockLocks.PerParticle; - randomRotMin.output.connectTo(randomRot.min); - randomRotMax.output.connectTo(randomRot.max); - randomRot.output.connectTo(spsInitTetra.rotation); - - const randomColorMin = new ParticleInputBlock("Random Color Min"); - randomColorMin.value = new Vector3(0, 0, 0); - const randomColorMax = new ParticleInputBlock("Random Color Max"); - randomColorMax.value = new Vector3(1, 1, 1); - const randomColorRGB = new ParticleRandomBlock("Random Color RGB"); - randomColorRGB.lockMode = ParticleRandomBlockLocks.PerParticle; - randomColorMin.output.connectTo(randomColorRGB.min); - randomColorMax.output.connectTo(randomColorRGB.max); - const colorAlpha = new ParticleInputBlock("Color Alpha"); - colorAlpha.value = 1; - const colorConverter = new ParticleConverterBlock("Color Converter"); - randomColorRGB.output.connectTo(colorConverter.xyzIn); - colorAlpha.output.connectTo(colorConverter.wIn); - colorConverter.colorOut.connectTo(spsInitTetra.color); - - // Create update block - const spsUpdateTetra = new SPSUpdateBlock("Update Tetrahedron Particles"); - spsUpdateTetra.updateData.connectTo(spsCreateTetra.updateBlock); - - // Get current position (X, Z stay the same, Y updates) - const currentPosition = new ParticleInputBlock("Current Position"); - currentPosition.contextualValue = NodeParticleContextualSources.Position; - - // Extract X and Z from current position - const extractPosition = new ParticleConverterBlock("Extract Position"); - currentPosition.output.connectTo(extractPosition.xyzIn); - - // Retrieve stored properties - const getAngleProp = new SpsParticlePropsGetBlock("Get Angle Prop"); - getAngleProp.propertyName = "angle"; - getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getRangeProp = new SpsParticlePropsGetBlock("Get Range Prop"); - getRangeProp.propertyName = "range"; - getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; - - // Accumulate angle using delta time to avoid relying on absolute frame id - const deltaBlock = new ParticleInputBlock("Delta Time"); - deltaBlock.systemSource = NodeParticleSystemSources.Delta; - - const milliToSecond = new ParticleInputBlock("Milli To Second"); - milliToSecond.value = 0.001; - - const deltaSeconds = new ParticleMathBlock("Delta Seconds"); - deltaSeconds.operation = ParticleMathBlockOperations.Multiply; - deltaBlock.output.connectTo(deltaSeconds.left); - milliToSecond.output.connectTo(deltaSeconds.right); - - const targetFps = new ParticleInputBlock("Target FPS"); - targetFps.value = 60; - - const normalizedDelta = new ParticleMathBlock("Normalized Delta"); - normalizedDelta.operation = ParticleMathBlockOperations.Multiply; - deltaSeconds.output.connectTo(normalizedDelta.left); - targetFps.output.connectTo(normalizedDelta.right); - - const speedPerFrame = new ParticleInputBlock("Speed Per Frame"); - speedPerFrame.value = Math.PI / 100; - - const scaledIncrement = new ParticleMathBlock("Scaled Increment"); - scaledIncrement.operation = ParticleMathBlockOperations.Multiply; - speedPerFrame.output.connectTo(scaledIncrement.left); - normalizedDelta.output.connectTo(scaledIncrement.right); - - const accumulateAngle = new ParticleMathBlock("Accumulate Angle"); - accumulateAngle.operation = ParticleMathBlockOperations.Add; - getAngleProp.output.connectTo(accumulateAngle.left); - scaledIncrement.output.connectTo(accumulateAngle.right); - - const setAnglePropUpdate = new SpsParticlePropsSetBlock("Set Angle Prop Update"); - setAnglePropUpdate.propertyName = "angle"; - setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; - accumulateAngle.output.connectTo(setAnglePropUpdate.value); - - // Calculate new Y position: range * (1 + cos(angle)) - const oneUpdate = new ParticleInputBlock("One Update"); - oneUpdate.value = 1; - const cosUpdatedAngle = new ParticleTrigonometryBlock("Cos Updated Angle"); - cosUpdatedAngle.operation = ParticleTrigonometryBlockOperations.Cos; - setAnglePropUpdate.output.connectTo(cosUpdatedAngle.input); - const addOneUpdate = new ParticleMathBlock("Add One Update"); - addOneUpdate.operation = ParticleMathBlockOperations.Add; - oneUpdate.output.connectTo(addOneUpdate.left); - cosUpdatedAngle.output.connectTo(addOneUpdate.right); - const multiplyRangeUpdate = new ParticleMathBlock("Multiply Range Update"); - multiplyRangeUpdate.operation = ParticleMathBlockOperations.Multiply; - getRangeProp.output.connectTo(multiplyRangeUpdate.left); - addOneUpdate.output.connectTo(multiplyRangeUpdate.right); - - // Combine X (from current position), Y (new), Z (from current position) - const updatePositionConverter = new ParticleConverterBlock("Update Position Converter"); - extractPosition.xOut.connectTo(updatePositionConverter.xIn); - multiplyRangeUpdate.output.connectTo(updatePositionConverter.yIn); - extractPosition.zOut.connectTo(updatePositionConverter.zIn); - updatePositionConverter.xyzOut.connectTo(spsUpdateTetra.position); - - this._systemBlocks.push(spsSystem); - } - - /** - * Sets the current set to an SPS shockwave preset inspired by Patrick Ryan's createShockwave sample - */ - public createShockwaveSps() { this.clear(); this.editorData = null; - - const spsSystem = new SPSSystemBlock("Shockwave SPS System"); - spsSystem.billboard = false; - - const lifetimeMs = new ParticleInputBlock("Shockwave Lifetime (ms)"); - lifetimeMs.value = 2500; - const minLifetimeMs = new ParticleInputBlock("Shockwave Min Lifetime (ms)"); - minLifetimeMs.value = 1; - const lifetimeSafe = new ParticleMathBlock("Shockwave Lifetime Safe"); - lifetimeSafe.operation = ParticleMathBlockOperations.Max; - lifetimeMs.output.connectTo(lifetimeSafe.left); - minLifetimeMs.output.connectTo(lifetimeSafe.right); - lifetimeSafe.output.connectTo(spsSystem.lifeTime); - spsSystem.disposeOnEnd = true; - - const spsCreateBlock = new SPSCreateBlock("Create Shockwave SPS"); - spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); - - const shockwaveConfig = new SPSParticleConfigBlock("Shockwave Particle Config"); - shockwaveConfig.count.value = 7; - shockwaveConfig.config.connectTo(spsCreateBlock.config); - - const shockwaveMesh = new SPSMeshSourceBlock("Shockwave Mesh Source"); - shockwaveMesh.remoteMeshUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/gltf/shockwaveMesh.glb"; - shockwaveMesh.remoteMeshName = "shockwaveMesh"; - shockwaveMesh.mesh.connectTo(shockwaveConfig.mesh); - - const shockwaveMaterial = new SPSNodeMaterialBlock("Shockwave Material"); - shockwaveMaterial.shaderUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/shaders/shockwaveParticleShader.json"; - shockwaveMaterial.textureBlockName = "particleTex"; - const shockwaveTexture = new ParticleTextureSourceBlock("Shockwave Texture"); - shockwaveTexture.url = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/textures/electricityRing.png"; - shockwaveTexture.texture.connectTo(shockwaveMaterial.texture); - shockwaveMaterial.material.connectTo(shockwaveConfig.material); - - const shockwaveInit = new SPSInitBlock("Initialize Shockwave Particles"); - shockwaveInit.initData.connectTo(shockwaveConfig.initBlock); - - const shockwaveUpdate = new SPSUpdateBlock("Update Shockwave Particles"); - shockwaveUpdate.updateData.connectTo(shockwaveConfig.updateBlock); - - const deltaBlock = new ParticleInputBlock("Shockwave Delta Time"); - deltaBlock.systemSource = NodeParticleSystemSources.Delta; - const milliToSecond = new ParticleInputBlock("Shockwave Milli To Second"); - milliToSecond.value = 0.001; - const deltaSeconds = new ParticleMathBlock("Shockwave Delta Seconds"); - deltaSeconds.operation = ParticleMathBlockOperations.Multiply; - deltaBlock.output.connectTo(deltaSeconds.left); - milliToSecond.output.connectTo(deltaSeconds.right); - const targetFps = new ParticleInputBlock("Shockwave Target FPS"); - targetFps.value = 60; - const normalizedDelta = new ParticleMathBlock("Shockwave Normalized Delta"); - normalizedDelta.operation = ParticleMathBlockOperations.Multiply; - deltaSeconds.output.connectTo(normalizedDelta.left); - targetFps.output.connectTo(normalizedDelta.right); - - const lifetimeSeconds = new ParticleMathBlock("Shockwave Lifetime Seconds"); - lifetimeSeconds.operation = ParticleMathBlockOperations.Multiply; - lifetimeSafe.output.connectTo(lifetimeSeconds.left); - milliToSecond.output.connectTo(lifetimeSeconds.right); - const framesPerLifetime = new ParticleMathBlock("Shockwave Frames Per Lifetime"); - framesPerLifetime.operation = ParticleMathBlockOperations.Multiply; - lifetimeSeconds.output.connectTo(framesPerLifetime.left); - targetFps.output.connectTo(framesPerLifetime.right); - - const origin = new ParticleInputBlock("Shockwave Origin"); - origin.value = new Vector3(0, 0.05, 0); - origin.output.connectTo(shockwaveInit.position); - - const shockwaveColor = new ParticleInputBlock("Shockwave Base Color"); - shockwaveColor.value = new Color4(0.33, 0.49, 0.88, 0.9); - shockwaveColor.output.connectTo(shockwaveInit.color); - - const zeroValue = new ParticleInputBlock("Shockwave Zero"); - zeroValue.value = 0; - - const radiusStart = new ParticleInputBlock("Shockwave Radius Start"); - radiusStart.value = 1; - const storeRadiusInit = new SpsParticlePropsSetBlock("Store Radius Init"); - storeRadiusInit.propertyName = "radius"; - storeRadiusInit.type = NodeParticleBlockConnectionPointTypes.Float; - radiusStart.output.connectTo(storeRadiusInit.value); - - const maxRadius = new ParticleInputBlock("Shockwave Max Radius"); - maxRadius.value = 4; - - const radiusRangeBlock = new ParticleMathBlock("Shockwave Radius Range"); - radiusRangeBlock.operation = ParticleMathBlockOperations.Subtract; - maxRadius.output.connectTo(radiusRangeBlock.left); - radiusStart.output.connectTo(radiusRangeBlock.right); - - const growthMultiplierMin = new ParticleInputBlock("Shockwave Growth Multiplier Min"); - growthMultiplierMin.value = 0.85; - const growthMultiplierMax = new ParticleInputBlock("Shockwave Growth Multiplier Max"); - growthMultiplierMax.value = 1.15; - const growthMultiplier = new ParticleRandomBlock("Shockwave Growth Multiplier"); - growthMultiplier.lockMode = ParticleRandomBlockLocks.OncePerParticle; - growthMultiplierMin.output.connectTo(growthMultiplier.min); - growthMultiplierMax.output.connectTo(growthMultiplier.max); - - const baseGrowthPerFrame = new ParticleMathBlock("Shockwave Base Growth Per Frame"); - baseGrowthPerFrame.operation = ParticleMathBlockOperations.Divide; - radiusRangeBlock.output.connectTo(baseGrowthPerFrame.left); - framesPerLifetime.output.connectTo(baseGrowthPerFrame.right); - - const growthPerFrame = new ParticleMathBlock("Shockwave Growth Per Frame"); - growthPerFrame.operation = ParticleMathBlockOperations.Multiply; - baseGrowthPerFrame.output.connectTo(growthPerFrame.left); - growthMultiplier.output.connectTo(growthPerFrame.right); - - const storeScaleStepInit = new SpsParticlePropsSetBlock("Store Scale Step Init"); - storeScaleStepInit.propertyName = "scaleStep"; - storeScaleStepInit.type = NodeParticleBlockConnectionPointTypes.Float; - growthPerFrame.output.connectTo(storeScaleStepInit.value); - - const initScaleConverter = new ParticleConverterBlock("Shockwave Init Scale Converter"); - storeRadiusInit.output.connectTo(initScaleConverter.xIn); - storeScaleStepInit.output.connectTo(initScaleConverter.yIn); - storeRadiusInit.output.connectTo(initScaleConverter.zIn); - initScaleConverter.xyzOut.connectTo(shockwaveInit.scaling); - - const rotationMin = new ParticleInputBlock("Shockwave Rotation Min"); - rotationMin.value = new Vector3(0, -Math.PI, 0); - const rotationMax = new ParticleInputBlock("Shockwave Rotation Max"); - rotationMax.value = new Vector3(0, Math.PI, 0); - const initialRotation = new ParticleRandomBlock("Shockwave Initial Rotation"); - initialRotation.lockMode = ParticleRandomBlockLocks.OncePerParticle; - rotationMin.output.connectTo(initialRotation.min); - rotationMax.output.connectTo(initialRotation.max); - - const rotationConverter = new ParticleConverterBlock("Shockwave Rotation Converter"); - initialRotation.output.connectTo(rotationConverter.xyzIn); - const storeRotationAngleInit = new SpsParticlePropsSetBlock("Store Rotation Angle Init"); - storeRotationAngleInit.propertyName = "rotationAngle"; - storeRotationAngleInit.type = NodeParticleBlockConnectionPointTypes.Float; - rotationConverter.yOut.connectTo(storeRotationAngleInit.value); - - const rotationCompose = new ParticleConverterBlock("Shockwave Rotation Compose"); - rotationConverter.xOut.connectTo(rotationCompose.xIn); - storeRotationAngleInit.output.connectTo(rotationCompose.yIn); - rotationConverter.zOut.connectTo(rotationCompose.zIn); - rotationCompose.xyzOut.connectTo(shockwaveInit.rotation); - - const rotationSpeedMin = new ParticleInputBlock("Shockwave Rotation Speed Min"); - rotationSpeedMin.value = -0.06; - const rotationSpeedMax = new ParticleInputBlock("Shockwave Rotation Speed Max"); - rotationSpeedMax.value = 0.06; - const rotationSpeedRandom = new ParticleRandomBlock("Shockwave Rotation Speed Random"); - rotationSpeedRandom.lockMode = ParticleRandomBlockLocks.OncePerParticle; - rotationSpeedMin.output.connectTo(rotationSpeedRandom.min); - rotationSpeedMax.output.connectTo(rotationSpeedRandom.max); - const storeRotationSpeed = new SpsParticlePropsSetBlock("Store Rotation Speed"); - storeRotationSpeed.propertyName = "rotationSpeed"; - storeRotationSpeed.type = NodeParticleBlockConnectionPointTypes.Float; - rotationSpeedRandom.output.connectTo(storeRotationSpeed.value); - - const rotationSpeedSink = new ParticleMathBlock("Shockwave Rotation Speed Sink"); - rotationSpeedSink.operation = ParticleMathBlockOperations.Multiply; - storeRotationSpeed.output.connectTo(rotationSpeedSink.left); - zeroValue.output.connectTo(rotationSpeedSink.right); - const rotationSpeedVelocity = new ParticleConverterBlock("Shockwave Rotation Speed Velocity"); - rotationSpeedSink.output.connectTo(rotationSpeedVelocity.xIn); - zeroValue.output.connectTo(rotationSpeedVelocity.yIn); - zeroValue.output.connectTo(rotationSpeedVelocity.zIn); - rotationSpeedVelocity.xyzOut.connectTo(shockwaveInit.velocity); - - const getRadiusProp = new SpsParticlePropsGetBlock("Get Radius Prop"); - getRadiusProp.propertyName = "radius"; - getRadiusProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getScaleStepProp = new SpsParticlePropsGetBlock("Get Scale Step Prop"); - getScaleStepProp.propertyName = "scaleStep"; - getScaleStepProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getRotationSpeedProp = new SpsParticlePropsGetBlock("Get Rotation Speed Prop"); - getRotationSpeedProp.propertyName = "rotationSpeed"; - getRotationSpeedProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const getRotationAngleProp = new SpsParticlePropsGetBlock("Get Rotation Angle Prop"); - getRotationAngleProp.propertyName = "rotationAngle"; - getRotationAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; - - const scaleStepDelta = new ParticleMathBlock("Shockwave Radius Delta"); - scaleStepDelta.operation = ParticleMathBlockOperations.Multiply; - getScaleStepProp.output.connectTo(scaleStepDelta.left); - normalizedDelta.output.connectTo(scaleStepDelta.right); - - const radiusIncrement = new ParticleMathBlock("Shockwave Radius Increment"); - radiusIncrement.operation = ParticleMathBlockOperations.Add; - getRadiusProp.output.connectTo(radiusIncrement.left); - scaleStepDelta.output.connectTo(radiusIncrement.right); - - const setRadiusPropUpdate = new SpsParticlePropsSetBlock("Set Radius Prop Update"); - setRadiusPropUpdate.propertyName = "radius"; - setRadiusPropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; - radiusIncrement.output.connectTo(setRadiusPropUpdate.value); - - const clampRadius = new ParticleMathBlock("Shockwave Clamp Radius"); - clampRadius.operation = ParticleMathBlockOperations.Min; - setRadiusPropUpdate.output.connectTo(clampRadius.left); - maxRadius.output.connectTo(clampRadius.right); - - const normalizedRadius = new ParticleMathBlock("Shockwave Normalized Radius"); - normalizedRadius.operation = ParticleMathBlockOperations.Divide; - clampRadius.output.connectTo(normalizedRadius.left); - maxRadius.output.connectTo(normalizedRadius.right); - - const normalizedMin = new ParticleMathBlock("Shockwave Normalized Min"); - normalizedMin.operation = ParticleMathBlockOperations.Max; - zeroValue.output.connectTo(normalizedMin.left); - normalizedRadius.output.connectTo(normalizedMin.right); - - const oneValue = new ParticleInputBlock("Shockwave One"); - oneValue.value = 1; - const normalizedClamp = new ParticleMathBlock("Shockwave Normalized Clamp"); - normalizedClamp.operation = ParticleMathBlockOperations.Min; - normalizedMin.output.connectTo(normalizedClamp.left); - oneValue.output.connectTo(normalizedClamp.right); - - const minThickness = new ParticleInputBlock("Shockwave Min Thickness"); - minThickness.value = 0.25; - const maxThickness = new ParticleInputBlock("Shockwave Max Thickness"); - maxThickness.value = 4; - const thicknessRange = new ParticleMathBlock("Shockwave Thickness Range"); - thicknessRange.operation = ParticleMathBlockOperations.Subtract; - maxThickness.output.connectTo(thicknessRange.left); - minThickness.output.connectTo(thicknessRange.right); - const thicknessScale = new ParticleMathBlock("Shockwave Thickness Scale"); - thicknessScale.operation = ParticleMathBlockOperations.Multiply; - thicknessRange.output.connectTo(thicknessScale.left); - normalizedClamp.output.connectTo(thicknessScale.right); - const thicknessValue = new ParticleMathBlock("Shockwave Thickness Value"); - thicknessValue.operation = ParticleMathBlockOperations.Add; - minThickness.output.connectTo(thicknessValue.left); - thicknessScale.output.connectTo(thicknessValue.right); - - const minHeight = new ParticleInputBlock("Shockwave Min Height"); - minHeight.value = 0.05; - const maxHeight = new ParticleInputBlock("Shockwave Max Height"); - maxHeight.value = 0.25; - const heightRange = new ParticleMathBlock("Shockwave Height Range"); - heightRange.operation = ParticleMathBlockOperations.Subtract; - maxHeight.output.connectTo(heightRange.left); - minHeight.output.connectTo(heightRange.right); - const heightScale = new ParticleMathBlock("Shockwave Height Scale"); - heightScale.operation = ParticleMathBlockOperations.Multiply; - heightRange.output.connectTo(heightScale.left); - normalizedClamp.output.connectTo(heightScale.right); - const heightValue = new ParticleMathBlock("Shockwave Height Value"); - heightValue.operation = ParticleMathBlockOperations.Add; - minHeight.output.connectTo(heightValue.left); - heightScale.output.connectTo(heightValue.right); - - const scalingConverter = new ParticleConverterBlock("Shockwave Scaling Converter"); - clampRadius.output.connectTo(scalingConverter.xIn); - thicknessValue.output.connectTo(scalingConverter.yIn); - clampRadius.output.connectTo(scalingConverter.zIn); - scalingConverter.xyzOut.connectTo(shockwaveUpdate.scaling); - - const positionConverter = new ParticleConverterBlock("Shockwave Position Converter"); - zeroValue.output.connectTo(positionConverter.xIn); - heightValue.output.connectTo(positionConverter.yIn); - zeroValue.output.connectTo(positionConverter.zIn); - positionConverter.xyzOut.connectTo(shockwaveUpdate.position); - - const rotationIncrement = new ParticleMathBlock("Shockwave Rotation Increment"); - rotationIncrement.operation = ParticleMathBlockOperations.Multiply; - getRotationSpeedProp.output.connectTo(rotationIncrement.left); - normalizedDelta.output.connectTo(rotationIncrement.right); - - const updatedRotationAngle = new ParticleMathBlock("Shockwave Updated Rotation Angle"); - updatedRotationAngle.operation = ParticleMathBlockOperations.Add; - getRotationAngleProp.output.connectTo(updatedRotationAngle.left); - rotationIncrement.output.connectTo(updatedRotationAngle.right); - - const setRotationAngleUpdate = new SpsParticlePropsSetBlock("Set Rotation Angle Update"); - setRotationAngleUpdate.propertyName = "rotationAngle"; - setRotationAngleUpdate.type = NodeParticleBlockConnectionPointTypes.Float; - updatedRotationAngle.output.connectTo(setRotationAngleUpdate.value); - - const rotationUpdateConverter = new ParticleConverterBlock("Shockwave Rotation Update Converter"); - zeroValue.output.connectTo(rotationUpdateConverter.xIn); - setRotationAngleUpdate.output.connectTo(rotationUpdateConverter.yIn); - zeroValue.output.connectTo(rotationUpdateConverter.zIn); - rotationUpdateConverter.xyzOut.connectTo(shockwaveUpdate.rotation); - - const colorEnd = new ParticleInputBlock("Shockwave Color End"); - colorEnd.value = new Color4(0, 0, 0, 0); - const colorRange = new ParticleMathBlock("Shockwave Color Range"); - colorRange.operation = ParticleMathBlockOperations.Subtract; - colorEnd.output.connectTo(colorRange.left); - shockwaveColor.output.connectTo(colorRange.right); - const colorScale = new ParticleMathBlock("Shockwave Color Scale"); - colorScale.operation = ParticleMathBlockOperations.Multiply; - colorRange.output.connectTo(colorScale.left); - normalizedClamp.output.connectTo(colorScale.right); - const colorValue = new ParticleMathBlock("Shockwave Color Value"); - colorValue.operation = ParticleMathBlockOperations.Add; - shockwaveColor.output.connectTo(colorValue.left); - colorScale.output.connectTo(colorValue.right); - colorValue.output.connectTo(shockwaveUpdate.color); - - this._systemBlocks.push(spsSystem); } /** diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index adfe9a175d3..db24e45debb 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -1566,7 +1566,9 @@ export class SolidParticleSystem implements IDisposable { */ public dispose(): void { this.stop(); - this.mesh.dispose(); + if (this.mesh) { + this.mesh.dispose(); + } this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 7a0d554b71a..d8e2a3abff1 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -43,7 +43,7 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock, SPSUpdateBlock, SPSParticleConfigBlock } from "core/Particles/Node/Blocks"; +import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSUpdateBlock, SPSParticleConfigBlock, SPSNodeMaterialBlock } from "core/Particles/Node/Blocks"; import { ParticleFloatToIntBlock } from "core/Particles/Node/Blocks/particleFloatToIntBlock"; /** @@ -159,12 +159,12 @@ export class BlockTools { return new SPSMeshSourceBlock("SPS Mesh Source"); case "SPSParticleConfigBlock": return new SPSParticleConfigBlock("SPS Particle Config"); + case "SPSNodeMaterialBlock": + return new SPSNodeMaterialBlock("SPS Node Material"); case "SPSSystemBlock": return new SPSSystemBlock("SPS System"); case "SPSCreateBlock": return new SPSCreateBlock("SPS Create"); - case "SPSInitBlock": - return new SPSInitBlock("SPS Init"); case "SPSUpdateBlock": return new SPSUpdateBlock("SPS Update"); case "TextureBlock": diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 13349c082ed..db7e4bdfa39 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -24,12 +24,12 @@ export class NodeListComponent extends React.Component { DisplayLedger.RegisteredControls["SystemBlock"] = SystemDisplayManager; DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSNodeMaterialBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSParticleConfigBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; - DisplayLedger.RegisteredControls["SPSInitBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["SPSUpdateBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 4ce78a4ac1e..a12909b5091 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -20,7 +20,6 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["SPSParticleConfigBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SPSUpdateBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SpsParticlePropsSetBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SpsParticlePropsGetBlock"] = GenericPropertyComponent; From 52877efea734c5fbbf484d9ef945217e3e1f690f Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 24 Nov 2025 14:19:49 +0300 Subject: [PATCH 54/68] Refactor SPSParticleConfigBlock and SPSUpdateBlock for improved particle configuration and update handling - Enhanced `SPSParticleConfigBlock` to include additional parameters such as `position`, `velocity`, `color`, `scaling`, and `rotation`. - Updated the `lifeTime` input to default to `Infinity` for better lifecycle management. - Removed the `updateBlock` input from `SPSParticleConfigBlock` and adjusted the output structure in `SPSUpdateBlock` to incorporate the new configuration data. - Streamlined input and output handling in `SPSUpdateBlock` to improve clarity and maintainability. These changes enhance the flexibility and robustness of the solid particle system, aligning it with the recent architectural improvements in the particle system framework. --- .../SolidParticle/SPSParticleConfigBlock.ts | 17 ++++---------- .../Blocks/SolidParticle/SPSUpdateBlock.ts | 23 +++++++++++-------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts index 18d71314978..1b563e9f301 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -6,24 +6,24 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISpsParticleConfigData, ISpsUpdateData } from "./ISPSData"; +import type { ISpsParticleConfigData } from "./ISPSData"; /** - * Block used to configure SPS particle parameters (mesh, count, material, initBlock, updateBlock) + * Block used to configure SPS particle parameters (mesh, count, material, position, velocity, color, scaling, rotation) */ export class SPSParticleConfigBlock extends NodeParticleBlock { public constructor(name: string) { super(name); - this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); - this.registerInput("lifeTime", NodeParticleBlockConnectionPointTypes.Float, true); + this.registerInput("lifeTime", NodeParticleBlockConnectionPointTypes.Float, true, Infinity); this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); this.registerInput("material", NodeParticleBlockConnectionPointTypes.Material, true); - this.registerInput("updateBlock", NodeParticleBlockConnectionPointTypes.System, true); this.registerOutput("config", NodeParticleBlockConnectionPointTypes.SolidParticleConfig); } @@ -68,10 +68,6 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { return this._inputs[8]; } - public get updateBlock(): NodeParticleConnectionPoint { - return this._inputs[9]; - } - public get config(): NodeParticleConnectionPoint { return this._outputs[0]; } @@ -88,8 +84,6 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { const scaling = (this.scaling.getConnectedValue(state) as Vector3) ?? new Vector3(1, 1, 1); const rotation = (this.rotation.getConnectedValue(state) as Vector3) ?? new Vector3(0, 0, 0); - const updateBlock = this.updateBlock.isConnected ? (this.updateBlock.getConnectedValue(state) as ISpsUpdateData) : null; - const particleConfig: ISpsParticleConfigData = { meshData, count, @@ -100,7 +94,6 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { color, scaling, rotation, - updateBlock, }; this.config._storedValue = particleConfig; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts index f3cdafc4a77..598eb3a805a 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -5,7 +5,7 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISpsUpdateData } from "./ISPSData"; +import type { ISpsParticleConfigData, ISpsUpdateData } from "./ISPSData"; /** * Block used to generate update function for SPS particles @@ -14,40 +14,45 @@ export class SPSUpdateBlock extends NodeParticleBlock { public constructor(name: string) { super(name); + this.registerInput("config", NodeParticleBlockConnectionPointTypes.SolidParticleConfig); this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); - this.registerOutput("updateData", NodeParticleBlockConnectionPointTypes.System); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticleConfig); } public override getClassName() { return "SPSUpdateBlock"; } - public get position(): NodeParticleConnectionPoint { + public get configInput(): NodeParticleConnectionPoint { return this._inputs[0]; } - public get velocity(): NodeParticleConnectionPoint { + public get position(): NodeParticleConnectionPoint { return this._inputs[1]; } - public get color(): NodeParticleConnectionPoint { + public get velocity(): NodeParticleConnectionPoint { return this._inputs[2]; } - public get scaling(): NodeParticleConnectionPoint { + public get color(): NodeParticleConnectionPoint { return this._inputs[3]; } - public get rotation(): NodeParticleConnectionPoint { + public get scaling(): NodeParticleConnectionPoint { return this._inputs[4]; } - public get updateData(): NodeParticleConnectionPoint { + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[5]; + } + + public get output(): NodeParticleConnectionPoint { return this._outputs[0]; } @@ -78,7 +83,7 @@ export class SPSUpdateBlock extends NodeParticleBlock { return this.rotation.getConnectedValue(state); }; } - this.updateData._storedValue = updateData; + this.output._storedValue = { ...(this.configInput.getConnectedValue(state) as ISpsParticleConfigData), updateBlock: updateData }; } public override serialize(): any { From a3b6630dcb0d8e1bf1c00910693e7b437fdf8263 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 24 Nov 2025 14:21:24 +0300 Subject: [PATCH 55/68] Refactor SPSMeshSourcePropertyTabComponent and SPSNodeMaterialPropertyTabComponent for improved readability - Simplified conditional rendering in `SPSMeshSourcePropertyTabComponent` by consolidating JSX elements into single lines. - Removed unnecessary whitespace and comments in `SPSNodeMaterialPropertyTabComponent` to enhance code clarity. These changes improve the maintainability and readability of the property components in the node particle editor. --- .../properties/spsMeshSourceNodePropertyComponent.tsx | 11 ++--------- .../properties/spsNodeMaterialPropertyComponent.tsx | 1 - 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsMeshSourceNodePropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsMeshSourceNodePropertyComponent.tsx index f1ee1c316d4..accaacb93fe 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsMeshSourceNodePropertyComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsMeshSourceNodePropertyComponent.tsx @@ -113,11 +113,7 @@ export class SPSMeshSourcePropertyTabComponent extends React.Component - {block.hasCustomMesh ? ( - - ) : ( - - )} + {block.hasCustomMesh ? : } {this.state.isLoading && } {!this.state.isLoading && await this.loadMesh(file)} />} {scene && meshOptions.length > 1 && ( @@ -139,12 +135,9 @@ export class SPSMeshSourcePropertyTabComponent extends React.Component )} - {block.hasCustomMesh && ( - this.removeData()} /> - )} + {block.hasCustomMesh && this.removeData()} />} ); } } - diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx index 51de414aa97..8f0ea22a763 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx @@ -70,4 +70,3 @@ export class SPSNodeMaterialPropertyTabComponent extends React.Component Date: Mon, 24 Nov 2025 15:20:25 +0300 Subject: [PATCH 56/68] Enhance SolidParticle support in NodeParticle system - Added handling for `SolidParticleIndex` in `NodeParticleBuildState` and `ParticleInputBlock` to improve contextual data retrieval. - Updated `ISpsParticleConfigData` to use functions for properties like `position`, `velocity`, `color`, `scaling`, `rotation`, and `lifeTime`, enhancing flexibility in particle configuration. - Modified `SPSCreateBlock` to utilize these function-based properties for particle initialization. - Introduced `SolidParticleIndexBlock` in the node particle editor for better integration and usability. These changes improve the integration and functionality of solid particles within the node particle system, aligning with recent architectural enhancements. --- .../Node/Blocks/SolidParticle/ISPSData.ts | 12 ++++++------ .../Blocks/SolidParticle/SPSCreateBlock.ts | 15 +++++++++------ .../SolidParticle/SPSParticleConfigBlock.ts | 19 ++++++++++++------- .../Node/Blocks/particleInputBlock.ts | 1 + .../Enums/nodeParticleContextualSources.ts | 2 ++ .../Particles/Node/nodeParticleBuildState.ts | 5 +++++ .../nodeParticleEditor/src/blockTools.ts | 5 +++++ .../components/nodeList/nodeListComponent.tsx | 2 ++ 8 files changed, 42 insertions(+), 19 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts index df766ccaa1f..02f6d9802f9 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -26,11 +26,11 @@ export interface ISpsParticleConfigData { meshData: ISpsMeshSourceData | null; count: number; material?: Material; - position?: Vector3; - velocity?: Vector3; - color?: Color4; - scaling?: Vector3; - rotation?: Vector3; - lifeTime?: number; + position?: () => Vector3; + velocity?: () => Vector3; + color?: () => Color4; + scaling?: () => Vector3; + rotation?: () => Vector3; + lifeTime?: () => number; updateBlock?: ISpsUpdateData | null; } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts index e1f94328795..ac92571e8d7 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -141,25 +141,28 @@ export class SPSCreateBlock extends NodeParticleBlock { state.systemContext = sps; if (lifeTime) { - particle.lifeTime = lifeTime; + particle.lifeTime = lifeTime(); particle.age = 0; particle.alive = true; } if (position) { - particle.position.copyFrom(position); + particle.position.copyFrom(position()); } if (velocity) { - particle.velocity.copyFrom(velocity); + particle.velocity.copyFrom(velocity()); } if (color) { - particle.color?.copyFrom(color); + const particleColor = particle.color; + if (particleColor) { + particleColor.copyFrom(color()); + } } if (scaling) { - particle.scaling.copyFrom(scaling); + particle.scaling.copyFrom(scaling()); } if (rotation) { - particle.rotation.copyFrom(rotation); + particle.rotation.copyFrom(rotation()); } } } finally { diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts index 1b563e9f301..c57ab5d237d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { Color4, Vector3 } from "../../../../Maths"; +import type { Color4, Vector3 } from "../../../../Maths"; import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; @@ -75,14 +75,18 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { public override _build(state: NodeParticleBuildState) { const meshData = this.mesh.getConnectedValue(state); const count = (this.count.getConnectedValue(state) as number) ?? 1; - const lifeTime = (this.lifeTime.getConnectedValue(state) as number) ?? Infinity; + const lifeTime = this.lifeTime.isConnected + ? () => { + return (this.lifeTime.getConnectedValue(state) as number) ?? Infinity; + } + : undefined; const material = this.material.getConnectedValue(state); - const position = (this.position.getConnectedValue(state) as Vector3) ?? new Vector3(0, 0, 0); - const velocity = (this.velocity.getConnectedValue(state) as Vector3) ?? new Vector3(0, 0, 0); - const color = (this.color.getConnectedValue(state) as Color4) ?? new Color4(1, 1, 1, 1); - const scaling = (this.scaling.getConnectedValue(state) as Vector3) ?? new Vector3(1, 1, 1); - const rotation = (this.rotation.getConnectedValue(state) as Vector3) ?? new Vector3(0, 0, 0); + const position = this.position.isConnected ? () => this.position.getConnectedValue(state) as Vector3 : undefined; + const velocity = this.velocity.isConnected ? () => this.velocity.getConnectedValue(state) as Vector3 : undefined; + const color = this.color.isConnected ? () => this.color.getConnectedValue(state) as Color4 : undefined; + const scaling = this.scaling.isConnected ? () => this.scaling.getConnectedValue(state) as Vector3 : undefined; + const rotation = this.rotation.isConnected ? () => this.rotation.getConnectedValue(state) as Vector3 : undefined; const particleConfig: ISpsParticleConfigData = { meshData, @@ -94,6 +98,7 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { color, scaling, rotation, + updateBlock: null, }; this.config._storedValue = particleConfig; diff --git a/packages/dev/core/src/Particles/Node/Blocks/particleInputBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/particleInputBlock.ts index 11a52bc29a2..2c4476487cc 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/particleInputBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/particleInputBlock.ts @@ -156,6 +156,7 @@ export class ParticleInputBlock extends NodeParticleBlock { case NodeParticleContextualSources.SpriteCellEnd: case NodeParticleContextualSources.SpriteCellStart: case NodeParticleContextualSources.SpriteCellIndex: + case NodeParticleContextualSources.SolidParticleIndex: this._type = NodeParticleBlockConnectionPointTypes.Int; break; } diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleContextualSources.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleContextualSources.ts index 80a385f39ae..b1452669efd 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleContextualSources.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleContextualSources.ts @@ -45,4 +45,6 @@ export enum NodeParticleContextualSources { Size = 0x0019, /** Direction Scale */ DirectionScale = 0x0020, + /** Solid Particle Index */ + SolidParticleIndex = 0x0021, } diff --git a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts index 8a1acfe970e..694ac0c3d22 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -176,6 +176,11 @@ export class NodeParticleBuildState { return null; } return this.systemContext.startSpriteCellID; + case NodeParticleContextualSources.SolidParticleIndex: + if (!(this.particleContext instanceof SolidParticle)) { + return null; + } + return this.particleContext.idx; case NodeParticleContextualSources.InitialDirection: if (this.particleContext instanceof SolidParticle) { return null; diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index d8e2a3abff1..8ee9bc2c202 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -263,6 +263,11 @@ export class BlockTools { block.contextualValue = NodeParticleContextualSources.SpriteCellIndex; return block; } + case "SolidParticleIndexBlock": { + const block = new ParticleInputBlock("Solid Particle Index"); + block.contextualValue = NodeParticleContextualSources.SolidParticleIndex; + return block; + } case "SpriteCellStartBlock": { const block = new ParticleInputBlock("Sprite cell start"); block.contextualValue = NodeParticleContextualSources.SpriteCellStart; diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index db7e4bdfa39..1b38d292b51 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -65,6 +65,7 @@ export class NodeListComponent extends React.Component Date: Mon, 24 Nov 2025 16:12:39 +0300 Subject: [PATCH 57/68] Implement Solid Particle System Blocks in Node Particle Editor - Introduced new blocks for solid particle systems, including `CreateSolidParticleBlock`, `InitSolidParticleBlock`, `UpdateSolidParticleBlock`, `MeshSourceBlock`, `NodeMaterialBlock`, and `SolidParticleSystemBlock`. - Updated the `NodeParticleSystemSet` to utilize `SolidParticleSystemBlock` instead of the deprecated `SPSSystemBlock`. - Enhanced the property management and serialization for solid particles, improving integration within the node particle editor. - Removed obsolete blocks related to the previous solid particle system architecture, streamlining the codebase. These changes significantly enhance the functionality and usability of solid particles in the node particle editor, aligning with recent architectural improvements. --- .../{ISPSData.ts => ISolidParticleData.ts} | 17 ++- .../SolidParticle/SPSParticlePropsGetBlock.ts | 129 ----------------- .../SolidParticle/SPSParticlePropsSetBlock.ts | 133 ------------------ ...teBlock.ts => createSolidParticleBlock.ts} | 14 +- .../Node/Blocks/SolidParticle/index.ts | 16 +-- ...nfigBlock.ts => initSolidParticleBlock.ts} | 12 +- ...SMeshSourceBlock.ts => meshSourceBlock.ts} | 12 +- ...eMaterialBlock.ts => nodeMaterialBlock.ts} | 8 +- ...emBlock.ts => solidParticleSystemBlock.ts} | 12 +- ...teBlock.ts => updateSolidParticleBlock.ts} | 14 +- .../Particles/Node/nodeParticleSystemSet.ts | 6 +- .../nodeParticleEditor/src/blockTools.ts | 33 +++-- .../components/nodeList/nodeListComponent.tsx | 30 ++-- ...sx => meshSourceNodePropertyComponent.tsx} | 18 +-- ... => nodeMaterialNodePropertyComponent.tsx} | 16 +-- .../graphSystem/registerToDisplayLedger.ts | 14 +- .../graphSystem/registerToPropertyLedger.ts | 18 ++- 17 files changed, 116 insertions(+), 386 deletions(-) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{ISPSData.ts => ISolidParticleData.ts} (60%) delete mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts delete mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{SPSCreateBlock.ts => createSolidParticleBlock.ts} (93%) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{SPSParticleConfigBlock.ts => initSolidParticleBlock.ts} (89%) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{SPSMeshSourceBlock.ts => meshSourceBlock.ts} (91%) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{SPSNodeMaterialBlock.ts => nodeMaterialBlock.ts} (91%) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{SPSSystemBlock.ts => solidParticleSystemBlock.ts} (86%) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{SPSUpdateBlock.ts => updateSolidParticleBlock.ts} (85%) rename packages/tools/nodeParticleEditor/src/graphSystem/properties/{spsMeshSourceNodePropertyComponent.tsx => meshSourceNodePropertyComponent.tsx} (86%) rename packages/tools/nodeParticleEditor/src/graphSystem/properties/{spsNodeMaterialPropertyComponent.tsx => nodeMaterialNodePropertyComponent.tsx} (80%) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISolidParticleData.ts similarity index 60% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISolidParticleData.ts index 02f6d9802f9..1e2dee37efa 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISolidParticleData.ts @@ -3,15 +3,18 @@ import type { Color4 } from "core/Maths/math.color"; import type { Material } from "core/Materials/material"; import type { VertexData } from "core/Meshes/mesh.vertexData"; -export interface ISpsMeshSourceData { +/** + * Interface for solid particle mesh source data + */ +export interface ISolidParticleMeshSourceData { customMeshName?: string; vertexData?: VertexData; } /** - * Interface for SPS update block data + * Interface for solid particle update block data */ -export interface ISpsUpdateData { +export interface ISolidParticleUpdateData { position?: () => Vector3; velocity?: () => Vector3; color?: () => Color4; @@ -20,10 +23,10 @@ export interface ISpsUpdateData { } /** - * Interface for SPS create block data + * Interface for solid particle create block data */ -export interface ISpsParticleConfigData { - meshData: ISpsMeshSourceData | null; +export interface ISolidParticleInitData { + meshData: ISolidParticleMeshSourceData | null; count: number; material?: Material; position?: () => Vector3; @@ -32,5 +35,5 @@ export interface ISpsParticleConfigData { scaling?: () => Vector3; rotation?: () => Vector3; lifeTime?: () => number; - updateBlock?: ISpsUpdateData | null; + updateBlock?: ISolidParticleUpdateData | null; } diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts deleted file mode 100644 index 6798d44ab4a..00000000000 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { RegisterClass } from "../../../../Misc/typeStore"; -import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; -import { NodeParticleBlock } from "../../nodeParticleBlock"; -import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; -import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { SolidParticle } from "../../../solidParticle"; -import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; -import { serialize } from "../../../../Misc/decorators"; - -/** - * Block used to get custom properties from particle.props - * Works similar to contextual blocks but for dynamic property names - */ -export class SpsParticlePropsGetBlock extends NodeParticleBlock { - /** - * Gets or sets the property name to read from particle.props - */ - @serialize("propertyName") - @editableInPropertyPage("Property Name", PropertyTypeForEdition.String, "PROPERTIES", { - embedded: false, - notifiers: { rebuild: true }, - }) - public propertyName: string = "value"; - - /** - * Gets or sets the connection point type (default float) - */ - private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; - - /** - * Gets the value to display (returns propertyName as string) - */ - public get displayValue(): string { - return this.propertyName || "value"; - } - - public constructor(name: string) { - super(name); - - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.AutoDetect); - // Set default type - (this._outputs[0] as any)._defaultConnectionPointType = this._type; - } - - public override getClassName() { - return "SpsParticlePropsGetBlock"; - } - - /** - * Gets or sets the connection point type - */ - public get type(): NodeParticleBlockConnectionPointTypes { - return this._type; - } - - public set type(value: NodeParticleBlockConnectionPointTypes) { - if (this._type !== value) { - this._type = value; - // Update output type - (this._outputs[0] as any)._type = value; - (this._outputs[0] as any)._defaultConnectionPointType = value; - } - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - public override _build(state: NodeParticleBuildState) { - // Validate property name - if (!this.propertyName || this.propertyName.trim() === "") { - this.output._storedFunction = null; - this.output._storedValue = null; - return; - } - - // Validate type - if (this._type === NodeParticleBlockConnectionPointTypes.Undefined || this._type === NodeParticleBlockConnectionPointTypes.AutoDetect) { - this._type = NodeParticleBlockConnectionPointTypes.Float; - (this._outputs[0] as any)._type = this._type; - (this._outputs[0] as any)._defaultConnectionPointType = this._type; - } - - const propertyName = this.propertyName; - - const func = (state: NodeParticleBuildState) => { - if (!state.particleContext) { - return null; - } - - const particle = state.particleContext as SolidParticle; - - if (!particle.props) { - return null; - } - - const value = particle.props[propertyName]; - - if (value === undefined) { - return null; - } - - return value; - }; - - if (this.output.isConnected) { - this.output._storedFunction = func; - } else { - this.output._storedValue = func(state); - } - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.propertyName = this.propertyName; - serializationObject.type = this._type; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.propertyName = serializationObject.propertyName || "value"; - this._type = serializationObject.type || NodeParticleBlockConnectionPointTypes.Float; - (this._outputs[0] as any)._type = this._type; - (this._outputs[0] as any)._defaultConnectionPointType = this._type; - } -} - -RegisterClass("BABYLON.SpsParticlePropsGetBlock", SpsParticlePropsGetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts deleted file mode 100644 index 4d48cbb71a9..00000000000 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { RegisterClass } from "../../../../Misc/typeStore"; -import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; -import { NodeParticleBlock } from "../../nodeParticleBlock"; -import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; -import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { SolidParticle } from "../../../solidParticle"; -import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; -import { serialize } from "../../../../Misc/decorators"; - -/** - * Block used to set custom properties in particle.props - * Works as a side-effect block that stores values and passes them through - */ -export class SpsParticlePropsSetBlock extends NodeParticleBlock { - /** - * Gets or sets the property name to store in particle.props - */ - @serialize("propertyName") - @editableInPropertyPage("Property Name", PropertyTypeForEdition.String, "PROPERTIES", { - embedded: false, - notifiers: { rebuild: true }, - }) - public propertyName: string = "value"; - - /** - * Gets or sets the connection point type (default float) - */ - private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; - - public constructor(name: string) { - super(name); - - this.registerInput("value", NodeParticleBlockConnectionPointTypes.AutoDetect, true); - this.registerOutput("output", NodeParticleBlockConnectionPointTypes.BasedOnInput); - - // Link output type to input type - this._outputs[0]._typeConnectionSource = this._inputs[0]; - // Set default type for when input is not connected - (this._outputs[0] as any)._defaultConnectionPointType = this._type; - } - - public override getClassName() { - return "SpsParticlePropsSetBlock"; - } - - public get value(): NodeParticleConnectionPoint { - return this._inputs[0]; - } - - /** - * Gets the value to display (returns propertyName as string) - * This shadows the connection point name for display purposes - */ - public get displayValue(): string { - return this.propertyName || "value"; - } - - public get output(): NodeParticleConnectionPoint { - return this._outputs[0]; - } - - /** - * Gets or sets the connection point type - */ - public get type(): NodeParticleBlockConnectionPointTypes { - return this._type; - } - - public set type(value: NodeParticleBlockConnectionPointTypes) { - if (this._type !== value) { - this._type = value; - // Update default type (used when input is not connected) - (this._outputs[0] as any)._defaultConnectionPointType = value; - } - } - - public override _build(state: NodeParticleBuildState) { - // Validate property name - if (!this.propertyName || this.propertyName.trim() === "") { - this.output._storedFunction = null; - this.output._storedValue = null; - return; - } - - if (!this.value.isConnected) { - this.output._storedFunction = null; - this.output._storedValue = null; - return; - } - - const propertyName = this.propertyName; - - const func = (state: NodeParticleBuildState) => { - if (!state.particleContext) { - return null; - } - - const particle = state.particleContext as SolidParticle; - - const value = this.value.getConnectedValue(state); - - if (!particle.props) { - particle.props = {}; - } - - particle.props[propertyName] = value; - - return value; - }; - - if (this.output.isConnected) { - this.output._storedFunction = func; - } else { - this.output._storedValue = func(state); - } - } - - public override serialize(): any { - const serializationObject = super.serialize(); - serializationObject.propertyName = this.propertyName; - serializationObject.type = this._type; - return serializationObject; - } - - public override _deserialize(serializationObject: any) { - super._deserialize(serializationObject); - this.propertyName = serializationObject.propertyName || "value"; - this._type = serializationObject.type || NodeParticleBlockConnectionPointTypes.Float; - (this._outputs[0] as any)._defaultConnectionPointType = this._type; - } -} - -RegisterClass("BABYLON.SpsParticlePropsSetBlock", SpsParticlePropsSetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/createSolidParticleBlock.ts similarity index 93% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/createSolidParticleBlock.ts index ac92571e8d7..d37c62b8928 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/createSolidParticleBlock.ts @@ -1,12 +1,10 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; -import type { ISpsParticleConfigData } from "./ISPSData"; +import type { ISolidParticleInitData } from "./ISolidParticleData"; import { Mesh } from "core/Meshes/mesh"; import type { SolidParticle } from "../../../solidParticle"; import type { Observer } from "core/Misc/observable"; @@ -14,7 +12,7 @@ import type { Observer } from "core/Misc/observable"; /** * Block used to create SolidParticleSystem and collect all Create blocks */ -export class SPSCreateBlock extends NodeParticleBlock { +export class CreateSolidParticleBlock extends NodeParticleBlock { private _connectionObservers = new Map>(); private _disconnectionObservers = new Map>(); @@ -27,7 +25,7 @@ export class SPSCreateBlock extends NodeParticleBlock { } public override getClassName() { - return "SPSCreateBlock"; + return "CreateSolidParticleBlock"; } private _entryCount = 1; @@ -98,9 +96,9 @@ export class SPSCreateBlock extends NodeParticleBlock { useModelMaterial: true, }); - const createBlocks = new Map(); + const createBlocks = new Map(); for (let i = 0; i < this._inputs.length; i++) { - const creatData = this._inputs[i].getConnectedValue(state) as ISpsParticleConfigData; + const creatData = this._inputs[i].getConnectedValue(state) as ISolidParticleInitData; if (!this._inputs[i].isConnected || !creatData || !creatData.meshData || !creatData.count) { continue; } @@ -232,4 +230,4 @@ export class SPSCreateBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.SPSCreateBlock", SPSCreateBlock); +RegisterClass("BABYLON.CreateSolidParticleBlock", CreateSolidParticleBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 29dcae10c7d..07db26ede47 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -1,9 +1,7 @@ -export * from "./ISPSData"; -export * from "./SPSMeshSourceBlock"; -export * from "./SPSParticleConfigBlock"; -export * from "./SPSSystemBlock"; -export * from "./SPSUpdateBlock"; -export * from "./SPSCreateBlock"; -export * from "./SPSParticlePropsSetBlock"; -export * from "./SPSParticlePropsGetBlock"; -export * from "./SPSNodeMaterialBlock"; +export * from "./ISolidParticleData"; +export * from "./meshSourceBlock"; +export * from "./initSolidParticleBlock"; +export * from "./solidParticleSystemBlock"; +export * from "./updateSolidParticleBlock"; +export * from "./createSolidParticleBlock"; +export * from "./nodeMaterialBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/initSolidParticleBlock.ts similarity index 89% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/initSolidParticleBlock.ts index c57ab5d237d..4d2c79e1a9d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/initSolidParticleBlock.ts @@ -1,17 +1,15 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - import type { Color4, Vector3 } from "../../../../Maths"; import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISpsParticleConfigData } from "./ISPSData"; +import type { ISolidParticleInitData } from "./ISolidParticleData"; /** * Block used to configure SPS particle parameters (mesh, count, material, position, velocity, color, scaling, rotation) */ -export class SPSParticleConfigBlock extends NodeParticleBlock { +export class InitSolidParticleBlock extends NodeParticleBlock { public constructor(name: string) { super(name); @@ -29,7 +27,7 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { } public override getClassName() { - return "SPSParticleConfigBlock"; + return "InitSolidParticleBlock"; } public get count(): NodeParticleConnectionPoint { @@ -88,7 +86,7 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { const scaling = this.scaling.isConnected ? () => this.scaling.getConnectedValue(state) as Vector3 : undefined; const rotation = this.rotation.isConnected ? () => this.rotation.getConnectedValue(state) as Vector3 : undefined; - const particleConfig: ISpsParticleConfigData = { + const particleConfig: ISolidParticleInitData = { meshData, count, material, @@ -105,4 +103,4 @@ export class SPSParticleConfigBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.SPSParticleConfigBlock", SPSParticleConfigBlock); +RegisterClass("BABYLON.InitSolidParticleBlock", InitSolidParticleBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts similarity index 91% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts index 30a88f6fa43..28265a6be21 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts @@ -11,18 +11,18 @@ import { Observable } from "core/Misc/observable"; import type { Nullable } from "core/types"; import { Tools } from "core/Misc/tools"; import { ImportMeshAsync } from "core/Loading/sceneLoader"; -import type { ISpsMeshSourceData } from "./ISPSData"; +import type { ISolidParticleMeshSourceData } from "./ISolidParticleData"; /** * Block used to provide mesh source for SPS */ -export class SPSMeshSourceBlock extends NodeParticleBlock { +export class MeshSourceBlock extends NodeParticleBlock { private _customVertexData: Nullable = null; private _customMeshName = ""; private _isRemoteMeshLoading = false; /** Gets an observable raised when the block data changes */ - public onValueChangedObservable = new Observable(); + public onValueChangedObservable = new Observable(); /** Optional remote mesh URL used to auto load geometry */ public remoteMeshUrl = ""; @@ -36,7 +36,7 @@ export class SPSMeshSourceBlock extends NodeParticleBlock { } public override getClassName() { - return "SPSMeshSourceBlock"; + return "MeshSourceBlock"; } public get mesh(): NodeParticleConnectionPoint { @@ -148,7 +148,7 @@ export class SPSMeshSourceBlock extends NodeParticleBlock { return; } - const meshData: ISpsMeshSourceData = { + const meshData: ISolidParticleMeshSourceData = { vertexData: this._customVertexData, customMeshName: this._customMeshName, }; @@ -179,4 +179,4 @@ export class SPSMeshSourceBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.SPSMeshSourceBlock", SPSMeshSourceBlock); +RegisterClass("BABYLON.MeshSourceBlock", MeshSourceBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialBlock.ts similarity index 91% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialBlock.ts index ae43af82e7e..ba96b153735 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialBlock.ts @@ -12,7 +12,7 @@ import type { Texture } from "core/Materials/Textures/texture"; /** * Block used to load a node material for SPS */ -export class SPSNodeMaterialBlock extends NodeParticleBlock { +export class NodeMaterialBlock extends NodeParticleBlock { private _serializedMaterial: Nullable = null; private _customMaterialName = ""; @@ -23,11 +23,11 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { } public override getClassName() { - return "SPSNodeMaterialBlock"; + return "NodeMaterialBlock"; } /** Raised when material data changes */ - public onValueChangedObservable = new Observable(); + public onValueChangedObservable = new Observable(); public get texture(): NodeParticleConnectionPoint { return this._inputs[0]; @@ -121,4 +121,4 @@ export class SPSNodeMaterialBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.SPSNodeMaterialBlock", SPSNodeMaterialBlock); +RegisterClass("BABYLON.NodeMaterialBlock", NodeMaterialBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/solidParticleSystemBlock.ts similarity index 86% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/solidParticleSystemBlock.ts index 44417fe074b..e867a0e8fbb 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/solidParticleSystemBlock.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; @@ -11,7 +9,7 @@ import type { SolidParticleSystem } from "core/Particles/solidParticleSystem"; /** * Block used to create SolidParticleSystem and collect all Create blocks */ -export class SPSSystemBlock extends NodeParticleBlock { +export class SolidParticleSystemBlock extends NodeParticleBlock { private static _IdCounter = 0; @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { @@ -20,7 +18,7 @@ export class SPSSystemBlock extends NodeParticleBlock { }) public billboard = false; - public _internalId = SPSSystemBlock._IdCounter++; + public _internalId = SolidParticleSystemBlock._IdCounter++; public constructor(name: string) { super(name); @@ -30,7 +28,7 @@ export class SPSSystemBlock extends NodeParticleBlock { } public override getClassName() { - return "SPSSystemBlock"; + return "SolidParticleSystemBlock"; } public get solidParticle(): NodeParticleConnectionPoint { @@ -49,7 +47,7 @@ export class SPSSystemBlock extends NodeParticleBlock { const solidParticle = this.solidParticle.getConnectedValue(state) as SolidParticleSystem; if (!solidParticle) { - throw new Error("No SolidParticleSystem connected to SPSSystemBlock"); + throw new Error("No SolidParticleSystem connected to SolidParticleSystemBlock"); } solidParticle.billboard = this.billboard; @@ -70,4 +68,4 @@ export class SPSSystemBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.SPSSystemBlock", SPSSystemBlock); +RegisterClass("BABYLON.SolidParticleSystemBlock", SolidParticleSystemBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleBlock.ts similarity index 85% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleBlock.ts index 598eb3a805a..8e55f1c5a48 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleBlock.ts @@ -1,16 +1,14 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - import { RegisterClass } from "../../../../Misc/typeStore"; import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import type { ISpsParticleConfigData, ISpsUpdateData } from "./ISPSData"; +import type { ISolidParticleInitData, ISolidParticleUpdateData } from "./ISolidParticleData"; /** * Block used to generate update function for SPS particles */ -export class SPSUpdateBlock extends NodeParticleBlock { +export class UpdateSolidParticleBlock extends NodeParticleBlock { public constructor(name: string) { super(name); @@ -25,7 +23,7 @@ export class SPSUpdateBlock extends NodeParticleBlock { } public override getClassName() { - return "SPSUpdateBlock"; + return "UpdateSolidParticleBlock"; } public get configInput(): NodeParticleConnectionPoint { @@ -57,7 +55,7 @@ export class SPSUpdateBlock extends NodeParticleBlock { } public override _build(state: NodeParticleBuildState) { - const updateData: ISpsUpdateData = {} as ISpsUpdateData; + const updateData: ISolidParticleUpdateData = {} as ISolidParticleUpdateData; if (this.position.isConnected) { updateData.position = () => { return this.position.getConnectedValue(state); @@ -83,7 +81,7 @@ export class SPSUpdateBlock extends NodeParticleBlock { return this.rotation.getConnectedValue(state); }; } - this.output._storedValue = { ...(this.configInput.getConnectedValue(state) as ISpsParticleConfigData), updateBlock: updateData }; + this.output._storedValue = { ...(this.configInput.getConnectedValue(state) as ISolidParticleInitData), updateBlock: updateData }; } public override serialize(): any { @@ -96,4 +94,4 @@ export class SPSUpdateBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.SPSUpdateBlock", SPSUpdateBlock); +RegisterClass("BABYLON.UpdateSolidParticleBlock", UpdateSolidParticleBlock); diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index c9297d16e74..3713812f090 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -22,7 +22,7 @@ import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import type { Nullable } from "core/types"; import { Color4 } from "core/Maths/math.color"; -import { SPSSystemBlock } from "./Blocks"; +import { SolidParticleSystemBlock } from "./Blocks"; import { ParticleSystem } from "core/Particles/particleSystem"; // declare NODEPARTICLEEDITOR namespace for compilation issue @@ -47,7 +47,7 @@ export interface INodeParticleEditorOptions { * PG: #ZT509U#1 */ export class NodeParticleSystemSet { - private _systemBlocks: (SystemBlock | SPSSystemBlock)[] = []; + private _systemBlocks: (SystemBlock | SolidParticleSystemBlock)[] = []; private _buildId: number = 0; /** Define the Url to load node editor script */ @@ -92,7 +92,7 @@ export class NodeParticleSystemSet { /** * Gets the system blocks */ - public get systemBlocks(): (SystemBlock | SPSSystemBlock)[] { + public get systemBlocks(): (SystemBlock | SolidParticleSystemBlock)[] { return this._systemBlocks; } diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 8ee9bc2c202..204bd0cb77c 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -43,7 +43,14 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSUpdateBlock, SPSParticleConfigBlock, SPSNodeMaterialBlock } from "core/Particles/Node/Blocks"; +import { + MeshSourceBlock, + SolidParticleSystemBlock, + CreateSolidParticleBlock, + UpdateSolidParticleBlock, + InitSolidParticleBlock, + NodeMaterialBlock, +} from "core/Particles/Node/Blocks"; import { ParticleFloatToIntBlock } from "core/Particles/Node/Blocks/particleFloatToIntBlock"; /** @@ -155,18 +162,18 @@ export class BlockTools { return new UpdateAttractorBlock("Update attractor"); case "SystemBlock": return new SystemBlock("System"); - case "SPSMeshSourceBlock": - return new SPSMeshSourceBlock("SPS Mesh Source"); - case "SPSParticleConfigBlock": - return new SPSParticleConfigBlock("SPS Particle Config"); - case "SPSNodeMaterialBlock": - return new SPSNodeMaterialBlock("SPS Node Material"); - case "SPSSystemBlock": - return new SPSSystemBlock("SPS System"); - case "SPSCreateBlock": - return new SPSCreateBlock("SPS Create"); - case "SPSUpdateBlock": - return new SPSUpdateBlock("SPS Update"); + case "MeshSourceBlock": + return new MeshSourceBlock("Mesh Source for SPS"); + case "InitSolidParticleBlock": + return new InitSolidParticleBlock("Init Solid Particle"); + case "NodeMaterialBlock": + return new NodeMaterialBlock("Node Material for SPS"); + case "SolidParticleSystemBlock": + return new SolidParticleSystemBlock("SPS System"); + case "CreateSolidParticleBlock": + return new CreateSolidParticleBlock("Create Solid Particle"); + case "UpdateSolidParticleBlock": + return new UpdateSolidParticleBlock("SPS Update"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index 1b38d292b51..c9f5cf50f25 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -23,14 +23,12 @@ export class NodeListComponent extends React.Component { - private _onValueChangedObserver: Nullable> = null; +export class MeshSourcePropertyTabComponent extends React.Component { + private _onValueChangedObserver: Nullable> = null; constructor(props: IPropertyComponentProps) { super(props); @@ -23,7 +23,7 @@ export class SPSMeshSourcePropertyTabComponent extends React.Component { this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); this.forceUpdate(); @@ -58,7 +58,7 @@ export class SPSMeshSourcePropertyTabComponent extends React.Component !!m.name && m.getTotalVertices() > 0) as Mesh[]; if (meshes.length) { - const block = this.props.nodeData.data as SPSMeshSourceBlock; + const block = this.props.nodeData.data as MeshSourceBlock; block.setCustomMesh(meshes[0]); this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); } @@ -67,7 +67,7 @@ export class SPSMeshSourcePropertyTabComponent extends React.Component) { - const block = this.props.nodeData.data as SPSMeshSourceBlock; + const block = this.props.nodeData.data as MeshSourceBlock; block.setCustomMesh(mesh ?? null); this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); this.forceUpdate(); @@ -87,7 +87,7 @@ export class SPSMeshSourcePropertyTabComponent extends React.Component !!m.name && m.getTotalVertices() > 0) as Mesh[]) : []; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialNodePropertyComponent.tsx similarity index 80% rename from packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx rename to packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialNodePropertyComponent.tsx index 8f0ea22a763..c48f7d586a9 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialNodePropertyComponent.tsx @@ -7,10 +7,10 @@ import { TextLineComponent } from "shared-ui-components/lines/textLineComponent" import { ButtonLineComponent } from "shared-ui-components/lines/buttonLineComponent"; import type { Nullable } from "core/types"; import type { Observer } from "core/Misc/observable"; -import { SPSNodeMaterialBlock } from "core/Particles/Node/Blocks"; +import { NodeMaterialBlock } from "core/Particles/Node/Blocks"; -export class SPSNodeMaterialPropertyTabComponent extends React.Component { - private _onValueChangedObserver: Nullable> = null; +export class NodeMaterialPropertyTabComponent extends React.Component { + private _onValueChangedObserver: Nullable> = null; constructor(props: IPropertyComponentProps) { super(props); @@ -18,7 +18,7 @@ export class SPSNodeMaterialPropertyTabComponent extends React.Component { this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); this.forceUpdate(); @@ -26,7 +26,7 @@ export class SPSNodeMaterialPropertyTabComponent extends React.Component diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts index 86b172c3e09..5d0ea0bc29b 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts @@ -38,12 +38,12 @@ export const RegisterToDisplayManagers = () => { DisplayLedger.RegisteredControls["UpdateFlowMapBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["SystemBlock"] = SystemDisplayManager; - DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; - DisplayLedger.RegisteredControls["SPSNodeMaterialBlock"] = EmitterDisplayManager; - DisplayLedger.RegisteredControls["SPSParticleConfigBlock"] = EmitterDisplayManager; - DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; - DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; - DisplayLedger.RegisteredControls["SPSUpdateBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["MeshSourceBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["NodeMaterialBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["InitSolidParticleBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["CreateSolidParticleBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SolidParticleSystemBlock"] = SystemDisplayManager; + DisplayLedger.RegisteredControls["UpdateSolidParticleBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; DisplayLedger.RegisteredControls["ParticleElbowBlock"] = ElbowDisplayManager; @@ -51,6 +51,4 @@ export const RegisterToDisplayManagers = () => { DisplayLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutDisplayManager; DisplayLedger.RegisteredControls["BasicConditionBlock"] = ConditionDisplayManager; DisplayLedger.RegisteredControls["ParticleTriggerBlock"] = TriggerDisplayManager; - DisplayLedger.RegisteredControls["SpsParticlePropsGetBlock"] = InputDisplayManager; - DisplayLedger.RegisteredControls["SpsParticlePropsSetBlock"] = InputDisplayManager; }; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index a12909b5091..7631efa0499 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -5,8 +5,8 @@ import { TextureSourcePropertyTabComponent } from "./properties/textureSourceNod import { DebugPropertyTabComponent } from "./properties/debugNodePropertyComponent"; import { TeleportOutPropertyTabComponent } from "./properties/teleportOutNodePropertyComponent"; import { MeshShapePropertyTabComponent } from "./properties/meshShapeNodePropertyComponent"; -import { SPSMeshSourcePropertyTabComponent } from "./properties/spsMeshSourceNodePropertyComponent"; -import { SPSNodeMaterialPropertyTabComponent } from "./properties/spsNodeMaterialPropertyComponent"; +import { MeshSourcePropertyTabComponent } from "./properties/meshSourceNodePropertyComponent"; +import { NodeMaterialPropertyTabComponent } from "./properties/nodeMaterialNodePropertyComponent"; export const RegisterToPropertyTabManagers = () => { PropertyLedger.DefaultControl = GenericPropertyComponent; @@ -16,12 +16,10 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; - PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = SPSMeshSourcePropertyTabComponent; - PropertyLedger.RegisteredControls["SPSParticleConfigBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSUpdateBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SpsParticlePropsSetBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SpsParticlePropsGetBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["SPSNodeMaterialBlock"] = SPSNodeMaterialPropertyTabComponent; + PropertyLedger.RegisteredControls["MeshSourceBlock"] = MeshSourcePropertyTabComponent; + PropertyLedger.RegisteredControls["InitSolidParticleBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["CreateSolidParticleBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SolidParticleSystemBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["UpdateSolidParticleBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["NodeMaterialBlock"] = NodeMaterialPropertyTabComponent; }; From 0e003daaaa1a55e40eb7056f5e492c9c7d6817c7 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 24 Nov 2025 16:44:29 +0300 Subject: [PATCH 58/68] Refactor Node Particle System and Mesh Source Blocks for improved type handling - Updated imports in `nodeParticleSystemSet.ts` and `meshSourceBlock.ts` to use type imports for better clarity and performance. - Removed unused properties and methods related to remote mesh loading in `meshSourceBlock.ts`, streamlining the code. - Renamed `loadMesh` to `loadMeshAsync` in `meshSourceNodePropertyComponent.tsx` and `loadMaterial` to `loadMaterialAsync` in `nodeMaterialNodePropertyComponent.tsx` for consistency and clarity in asynchronous operations. These changes enhance the maintainability and readability of the codebase, aligning with recent architectural improvements in the node particle editor. --- .../Blocks/SolidParticle/meshSourceBlock.ts | 65 +------------------ .../Particles/Node/nodeParticleSystemSet.ts | 10 +-- .../meshSourceNodePropertyComponent.tsx | 6 +- .../nodeMaterialNodePropertyComponent.tsx | 6 +- 4 files changed, 12 insertions(+), 75 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts index 28265a6be21..c6cd148307c 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts @@ -5,12 +5,10 @@ import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleB import { NodeParticleBlock } from "../../nodeParticleBlock"; import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; -import { Mesh } from "core/Meshes/mesh"; +import type { Mesh } from "core/Meshes/mesh"; import { VertexData } from "core/Meshes/mesh.vertexData"; import { Observable } from "core/Misc/observable"; import type { Nullable } from "core/types"; -import { Tools } from "core/Misc/tools"; -import { ImportMeshAsync } from "core/Loading/sceneLoader"; import type { ISolidParticleMeshSourceData } from "./ISolidParticleData"; /** @@ -19,16 +17,10 @@ import type { ISolidParticleMeshSourceData } from "./ISolidParticleData"; export class MeshSourceBlock extends NodeParticleBlock { private _customVertexData: Nullable = null; private _customMeshName = ""; - private _isRemoteMeshLoading = false; /** Gets an observable raised when the block data changes */ public onValueChangedObservable = new Observable(); - /** Optional remote mesh URL used to auto load geometry */ - public remoteMeshUrl = ""; - /** Optional mesh name filter when loading remote geometry */ - public remoteMeshName = ""; - public constructor(name: string) { super(name); @@ -69,8 +61,6 @@ export class MeshSourceBlock extends NodeParticleBlock { this._customVertexData = VertexData.ExtractFromMesh(mesh, true, true); this._customMeshName = mesh.name || ""; - this.remoteMeshUrl = ""; - this.remoteMeshName = ""; this.onValueChangedObservable.notifyObservers(this); } @@ -82,8 +72,6 @@ export class MeshSourceBlock extends NodeParticleBlock { public setCustomVertexData(vertexData: VertexData, name = "") { this._customVertexData = vertexData; this._customMeshName = name; - this.remoteMeshUrl = ""; - this.remoteMeshName = ""; this.onValueChangedObservable.notifyObservers(this); } @@ -93,56 +81,10 @@ export class MeshSourceBlock extends NodeParticleBlock { public clearCustomMesh() { this._customVertexData = null; this._customMeshName = ""; - this.remoteMeshUrl = ""; - this.remoteMeshName = ""; this.onValueChangedObservable.notifyObservers(this); } - private _tryLoadRemoteMesh(state: NodeParticleBuildState) { - if (this._customVertexData || !this.remoteMeshUrl || this._isRemoteMeshLoading) { - return; - } - - this._isRemoteMeshLoading = true; - const fileName = Tools.GetFilename(this.remoteMeshUrl); - const rootUrl = this.remoteMeshUrl.substring(0, this.remoteMeshUrl.length - fileName.length); - - ImportMeshAsync(fileName, state.scene, { meshNames: "", rootUrl }) - .then((result) => { - let mesh = result.meshes.find((m) => (this.remoteMeshName ? m.name === this.remoteMeshName : !!m && m.name !== "__root__")); - if (!mesh && result.meshes.length) { - mesh = result.meshes[0]; - } - - if (mesh) { - this.setCustomMesh(mesh as Mesh); - this.onValueChangedObservable.notifyObservers(this); - } - - for (const loadedMesh of result.meshes) { - loadedMesh.dispose(); - } - for (const skeleton of result.skeletons) { - skeleton.dispose(); - } - for (const animationGroup of result.animationGroups) { - animationGroup.dispose(); - } - for (const particleSystem of result.particleSystems) { - particleSystem.dispose(); - } - }) - .catch(() => { - // Ignore load errors - }) - .finally(() => { - this._isRemoteMeshLoading = false; - }); - } - public override _build(state: NodeParticleBuildState) { - this._tryLoadRemoteMesh(state); - if (!this._customVertexData) { this.mesh._storedValue = null; return; @@ -158,8 +100,6 @@ export class MeshSourceBlock extends NodeParticleBlock { public override serialize(): any { const serializationObject = super.serialize(); - serializationObject.remoteMeshUrl = this.remoteMeshUrl; - serializationObject.remoteMeshName = this.remoteMeshName; serializationObject.customMeshName = this._customMeshName; if (this._customVertexData) { serializationObject.customVertexData = this._customVertexData.serialize(); @@ -169,9 +109,6 @@ export class MeshSourceBlock extends NodeParticleBlock { public override _deserialize(serializationObject: any) { super._deserialize(serializationObject); - this.remoteMeshUrl = serializationObject.remoteMeshUrl ?? ""; - this.remoteMeshName = serializationObject.remoteMeshName ?? ""; - if (serializationObject.customVertexData) { this._customVertexData = VertexData.Parse(serializationObject.customVertexData); this._customMeshName = serializationObject.customMeshName || ""; diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 3713812f090..a013ebc8aff 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -16,14 +16,14 @@ import { ParticleTextureSourceBlock } from "./Blocks/particleSourceTextureBlock" import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock"; import { ParticleMathBlock, ParticleMathBlockOperations } from "./Blocks/particleMathBlock"; -import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTeleportOutBlock"; -import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; -import type { Nullable } from "core/types"; -import { Color4 } from "core/Maths/math.color"; -import { SolidParticleSystemBlock } from "./Blocks"; import { ParticleSystem } from "core/Particles/particleSystem"; +import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTeleportOutBlock"; +import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; +import type { Nullable } from "core/types"; +import type { Color4 } from "core/Maths/math.color"; +import type { SolidParticleSystemBlock } from "./Blocks"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/meshSourceNodePropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/meshSourceNodePropertyComponent.tsx index 6d6b4da7eb7..f206e755065 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/properties/meshSourceNodePropertyComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/meshSourceNodePropertyComponent.tsx @@ -11,7 +11,7 @@ import { EngineStore } from "core/Engines/engineStore"; import type { Nullable } from "core/types"; import type { Scene } from "core/scene"; import type { Mesh } from "core/Meshes/mesh"; -import { MeshSourceBlock } from "core/Particles/Node/Blocks"; +import type { MeshSourceBlock } from "core/Particles/Node/Blocks"; import type { Observer } from "core/Misc/observable"; export class MeshSourcePropertyTabComponent extends React.Component { @@ -42,7 +42,7 @@ export class MeshSourcePropertyTabComponent extends React.Component {block.hasCustomMesh ? : } {this.state.isLoading && } - {!this.state.isLoading && await this.loadMesh(file)} />} + {!this.state.isLoading && await this.loadMeshAsync(file)} />} {scene && meshOptions.length > 1 && ( { private _onValueChangedObserver: Nullable> = null; @@ -33,7 +33,7 @@ export class NodeMaterialPropertyTabComponent extends React.Component {block.hasCustomMaterial && } {this.state.isLoading && } - {!this.state.isLoading && await this.loadMaterial(file)} accept=".json" />} + {!this.state.isLoading && await this.loadMaterialAsync(file)} accept=".json" />} {block.hasCustomMaterial && this.removeMaterial()} />}
From 330b50f4d8840f0e371dfa360554c418042538cb Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 24 Nov 2025 17:38:29 +0300 Subject: [PATCH 59/68] Add default mesh source and enhance particle initialization in NodeParticleSystemSet - Introduced a default mesh source with a tiny box shape for immediate rendering in `NodeParticleSystemSet`. - Enhanced particle initialization by adding contextual inputs for solid particle index, center offset, spacing, and orbit parameters. - Implemented mathematical operations for particle positioning and rotation, improving the overall functionality of the solid particle system. These changes streamline the setup process for solid particles, enhancing usability and visual feedback in the node particle editor. --- .../Particles/Node/nodeParticleSystemSet.ts | 121 +++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index a013ebc8aff..62c37c2a24f 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -12,10 +12,12 @@ import { Constants } from "core/Engines/constants"; import { Tools } from "core/Misc/tools"; import { AbstractEngine } from "core/Engines/abstractEngine"; import { ParticleInputBlock } from "./Blocks/particleInputBlock"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; import { ParticleTextureSourceBlock } from "./Blocks/particleSourceTextureBlock"; import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock"; import { ParticleMathBlock, ParticleMathBlockOperations } from "./Blocks/particleMathBlock"; +import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; import { ParticleSystem } from "core/Particles/particleSystem"; @@ -23,7 +25,10 @@ import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTelepor import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; import type { Nullable } from "core/types"; import type { Color4 } from "core/Maths/math.color"; -import type { SolidParticleSystemBlock } from "./Blocks"; +import { SolidParticleSystemBlock, MeshSourceBlock, InitSolidParticleBlock, UpdateSolidParticleBlock, CreateSolidParticleBlock } from "./Blocks"; +import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; +import { VertexData } from "core/Meshes/mesh.vertexData"; +import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -341,6 +346,120 @@ export class NodeParticleSystemSet { public setToDefaultSps() { this.clear(); this.editorData = null; + + // Mesh source with a tiny box shape so something renders immediately + const meshSource = new MeshSourceBlock("Mesh Source"); + const defaultMeshData = VertexData.CreateBox({ size: 0.5 }); + meshSource.setCustomVertexData(defaultMeshData, "Box"); + + // Solid particle index contextual input + const solidIndex = new ParticleInputBlock("Solid Particle Index", NodeParticleBlockConnectionPointTypes.Int); + solidIndex.contextualValue = NodeParticleContextualSources.SolidParticleIndex; + + // Helpers to center and space particles + const centerOffset = new ParticleInputBlock("Center Offset", NodeParticleBlockConnectionPointTypes.Float); + centerOffset.value = 1; + + const spacing = new ParticleInputBlock("Spacing", NodeParticleBlockConnectionPointTypes.Float); + spacing.value = 2; + + const centeredIndex = new ParticleMathBlock("Index - Center"); + centeredIndex.operation = ParticleMathBlockOperations.Subtract; + solidIndex.output.connectTo(centeredIndex.left); + centerOffset.output.connectTo(centeredIndex.right); + + const spacedIndex = new ParticleMathBlock("Index * Spacing"); + spacedIndex.operation = ParticleMathBlockOperations.Multiply; + centeredIndex.output.connectTo(spacedIndex.left); + spacing.output.connectTo(spacedIndex.right); + + const absIndex = new ParticleTrigonometryBlock("Abs Index"); + absIndex.operation = ParticleTrigonometryBlockOperations.Abs; + centeredIndex.output.connectTo(absIndex.input); + + const orbitRadius = new ParticleInputBlock("Orbit Radius", NodeParticleBlockConnectionPointTypes.Float); + orbitRadius.value = 1; + + const zeroFloat = new ParticleInputBlock("Zero Float", NodeParticleBlockConnectionPointTypes.Float); + zeroFloat.value = 0; + + const basePositionVector = new ParticleConverterBlock("Base Position"); + spacedIndex.output.connectTo(basePositionVector.xIn); + zeroFloat.output.connectTo(basePositionVector.yIn); + zeroFloat.output.connectTo(basePositionVector.zIn); + + const orbitPositionVector = new ParticleConverterBlock("Orbit Position"); + + // Time-based rotation for the update block + const timeInput = new ParticleInputBlock("Time", NodeParticleBlockConnectionPointTypes.Float); + timeInput.systemSource = NodeParticleSystemSources.Time; + + const rotationSpeed = new ParticleInputBlock("Rotation Speed", NodeParticleBlockConnectionPointTypes.Float); + rotationSpeed.value = 2; + + const timeScale = new ParticleMathBlock("Time * Speed"); + timeScale.operation = ParticleMathBlockOperations.Multiply; + timeInput.output.connectTo(timeScale.left); + rotationSpeed.output.connectTo(timeScale.right); + + const orbitSin = new ParticleTrigonometryBlock("Orbit Sin"); + orbitSin.operation = ParticleTrigonometryBlockOperations.Sin; + timeScale.output.connectTo(orbitSin.input); + + const orbitCos = new ParticleTrigonometryBlock("Orbit Cos"); + orbitCos.operation = ParticleTrigonometryBlockOperations.Cos; + timeScale.output.connectTo(orbitCos.input); + + const orbitMagnitude = new ParticleMathBlock("Orbit Magnitude"); + orbitMagnitude.operation = ParticleMathBlockOperations.Multiply; + absIndex.output.connectTo(orbitMagnitude.left); + orbitRadius.output.connectTo(orbitMagnitude.right); + + const orbitSinRadius = new ParticleMathBlock("Orbit Sin * Radius"); + orbitSinRadius.operation = ParticleMathBlockOperations.Multiply; + orbitSin.output.connectTo(orbitSinRadius.left); + orbitMagnitude.output.connectTo(orbitSinRadius.right); + + const orbitCosRadius = new ParticleMathBlock("Orbit Cos * Radius"); + orbitCosRadius.operation = ParticleMathBlockOperations.Multiply; + orbitCos.output.connectTo(orbitCosRadius.left); + orbitMagnitude.output.connectTo(orbitCosRadius.right); + + const orbitCosSigned = new ParticleMathBlock("Orbit Cos * Sign"); + orbitCosSigned.operation = ParticleMathBlockOperations.Multiply; + orbitCosRadius.output.connectTo(orbitCosSigned.left); + centeredIndex.output.connectTo(orbitCosSigned.right); + + orbitCosSigned.output.connectTo(orbitPositionVector.xIn); + orbitSinRadius.output.connectTo(orbitPositionVector.yIn); + zeroFloat.output.connectTo(orbitPositionVector.zIn); + + const rotationVector = new ParticleConverterBlock("Rotation Vector"); + zeroFloat.output.connectTo(rotationVector.xIn); + timeScale.output.connectTo(rotationVector.yIn); + zeroFloat.output.connectTo(rotationVector.zIn); + + // Particle init (mesh and base state) + const initBlock = new InitSolidParticleBlock("Init Solid Particle"); + initBlock.count.value = 3; + meshSource.mesh.connectTo(initBlock.mesh); + basePositionVector.xyzOut.connectTo(initBlock.position); + + // Update block applying rotation + const updateBlock = new UpdateSolidParticleBlock("Update Solid Particle"); + initBlock.config.connectTo(updateBlock.configInput); + rotationVector.xyzOut.connectTo(updateBlock.rotation); + orbitPositionVector.xyzOut.connectTo(updateBlock.position); + + // Create the SPS from the config + const createBlock = new CreateSolidParticleBlock("Create Solid Particle"); + updateBlock.output.connectTo(createBlock.config); + + // System block exposes the SPS instance + const systemBlock = new SolidParticleSystemBlock("SPS System"); + createBlock.solidParticle.connectTo(systemBlock.solidParticle); + + this._systemBlocks.push(systemBlock); } /** From 716244c70f5ad18430cdce6404d58016f27178ac Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 24 Nov 2025 17:56:21 +0300 Subject: [PATCH 60/68] Rename NodeParticleModes for clarity and consistency - Updated enum values in `NodeParticleModes` from `Standard` to `Particle` and `SPS` to `SolidParticle` to better reflect their functionality. - Adjusted references in `PropertyTabComponent` to align with the new enum names, ensuring consistency throughout the codebase. These changes enhance the readability and understanding of particle modes in the node particle editor. --- .../src/components/propertyTab/propertyTabComponent.tsx | 8 ++++---- .../tools/nodeParticleEditor/src/nodeParticleModes.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx index 59140bf7594..ddd107c74d8 100644 --- a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx @@ -223,10 +223,10 @@ export class PropertyTabComponent extends React.Component Date: Mon, 24 Nov 2025 18:17:53 +0300 Subject: [PATCH 61/68] Implement NodeMaterialSourceBlock and update references in Node Particle Editor - Introduced `NodeMaterialSourceBlock` to handle loading and managing node materials for solid particles. - Updated existing references from `NodeMaterialBlock` to `NodeMaterialSourceBlock` across the codebase for consistency. - Added a new property tab component for `NodeMaterialSourceBlock` to facilitate material loading and management in the editor. - Adjusted the global state to reflect changes in particle mode enumeration. These changes enhance the functionality and usability of node materials within the particle editor, aligning with recent architectural improvements. --- .../Node/Blocks/SolidParticle/index.ts | 2 +- ...erialBlock.ts => nodeMaterialSourceBlock.ts} | 8 ++++---- .../tools/nodeParticleEditor/src/blockTools.ts | 6 +++--- .../tools/nodeParticleEditor/src/globalState.ts | 2 +- ...nodeMaterialSourceNodePropertyComponent.tsx} | 17 ++++++++--------- .../src/graphSystem/registerToPropertyLedger.ts | 4 ++-- 6 files changed, 19 insertions(+), 20 deletions(-) rename packages/dev/core/src/Particles/Node/Blocks/SolidParticle/{nodeMaterialBlock.ts => nodeMaterialSourceBlock.ts} (92%) rename packages/tools/nodeParticleEditor/src/graphSystem/properties/{nodeMaterialNodePropertyComponent.tsx => nodeMaterialSourceNodePropertyComponent.tsx} (81%) diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts index 07db26ede47..c87421e542c 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -4,4 +4,4 @@ export * from "./initSolidParticleBlock"; export * from "./solidParticleSystemBlock"; export * from "./updateSolidParticleBlock"; export * from "./createSolidParticleBlock"; -export * from "./nodeMaterialBlock"; +export * from "./nodeMaterialSourceBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialSourceBlock.ts similarity index 92% rename from packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialBlock.ts rename to packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialSourceBlock.ts index ba96b153735..d12f7ae6082 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/nodeMaterialSourceBlock.ts @@ -12,7 +12,7 @@ import type { Texture } from "core/Materials/Textures/texture"; /** * Block used to load a node material for SPS */ -export class NodeMaterialBlock extends NodeParticleBlock { +export class NodeMaterialSourceBlock extends NodeParticleBlock { private _serializedMaterial: Nullable = null; private _customMaterialName = ""; @@ -23,11 +23,11 @@ export class NodeMaterialBlock extends NodeParticleBlock { } public override getClassName() { - return "NodeMaterialBlock"; + return "NodeMaterialSourceBlock"; } /** Raised when material data changes */ - public onValueChangedObservable = new Observable(); + public onValueChangedObservable = new Observable(); public get texture(): NodeParticleConnectionPoint { return this._inputs[0]; @@ -121,4 +121,4 @@ export class NodeMaterialBlock extends NodeParticleBlock { } } -RegisterClass("BABYLON.NodeMaterialBlock", NodeMaterialBlock); +RegisterClass("BABYLON.NodeMaterialBlock", NodeMaterialSourceBlock); diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 204bd0cb77c..f4e59df1d24 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -49,7 +49,7 @@ import { CreateSolidParticleBlock, UpdateSolidParticleBlock, InitSolidParticleBlock, - NodeMaterialBlock, + NodeMaterialSourceBlock, } from "core/Particles/Node/Blocks"; import { ParticleFloatToIntBlock } from "core/Particles/Node/Blocks/particleFloatToIntBlock"; @@ -166,8 +166,8 @@ export class BlockTools { return new MeshSourceBlock("Mesh Source for SPS"); case "InitSolidParticleBlock": return new InitSolidParticleBlock("Init Solid Particle"); - case "NodeMaterialBlock": - return new NodeMaterialBlock("Node Material for SPS"); + case "NodeMaterialSourceBlock": + return new NodeMaterialSourceBlock("Node Material Source for SPS"); case "SolidParticleSystemBlock": return new SolidParticleSystemBlock("SPS System"); case "CreateSolidParticleBlock": diff --git a/packages/tools/nodeParticleEditor/src/globalState.ts b/packages/tools/nodeParticleEditor/src/globalState.ts index 4fdb6def236..96229e3e382 100644 --- a/packages/tools/nodeParticleEditor/src/globalState.ts +++ b/packages/tools/nodeParticleEditor/src/globalState.ts @@ -45,7 +45,7 @@ export class GlobalState { updateState: (left: string, right: string) => void; customSave?: { label: string; action: (data: string) => Promise }; - mode: NodeParticleModes = NodeParticleModes.Standard; + mode: NodeParticleModes = NodeParticleModes.Particle; public constructor() { this.stateManager = new StateManager(); diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialNodePropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialSourceNodePropertyComponent.tsx similarity index 81% rename from packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialNodePropertyComponent.tsx rename to packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialSourceNodePropertyComponent.tsx index 1405224069f..4efa0f5c219 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialNodePropertyComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/nodeMaterialSourceNodePropertyComponent.tsx @@ -7,10 +7,10 @@ import { TextLineComponent } from "shared-ui-components/lines/textLineComponent" import { ButtonLineComponent } from "shared-ui-components/lines/buttonLineComponent"; import type { Nullable } from "core/types"; import type { Observer } from "core/Misc/observable"; -import type { NodeMaterialBlock } from "core/Particles/Node/Blocks"; +import type { NodeMaterialSourceBlock } from "core/Particles/Node/Blocks"; -export class NodeMaterialPropertyTabComponent extends React.Component { - private _onValueChangedObserver: Nullable> = null; +export class NodeMaterialSourcePropertyTabComponent extends React.Component { + private _onValueChangedObserver: Nullable> = null; constructor(props: IPropertyComponentProps) { super(props); @@ -18,7 +18,7 @@ export class NodeMaterialPropertyTabComponent extends React.Component { this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); this.forceUpdate(); @@ -26,7 +26,7 @@ export class NodeMaterialPropertyTabComponent extends React.Component diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 7631efa0499..69750aff10f 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -6,7 +6,7 @@ import { DebugPropertyTabComponent } from "./properties/debugNodePropertyCompone import { TeleportOutPropertyTabComponent } from "./properties/teleportOutNodePropertyComponent"; import { MeshShapePropertyTabComponent } from "./properties/meshShapeNodePropertyComponent"; import { MeshSourcePropertyTabComponent } from "./properties/meshSourceNodePropertyComponent"; -import { NodeMaterialPropertyTabComponent } from "./properties/nodeMaterialNodePropertyComponent"; +import { NodeMaterialSourcePropertyTabComponent } from "./properties/nodeMaterialSourceNodePropertyComponent"; export const RegisterToPropertyTabManagers = () => { PropertyLedger.DefaultControl = GenericPropertyComponent; @@ -21,5 +21,5 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["CreateSolidParticleBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["SolidParticleSystemBlock"] = GenericPropertyComponent; PropertyLedger.RegisteredControls["UpdateSolidParticleBlock"] = GenericPropertyComponent; - PropertyLedger.RegisteredControls["NodeMaterialBlock"] = NodeMaterialPropertyTabComponent; + PropertyLedger.RegisteredControls["NodeMaterialSourceBlock"] = NodeMaterialSourcePropertyTabComponent; }; From 1d52dc264a065821d0e20c4a0a4cd0b1ddbf15f5 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Mon, 24 Nov 2025 23:35:38 +0300 Subject: [PATCH 62/68] Enhance serialization and block definitions in Node Particle Editor - Updated `SerializationTools` to ensure `editorData` is initialized properly and to store the current particle mode during serialization and deserialization. - Introduced a new file `blockDefinitions.ts` to define all available blocks with their categories, supported modes, and tooltips, improving organization and accessibility. - Refactored `NodeListComponent` to utilize the new block definitions, streamlining the block menu creation process and ensuring tooltips are dynamically fetched. - Enhanced `PropertyTabComponent` to update the global state mode based on the serialized data, improving user experience during particle system loading. These changes improve the overall functionality and usability of the node particle editor, aligning with recent architectural enhancements. --- .../components/nodeList/blockDefinitions.ts | 374 ++++++++++++++++++ .../components/nodeList/nodeListComponent.tsx | 282 +++---------- .../propertyTab/propertyTabComponent.tsx | 7 + .../src/serializationTools.ts | 26 +- 4 files changed, 450 insertions(+), 239 deletions(-) create mode 100644 packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts b/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts new file mode 100644 index 00000000000..0dca2745e80 --- /dev/null +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts @@ -0,0 +1,374 @@ +import { NodeParticleModes } from "../../nodeParticleModes"; + +export interface IBlockDefinition { + name: string; + category: string; + modes: NodeParticleModes[]; + tooltip: string; +} + +/** + * Defines all available blocks with their categories, supported modes, and tooltips + */ +export const BlockDefinitions: IBlockDefinition[] = [ + // Shapes (Particle only) + { name: "BoxShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a box shape" }, + { name: "ConeShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a cone shape" }, + { name: "SphereShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a sphere shape" }, + { name: "PointShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a point" }, + { name: "CustomShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a custom position" }, + { name: "CylinderShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a cylinder shape" }, + { name: "MeshShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a mesh shape" }, + + // Inputs + { name: "Float", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a float value" }, + { name: "Vector2", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a Vector2 value" }, + { name: "Vector3", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a Vector3 value" }, + { name: "Int", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a integer value" }, + { name: "TextureBlock", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Provide a texture" }, + { name: "Color4", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a Color4 value" }, + { name: "MeshSourceBlock", category: "Inputs", modes: [NodeParticleModes.SolidParticle], tooltip: "Mesh source for SPS - load custom geometry from the inspector" }, + { name: "NodeMaterialBlock", category: "Inputs", modes: [NodeParticleModes.SolidParticle], tooltip: "Load a Node Material for SPS particles" }, + + // Updates (Particle only - use _updateQueueStart) + { name: "UpdateDirectionBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the direction of a particle" }, + { name: "UpdatePositionBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the position of a particle" }, + { name: "UpdateColorBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the color of a particle" }, + { name: "UpdateScaleBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the scale of a particle" }, + { name: "UpdateSizeBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the size of a particle" }, + { name: "UpdateAngleBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the angle of a particle" }, + { name: "UpdateAgeBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the age of a particle" }, + { + name: "BasicColorUpdateBlock", + category: "Updates", + modes: [NodeParticleModes.Particle], + tooltip: "Block used to update the color of a particle with a basic update (eg. color * delta)", + }, + { + name: "BasicPositionUpdateBlock", + category: "Updates", + modes: [NodeParticleModes.Particle], + tooltip: "Block used to update the position of a particle with a basic update (eg. direction * delta)", + }, + { name: "UpdateFlowMapBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to update particle position based on a flow map" }, + { name: "UpdateAttractorBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to update particle position based on an attractor" }, + { name: "AlignAngleBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to align the angle of a particle to its direction" }, + // Updates (SolidParticle only) + { + name: "UpdateSolidParticleBlock", + category: "Updates", + modes: [NodeParticleModes.SolidParticle], + tooltip: "Update solid particle - connect SolidParticleSystemBlock output here", + }, + { + name: "BasicSpriteUpdateBlock", + category: "Updates", + modes: [NodeParticleModes.Particle], + tooltip: "Block used to update the sprite index of a particle with a basic update (eg. incrementing the index by 1)", + }, + { name: "UpdateSpriteCellIndexBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to update the sprite cell index of a particle" }, + + // Triggers (Particle only) + { name: "TriggerBlock", category: "Triggers", modes: [NodeParticleModes.Particle], tooltip: "Block used to trigger a particle system based on a condition" }, + + // Setup + { + name: "CreateParticleBlock", + category: "Setup", + modes: [NodeParticleModes.Particle], + tooltip: "Block used to create a particle with properties such as emit power, lifetime, color, scale, and angle", + }, + { name: "SetupSpriteSheetBlock", category: "Setup", modes: [NodeParticleModes.Particle], tooltip: "Block used to setup a sprite sheet for particles" }, + { name: "CreateSolidParticleBlock", category: "Setup", modes: [NodeParticleModes.SolidParticle], tooltip: "Create Solid Particle System - outputs SolidParticleSystem" }, + { name: "InitSolidParticleBlock", category: "Setup", modes: [NodeParticleModes.SolidParticle], tooltip: "Configure solid particle: mesh, count, material, init/update" }, + + // Math - Standard + { name: "AddBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Add" }, + { name: "DivideBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Divide" }, + { name: "MaxBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Max" }, + { name: "MinBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Min" }, + { name: "MultiplyBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Multiply" }, + { name: "SubtractBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Subtract" }, + { name: "NegateBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Negate" }, + { name: "OneMinusBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to One Minus" }, + { name: "ReciprocalBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Reciprocal" }, + { name: "SignBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Sign" }, + { name: "SqrtBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Square Root" }, + { name: "RoundBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Round" }, + { name: "FloorBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Floor" }, + { name: "CeilingBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Ceiling" }, + { + name: "FloatToIntBlock", + category: "Math__Standard", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to convert a float value to an integer value using a specified operation (Round, Ceil, Floor, Truncate)", + }, + + // Math - Scientific + { name: "AbsBlock", category: "Math__Scientific", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Abs" }, + { + name: "ArcCosBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Arc cos (using radians)", + }, + { + name: "ArcSinBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Arc sin (using radians)", + }, + { + name: "ArcTanBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Arc tan (using radians)", + }, + { + name: "CosBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Cos (using radians)", + }, + { + name: "ExpBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Exp (using radians)", + }, + { + name: "Exp2Block", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Exp2 (using radians)", + }, + { + name: "LogBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Log (using radians)", + }, + { + name: "SinBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Sin (using radians)", + }, + { + name: "TanBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Tan (using radians)", + }, + { + name: "ToDegreesBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Conversion block used to convert radians to degree", + }, + { + name: "ToRadiansBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Conversion block used to convert degrees to radians", + }, + { + name: "FractBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Outputs only the fractional value of a floating point number", + }, + { + name: "VectorLengthBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to get the length of a vector", + }, + + // Logical + { name: "EqualBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to Equal" }, + { name: "NotEqualBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to NotEqual" }, + { name: "LessThanBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to LessThan" }, + { name: "LessOrEqualBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to LessOrEqual" }, + { name: "GreaterThanBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to GreaterThan" }, + { name: "GreaterOrEqualBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to GreaterOrEqual" }, + { name: "XorBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to Xor" }, + { name: "OrBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to Or" }, + { name: "AndBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to And" }, + { + name: "ConditionBlock", + category: "Logical", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to evaluate a condition and return a value", + }, + + // Interpolation + { name: "LerpBlock", category: "Interpolation", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Interpolate between two values" }, + { + name: "GradientValueBlock", + category: "Interpolation", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "A gradient value block used to define a value at a specific age", + }, + { + name: "GradientBlock", + category: "Interpolation", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "A gradient block used to define a gradient of values over the lifetime of a particle", + }, + + // Misc + { + name: "ConverterBlock", + category: "Misc", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Convert between different types of values, such as Color4, Vector2, Vector3, and Float", + }, + { name: "RandomBlock", category: "Misc", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Generate a random value" }, + { + name: "DebugBlock", + category: "Misc", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Debug block used to output values of connection ports", + }, + { name: "ElbowBlock", category: "Misc", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Passthrough block mostly used to organize your graph" }, + { + name: "TeleportInBlock", + category: "Misc", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Passthrough block mostly used to organize your graph (but without visible lines). It works like a teleportation point for the graph.", + }, + { name: "TeleportOutBlock", category: "Misc", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Endpoint for a TeleportInBlock." }, + { + name: "LocalVariableBlock", + category: "Misc", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to store local values (eg. within a loop)", + }, + { name: "FresnelBlock", category: "Misc", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Block used to compute the Fresnel term" }, + + // System Nodes + { name: "SystemBlock", category: "System_Nodes", modes: [NodeParticleModes.Particle], tooltip: "Generate a particle system" }, + { + name: "SolidParticleSystemBlock", + category: "System_Nodes", + modes: [NodeParticleModes.SolidParticle], + tooltip: "Configure Solid Particle System - connect SolidParticleSystemBlock output here", + }, + { name: "TimeBlock", category: "System_Nodes", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Block used to get the current time in ms" }, + { + name: "DeltaBlock", + category: "System_Nodes", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to get the delta value for animations", + }, + { + name: "EmitterPositionBlock", + category: "System_Nodes", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the coordinates of the emitter", + }, + { + name: "CameraPositionBlock", + category: "System_Nodes", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the position of the active camera", + }, + + // Contextual + { + name: "PositionBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the position of a particle", + }, + { + name: "DirectionBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle], + tooltip: "Contextual block to get the direction of a particle", + }, + { + name: "DirectionScaleBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle], + tooltip: "Contextual block to get the direction scale of a particle", + }, + { + name: "ScaledDirectionBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle], + tooltip: "Contextual block to get the scaled direction of a particle", + }, + { + name: "ColorBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the color of a particle", + }, + { name: "AgeBlock", category: "Contextual", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Contextual block to get the age of a particle" }, + { + name: "LifetimeBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the lifetime of a particle", + }, + { + name: "ScaleBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the scale of a particle", + }, + { name: "SizeBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the size of a particle" }, + { + name: "AgeGradientBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the age gradient of a particle ie. the age divided by the lifetime", + }, + { + name: "AngleBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle], + tooltip: "Contextual block to get the angle of a particle", + }, + { name: "SolidParticleIndexBlock", category: "Contextual", modes: [NodeParticleModes.SolidParticle], tooltip: "Contextual block to get the index of a solid particle" }, + { name: "InitialColorBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the initial color of a particle" }, + { name: "ColorDeadBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the expected dead color of a particle" }, + { name: "SpriteCellEndBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the end cell of a sprite sheet" }, + { name: "SpriteCellStartBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the start cell of a sprite sheet" }, + { name: "SpriteCellIndexBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the sprite cell index of a particle" }, + { name: "InitialDirectionBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the initial direction of a particle" }, + { name: "ColorStepBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the expected color step of a particle" }, + { name: "ScaledColorStepBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the expected scaled color step of a particle" }, +]; + +/** + * Groups blocks by category for a given mode + * @param mode The particle mode to filter blocks for + * @returns A record mapping category names to arrays of block definitions + */ +export function GetBlocksByMode(mode: NodeParticleModes): Record { + const result: Record = {}; + for (const block of BlockDefinitions) { + if (block.modes.includes(mode)) { + if (!result[block.category]) { + result[block.category] = []; + } + result[block.category].push(block); + } + } + // Sort blocks within each category by name + for (const category in result) { + result[category].sort((a, b) => a.name.localeCompare(b.name)); + } + return result; +} + +/** + * Gets all block names for registration (all modes combined) + * @returns An array of all block names + */ +export function GetAllBlockNames(): string[] { + return BlockDefinitions.map((block) => block.name); +} diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index c9f5cf50f25..9e195f0d12a 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -11,6 +11,7 @@ import { Tools } from "core/Misc/tools"; import addButton from "../../imgs/add.svg"; import deleteButton from "../../imgs/delete.svg"; import { NodeLedger } from "shared-ui-components/nodeGraphSystem/nodeLedger"; +import { GetBlocksByMode, GetAllBlockNames, type IBlockDefinition } from "./blockDefinitions"; import "./nodeList.scss"; @@ -21,121 +22,6 @@ interface INodeListComponentProps { export class NodeListComponent extends React.Component { private _onResetRequiredObserver: Nullable>; - private static _Tooltips: { [key: string]: string } = { - SystemBlock: "Generate a particle system", - MeshSourceBlock: "Mesh source for SPS - load custom geometry from the inspector", - NodeMaterialBlock: "Load a Node Material for SPS particles", - InitSolidParticleBlock: "Configure solid particle: mesh, count, material, init/update", - SolidParticleSystemBlock: "Configure Solid Particle System - connect SolidParticleSystemBlock output here", - UpdateSolidParticleBlock: "Update solid particle - connect SolidParticleSystemBlock output here", - CreateSolidParticleBlock: "Create Solid Particle System - outputs SolidParticleSystem", - Float: "Input block set to a float value", - Vector2: "Input block set to a Vector2 value", - Vector3: "Input block set to a Vector3 value", - Color4: "Input block set to a Color4 value", - Int: "Input block set to a integer value", - TextureBlock: "Provide a texture", - BoxShapeBlock: "Emit particles from a box shape", - ConeShapeBlock: "Emit particles from a cone shape", - CustomShapeBlock: "Emit particles from a custom position", - CylinderShapeBlock: "Emit particles from a cylinder shape", - MeshShapeBlock: "Emit particles from a mesh shape", - PointShapeBlock: "Emit particles from a point", - SphereShapeBlock: "Emit particles from a sphere shape", - UpdateDirectionBlock: "Update the direction of a particle", - UpdatePositionBlock: "Update the position of a particle", - UpdateColorBlock: "Update the color of a particle", - UpdateScaleBlock: "Update the scale of a particle", - UpdateSizeBlock: "Update the size of a particle", - UpdateAngleBlock: "Update the angle of a particle", - UpdateAgeBlock: "Update the age of a particle", - AddBlock: "Math block set to Add", - DivideBlock: "Math block set to Divide", - MaxBlock: "Math block set to Max", - MinBlock: "Math block set to Min", - MultiplyBlock: "Math block set to Multiply", - SubtractBlock: "Math block set to Subtract", - PositionBlock: "Contextual block to get the position of a particle", - DirectionBlock: "Contextual block to get the direction of a particle", - DirectionScaleBlock: "Contextual block to get the direction scale of a particle", - ScaledDirectionBlock: "Contextual block to get the scaled direction of a particle", - ColorBlock: "Contextual block to get the color of a particle", - InitialColorBlock: "Contextual block to get the initial color of a particle", - InitialDirectionBlock: "Contextual block to get the initial direction of a particle", - ColorDeadBlock: "Contextual block to get the expected dead color of a particle", - SolidParticleIndexBlock: "Contextual block to get the index of a solid particle", - AgeBlock: "Contextual block to get the age of a particle", - AngleBlock: "Contextual block to get the angle of a particle", - LifetimeBlock: "Contextual block to get the lifetime of a particle", - ScaleBlock: "Contextual block to get the scale of a particle", - SizeBlock: "Contextual block to get the size of a particle", - AgeGradientBlock: "Contextual block to get the age gradient of a particle ie. the age divided by the lifetime", - LerpBlock: "Interpolate between two values", - GradientValueBlock: "A gradient value block used to define a value at a specific age", - GradientBlock: "A gradient block used to define a gradient of values over the lifetime of a particle", - ConverterBlock: "Convert between different types of values, such as Color4, Vector2, Vector3, and Float", - FractBlock: "Outputs only the fractional value of a floating point number", - AbsBlock: "Trigonometry block set to Abs", - ArcCosBlock: "Trigonometry block set to Arc cos (using radians)", - ArcSinBlock: "Trigonometry block set to Arc sin (using radians)", - ArcTanBlock: "Trigonometry block set to Arc tan (using radians)", - ArcTan2Block: "Trigonometry block set to Arc tan2 (using radians)", - CosBlock: "Trigonometry block set to Cos (using radians)", - ExpBlock: "Trigonometry block set to Exp (using radians)", - Exp2Block: "Trigonometry block set to Exp2 (using radians)", - LogBlock: "Trigonometry block set to Log (using radians)", - SinBlock: "Trigonometry block set to Sin (using radians)", - TanBlock: "Trigonometry block set to Tan (using radians)", - ToDegreesBlock: "Conversion block used to convert radians to degree", - ToRadiansBlock: "Conversion block used to convert degrees to radians", - NegateBlock: "Math block set to Negate", - OneMinusBlock: "Trigonometry block set to One Minus", - ReciprocalBlock: "Trigonometry block set to Reciprocal", - SignBlock: "Trigonometry block set to Sign", - SqrtBlock: "Trigonometry block set to Square Root", - RoundBlock: "Trigonometry block set to Round", - FloorBlock: "Trigonometry block set to Floor", - CeilingBlock: "Trigonometry block set to Ceiling", - FloatToIntBlock: "Block used to convert a float value to an integer value using a specified operation (Round, Ceil, Floor, Truncate)", - RandomBlock: "Generate a random value", - DebugBlock: "Debug block used to output values of connection ports", - ElbowBlock: "Passthrough block mostly used to organize your graph", - TeleportInBlock: "Passthrough block mostly used to organize your graph (but without visible lines). It works like a teleportation point for the graph.", - TeleportOutBlock: "Endpoint for a TeleportInBlock.", - TimeBlock: "Block used to get the current time in ms", - DeltaBlock: "Block used to get the delta value for animations", - BasicPositionUpdateBlock: "Block used to update the position of a particle with a basic update (eg. direction * delta)", - BasicColorUpdateBlock: "Block used to update the color of a particle with a basic update (eg. color * delta)", - TriggerBlock: "Block used to trigger a particle system based on a condition", - SetupSpriteSheetBlock: "Block used to setup a sprite sheet for particles", - BasicUpdateSpriteBlock: "Block used to update the sprite index of a particle with a basic update (eg. incrementing the index by 1)", - UpdateSpriteCellIndexBlock: "Block used to update the sprite cell index of a particle", - SpriteCellEndBlock: "Contextual block to get the end cell of a sprite sheet", - SpriteCellStartBlock: "Contextual block to get the start cell of a sprite sheet", - SpriteCellIndexBlock: "Contextual block to get the sprite cell index of a particle", - EmitterPositionBlock: "Contextual block to get the coordinates of the emitter", - CameraPositionBlock: "Contextual block to get the position of the active camera", - UpdateFlowMapBlock: "Block used to update particle position based on a flow map", - UpdateAttractorBlock: "Block used to update particle position based on an attractor", - ConditionBlock: "Block used to evaluate a condition and return a value", - EqualBlock: "Conditional block set to Equal", - NotEqualBlock: "Conditional block set to NotEqual", - LessThanBlock: "Conditional block set to LessThan", - LessOrEqualBlock: "Conditional block set to LessOrEqual", - GreaterThanBlock: "Conditional block set to GreaterThan", - GreaterOrEqualBlock: "Conditional block set to GreaterOrEqual", - XorBlock: "Conditional block set to Xor", - OrBlock: "Conditional block set to Or", - AndBlock: "Conditional block set to And", - CreateParticleBlock: "Block used to create a particle with properties such as emit power, lifetime, color, scale, and angle", - AlignAngleBlock: "Block used to align the angle of a particle to its direction", - VectorLengthBlock: "Block used to get the length of a vector", - LocalVariableBlock: "Block used to store local values (eg. within a loop)", - FresnelBlock: "Block used to compute the Fresnel term", - ColorStepBlock: "Contextual block to get the expected color step of a particle", - ScaledColorStepBlock: "Contextual block to get the expected scaled color step of a particle", - }; - private _customFrameList: { [key: string]: string }; constructor(props: INodeListComponentProps) { @@ -215,119 +101,49 @@ export class NodeListComponent extends React.Component = { Custom_Frames: customFrameNames, - Shapes: ["BoxShapeBlock", "ConeShapeBlock", "SphereShapeBlock", "PointShapeBlock", "CustomShapeBlock", "CylinderShapeBlock", "MeshShapeBlock"], - Inputs: ["Float", "Vector2", "Vector3", "Int", "TextureBlock", "Color4"], - Updates: [ - "UpdateDirectionBlock", - "UpdatePositionBlock", - "UpdateColorBlock", - "UpdateScaleBlock", - "UpdateSizeBlock", - "UpdateAngleBlock", - "UpdateAgeBlock", - "BasicColorUpdateBlock", - "BasicPositionUpdateBlock", - "BasicSpriteUpdateBlock", - "UpdateSpriteCellIndexBlock", - "UpdateFlowMapBlock", - "UpdateAttractorBlock", - "AlignAngleBlock", - ], - Triggers: ["TriggerBlock"], - Setup: ["CreateParticleBlock", "SetupSpriteSheetBlock"], - Math__Standard: [ - "AddBlock", - "DivideBlock", - "MaxBlock", - "MinBlock", - "MultiplyBlock", - "SubtractBlock", - "NegateBlock", - "OneMinusBlock", - "ReciprocalBlock", - "SignBlock", - "SqrtBlock", - "RoundBlock", - "FloorBlock", - "CeilingBlock", - "FloatToIntBlock", - ], - Math__Scientific: [ - "AbsBlock", - "ArcCosBlock", - "ArcSinBlock", - "ArcTanBlock", - "CosBlock", - "ExpBlock", - "Exp2Block", - "LogBlock", - "SinBlock", - "TanBlock", - "ToDegreesBlock", - "ToRadiansBlock", - "FractBlock", - "VectorLengthBlock", - ], - Logical: ["EqualBlock", "NotEqualBlock", "LessThanBlock", "LessOrEqualBlock", "GreaterThanBlock", "GreaterOrEqualBlock", "XorBlock", "OrBlock", "AndBlock"], - Interpolation: ["LerpBlock", "GradientValueBlock", "GradientBlock"], - Misc: ["ConverterBlock", "RandomBlock", "DebugBlock", "ElbowBlock", "TeleportInBlock", "TeleportOutBlock", "LocalVariableBlock", "FresnelBlock"], - System_Nodes: ["SystemBlock", "TimeBlock", "DeltaBlock", "EmitterPositionBlock", "CameraPositionBlock"], - SolidParticle_Nodes: [ - "MeshSourceBlock", - "InitSolidParticleBlock", - "SolidParticleSystemBlock", - "CreateSolidParticleBlock", - "UpdateSolidParticleBlock", - "NodeMaterialBlock", - ], - Contextual: [ - "PositionBlock", - "DirectionBlock", - "DirectionScaleBlock", - "ScaledDirectionBlock", - "ColorBlock", - "AgeBlock", - "LifetimeBlock", - "ScaleBlock", - "SizeBlock", - "AgeGradientBlock", - "AngleBlock", - "InitialColorBlock", - "ColorDeadBlock", - "SpriteCellEndBlock", - "SpriteCellStartBlock", - "SpriteCellIndexBlock", - "SolidParticleIndexBlock", - "InitialDirectionBlock", - "ColorStepBlock", - "ScaledColorStepBlock", - ], + ...blocksByCategory, }; // Create node menu const blockMenu = []; for (const key in allBlocks) { - const blockList = allBlocks[key] - .filter((b: string) => !this.state.filter || b.toLowerCase().indexOf(this.state.filter.toLowerCase()) !== -1) - .sort((a: string, b: string) => a.localeCompare(b)) - .map((block: any) => { + const blocks = allBlocks[key]; + // Skip empty categories + if (!blocks || blocks.length === 0) { + continue; + } + + const blockList = blocks + .filter((block: string | IBlockDefinition) => { + const blockName = typeof block === "string" ? block : block.name; + return !this.state.filter || blockName.toLowerCase().indexOf(this.state.filter.toLowerCase()) !== -1; + }) + .sort((a: string | IBlockDefinition, b: string | IBlockDefinition) => { + const nameA = typeof a === "string" ? a : a.name; + const nameB = typeof b === "string" ? b : b.name; + return nameA.localeCompare(nameB); + }) + .map((block: string | IBlockDefinition) => { if (key === "Custom_Frames") { + const blockName = block as string; return ( this.removeItem(value)} /> ); } - return ; + const blockDef = block as IBlockDefinition; + return ; }); if (key === "Custom_Frames") { @@ -354,32 +170,28 @@ export class NodeListComponent extends React.Component ); } + } - // Register blocks - const ledger = NodeLedger.RegisteredNodeNames; - for (const key in allBlocks) { - const blocks = allBlocks[key] as string[]; - if (blocks.length) { - for (const block of blocks) { - if (!ledger.includes(block)) { - ledger.push(block); - } - } - } + // Register all blocks (register all blocks regardless of mode, as they may exist in saved files) + const ledger = NodeLedger.RegisteredNodeNames; + const allBlockNames = GetAllBlockNames(); + for (const blockName of allBlockNames) { + if (!ledger.includes(blockName)) { + ledger.push(blockName); } - NodeLedger.NameFormatter = (name) => { - let finalName = name; - // custom frame - if (name.endsWith("Custom")) { - const nameIndex = name.lastIndexOf("Custom"); - finalName = name.substring(0, nameIndex); - finalName += " [custom]"; - } else { - finalName = name.replace("Block", ""); - } - return finalName; - }; } + NodeLedger.NameFormatter = (name) => { + let finalName = name; + // custom frame + if (name.endsWith("Custom")) { + const nameIndex = name.lastIndexOf("Custom"); + finalName = name.substring(0, nameIndex); + finalName += " [custom]"; + } else { + finalName = name.replace("Block", ""); + } + return finalName; + }; return (
diff --git a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx index ddd107c74d8..133b7213602 100644 --- a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx @@ -204,6 +204,13 @@ export class PropertyTabComponent extends React.Component { + if (nodeParticleSet.editorData?.mode !== undefined && nodeParticleSet.editorData?.mode !== null) { + this.props.globalState.mode = nodeParticleSet.editorData.mode; + this.props.globalState.onResetRequiredObservable.notifyObservers(true); + } else { + this.props.globalState.mode = NodeParticleModes.Particle; + } + await nodeParticleSet.buildAsync(this.props.globalState.hostScene); this.props.globalState.onClearUndoStack.notifyObservers(); }) diff --git a/packages/tools/nodeParticleEditor/src/serializationTools.ts b/packages/tools/nodeParticleEditor/src/serializationTools.ts index 36860749b76..a237bc9e3b2 100644 --- a/packages/tools/nodeParticleEditor/src/serializationTools.ts +++ b/packages/tools/nodeParticleEditor/src/serializationTools.ts @@ -3,14 +3,15 @@ import type { GlobalState } from "./globalState"; import type { Nullable } from "core/types"; import type { GraphFrame } from "shared-ui-components/nodeGraphSystem/graphFrame"; import type { NodeParticleBlock } from "core/Particles/Node/nodeParticleBlock"; +import { NodeParticleModes } from "./nodeParticleModes"; export class SerializationTools { public static UpdateLocations(particleSet: NodeParticleSystemSet, globalState: GlobalState, frame?: Nullable) { - particleSet.editorData = { - locations: [], - }; + if (!particleSet.editorData) { + particleSet.editorData = {}; + } + particleSet.editorData.locations = []; - // Store node locations const blocks: NodeParticleBlock[] = frame ? frame.nodes.map((n) => n.content.data) : particleSet.attachedBlocks; for (const block of blocks) { @@ -34,15 +35,32 @@ export class SerializationTools { const serializationObject = particleSet.serialize(selectedBlocks); + if (!serializationObject.editorData) { + serializationObject.editorData = {}; + } + serializationObject.editorData.mode = globalState.mode; + return JSON.stringify(serializationObject, undefined, 2); } public static Deserialize(serializationObject: any, globalState: GlobalState) { + const savedMode = serializationObject.editorData?.mode; + if (savedMode !== undefined && savedMode !== null) { + globalState.mode = savedMode; + } else { + globalState.mode = NodeParticleModes.Particle; + } + globalState.nodeParticleSet.parseSerializedObject(serializationObject); globalState.onIsLoadingChanged.notifyObservers(false); } public static AddFrameToParticleSystemSet(serializationObject: any, globalState: GlobalState, currentSystemSet: NodeParticleSystemSet) { + const savedMode = serializationObject.editorData?.mode; + if (savedMode !== undefined && savedMode !== null) { + globalState.mode = savedMode; + } + this.UpdateLocations(currentSystemSet, globalState); globalState.nodeParticleSet.parseSerializedObject(serializationObject, true); globalState.onImportFrameObservable.notifyObservers(serializationObject); From f6ee8537b3edc95321dcda8191a943b0273e0d6d Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 25 Nov 2025 00:17:41 +0300 Subject: [PATCH 63/68] Update block definition for NodeMaterialSourceBlock in Node Particle Editor - Renamed `NodeMaterialBlock` to `NodeMaterialSourceBlock` in the block definitions to ensure consistency and clarity in naming conventions. - This change aligns with previous updates to enhance the usability and organization of the node particle editor. These modifications improve the overall coherence of block definitions within the editor. --- .../src/components/nodeList/blockDefinitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts b/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts index 0dca2745e80..252ef595e6c 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts @@ -28,7 +28,7 @@ export const BlockDefinitions: IBlockDefinition[] = [ { name: "TextureBlock", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Provide a texture" }, { name: "Color4", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a Color4 value" }, { name: "MeshSourceBlock", category: "Inputs", modes: [NodeParticleModes.SolidParticle], tooltip: "Mesh source for SPS - load custom geometry from the inspector" }, - { name: "NodeMaterialBlock", category: "Inputs", modes: [NodeParticleModes.SolidParticle], tooltip: "Load a Node Material for SPS particles" }, + { name: "NodeMaterialSourceBlock", category: "Inputs", modes: [NodeParticleModes.SolidParticle], tooltip: "Load a Node Material for SPS particles" }, // Updates (Particle only - use _updateQueueStart) { name: "UpdateDirectionBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the direction of a particle" }, From 43ac6acf304895fb76dd7e070c66f39b4ba42484 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 25 Nov 2025 09:21:36 +0300 Subject: [PATCH 64/68] Refactor SolidParticle exports in Node Particle Editor - Updated the exports in `index.ts` to include individual SolidParticle block files, enhancing modularity and organization. - Removed the obsolete `SolidParticle/index.ts` file to streamline the codebase. These changes improve the structure and maintainability of the SolidParticle components within the node particle editor. --- .../core/src/Particles/Node/Blocks/SolidParticle/index.ts | 7 ------- packages/dev/core/src/Particles/Node/Blocks/index.ts | 8 +++++++- 2 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts deleted file mode 100644 index c87421e542c..00000000000 --- a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from "./ISolidParticleData"; -export * from "./meshSourceBlock"; -export * from "./initSolidParticleBlock"; -export * from "./solidParticleSystemBlock"; -export * from "./updateSolidParticleBlock"; -export * from "./createSolidParticleBlock"; -export * from "./nodeMaterialSourceBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/index.ts b/packages/dev/core/src/Particles/Node/Blocks/index.ts index db03401ca7f..f3f96cd2507 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/index.ts @@ -34,4 +34,10 @@ export * from "./Triggers/particleTriggerBlock"; export * from "./particleLocalVariableBlock"; export * from "./particleVectorLengthBlock"; export * from "./particleFresnelBlock"; -export * from "./SolidParticle"; +export * from "./SolidParticle/ISolidParticleData"; +export * from "./SolidParticle/meshSourceBlock"; +export * from "./SolidParticle/initSolidParticleBlock"; +export * from "./SolidParticle/solidParticleSystemBlock"; +export * from "./SolidParticle/updateSolidParticleBlock"; +export * from "./SolidParticle/createSolidParticleBlock"; +export * from "./SolidParticle/nodeMaterialSourceBlock"; From 5fde8209d06780a3272b35c4af3067f942d288e9 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 25 Nov 2025 10:10:21 +0300 Subject: [PATCH 65/68] Refactor SolidParticle imports in Node Particle System and Editor - Updated import statements in `nodeParticleSystemSet.ts` and `blockTools.ts` to reference individual SolidParticle block files, enhancing modularity and organization. - This change improves the clarity and maintainability of the codebase by ensuring that each block is imported from its specific file. These modifications align with recent efforts to streamline the structure of the node particle editor. --- .../src/Particles/Node/nodeParticleSystemSet.ts | 6 +++++- .../tools/nodeParticleEditor/src/blockTools.ts | 14 ++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index 62c37c2a24f..860a291234f 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -25,7 +25,11 @@ import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTelepor import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; import type { Nullable } from "core/types"; import type { Color4 } from "core/Maths/math.color"; -import { SolidParticleSystemBlock, MeshSourceBlock, InitSolidParticleBlock, UpdateSolidParticleBlock, CreateSolidParticleBlock } from "./Blocks"; +import { SolidParticleSystemBlock } from "./Blocks/SolidParticle/solidParticleSystemBlock"; +import { MeshSourceBlock } from "./Blocks/SolidParticle/meshSourceBlock"; +import { InitSolidParticleBlock } from "./Blocks/SolidParticle/initSolidParticleBlock"; +import { UpdateSolidParticleBlock } from "./Blocks/SolidParticle/updateSolidParticleBlock"; +import { CreateSolidParticleBlock } from "./Blocks/SolidParticle/createSolidParticleBlock"; import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; import { VertexData } from "core/Meshes/mesh.vertexData"; import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index f4e59df1d24..c2dca3fbfec 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -43,14 +43,12 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; -import { - MeshSourceBlock, - SolidParticleSystemBlock, - CreateSolidParticleBlock, - UpdateSolidParticleBlock, - InitSolidParticleBlock, - NodeMaterialSourceBlock, -} from "core/Particles/Node/Blocks"; +import { MeshSourceBlock } from "core/Particles/Node/Blocks/SolidParticle/meshSourceBlock"; +import { InitSolidParticleBlock } from "core/Particles/Node/Blocks/SolidParticle/initSolidParticleBlock"; +import { UpdateSolidParticleBlock } from "core/Particles/Node/Blocks/SolidParticle/updateSolidParticleBlock"; +import { CreateSolidParticleBlock } from "core/Particles/Node/Blocks/SolidParticle/createSolidParticleBlock"; +import { NodeMaterialSourceBlock } from "core/Particles/Node/Blocks/SolidParticle/nodeMaterialSourceBlock"; +import { SolidParticleSystemBlock } from "core/Particles/Node/Blocks/SolidParticle/solidParticleSystemBlock"; import { ParticleFloatToIntBlock } from "core/Particles/Node/Blocks/particleFloatToIntBlock"; /** From 20493b544327579f8b919631a5c6937f6b7f6ef4 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Tue, 25 Nov 2025 11:34:49 +0300 Subject: [PATCH 66/68] Enhance BlockNodeData logic in Node Particle Editor - Updated the `isConnectedToOutput` method in `BlockNodeData` to include a check for `SolidParticleSystemBlock` in addition to existing conditions. This change improves the logic for determining block connectivity, enhancing the functionality of the node particle editor. These modifications align with ongoing efforts to refine block interactions within the editor. --- .../tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts b/packages/tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts index 695951935f0..95083fedd69 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts @@ -77,7 +77,7 @@ export class BlockNodeData implements INodeData { public isConnectedToOutput() { const block = this.data; - return block.isDebug || block.isAnAncestorOfType("SystemBlock"); + return block.isDebug || block.isAnAncestorOfType("SystemBlock") || block.isAnAncestorOfType("SolidParticleSystemBlock"); } public dispose() { From e568f3c26ed5f7cf86bb01dd119d6803d8c67c3f Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Wed, 26 Nov 2025 10:30:46 +0300 Subject: [PATCH 67/68] Refactor NodeParticleBlockConnectionPointTypes enum for consistency - Removed obsolete connection point types from the `NodeParticleBlockConnectionPointTypes` enum to streamline the code and improve clarity. - Adjusted values for `AutoDetect`, `BasedOnInput`, `Undefined`, and `All` to ensure proper bitmask representation. These changes enhance the organization and maintainability of the node particle editor's connection point definitions. --- .../nodeParticleBlockConnectionPointTypes.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts index ef737a5a955..0a8b63b2d6c 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts @@ -36,20 +36,12 @@ export enum NodeParticleBlockConnectionPointTypes { Mesh = 0x8000, /** Material */ Material = 0x10000, - /** Camera */ - Camera = 0x20000, - /** Function */ - Function = 0x40000, - /** Vector4 */ - Vector4 = 0x80000, - /** Boolean */ - Boolean = 0x100000, /** Detect type based on connection */ - AutoDetect = 0x200000, + AutoDetect = 0x20000, /** Output type that will be defined by input type */ - BasedOnInput = 0x400000, + BasedOnInput = 0x40000, /** Undefined */ - Undefined = 0x800000, + Undefined = 0x80000, /** Bitmask of all types */ - All = 0xffffff, + All = 0x1ffff, } From 5b11243d7da916350bdc93c213a271352e972f9e Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Wed, 26 Nov 2025 10:33:45 +0300 Subject: [PATCH 68/68] Update BlockTools to include SolidParticleConfig type - Added support for the `SolidParticleConfig` connection point type in the `BlockTools` class, enhancing the color mapping and type detection logic. - Removed obsolete connection point types (`Camera` and `Function`) to streamline the code and improve clarity. These changes improve the organization and maintainability of the node particle editor's block tools. --- .../nodeParticleEditor/src/blockTools.ts | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index c2dca3fbfec..4e10bcb600c 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -496,18 +496,15 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.SolidParticle: color = "#2e8b57"; break; + case NodeParticleBlockConnectionPointTypes.SolidParticleConfig: + color = "#1f6f5a"; + break; case NodeParticleBlockConnectionPointTypes.Mesh: color = "#4682b4"; break; case NodeParticleBlockConnectionPointTypes.Material: color = "#daa520"; break; - case NodeParticleBlockConnectionPointTypes.Camera: - color = "#9370db"; - break; - case NodeParticleBlockConnectionPointTypes.Function: - color = "#ff6347"; - break; } return color; @@ -529,14 +526,12 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Matrix; case "SolidParticle": return NodeParticleBlockConnectionPointTypes.SolidParticle; + case "SolidParticleConfig": + return NodeParticleBlockConnectionPointTypes.SolidParticleConfig; case "Mesh": return NodeParticleBlockConnectionPointTypes.Mesh; case "Material": return NodeParticleBlockConnectionPointTypes.Material; - case "Camera": - return NodeParticleBlockConnectionPointTypes.Camera; - case "Function": - return NodeParticleBlockConnectionPointTypes.Function; } return NodeParticleBlockConnectionPointTypes.AutoDetect; @@ -558,14 +553,12 @@ export class BlockTools { return "Matrix"; case NodeParticleBlockConnectionPointTypes.SolidParticle: return "SolidParticle"; + case NodeParticleBlockConnectionPointTypes.SolidParticleConfig: + return "SolidParticleConfig"; case NodeParticleBlockConnectionPointTypes.Mesh: return "Mesh"; case NodeParticleBlockConnectionPointTypes.Material: return "Material"; - case NodeParticleBlockConnectionPointTypes.Camera: - return "Camera"; - case NodeParticleBlockConnectionPointTypes.Function: - return "Function"; } return "";