From 4629a7ddae0ca63412433db17eb4fdcca0fea723 Mon Sep 17 00:00:00 2001 From: Mikalai Lazitski Date: Fri, 10 Oct 2025 19:53:17 +0300 Subject: [PATCH 01/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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/22] 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; };