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..8d0e2f0227f --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -0,0 +1,26 @@ +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 update block data + */ +export interface ISPSUpdateData { + position?: () => Vector3; + velocity?: () => Vector3; + color?: () => Color4; + scaling?: () => Vector3; + rotation?: () => Vector3; +} + +/** + * Interface for SPS create block data + */ +export interface ISPSParticleConfigData { + mesh: Mesh; + count: number; + material?: Material; + initBlock?: ISPSUpdateData; + 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..6a5c475662d --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -0,0 +1,185 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { ISPSParticleConfigData } from "./ISPSData"; +import { SolidParticle } from "../../../solidParticle"; +import { Observer } from "../../../../Misc"; + +/** + * Block used to create SolidParticleSystem and collect all Create blocks + */ +export class SPSCreateBlock extends NodeParticleBlock { + private _connectionObservers = new Map>(); + private _disconnectionObservers = new Map>(); + + public constructor(name: string) { + super(name); + this.registerInput(`particleConfig-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + + this._manageExtendedInputs(0); + } + + public override getClassName() { + return "SPSCreateBlock"; + } + + private _entryCount = 1; + + private _extend() { + this._entryCount++; + this.registerInput(`particleConfig-${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(`particleConfig-${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 particleConfig(): NodeParticleConnectionPoint { + return this._inputs[this._entryCount - 1]; + } + + public get solidParticleSystem(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + if (!state.scene) { + throw new Error("Scene is not initialized in NodeParticleBuildState"); + } + + 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.solidParticleSystem._storedValue = 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(); + } + } + } +} + +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..df424f3d5b3 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -0,0 +1,128 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISPSUpdateData } from "./ISPSData"; + +/** + * Block used to generate initialization function for SPS particles + */ +export class SPSInitBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); + + this.registerOutput("initData", NodeParticleBlockConnectionPointTypes.System); + } + + public override getClassName() { + return "SPSInitBlock"; + } + + public get initData(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public override _build(state: NodeParticleBuildState) { + const initData = {} as ISPSUpdateData; + if (this.position.isConnected) { + initData.position = () => { + return this.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._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; + } + + 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..fd4e56d9a5e --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts @@ -0,0 +1,130 @@ +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"; +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, + 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) { + if (this._mesh) { + this._mesh.dispose(); + this._mesh = null; + } + if (this.shapeType === SPSMeshShapeType.Custom) { + if (this.customMesh.isConnected) { + const customMesh = this.customMesh.getConnectedValue(state); + if (customMesh) { + this._mesh = customMesh; + } else { + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + } + } else { + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + } + } else { + switch (this.shapeType) { + case SPSMeshShapeType.Box: + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + case SPSMeshShapeType.Sphere: + this._mesh = CreateSphere("sps_mesh_source", { diameter: this.size, segments: this.segments }, state.scene); + break; + case SPSMeshShapeType.Cylinder: + this._mesh = CreateCylinder("sps_mesh_source", { height: this.size, diameter: this.size, tessellation: this.segments }, state.scene); + break; + case SPSMeshShapeType.Plane: + this._mesh = CreatePlane("sps_mesh_source", { size: this.size }, state.scene); + break; + default: + this._mesh = CreateBox("sps_mesh_source", { size: this.size }, state.scene); + break; + } + } + if (this._mesh) { + this._mesh.isVisible = false; + } + + if (!this._disposeHandlerAdded) { + this.onDisposeObservable.addOnce(() => { + this._mesh?.dispose(); + this._mesh = null; + }); + this._disposeHandlerAdded = true; + } + this.mesh._storedValue = this._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/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 new file mode 100644 index 00000000000..4d0a8e4340d --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -0,0 +1,74 @@ +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"; + +/** + * Block used to create SolidParticleSystem and collect all Create blocks + */ +export class SPSSystemBlock extends NodeParticleBlock { + private static _IdCounter = 0; + + @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("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + this.registerOutput("solidParticleSystem", NodeParticleBlockConnectionPointTypes.SolidParticleSystem); + } + + public override getClassName() { + return "SPSSystemBlock"; + } + + public get solidParticleSystem(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { + state.buildId = ++this._buildId; + + this.build(state); + + const solidParticleSystem = this.solidParticleSystem.getConnectedValue(state) as SolidParticleSystem; + + if (!solidParticleSystem) { + throw new Error("No SolidParticleSystem connected to SPSSystemBlock"); + } + + solidParticleSystem.billboard = this.billboard; + solidParticleSystem.name = this.name; + + this.onDisposeObservable.addOnce(() => { + solidParticleSystem.dispose(); + }); + return solidParticleSystem; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.billboard = this.billboard; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + 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..cadae7dea94 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -0,0 +1,127 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISPSUpdateData } from "./ISPSData"; + +/** + * Block used to generate 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 = {} as ISPSUpdateData; + 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._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; + } + + 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..070d7ebd205 --- /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 "./SPSParticleConfigBlock"; +export * from "./SPSSystemBlock"; +export * from "./SPSUpdateBlock"; +export * from "./SPSInitBlock"; +export * from "./SPSCreateBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/index.ts b/packages/dev/core/src/Particles/Node/Blocks/index.ts index 3e086086f35..c691fcccbf1 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 "./SolidParticle"; diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts index 87d59a00192..88d31930cd9 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 */ + SolidParticleSystem = 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/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/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..46c7e66ed5b 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -20,8 +20,11 @@ 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 { Color4 } from "core/Maths/math.color"; +import { SPSParticleConfigBlock, SPSInitBlock, SPSMeshShapeType, SPSMeshSourceBlock, SPSSystemBlock, SPSUpdateBlock, SPSCreateBlock } 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,63 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } + public setToDefaultSPS() { + this.clear(); + this.editorData = null; + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.billboard = false; + + const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + spsCreateBlock.solidParticleSystem.connectTo(spsSystem.solidParticleSystem); + + const 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); + } + /** * Remove a block from the current system set * @param block defines the block to remove 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. diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 73877983551..97fc5e54f37 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -41,6 +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, SPSParticleConfigBlock } from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -149,6 +150,18 @@ export class BlockTools { return new UpdateAttractorBlock("Update attractor"); case "SystemBlock": return new SystemBlock("System"); + case "SPSMeshSourceBlock": + return new SPSMeshSourceBlock("SPS Mesh Source"); + case "SPSParticleConfigBlock": + return new SPSParticleConfigBlock("SPS Particle Config"); + case "SPSSystemBlock": + return new SPSSystemBlock("SPS System"); + case "SPSCreateBlock": + return new SPSCreateBlock("SPS Create"); + case "SPSInitBlock": + return new SPSInitBlock("SPS Init"); + case "SPSUpdateBlock": + return new SPSUpdateBlock("SPS Update"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": @@ -435,6 +448,24 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.System: color = "#f20a2e"; break; + case NodeParticleBlockConnectionPointTypes.SolidParticleSystem: + 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 +485,18 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Color4; case "Matrix": return NodeParticleBlockConnectionPointTypes.Matrix; + case "SolidParticleSystem": + return NodeParticleBlockConnectionPointTypes.SolidParticleSystem; + 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 +516,18 @@ export class BlockTools { return "Color4"; case NodeParticleBlockConnectionPointTypes.Matrix: return "Matrix"; + case NodeParticleBlockConnectionPointTypes.SolidParticleSystem: + return "SolidParticleSystem"; + 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..412eed10b4e 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -23,6 +23,12 @@ export class NodeListComponent 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/graphSystem/registerToDisplayLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts index db180f6be2b..327161e98b7 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts @@ -35,6 +35,14 @@ export const RegisterToDisplayManagers = () => { DisplayLedger.RegisteredControls["BasicColorUpdateBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["UpdateFlowMapBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["SystemBlock"] = SystemDisplayManager; + + DisplayLedger.RegisteredControls["SPSMeshSourceBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSParticleConfigBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSCreateBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SPSSystemBlock"] = SystemDisplayManager; + DisplayLedger.RegisteredControls["SPSInitBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["SPSUpdateBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; 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..57075b01779 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -13,4 +13,11 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleDebugBlock"] = DebugPropertyTabComponent; PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; + + PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSParticleConfigBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateBlock"] = GenericPropertyComponent; }; 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, +}