diff --git a/packages/dev/core/src/Decorators/nodeDecorator.ts b/packages/dev/core/src/Decorators/nodeDecorator.ts index b0ad690caca..293140903a4 100644 --- a/packages/dev/core/src/Decorators/nodeDecorator.ts +++ b/packages/dev/core/src/Decorators/nodeDecorator.ts @@ -21,14 +21,14 @@ export const enum PropertyTypeForEdition { Color3, /** property is a Color4 */ Color4, + /** property is a string */ + String, /** property (int) should be edited as a combo box with a list of sampling modes */ SamplingMode, /** property (int) should be edited as a combo box with a list of texture formats */ TextureFormat, /** property (int) should be edited as a combo box with a list of texture types */ TextureType, - /** property is a string */ - String, /** property is a matrix */ Matrix, /** property is a viewport */ diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts new file mode 100644 index 00000000000..825ee68c170 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISPSData.ts @@ -0,0 +1,31 @@ +import type { Vector3 } from "core/Maths/math.vector"; +import type { Color4 } from "core/Maths/math.color"; +import type { Material } from "core/Materials/material"; +import type { VertexData } from "core/Meshes/mesh.vertexData"; + +export interface ISpsMeshSourceData { + customMeshName?: string; + vertexData?: VertexData; +} + +/** + * 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 { + meshData: ISpsMeshSourceData | null; + 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..f1e32f67d25 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSCreateBlock.ts @@ -0,0 +1,227 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { ISpsParticleConfigData } from "./ISPSData"; +import { Mesh } from "core/Meshes/mesh"; +import type { SolidParticle } from "../../../solidParticle"; +import type { Observer } from "core/Misc/observable"; + +/** + * 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(`config-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticleConfig); + this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + + this._manageExtendedInputs(0); + } + + public override getClassName() { + return "SPSCreateBlock"; + } + + private _entryCount = 1; + + private _extend() { + this._entryCount++; + this.registerInput(`config-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticleConfig, true); + this._manageExtendedInputs(this._entryCount - 1); + } + + private _shrink() { + if (this._entryCount > 1) { + this._unmanageExtendedInputs(this._entryCount - 1); + this._entryCount--; + this.unregisterInput(`config-${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 config(): NodeParticleConnectionPoint { + return this._inputs[this._entryCount - 1]; + } + + public get solidParticle(): 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 || !creatData.meshData || !creatData.count) { + continue; + } + + if (!creatData.meshData.vertexData) { + continue; + } + + const mesh = new Mesh(`${this.name}_shape_${i}`, state.scene); + creatData.meshData.vertexData.applyToMesh(mesh, true); + mesh.isVisible = false; + if (creatData.material) { + mesh.material = creatData.material; + } + + const shapeId = sps.addShape(mesh, creatData.count); + createBlocks.set(shapeId, creatData); + mesh.dispose(); + } + + sps.initParticles = () => { + if (!sps) { + return; + } + + 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; + } + }; + + sps.updateParticle = (particle: SolidParticle) => { + if (!sps) { + return particle; + } + + const particleCreateData = createBlocks.get(particle.shapeId); + const updateBlock = particleCreateData?.updateBlock; + if (!updateBlock) { + return particle; + } + // 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; + }; + + this.solidParticle._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..d0cf1de3f19 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSInitBlock.ts @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISpsUpdateData } from "./ISPSData"; + +/** + * Block used to generate initialization function for SPS particles + */ +export class SPSInitBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); + + this.registerOutput("initData", NodeParticleBlockConnectionPointTypes.System); + } + + public override getClassName() { + return "SPSInitBlock"; + } + + public get initData(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public override _build(state: NodeParticleBuildState) { + const initData = {} as ISpsUpdateData; + if (this.position.isConnected) { + initData.position = () => { + return this.position.getConnectedValue(state); + }; + } + if (this.velocity.isConnected) { + initData.velocity = () => { + return this.velocity.getConnectedValue(state); + }; + } + if (this.color.isConnected) { + initData.color = () => { + return this.color.getConnectedValue(state); + }; + } + if (this.scaling.isConnected) { + initData.scaling = () => { + return this.scaling.getConnectedValue(state); + }; + } + if (this.rotation.isConnected) { + initData.rotation = () => { + return this.rotation.getConnectedValue(state); + }; + } + + this.initData._storedValue = initData; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.SPSInitBlock", SPSInitBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts new file mode 100644 index 00000000000..30a88f6fa43 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSMeshSourceBlock.ts @@ -0,0 +1,182 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { Mesh } from "core/Meshes/mesh"; +import { VertexData } from "core/Meshes/mesh.vertexData"; +import { Observable } from "core/Misc/observable"; +import type { Nullable } from "core/types"; +import { Tools } from "core/Misc/tools"; +import { ImportMeshAsync } from "core/Loading/sceneLoader"; +import type { ISpsMeshSourceData } from "./ISPSData"; + +/** + * Block used to provide mesh source for SPS + */ +export class SPSMeshSourceBlock extends NodeParticleBlock { + private _customVertexData: Nullable = null; + private _customMeshName = ""; + private _isRemoteMeshLoading = false; + + /** Gets an observable raised when the block data changes */ + public onValueChangedObservable = new Observable(); + + /** Optional remote mesh URL used to auto load geometry */ + public remoteMeshUrl = ""; + /** Optional mesh name filter when loading remote geometry */ + public remoteMeshName = ""; + + public constructor(name: string) { + super(name); + + this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + } + + public override getClassName() { + return "SPSMeshSourceBlock"; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * Gets whether a custom mesh is currently assigned + */ + public get hasCustomMesh(): boolean { + return !!this._customVertexData; + } + + /** + * Gets the friendly name of the assigned custom mesh + */ + public get customMeshName(): string { + return this._customMeshName; + } + + /** + * Assigns a mesh as custom geometry source + * @param mesh mesh providing geometry + */ + public setCustomMesh(mesh: Nullable) { + if (!mesh) { + this.clearCustomMesh(); + return; + } + + this._customVertexData = VertexData.ExtractFromMesh(mesh, true, true); + this._customMeshName = mesh.name || ""; + this.remoteMeshUrl = ""; + this.remoteMeshName = ""; + this.onValueChangedObservable.notifyObservers(this); + } + + /** + * Assigns vertex data directly + * @param vertexData vertex data + * @param name friendly name + */ + public setCustomVertexData(vertexData: VertexData, name = "") { + this._customVertexData = vertexData; + this._customMeshName = name; + this.remoteMeshUrl = ""; + this.remoteMeshName = ""; + this.onValueChangedObservable.notifyObservers(this); + } + + /** + * Clears any assigned custom mesh data + */ + public clearCustomMesh() { + this._customVertexData = null; + this._customMeshName = ""; + this.remoteMeshUrl = ""; + this.remoteMeshName = ""; + this.onValueChangedObservable.notifyObservers(this); + } + + private _tryLoadRemoteMesh(state: NodeParticleBuildState) { + if (this._customVertexData || !this.remoteMeshUrl || this._isRemoteMeshLoading) { + return; + } + + this._isRemoteMeshLoading = true; + const fileName = Tools.GetFilename(this.remoteMeshUrl); + const rootUrl = this.remoteMeshUrl.substring(0, this.remoteMeshUrl.length - fileName.length); + + ImportMeshAsync(fileName, state.scene, { meshNames: "", rootUrl }) + .then((result) => { + let mesh = result.meshes.find((m) => (this.remoteMeshName ? m.name === this.remoteMeshName : !!m && m.name !== "__root__")); + if (!mesh && result.meshes.length) { + mesh = result.meshes[0]; + } + + if (mesh) { + this.setCustomMesh(mesh as Mesh); + this.onValueChangedObservable.notifyObservers(this); + } + + for (const loadedMesh of result.meshes) { + loadedMesh.dispose(); + } + for (const skeleton of result.skeletons) { + skeleton.dispose(); + } + for (const animationGroup of result.animationGroups) { + animationGroup.dispose(); + } + for (const particleSystem of result.particleSystems) { + particleSystem.dispose(); + } + }) + .catch(() => { + // Ignore load errors + }) + .finally(() => { + this._isRemoteMeshLoading = false; + }); + } + + public override _build(state: NodeParticleBuildState) { + this._tryLoadRemoteMesh(state); + + if (!this._customVertexData) { + this.mesh._storedValue = null; + return; + } + + const meshData: ISpsMeshSourceData = { + vertexData: this._customVertexData, + customMeshName: this._customMeshName, + }; + + this.mesh._storedValue = meshData; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.remoteMeshUrl = this.remoteMeshUrl; + serializationObject.remoteMeshName = this.remoteMeshName; + serializationObject.customMeshName = this._customMeshName; + if (this._customVertexData) { + serializationObject.customVertexData = this._customVertexData.serialize(); + } + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.remoteMeshUrl = serializationObject.remoteMeshUrl ?? ""; + this.remoteMeshName = serializationObject.remoteMeshName ?? ""; + + if (serializationObject.customVertexData) { + this._customVertexData = VertexData.Parse(serializationObject.customVertexData); + this._customMeshName = serializationObject.customMeshName || ""; + } + } +} + +RegisterClass("BABYLON.SPSMeshSourceBlock", SPSMeshSourceBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts new file mode 100644 index 00000000000..66852628b99 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSNodeMaterialBlock.ts @@ -0,0 +1,205 @@ +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { Tools } from "core/Misc/tools"; +import { NodeMaterial } from "core/Materials/Node/nodeMaterial"; +import type { Nullable } from "core/types"; +import { Texture } from "core/Materials/Textures/texture"; +import { RegisterClass } from "../../../../Misc/typeStore"; +import { Observable } from "core/Misc/observable"; +import type { Scene } from "core/scene"; + +/** + * Block used to load a node material for SPS + */ +export class SPSNodeMaterialBlock extends NodeParticleBlock { + @editableInPropertyPage("Shader URL", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + }) + public shaderUrl = ""; + + @editableInPropertyPage("Texture URL", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + }) + public textureUrl = ""; + + @editableInPropertyPage("Texture Block Name", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + }) + public textureBlockName = ""; + + private _nodeMaterial: Nullable = null; + private _serializedMaterial: Nullable = null; + private _customMaterialName = ""; + private _textureInstance: Nullable = null; + private _isLoading = false; + + public constructor(name: string) { + super(name); + this.registerInput("texture", NodeParticleBlockConnectionPointTypes.Texture, true); + this.registerOutput("material", NodeParticleBlockConnectionPointTypes.Material); + } + + public override getClassName() { + return "SPSNodeMaterialBlock"; + } + + /** Raised when material data changes */ + public onValueChangedObservable = new Observable(); + + public get texture(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get material(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public get hasCustomMaterial(): boolean { + return !!this._serializedMaterial; + } + + public get customMaterialName(): string { + return this._customMaterialName; + } + + public setSerializedMaterial(serializedData: string, name?: string) { + this._serializedMaterial = serializedData; + this._customMaterialName = name || "Custom Node Material"; + this.shaderUrl = ""; + this._disposeMaterial(); + this.onValueChangedObservable.notifyObservers(this); + } + + public clearMaterial() { + this._serializedMaterial = null; + this._customMaterialName = ""; + this._disposeMaterial(); + this.onValueChangedObservable.notifyObservers(this); + } + + public override _build(state: NodeParticleBuildState) { + if (this._nodeMaterial) { + this._applyTexture(state, this._nodeMaterial); + this.material._storedValue = this._nodeMaterial; + return; + } + + this.material._storedValue = null; + + if (this._serializedMaterial) { + if (this._instantiateMaterial(this._serializedMaterial, state.scene)) { + this._applyTexture(state, this._nodeMaterial!); + this.material._storedValue = this._nodeMaterial; + } + return; + } + + if (!this.shaderUrl || this._isLoading) { + return; + } + + this._isLoading = true; + const scene = state.scene; + Tools.LoadFile( + this.shaderUrl, + (data) => { + try { + this._serializedMaterial = data as string; + this._customMaterialName = this.shaderUrl; + this._instantiateMaterial(this._serializedMaterial, scene); + this.onValueChangedObservable.notifyObservers(this); + } finally { + this._isLoading = false; + } + }, + undefined, + undefined, + false, + () => { + this._isLoading = false; + } + ); + } + + public override dispose() { + this._disposeMaterial(); + this._textureInstance?.dispose(); + this._textureInstance = null; + super.dispose(); + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.shaderUrl = this.shaderUrl; + serializationObject.textureUrl = this.textureUrl; + serializationObject.textureBlockName = this.textureBlockName; + serializationObject.serializedMaterial = this._serializedMaterial; + serializationObject.customMaterialName = this._customMaterialName; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.shaderUrl = serializationObject.shaderUrl || ""; + this.textureUrl = serializationObject.textureUrl || ""; + this.textureBlockName = serializationObject.textureBlockName || ""; + this._serializedMaterial = serializationObject.serializedMaterial || null; + this._customMaterialName = serializationObject.customMaterialName || ""; + this._disposeMaterial(); + } + + private _disposeMaterial() { + this._nodeMaterial?.dispose(); + this._nodeMaterial = null; + } + + private _instantiateMaterial(serializedData: string, scene: Scene): boolean { + try { + const json = JSON.parse(serializedData); + const nodeMaterial = NodeMaterial.Parse(json, scene); + nodeMaterial.build(false); + this._disposeMaterial(); + this._nodeMaterial = nodeMaterial; + return true; + } catch { + this._nodeMaterial = null; + } + return false; + } + + private _applyTexture(state: NodeParticleBuildState, nodeMaterial: NodeMaterial) { + if (!this.textureBlockName) { + return; + } + + const block = nodeMaterial.getBlockByName(this.textureBlockName) as { texture?: Texture | null }; + if (!block || block.texture === undefined) { + return; + } + + if (this.texture.isConnected) { + const connectedTexture = this.texture.getConnectedValue(state) as Texture; + if (connectedTexture) { + block.texture = connectedTexture; + return; + } + } + + if (!this.textureUrl) { + block.texture = null; + return; + } + + if (!this._textureInstance || this._textureInstance.url !== this.textureUrl) { + this._textureInstance?.dispose(); + this._textureInstance = new Texture(this.textureUrl, state.scene); + } + + block.texture = this._textureInstance; + } +} + +RegisterClass("BABYLON.SPSNodeMaterialBlock", SPSNodeMaterialBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts new file mode 100644 index 00000000000..ae513debca0 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticleConfigBlock.ts @@ -0,0 +1,73 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISpsParticleConfigData } from "./ISPSData"; + +/** + * 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("config", NodeParticleBlockConnectionPointTypes.SolidParticleConfig); + } + + 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 config(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const meshData = this.mesh.getConnectedValue(state); + const count = (this.count.getConnectedValue(state) as number) || 1; + const 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 = { + meshData, + count, + material, + initBlock, + updateBlock, + }; + + this.config._storedValue = particleConfig; + } +} + +RegisterClass("BABYLON.SPSParticleConfigBlock", SPSParticleConfigBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts new file mode 100644 index 00000000000..6798d44ab4a --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsGetBlock.ts @@ -0,0 +1,129 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { SolidParticle } from "../../../solidParticle"; +import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; +import { serialize } from "../../../../Misc/decorators"; + +/** + * Block used to get custom properties from particle.props + * Works similar to contextual blocks but for dynamic property names + */ +export class SpsParticlePropsGetBlock extends NodeParticleBlock { + /** + * Gets or sets the property name to read from particle.props + */ + @serialize("propertyName") + @editableInPropertyPage("Property Name", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + notifiers: { rebuild: true }, + }) + public propertyName: string = "value"; + + /** + * Gets or sets the connection point type (default float) + */ + private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; + + /** + * Gets the value to display (returns propertyName as string) + */ + public get displayValue(): string { + return this.propertyName || "value"; + } + + public constructor(name: string) { + super(name); + + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.AutoDetect); + // Set default type + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + public override getClassName() { + return "SpsParticlePropsGetBlock"; + } + + /** + * Gets or sets the connection point type + */ + public get type(): NodeParticleBlockConnectionPointTypes { + return this._type; + } + + public set type(value: NodeParticleBlockConnectionPointTypes) { + if (this._type !== value) { + this._type = value; + // Update output type + (this._outputs[0] as any)._type = value; + (this._outputs[0] as any)._defaultConnectionPointType = value; + } + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + // Validate property name + if (!this.propertyName || this.propertyName.trim() === "") { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + // Validate type + if (this._type === NodeParticleBlockConnectionPointTypes.Undefined || this._type === NodeParticleBlockConnectionPointTypes.AutoDetect) { + this._type = NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._type = this._type; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + const propertyName = this.propertyName; + + const func = (state: NodeParticleBuildState) => { + if (!state.particleContext) { + return null; + } + + const particle = state.particleContext as SolidParticle; + + if (!particle.props) { + return null; + } + + const value = particle.props[propertyName]; + + if (value === undefined) { + return null; + } + + return value; + }; + + if (this.output.isConnected) { + this.output._storedFunction = func; + } else { + this.output._storedValue = func(state); + } + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.propertyName = this.propertyName; + serializationObject.type = this._type; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.propertyName = serializationObject.propertyName || "value"; + this._type = serializationObject.type || NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._type = this._type; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } +} + +RegisterClass("BABYLON.SpsParticlePropsGetBlock", SpsParticlePropsGetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts new file mode 100644 index 00000000000..4d48cbb71a9 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSParticlePropsSetBlock.ts @@ -0,0 +1,133 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { SolidParticle } from "../../../solidParticle"; +import { editableInPropertyPage, PropertyTypeForEdition } from "../../../../Decorators/nodeDecorator"; +import { serialize } from "../../../../Misc/decorators"; + +/** + * Block used to set custom properties in particle.props + * Works as a side-effect block that stores values and passes them through + */ +export class SpsParticlePropsSetBlock extends NodeParticleBlock { + /** + * Gets or sets the property name to store in particle.props + */ + @serialize("propertyName") + @editableInPropertyPage("Property Name", PropertyTypeForEdition.String, "PROPERTIES", { + embedded: false, + notifiers: { rebuild: true }, + }) + public propertyName: string = "value"; + + /** + * Gets or sets the connection point type (default float) + */ + private _type: NodeParticleBlockConnectionPointTypes = NodeParticleBlockConnectionPointTypes.Float; + + public constructor(name: string) { + super(name); + + this.registerInput("value", NodeParticleBlockConnectionPointTypes.AutoDetect, true); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.BasedOnInput); + + // Link output type to input type + this._outputs[0]._typeConnectionSource = this._inputs[0]; + // Set default type for when input is not connected + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } + + public override getClassName() { + return "SpsParticlePropsSetBlock"; + } + + public get value(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the value to display (returns propertyName as string) + * This shadows the connection point name for display purposes + */ + public get displayValue(): string { + return this.propertyName || "value"; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * Gets or sets the connection point type + */ + public get type(): NodeParticleBlockConnectionPointTypes { + return this._type; + } + + public set type(value: NodeParticleBlockConnectionPointTypes) { + if (this._type !== value) { + this._type = value; + // Update default type (used when input is not connected) + (this._outputs[0] as any)._defaultConnectionPointType = value; + } + } + + public override _build(state: NodeParticleBuildState) { + // Validate property name + if (!this.propertyName || this.propertyName.trim() === "") { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + if (!this.value.isConnected) { + this.output._storedFunction = null; + this.output._storedValue = null; + return; + } + + const propertyName = this.propertyName; + + const func = (state: NodeParticleBuildState) => { + if (!state.particleContext) { + return null; + } + + const particle = state.particleContext as SolidParticle; + + const value = this.value.getConnectedValue(state); + + if (!particle.props) { + particle.props = {}; + } + + particle.props[propertyName] = value; + + return value; + }; + + if (this.output.isConnected) { + this.output._storedFunction = func; + } else { + this.output._storedValue = func(state); + } + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.propertyName = this.propertyName; + serializationObject.type = this._type; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.propertyName = serializationObject.propertyName || "value"; + this._type = serializationObject.type || NodeParticleBlockConnectionPointTypes.Float; + (this._outputs[0] as any)._defaultConnectionPointType = this._type; + } +} + +RegisterClass("BABYLON.SpsParticlePropsSetBlock", SpsParticlePropsSetBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts new file mode 100644 index 00000000000..6f2437a5ce0 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSSystemBlock.ts @@ -0,0 +1,93 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import type { 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; + + @editableInPropertyPage("Dispose on end", PropertyTypeForEdition.Boolean, "ADVANCED", { + embedded: true, + }) + public disposeOnEnd = false; + + public _internalId = SPSSystemBlock._IdCounter++; + + public constructor(name: string) { + super(name); + this._isSystem = true; + this.registerInput("lifeTime", NodeParticleBlockConnectionPointTypes.Float, true, 0); + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System); + } + + public override getClassName() { + return "SPSSystemBlock"; + } + + public get lifeTime(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { + state.buildId = ++this._buildId; + + this.build(state); + + const solidParticle = this.solidParticle.getConnectedValue(state) as SolidParticleSystem; + + if (!solidParticle) { + throw new Error("No SolidParticleSystem connected to SPSSystemBlock"); + } + + solidParticle.billboard = this.billboard; + solidParticle.name = this.name; + if (this.lifeTime.isConnected) { + const connectedLifetime = this.lifeTime.getConnectedValue(state) as number; + solidParticle.lifetime = connectedLifetime ?? 0; + } else { + solidParticle.lifetime = this.lifeTime.value; + } + solidParticle.disposeOnEnd = this.disposeOnEnd; + return solidParticle; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.billboard = this.billboard; + serializationObject.lifeTime = this.lifeTime.value; + serializationObject.disposeOnEnd = this.disposeOnEnd; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.billboard = !!serializationObject.billboard; + this.lifeTime.value = serializationObject.lifeTime ?? 0; + this.disposeOnEnd = !!serializationObject.disposeOnEnd; + } +} + +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..f3cdafc4a77 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/SPSUpdateBlock.ts @@ -0,0 +1,94 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISpsUpdateData } from "./ISPSData"; + +/** + * Block used to generate 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.position.getConnectedValue(state); + }; + } + if (this.velocity.isConnected) { + updateData.velocity = () => { + return this.velocity.getConnectedValue(state); + }; + } + if (this.color.isConnected) { + updateData.color = () => { + return this.color.getConnectedValue(state); + }; + } + if (this.scaling.isConnected) { + updateData.scaling = () => { + return this.scaling.getConnectedValue(state); + }; + } + if (this.rotation.isConnected) { + updateData.rotation = () => { + return this.rotation.getConnectedValue(state); + }; + } + 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..a45e9499321 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/index.ts @@ -0,0 +1,10 @@ +export * from "./ISPSData"; +export * from "./SPSMeshSourceBlock"; +export * from "./SPSParticleConfigBlock"; +export * from "./SPSSystemBlock"; +export * from "./SPSUpdateBlock"; +export * from "./SPSInitBlock"; +export * from "./SPSCreateBlock"; +export * from "./SPSParticlePropsSetBlock"; +export * from "./SPSParticlePropsGetBlock"; +export * from "./SPSNodeMaterialBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/index.ts b/packages/dev/core/src/Particles/Node/Blocks/index.ts index cc57e966fc7..33cdbf3712e 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/index.ts @@ -33,3 +33,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/Blocks/particleLocalVariableBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/particleLocalVariableBlock.ts index 52eedf2d5d1..3e0b46a418d 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/particleLocalVariableBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/particleLocalVariableBlock.ts @@ -88,7 +88,7 @@ export class ParticleLocalVariableBlock extends NodeParticleBlock { return null; } - const id = (this.scope === ParticleLocalVariableBlockScope.Particle ? state.particleContext?.id : state.systemContext?.getScene()!.getFrameId()) || -1; + const id = (this.scope === ParticleLocalVariableBlockScope.Particle ? state.particleContext?.id : state.scene.getFrameId()) || -1; if (localId !== id) { localId = id; diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts index 87d59a00192..ef737a5a955 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, + /** Solid Particle */ + SolidParticle = 0x2000, + /** Solid Particle Config */ + SolidParticleConfig = 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 59726179969..1865348ceb4 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -1,14 +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 { SolidParticleSystem } from "../solidParticleSystem"; /** * Class used to store node based geometry build state @@ -36,12 +38,18 @@ 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; + public systemContext: 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 @@ -99,60 +107,174 @@ export class NodeParticleBuildState { return null; } + /** + * Type guard to check if particle context is a Particle and system context is ThinParticleSystem + * @returns true when the contexts are Particle + ThinParticleSystem + */ + private _isParticleWithThinSystem(): this is this & { particleContext: Particle; systemContext: ThinParticleSystem } { + return this.particleContext instanceof Particle && this.systemContext instanceof ThinParticleSystem; + } + + /** + * Type guard to check if particle context is a Particle + * @returns true when the particle context is a Particle + */ + private _isParticle(): this is this & { particleContext: Particle } { + return this.particleContext instanceof Particle; + } + + /** + * Type guard to check if particle context is a SolidParticle + * @returns true when the particle context is a SolidParticle + */ + private _isSolidParticle(): this is this & { particleContext: SolidParticle } { + return this.particleContext instanceof SolidParticle; + } + + /** + * Type guard to check if system context is a ThinParticleSystem + * @returns true when the system context is a ThinParticleSystem + */ + private _isThinParticleSystem(): this is this & { systemContext: ThinParticleSystem } { + return this.systemContext instanceof ThinParticleSystem; + } + + /** + * Type guard to check if system context is a SolidParticleSystem + * @returns true when the system context is a SolidParticleSystem + */ + private _isSolidParticleSystem(): this is this & { systemContext: SolidParticleSystem } { + return this.systemContext instanceof SolidParticleSystem; + } + /** * Gets the value associated with a contextual source * @param source Source of the contextual value * @returns the value associated with the source */ public getContextualValue(source: NodeParticleContextualSources) { - if (!this.particleContext || !this.systemContext) { + if (!this.particleContext) { return null; } switch (source) { + // Common properties available on both Particle and SolidParticle case NodeParticleContextualSources.Position: return this.particleContext.position; + case NodeParticleContextualSources.Color: + return this.particleContext.color; + case NodeParticleContextualSources.Scale: + if (this._isParticle()) { + return this.particleContext.scale; + } + if (this._isSolidParticle()) { + // Convert Vector3 scaling to Vector2 for compatibility + const scaling = this.particleContext.scaling; + return new Vector2(scaling.x, scaling.y); + } + return null; + case NodeParticleContextualSources.Direction: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.direction; + case NodeParticleContextualSources.ScaledDirection: + if (!this._isParticleWithThinSystem()) { + return null; + } this.particleContext.direction.scaleToRef(this.systemContext._directionScale, this.systemContext._scaledDirection); return this.systemContext._scaledDirection; - case NodeParticleContextualSources.Color: - return this.particleContext.color; + case NodeParticleContextualSources.InitialColor: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.initialColor; + case NodeParticleContextualSources.ColorDead: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.colorDead; + case NodeParticleContextualSources.Age: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.age; + case NodeParticleContextualSources.Lifetime: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.lifeTime; + case NodeParticleContextualSources.Angle: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.angle; - case NodeParticleContextualSources.Scale: - return this.particleContext.scale; + case NodeParticleContextualSources.Size: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.size; case NodeParticleContextualSources.AgeGradient: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.age / this.particleContext.lifeTime; - case NodeParticleContextualSources.SpriteCellEnd: - return this.systemContext.endSpriteCellID; + case NodeParticleContextualSources.SpriteCellIndex: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.cellIndex; - case NodeParticleContextualSources.SpriteCellStart: - return this.systemContext.startSpriteCellID; + case NodeParticleContextualSources.InitialDirection: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext._initialDirection; + case NodeParticleContextualSources.ColorStep: + if (!this._isParticleWithThinSystem()) { + return null; + } return this.particleContext.colorStep; + case NodeParticleContextualSources.ScaledColorStep: + if (!this._isParticleWithThinSystem()) { + return null; + } this.particleContext.colorStep.scaleToRef(this.systemContext._scaledUpdateSpeed, this.systemContext._scaledColorStep); return this.systemContext._scaledColorStep; + case NodeParticleContextualSources.LocalPositionUpdated: + if (!this._isParticleWithThinSystem()) { + return null; + } this.particleContext.direction.scaleToRef(this.systemContext._directionScale, this.systemContext._scaledDirection); - this.particleContext._localPosition!.addInPlace(this.systemContext._scaledDirection); - Vector3.TransformCoordinatesToRef(this.particleContext._localPosition!, this.systemContext._emitterWorldMatrix, this.particleContext.position); + if (this.particleContext._localPosition) { + this.particleContext._localPosition.addInPlace(this.systemContext._scaledDirection); + Vector3.TransformCoordinatesToRef(this.particleContext._localPosition, this.systemContext._emitterWorldMatrix, this.particleContext.position); + } return this.particleContext.position; + + case NodeParticleContextualSources.SpriteCellEnd: + if (!this._isThinParticleSystem()) { + return null; + } + return this.systemContext.endSpriteCellID; + + case NodeParticleContextualSources.SpriteCellStart: + if (!this._isThinParticleSystem()) { + return null; + } + return this.systemContext.startSpriteCellID; } return null; @@ -162,7 +284,7 @@ export class NodeParticleBuildState { * Gets the emitter world matrix */ public get emitterWorldMatrix() { - if (!this.systemContext) { + if (!this._isThinParticleSystem()) { return null; } return this.systemContext._emitterWorldMatrix; @@ -172,7 +294,7 @@ export class NodeParticleBuildState { * Gets the emitter inverse world matrix */ public get emitterInverseWorldMatrix() { - if (!this.systemContext) { + if (!this._isThinParticleSystem()) { return null; } return this.systemContext._emitterInverseWorldMatrix; @@ -186,6 +308,14 @@ export class NodeParticleBuildState { return null; } + if (this._isSolidParticleSystem()) { + return this.systemContext.mesh?.absolutePosition || Vector3.Zero(); + } + + if (!this._isThinParticleSystem()) { + return null; + } + if (!this.systemContext.emitter) { return null; } @@ -194,7 +324,27 @@ export class NodeParticleBuildState { return this.systemContext.emitter; } - return (this.systemContext.emitter).absolutePosition; + return (this.systemContext.emitter as AbstractMesh).absolutePosition; + } + + /** + * Gets the actual frame number + */ + public get actualFrame() { + if (this._isThinParticleSystem()) { + return this.systemContext._actualFrame; + } + return this.scene.getFrameId() || 0; + } + + /** + * Gets the delta time + */ + public get delta() { + if (this._isThinParticleSystem()) { + return this.systemContext._scaledUpdateSpeed; + } + return this.scene.getEngine().getDeltaTime() || this.deltaTime; } /** @@ -209,9 +359,9 @@ export class NodeParticleBuildState { switch (source) { case NodeParticleSystemSources.Time: - return this.systemContext._actualFrame; + return this.actualFrame; case NodeParticleSystemSources.Delta: - return this.systemContext._scaledUpdateSpeed; + return this.delta; case NodeParticleSystemSources.Emitter: return this.emitterPosition; case NodeParticleSystemSources.CameraPosition: diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index eb05f23fc1b..6fafe5e89b2 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -20,8 +20,27 @@ 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 type { Nullable } from "core/types"; +import { Color4 } from "core/Maths/math.color"; +import { Vector2, Vector3 } from "core/Maths/math.vector"; +import { VertexData } from "core/Meshes/mesh.vertexData"; +import { + SPSParticleConfigBlock, + SPSInitBlock, + SPSMeshSourceBlock, + SPSSystemBlock, + SPSCreateBlock, + SPSUpdateBlock, + SpsParticlePropsSetBlock, + SpsParticlePropsGetBlock, + SPSNodeMaterialBlock, +} from "./Blocks"; +import { ParticleSystem } from "core/Particles/particleSystem"; +import { ParticleRandomBlock, ParticleRandomBlockLocks } from "./Blocks/particleRandomBlock"; +import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; +import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; +import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -45,7 +64,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 +109,7 @@ export class NodeParticleSystemSet { /** * Gets the system blocks */ - public get systemBlocks(): SystemBlock[] { + public get systemBlocks(): (SystemBlock | SPSSystemBlock)[] { return this._systemBlocks; } @@ -269,13 +288,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 +355,508 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } + public setToDefaultSps() { + this.createShockwaveSps(); + } + + public createDefaultSps() { + this.clear(); + this.editorData = null; + + const spsSystem = new SPSSystemBlock("SPS System"); + spsSystem.billboard = false; + + const spsCreateBlock = new SPSCreateBlock("Create Particles System"); + spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); + + const spsCreateTetra = new SPSParticleConfigBlock("Create Tetrahedron Particles"); + spsCreateTetra.count.value = 2000; + spsCreateTetra.config.connectTo(spsCreateBlock.config); + + const meshSourceTetra = new SPSMeshSourceBlock("Tetrahedron Mesh"); + const tetraVertexData = VertexData.CreateBox({ size: 0.1 }); + meshSourceTetra.setCustomVertexData(tetraVertexData, "Default Box"); + meshSourceTetra.mesh.connectTo(spsCreateTetra.mesh); + + const spsInitTetra = new SPSInitBlock("Initialize Tetrahedron Particles"); + spsInitTetra.initData.connectTo(spsCreateTetra.initBlock); + + const randomXZMin = new ParticleInputBlock("Random XZ Min"); + randomXZMin.value = new Vector2(-10, -10); + const randomXZMax = new ParticleInputBlock("Random XZ Max"); + randomXZMax.value = new Vector2(10, 10); + const randomXZ = new ParticleRandomBlock("Random XZ"); + randomXZ.lockMode = ParticleRandomBlockLocks.PerParticle; + randomXZMin.output.connectTo(randomXZ.min); + randomXZMax.output.connectTo(randomXZ.max); + + const randomAngleMin = new ParticleInputBlock("Random Angle Min"); + randomAngleMin.value = -Math.PI; + const randomAngleMax = new ParticleInputBlock("Random Angle Max"); + randomAngleMax.value = Math.PI; + const randomAngle = new ParticleRandomBlock("Random Angle"); + randomAngle.lockMode = ParticleRandomBlockLocks.PerParticle; + randomAngleMin.output.connectTo(randomAngle.min); + randomAngleMax.output.connectTo(randomAngle.max); + + const randomRangeMin = new ParticleInputBlock("Random Range Min"); + randomRangeMin.value = 1; + const randomRangeMax = new ParticleInputBlock("Random Range Max"); + randomRangeMax.value = 5; + const randomRange = new ParticleRandomBlock("Random Range"); + randomRange.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRangeMin.output.connectTo(randomRange.min); + randomRangeMax.output.connectTo(randomRange.max); + + const one = new ParticleInputBlock("One"); + one.value = 1; + const cosAngle = new ParticleTrigonometryBlock("Cos Angle"); + cosAngle.operation = ParticleTrigonometryBlockOperations.Cos; + // Store angle in props so we can reuse during update + const setAnglePropInit = new SpsParticlePropsSetBlock("Set Angle Prop Init"); + setAnglePropInit.propertyName = "angle"; + randomAngle.output.connectTo(setAnglePropInit.value); + setAnglePropInit.output.connectTo(cosAngle.input); + const addOne = new ParticleMathBlock("Add One"); + addOne.operation = ParticleMathBlockOperations.Add; + one.output.connectTo(addOne.left); + cosAngle.output.connectTo(addOne.right); + const multiplyRange = new ParticleMathBlock("Multiply Range"); + multiplyRange.operation = ParticleMathBlockOperations.Multiply; + const setRangePropInit = new SpsParticlePropsSetBlock("Set Range Prop Init"); + setRangePropInit.propertyName = "range"; + randomRange.output.connectTo(setRangePropInit.value); + setRangePropInit.output.connectTo(multiplyRange.left); + addOne.output.connectTo(multiplyRange.right); + + const extractXZ = new ParticleConverterBlock("Extract XZ"); + randomXZ.output.connectTo(extractXZ.xyIn); + const positionConverter = new ParticleConverterBlock("Position Converter"); + extractXZ.xOut.connectTo(positionConverter.xIn); + multiplyRange.output.connectTo(positionConverter.yIn); + extractXZ.yOut.connectTo(positionConverter.zIn); + positionConverter.xyzOut.connectTo(spsInitTetra.position); + + const randomRotMin = new ParticleInputBlock("Random Rot Min"); + randomRotMin.value = new Vector3(-Math.PI, -Math.PI, -Math.PI); + const randomRotMax = new ParticleInputBlock("Random Rot Max"); + randomRotMax.value = new Vector3(Math.PI, Math.PI, Math.PI); + const randomRot = new ParticleRandomBlock("Random Rotation"); + randomRot.lockMode = ParticleRandomBlockLocks.PerParticle; + randomRotMin.output.connectTo(randomRot.min); + randomRotMax.output.connectTo(randomRot.max); + randomRot.output.connectTo(spsInitTetra.rotation); + + const randomColorMin = new ParticleInputBlock("Random Color Min"); + randomColorMin.value = new Vector3(0, 0, 0); + const randomColorMax = new ParticleInputBlock("Random Color Max"); + randomColorMax.value = new Vector3(1, 1, 1); + const randomColorRGB = new ParticleRandomBlock("Random Color RGB"); + randomColorRGB.lockMode = ParticleRandomBlockLocks.PerParticle; + randomColorMin.output.connectTo(randomColorRGB.min); + randomColorMax.output.connectTo(randomColorRGB.max); + const colorAlpha = new ParticleInputBlock("Color Alpha"); + colorAlpha.value = 1; + const colorConverter = new ParticleConverterBlock("Color Converter"); + randomColorRGB.output.connectTo(colorConverter.xyzIn); + colorAlpha.output.connectTo(colorConverter.wIn); + colorConverter.colorOut.connectTo(spsInitTetra.color); + + // Create update block + const spsUpdateTetra = new SPSUpdateBlock("Update Tetrahedron Particles"); + spsUpdateTetra.updateData.connectTo(spsCreateTetra.updateBlock); + + // Get current position (X, Z stay the same, Y updates) + const currentPosition = new ParticleInputBlock("Current Position"); + currentPosition.contextualValue = NodeParticleContextualSources.Position; + + // Extract X and Z from current position + const extractPosition = new ParticleConverterBlock("Extract Position"); + currentPosition.output.connectTo(extractPosition.xyzIn); + + // Retrieve stored properties + const getAngleProp = new SpsParticlePropsGetBlock("Get Angle Prop"); + getAngleProp.propertyName = "angle"; + getAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRangeProp = new SpsParticlePropsGetBlock("Get Range Prop"); + getRangeProp.propertyName = "range"; + getRangeProp.type = NodeParticleBlockConnectionPointTypes.Float; + + // Accumulate angle using delta time to avoid relying on absolute frame id + const deltaBlock = new ParticleInputBlock("Delta Time"); + deltaBlock.systemSource = NodeParticleSystemSources.Delta; + + const milliToSecond = new ParticleInputBlock("Milli To Second"); + milliToSecond.value = 0.001; + + const deltaSeconds = new ParticleMathBlock("Delta Seconds"); + deltaSeconds.operation = ParticleMathBlockOperations.Multiply; + deltaBlock.output.connectTo(deltaSeconds.left); + milliToSecond.output.connectTo(deltaSeconds.right); + + const targetFps = new ParticleInputBlock("Target FPS"); + targetFps.value = 60; + + const normalizedDelta = new ParticleMathBlock("Normalized Delta"); + normalizedDelta.operation = ParticleMathBlockOperations.Multiply; + deltaSeconds.output.connectTo(normalizedDelta.left); + targetFps.output.connectTo(normalizedDelta.right); + + const speedPerFrame = new ParticleInputBlock("Speed Per Frame"); + speedPerFrame.value = Math.PI / 100; + + const scaledIncrement = new ParticleMathBlock("Scaled Increment"); + scaledIncrement.operation = ParticleMathBlockOperations.Multiply; + speedPerFrame.output.connectTo(scaledIncrement.left); + normalizedDelta.output.connectTo(scaledIncrement.right); + + const accumulateAngle = new ParticleMathBlock("Accumulate Angle"); + accumulateAngle.operation = ParticleMathBlockOperations.Add; + getAngleProp.output.connectTo(accumulateAngle.left); + scaledIncrement.output.connectTo(accumulateAngle.right); + + const setAnglePropUpdate = new SpsParticlePropsSetBlock("Set Angle Prop Update"); + setAnglePropUpdate.propertyName = "angle"; + setAnglePropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + accumulateAngle.output.connectTo(setAnglePropUpdate.value); + + // Calculate new Y position: range * (1 + cos(angle)) + const oneUpdate = new ParticleInputBlock("One Update"); + oneUpdate.value = 1; + const cosUpdatedAngle = new ParticleTrigonometryBlock("Cos Updated Angle"); + cosUpdatedAngle.operation = ParticleTrigonometryBlockOperations.Cos; + setAnglePropUpdate.output.connectTo(cosUpdatedAngle.input); + const addOneUpdate = new ParticleMathBlock("Add One Update"); + addOneUpdate.operation = ParticleMathBlockOperations.Add; + oneUpdate.output.connectTo(addOneUpdate.left); + cosUpdatedAngle.output.connectTo(addOneUpdate.right); + const multiplyRangeUpdate = new ParticleMathBlock("Multiply Range Update"); + multiplyRangeUpdate.operation = ParticleMathBlockOperations.Multiply; + getRangeProp.output.connectTo(multiplyRangeUpdate.left); + addOneUpdate.output.connectTo(multiplyRangeUpdate.right); + + // Combine X (from current position), Y (new), Z (from current position) + const updatePositionConverter = new ParticleConverterBlock("Update Position Converter"); + extractPosition.xOut.connectTo(updatePositionConverter.xIn); + multiplyRangeUpdate.output.connectTo(updatePositionConverter.yIn); + extractPosition.zOut.connectTo(updatePositionConverter.zIn); + updatePositionConverter.xyzOut.connectTo(spsUpdateTetra.position); + + this._systemBlocks.push(spsSystem); + } + + /** + * Sets the current set to an SPS shockwave preset inspired by Patrick Ryan's createShockwave sample + */ + public createShockwaveSps() { + this.clear(); + this.editorData = null; + + const spsSystem = new SPSSystemBlock("Shockwave SPS System"); + spsSystem.billboard = false; + + const lifetimeMs = new ParticleInputBlock("Shockwave Lifetime (ms)"); + lifetimeMs.value = 2500; + const minLifetimeMs = new ParticleInputBlock("Shockwave Min Lifetime (ms)"); + minLifetimeMs.value = 1; + const lifetimeSafe = new ParticleMathBlock("Shockwave Lifetime Safe"); + lifetimeSafe.operation = ParticleMathBlockOperations.Max; + lifetimeMs.output.connectTo(lifetimeSafe.left); + minLifetimeMs.output.connectTo(lifetimeSafe.right); + lifetimeSafe.output.connectTo(spsSystem.lifeTime); + spsSystem.disposeOnEnd = true; + + const spsCreateBlock = new SPSCreateBlock("Create Shockwave SPS"); + spsCreateBlock.solidParticle.connectTo(spsSystem.solidParticle); + + const shockwaveConfig = new SPSParticleConfigBlock("Shockwave Particle Config"); + shockwaveConfig.count.value = 7; + shockwaveConfig.config.connectTo(spsCreateBlock.config); + + const shockwaveMesh = new SPSMeshSourceBlock("Shockwave Mesh Source"); + shockwaveMesh.remoteMeshUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/gltf/shockwaveMesh.glb"; + shockwaveMesh.remoteMeshName = "shockwaveMesh"; + shockwaveMesh.mesh.connectTo(shockwaveConfig.mesh); + + const shockwaveMaterial = new SPSNodeMaterialBlock("Shockwave Material"); + shockwaveMaterial.shaderUrl = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/shaders/shockwaveParticleShader.json"; + shockwaveMaterial.textureBlockName = "particleTex"; + const shockwaveTexture = new ParticleTextureSourceBlock("Shockwave Texture"); + shockwaveTexture.url = "https://patrickryanms.github.io/BabylonJStextures/Demos/attack_fx/assets/textures/electricityRing.png"; + shockwaveTexture.texture.connectTo(shockwaveMaterial.texture); + shockwaveMaterial.material.connectTo(shockwaveConfig.material); + + const shockwaveInit = new SPSInitBlock("Initialize Shockwave Particles"); + shockwaveInit.initData.connectTo(shockwaveConfig.initBlock); + + const shockwaveUpdate = new SPSUpdateBlock("Update Shockwave Particles"); + shockwaveUpdate.updateData.connectTo(shockwaveConfig.updateBlock); + + const deltaBlock = new ParticleInputBlock("Shockwave Delta Time"); + deltaBlock.systemSource = NodeParticleSystemSources.Delta; + const milliToSecond = new ParticleInputBlock("Shockwave Milli To Second"); + milliToSecond.value = 0.001; + const deltaSeconds = new ParticleMathBlock("Shockwave Delta Seconds"); + deltaSeconds.operation = ParticleMathBlockOperations.Multiply; + deltaBlock.output.connectTo(deltaSeconds.left); + milliToSecond.output.connectTo(deltaSeconds.right); + const targetFps = new ParticleInputBlock("Shockwave Target FPS"); + targetFps.value = 60; + const normalizedDelta = new ParticleMathBlock("Shockwave Normalized Delta"); + normalizedDelta.operation = ParticleMathBlockOperations.Multiply; + deltaSeconds.output.connectTo(normalizedDelta.left); + targetFps.output.connectTo(normalizedDelta.right); + + const lifetimeSeconds = new ParticleMathBlock("Shockwave Lifetime Seconds"); + lifetimeSeconds.operation = ParticleMathBlockOperations.Multiply; + lifetimeSafe.output.connectTo(lifetimeSeconds.left); + milliToSecond.output.connectTo(lifetimeSeconds.right); + const framesPerLifetime = new ParticleMathBlock("Shockwave Frames Per Lifetime"); + framesPerLifetime.operation = ParticleMathBlockOperations.Multiply; + lifetimeSeconds.output.connectTo(framesPerLifetime.left); + targetFps.output.connectTo(framesPerLifetime.right); + + const origin = new ParticleInputBlock("Shockwave Origin"); + origin.value = new Vector3(0, 0.05, 0); + origin.output.connectTo(shockwaveInit.position); + + const shockwaveColor = new ParticleInputBlock("Shockwave Base Color"); + shockwaveColor.value = new Color4(0.33, 0.49, 0.88, 0.9); + shockwaveColor.output.connectTo(shockwaveInit.color); + + const zeroValue = new ParticleInputBlock("Shockwave Zero"); + zeroValue.value = 0; + + const radiusStart = new ParticleInputBlock("Shockwave Radius Start"); + radiusStart.value = 1; + const storeRadiusInit = new SpsParticlePropsSetBlock("Store Radius Init"); + storeRadiusInit.propertyName = "radius"; + storeRadiusInit.type = NodeParticleBlockConnectionPointTypes.Float; + radiusStart.output.connectTo(storeRadiusInit.value); + + const maxRadius = new ParticleInputBlock("Shockwave Max Radius"); + maxRadius.value = 4; + + const radiusRangeBlock = new ParticleMathBlock("Shockwave Radius Range"); + radiusRangeBlock.operation = ParticleMathBlockOperations.Subtract; + maxRadius.output.connectTo(radiusRangeBlock.left); + radiusStart.output.connectTo(radiusRangeBlock.right); + + const growthMultiplierMin = new ParticleInputBlock("Shockwave Growth Multiplier Min"); + growthMultiplierMin.value = 0.85; + const growthMultiplierMax = new ParticleInputBlock("Shockwave Growth Multiplier Max"); + growthMultiplierMax.value = 1.15; + const growthMultiplier = new ParticleRandomBlock("Shockwave Growth Multiplier"); + growthMultiplier.lockMode = ParticleRandomBlockLocks.OncePerParticle; + growthMultiplierMin.output.connectTo(growthMultiplier.min); + growthMultiplierMax.output.connectTo(growthMultiplier.max); + + const baseGrowthPerFrame = new ParticleMathBlock("Shockwave Base Growth Per Frame"); + baseGrowthPerFrame.operation = ParticleMathBlockOperations.Divide; + radiusRangeBlock.output.connectTo(baseGrowthPerFrame.left); + framesPerLifetime.output.connectTo(baseGrowthPerFrame.right); + + const growthPerFrame = new ParticleMathBlock("Shockwave Growth Per Frame"); + growthPerFrame.operation = ParticleMathBlockOperations.Multiply; + baseGrowthPerFrame.output.connectTo(growthPerFrame.left); + growthMultiplier.output.connectTo(growthPerFrame.right); + + const storeScaleStepInit = new SpsParticlePropsSetBlock("Store Scale Step Init"); + storeScaleStepInit.propertyName = "scaleStep"; + storeScaleStepInit.type = NodeParticleBlockConnectionPointTypes.Float; + growthPerFrame.output.connectTo(storeScaleStepInit.value); + + const initScaleConverter = new ParticleConverterBlock("Shockwave Init Scale Converter"); + storeRadiusInit.output.connectTo(initScaleConverter.xIn); + storeScaleStepInit.output.connectTo(initScaleConverter.yIn); + storeRadiusInit.output.connectTo(initScaleConverter.zIn); + initScaleConverter.xyzOut.connectTo(shockwaveInit.scaling); + + const rotationMin = new ParticleInputBlock("Shockwave Rotation Min"); + rotationMin.value = new Vector3(0, -Math.PI, 0); + const rotationMax = new ParticleInputBlock("Shockwave Rotation Max"); + rotationMax.value = new Vector3(0, Math.PI, 0); + const initialRotation = new ParticleRandomBlock("Shockwave Initial Rotation"); + initialRotation.lockMode = ParticleRandomBlockLocks.OncePerParticle; + rotationMin.output.connectTo(initialRotation.min); + rotationMax.output.connectTo(initialRotation.max); + + const rotationConverter = new ParticleConverterBlock("Shockwave Rotation Converter"); + initialRotation.output.connectTo(rotationConverter.xyzIn); + const storeRotationAngleInit = new SpsParticlePropsSetBlock("Store Rotation Angle Init"); + storeRotationAngleInit.propertyName = "rotationAngle"; + storeRotationAngleInit.type = NodeParticleBlockConnectionPointTypes.Float; + rotationConverter.yOut.connectTo(storeRotationAngleInit.value); + + const rotationCompose = new ParticleConverterBlock("Shockwave Rotation Compose"); + rotationConverter.xOut.connectTo(rotationCompose.xIn); + storeRotationAngleInit.output.connectTo(rotationCompose.yIn); + rotationConverter.zOut.connectTo(rotationCompose.zIn); + rotationCompose.xyzOut.connectTo(shockwaveInit.rotation); + + const rotationSpeedMin = new ParticleInputBlock("Shockwave Rotation Speed Min"); + rotationSpeedMin.value = -0.06; + const rotationSpeedMax = new ParticleInputBlock("Shockwave Rotation Speed Max"); + rotationSpeedMax.value = 0.06; + const rotationSpeedRandom = new ParticleRandomBlock("Shockwave Rotation Speed Random"); + rotationSpeedRandom.lockMode = ParticleRandomBlockLocks.OncePerParticle; + rotationSpeedMin.output.connectTo(rotationSpeedRandom.min); + rotationSpeedMax.output.connectTo(rotationSpeedRandom.max); + const storeRotationSpeed = new SpsParticlePropsSetBlock("Store Rotation Speed"); + storeRotationSpeed.propertyName = "rotationSpeed"; + storeRotationSpeed.type = NodeParticleBlockConnectionPointTypes.Float; + rotationSpeedRandom.output.connectTo(storeRotationSpeed.value); + + const rotationSpeedSink = new ParticleMathBlock("Shockwave Rotation Speed Sink"); + rotationSpeedSink.operation = ParticleMathBlockOperations.Multiply; + storeRotationSpeed.output.connectTo(rotationSpeedSink.left); + zeroValue.output.connectTo(rotationSpeedSink.right); + const rotationSpeedVelocity = new ParticleConverterBlock("Shockwave Rotation Speed Velocity"); + rotationSpeedSink.output.connectTo(rotationSpeedVelocity.xIn); + zeroValue.output.connectTo(rotationSpeedVelocity.yIn); + zeroValue.output.connectTo(rotationSpeedVelocity.zIn); + rotationSpeedVelocity.xyzOut.connectTo(shockwaveInit.velocity); + + const getRadiusProp = new SpsParticlePropsGetBlock("Get Radius Prop"); + getRadiusProp.propertyName = "radius"; + getRadiusProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getScaleStepProp = new SpsParticlePropsGetBlock("Get Scale Step Prop"); + getScaleStepProp.propertyName = "scaleStep"; + getScaleStepProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRotationSpeedProp = new SpsParticlePropsGetBlock("Get Rotation Speed Prop"); + getRotationSpeedProp.propertyName = "rotationSpeed"; + getRotationSpeedProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const getRotationAngleProp = new SpsParticlePropsGetBlock("Get Rotation Angle Prop"); + getRotationAngleProp.propertyName = "rotationAngle"; + getRotationAngleProp.type = NodeParticleBlockConnectionPointTypes.Float; + + const scaleStepDelta = new ParticleMathBlock("Shockwave Radius Delta"); + scaleStepDelta.operation = ParticleMathBlockOperations.Multiply; + getScaleStepProp.output.connectTo(scaleStepDelta.left); + normalizedDelta.output.connectTo(scaleStepDelta.right); + + const radiusIncrement = new ParticleMathBlock("Shockwave Radius Increment"); + radiusIncrement.operation = ParticleMathBlockOperations.Add; + getRadiusProp.output.connectTo(radiusIncrement.left); + scaleStepDelta.output.connectTo(radiusIncrement.right); + + const setRadiusPropUpdate = new SpsParticlePropsSetBlock("Set Radius Prop Update"); + setRadiusPropUpdate.propertyName = "radius"; + setRadiusPropUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + radiusIncrement.output.connectTo(setRadiusPropUpdate.value); + + const clampRadius = new ParticleMathBlock("Shockwave Clamp Radius"); + clampRadius.operation = ParticleMathBlockOperations.Min; + setRadiusPropUpdate.output.connectTo(clampRadius.left); + maxRadius.output.connectTo(clampRadius.right); + + const normalizedRadius = new ParticleMathBlock("Shockwave Normalized Radius"); + normalizedRadius.operation = ParticleMathBlockOperations.Divide; + clampRadius.output.connectTo(normalizedRadius.left); + maxRadius.output.connectTo(normalizedRadius.right); + + const normalizedMin = new ParticleMathBlock("Shockwave Normalized Min"); + normalizedMin.operation = ParticleMathBlockOperations.Max; + zeroValue.output.connectTo(normalizedMin.left); + normalizedRadius.output.connectTo(normalizedMin.right); + + const oneValue = new ParticleInputBlock("Shockwave One"); + oneValue.value = 1; + const normalizedClamp = new ParticleMathBlock("Shockwave Normalized Clamp"); + normalizedClamp.operation = ParticleMathBlockOperations.Min; + normalizedMin.output.connectTo(normalizedClamp.left); + oneValue.output.connectTo(normalizedClamp.right); + + const minThickness = new ParticleInputBlock("Shockwave Min Thickness"); + minThickness.value = 0.25; + const maxThickness = new ParticleInputBlock("Shockwave Max Thickness"); + maxThickness.value = 4; + const thicknessRange = new ParticleMathBlock("Shockwave Thickness Range"); + thicknessRange.operation = ParticleMathBlockOperations.Subtract; + maxThickness.output.connectTo(thicknessRange.left); + minThickness.output.connectTo(thicknessRange.right); + const thicknessScale = new ParticleMathBlock("Shockwave Thickness Scale"); + thicknessScale.operation = ParticleMathBlockOperations.Multiply; + thicknessRange.output.connectTo(thicknessScale.left); + normalizedClamp.output.connectTo(thicknessScale.right); + const thicknessValue = new ParticleMathBlock("Shockwave Thickness Value"); + thicknessValue.operation = ParticleMathBlockOperations.Add; + minThickness.output.connectTo(thicknessValue.left); + thicknessScale.output.connectTo(thicknessValue.right); + + const minHeight = new ParticleInputBlock("Shockwave Min Height"); + minHeight.value = 0.05; + const maxHeight = new ParticleInputBlock("Shockwave Max Height"); + maxHeight.value = 0.25; + const heightRange = new ParticleMathBlock("Shockwave Height Range"); + heightRange.operation = ParticleMathBlockOperations.Subtract; + maxHeight.output.connectTo(heightRange.left); + minHeight.output.connectTo(heightRange.right); + const heightScale = new ParticleMathBlock("Shockwave Height Scale"); + heightScale.operation = ParticleMathBlockOperations.Multiply; + heightRange.output.connectTo(heightScale.left); + normalizedClamp.output.connectTo(heightScale.right); + const heightValue = new ParticleMathBlock("Shockwave Height Value"); + heightValue.operation = ParticleMathBlockOperations.Add; + minHeight.output.connectTo(heightValue.left); + heightScale.output.connectTo(heightValue.right); + + const scalingConverter = new ParticleConverterBlock("Shockwave Scaling Converter"); + clampRadius.output.connectTo(scalingConverter.xIn); + thicknessValue.output.connectTo(scalingConverter.yIn); + clampRadius.output.connectTo(scalingConverter.zIn); + scalingConverter.xyzOut.connectTo(shockwaveUpdate.scaling); + + const positionConverter = new ParticleConverterBlock("Shockwave Position Converter"); + zeroValue.output.connectTo(positionConverter.xIn); + heightValue.output.connectTo(positionConverter.yIn); + zeroValue.output.connectTo(positionConverter.zIn); + positionConverter.xyzOut.connectTo(shockwaveUpdate.position); + + const rotationIncrement = new ParticleMathBlock("Shockwave Rotation Increment"); + rotationIncrement.operation = ParticleMathBlockOperations.Multiply; + getRotationSpeedProp.output.connectTo(rotationIncrement.left); + normalizedDelta.output.connectTo(rotationIncrement.right); + + const updatedRotationAngle = new ParticleMathBlock("Shockwave Updated Rotation Angle"); + updatedRotationAngle.operation = ParticleMathBlockOperations.Add; + getRotationAngleProp.output.connectTo(updatedRotationAngle.left); + rotationIncrement.output.connectTo(updatedRotationAngle.right); + + const setRotationAngleUpdate = new SpsParticlePropsSetBlock("Set Rotation Angle Update"); + setRotationAngleUpdate.propertyName = "rotationAngle"; + setRotationAngleUpdate.type = NodeParticleBlockConnectionPointTypes.Float; + updatedRotationAngle.output.connectTo(setRotationAngleUpdate.value); + + const rotationUpdateConverter = new ParticleConverterBlock("Shockwave Rotation Update Converter"); + zeroValue.output.connectTo(rotationUpdateConverter.xIn); + setRotationAngleUpdate.output.connectTo(rotationUpdateConverter.yIn); + zeroValue.output.connectTo(rotationUpdateConverter.zIn); + rotationUpdateConverter.xyzOut.connectTo(shockwaveUpdate.rotation); + + const colorEnd = new ParticleInputBlock("Shockwave Color End"); + colorEnd.value = new Color4(0, 0, 0, 0); + const colorRange = new ParticleMathBlock("Shockwave Color Range"); + colorRange.operation = ParticleMathBlockOperations.Subtract; + colorEnd.output.connectTo(colorRange.left); + shockwaveColor.output.connectTo(colorRange.right); + const colorScale = new ParticleMathBlock("Shockwave Color Scale"); + colorScale.operation = ParticleMathBlockOperations.Multiply; + colorRange.output.connectTo(colorScale.left); + normalizedClamp.output.connectTo(colorScale.right); + const colorValue = new ParticleMathBlock("Shockwave Color Value"); + colorValue.operation = ParticleMathBlockOperations.Add; + shockwaveColor.output.connectTo(colorValue.left); + colorScale.output.connectTo(colorValue.right); + colorValue.output.connectTo(shockwaveUpdate.color); + + this._systemBlocks.push(spsSystem); + } + /** * Remove a block from the current system set * @param block defines the block to remove diff --git a/packages/dev/core/src/Particles/particleSystemSet.ts b/packages/dev/core/src/Particles/particleSystemSet.ts index 9537fdbbc51..75b52830ad7 100644 --- a/packages/dev/core/src/Particles/particleSystemSet.ts +++ b/packages/dev/core/src/Particles/particleSystemSet.ts @@ -9,6 +9,7 @@ import { ParticleSystem } from "../Particles/particleSystem"; import type { Scene, IDisposable } from "../scene"; import { StandardMaterial } from "../Materials/standardMaterial"; import type { Vector3 } from "../Maths/math.vector"; +import type { SolidParticleSystem } from "./solidParticleSystem"; /** Internal class used to store shapes for emitters */ class ParticleSystemSetEmitterCreationOptions { @@ -34,7 +35,7 @@ export class ParticleSystemSet implements IDisposable { /** * Gets the particle system list */ - public systems: IParticleSystem[] = []; + public systems: (IParticleSystem | SolidParticleSystem)[] = []; /** * Gets or sets the emitter node used with this set @@ -52,7 +53,9 @@ export class ParticleSystemSet implements IDisposable { } for (const system of this.systems) { - system.emitter = value; + if (system instanceof ParticleSystem) { + system.emitter = value; + } } this._emitterNode = value; @@ -90,7 +93,9 @@ export class ParticleSystemSet implements IDisposable { emitterMesh.material = material; for (const system of this.systems) { - system.emitter = emitterMesh; + if (system instanceof ParticleSystem) { + system.emitter = emitterMesh; + } } this._emitterNode = emitterMesh; @@ -103,7 +108,9 @@ export class ParticleSystemSet implements IDisposable { public start(emitter?: AbstractMesh): void { for (const system of this.systems) { if (emitter) { - system.emitter = emitter; + if (system instanceof ParticleSystem) { + system.emitter = emitter; + } } system.start(); } @@ -137,7 +144,7 @@ export class ParticleSystemSet implements IDisposable { result.systems = []; for (const system of this.systems) { - if (!system.doNotSerialize) { + if (system instanceof ParticleSystem && !system.doNotSerialize) { result.systems.push(system.serialize(serializeTexture)); } } diff --git a/packages/dev/core/src/Particles/solidParticleSystem.ts b/packages/dev/core/src/Particles/solidParticleSystem.ts index fa9a0a8a10b..9ed80e81a5a 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -17,6 +17,8 @@ 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"; +import { Observable } from "../Misc/observable"; /** * The SPS is a single updatable mesh. The solid particles are simply separate parts or faces of this big mesh. @@ -62,6 +64,18 @@ export class SolidParticleSystem implements IDisposable { * Please read : https://doc.babylonjs.com/features/featuresDeepDive/particles/solid_particle_system/optimize_sps#limit-garbage-collection */ public vars: any = {}; + /** + * Lifetime duration in milliseconds (0 means infinite) + */ + public lifetime = 0; + /** + * Defines if the SPS should dispose itself automatically once the lifetime ends + */ + public disposeOnEnd = false; + /** + * Observable raised when the SPS lifetime is reached + */ + public onLifeTimeEndedObservable = new Observable(); /** * This array is populated when the SPS is set as 'pickable'. * Each key of this array is a `faceId` value that you can get from a pickResult object. @@ -152,7 +166,9 @@ export class SolidParticleSystem implements IDisposable { protected _autoUpdateSubMeshes: boolean = false; protected _tmpVertex: SolidParticleVertex; protected _recomputeInvisibles: boolean = false; - + protected _onBeforeRenderObserver: Nullable> = null; + protected _elapsedLife: number = 0; + protected _lifeEnded: boolean = false; /** * Creates a SPS (Solid Particle System) object. * @param name (String) is the SPS name, this will be the underlying mesh name. @@ -1538,7 +1554,16 @@ 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; + } + this._lifeEnded = true; + this._elapsedLife = 0; + if (this.mesh) { + this.mesh.dispose(); + } + this.onLifeTimeEndedObservable.clear(); this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; @@ -1998,6 +2023,60 @@ export class SolidParticleSystem implements IDisposable { */ public initParticles(): void {} + /** + * Starts the SPS update loop + * @param lifetime optional lifetime override in milliseconds (0 means infinite) + * @param disposeOnEnd optional flag indicating if the SPS should dispose itself when the lifetime ends + */ + public start(lifetime?: number, disposeOnEnd?: boolean): void { + this.buildMesh(); + this.initParticles(); + this.setParticles(); + + if (lifetime !== undefined) { + this.lifetime = lifetime; + } + if (disposeOnEnd !== undefined) { + this.disposeOnEnd = disposeOnEnd; + } + + this._elapsedLife = 0; + this._lifeEnded = false; + + if (this._onBeforeRenderObserver) { + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + + this._onBeforeRenderObserver = this._scene.onBeforeRenderObservable.add(() => { + this.setParticles(); + + if (this.lifetime > 0 && !this._lifeEnded) { + this._elapsedLife += this._scene.getEngine().getDeltaTime(); + if (this._elapsedLife >= this.lifetime) { + this._lifeEnded = true; + this.stop(this.disposeOnEnd); + this.onLifeTimeEndedObservable.notifyObservers(this); + } + } + }); + } + + /** + * Stops the SPS update loop + * @param dispose if true, the SPS will be disposed after stopping + */ + public stop(dispose = false): void { + if (this._onBeforeRenderObserver) { + this._scene.onBeforeRenderObservable.remove(this._onBeforeRenderObserver); + this._onBeforeRenderObserver = null; + } + + if (dispose) { + this.dispose(); + } + } + /** * This function does nothing. It may be overwritten to recycle a particle. * The SPS doesn't call this function, you may have to call it by your own. diff --git a/packages/tools/nodeEditor/src/components/preview/previewManager.ts b/packages/tools/nodeEditor/src/components/preview/previewManager.ts index 08da0764f7f..fbf4fba746d 100644 --- a/packages/tools/nodeEditor/src/components/preview/previewManager.ts +++ b/packages/tools/nodeEditor/src/components/preview/previewManager.ts @@ -45,6 +45,7 @@ import { Engine } from "core/Engines/engine"; import { Animation } from "core/Animations/animation"; import { RenderTargetTexture } from "core/Materials/Textures/renderTargetTexture"; import { SceneLoaderFlags } from "core/Loading/sceneLoaderFlags"; +import type { SolidParticleSystem } from "core/Particles/solidParticleSystem"; const DontSerializeTextureContent = true; export class PreviewManager { @@ -69,7 +70,7 @@ export class PreviewManager { private _lightParent: TransformNode; private _postprocess: Nullable; private _proceduralTexture: Nullable; - private _particleSystem: Nullable; + private _particleSystem: Nullable; private _layer: Nullable; private _hdrSkyBox: Mesh; private _hdrTexture: CubeTexture; @@ -389,7 +390,8 @@ export class PreviewManager { case NodeMaterialModes.Particle: { this._camera.radius = this._globalState.previewType === PreviewType.Explosion ? 50 : this._globalState.previewType === PreviewType.DefaultParticleSystem ? 6 : 20; this._camera.upperRadiusLimit = 5000; - this._globalState.particleSystemBlendMode = this._particleSystem?.blendMode ?? ParticleSystem.BLENDMODE_STANDARD; + this._globalState.particleSystemBlendMode = + (this._particleSystem instanceof ParticleSystem ? this._particleSystem.blendMode : null) ?? ParticleSystem.BLENDMODE_STANDARD; break; } case NodeMaterialModes.GaussianSplatting: { @@ -454,9 +456,11 @@ export class PreviewManager { this._engine.releaseEffects(); if (this._particleSystem) { - this._particleSystem.onBeforeDrawParticlesObservable.clear(); - this._particleSystem.onDisposeObservable.clear(); - this._particleSystem.stop(); + if (this._particleSystem instanceof ParticleSystem) { + this._particleSystem.onBeforeDrawParticlesObservable.clear(); + this._particleSystem.onDisposeObservable.clear(); + this._particleSystem.stop(); + } this._particleSystem.dispose(); this._particleSystem = null; } @@ -631,12 +635,16 @@ export class PreviewManager { ParticleHelper.CreateAsync(name, this._scene).then((set) => { for (let i = 0; i < set.systems.length; ++i) { if (i == systemIndex) { - this._particleSystem = set.systems[i]; - this._particleSystem.disposeOnStop = true; - this._particleSystem.onDisposeObservable.add(() => { - this._loadParticleSystem(particleNumber, systemIndex, false); - }); - this._particleSystem.start(); + const system = set.systems[i]; + // Only handle ParticleSystem (which implements IParticleSystem), skip SolidParticleSystem + if (system instanceof ParticleSystem) { + this._particleSystem = system; + this._particleSystem.disposeOnStop = true; + this._particleSystem.onDisposeObservable.add(() => { + this._loadParticleSystem(particleNumber, systemIndex, false); + }); + this._particleSystem.start(); + } } else { set.systems[i].dispose(); } @@ -720,17 +728,19 @@ export class PreviewManager { case NodeMaterialModes.Particle: { this._globalState.onIsLoadingChanged.notifyObservers(false); - this._particleSystem!.onBeforeDrawParticlesObservable.clear(); + if (this._particleSystem instanceof ParticleSystem) { + this._particleSystem.onBeforeDrawParticlesObservable.clear(); - this._particleSystem!.onBeforeDrawParticlesObservable.add((effect) => { - const textureBlock = tempMaterial.getBlockByPredicate((block) => block instanceof ParticleTextureBlock); - if (textureBlock && (textureBlock as ParticleTextureBlock).texture && effect) { - effect.setTexture("diffuseSampler", (textureBlock as ParticleTextureBlock).texture); - } - }); - tempMaterial.createEffectForParticles(this._particleSystem!); + this._particleSystem.onBeforeDrawParticlesObservable.add((effect) => { + const textureBlock = tempMaterial.getBlockByPredicate((block) => block instanceof ParticleTextureBlock); + if (textureBlock && (textureBlock as ParticleTextureBlock).texture && effect) { + effect.setTexture("diffuseSampler", (textureBlock as ParticleTextureBlock).texture); + } + }); + tempMaterial.createEffectForParticles(this._particleSystem); - this._particleSystem!.blendMode = this._globalState.particleSystemBlendMode; + this._particleSystem.blendMode = this._globalState.particleSystemBlendMode; + } if (this._material) { this._material.dispose(); diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index 5fe12ccde1f..8ae55845a8d 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -43,6 +43,7 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; +import { SPSMeshSourceBlock, SPSSystemBlock, SPSCreateBlock, SPSInitBlock, SPSUpdateBlock, SPSParticleConfigBlock } from "core/Particles/Node/Blocks"; /** * Static class for BlockTools @@ -153,6 +154,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": @@ -461,6 +474,21 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.System: color = "#f20a2e"; 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; @@ -480,6 +508,16 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Color4; case "Matrix": return NodeParticleBlockConnectionPointTypes.Matrix; + 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; @@ -499,6 +537,16 @@ export class BlockTools { return "Color4"; case NodeParticleBlockConnectionPointTypes.Matrix: return "Matrix"; + 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 86fea98b1c4..b9484affcd1 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -23,6 +23,14 @@ 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/display/inputDisplayManager.ts b/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts index 45e8038beb8..8f990d9e260 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/display/inputDisplayManager.ts @@ -133,28 +133,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/properties/spsMeshSourceNodePropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsMeshSourceNodePropertyComponent.tsx new file mode 100644 index 00000000000..f1ee1c316d4 --- /dev/null +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsMeshSourceNodePropertyComponent.tsx @@ -0,0 +1,150 @@ +import * as React from "react"; +import { LineContainerComponent } from "shared-ui-components/lines/lineContainerComponent"; +import { GeneralPropertyTabComponent } from "./genericNodePropertyComponent"; +import type { IPropertyComponentProps } from "shared-ui-components/nodeGraphSystem/interfaces/propertyComponentProps"; +import { OptionsLine } from "shared-ui-components/lines/optionsLineComponent"; +import { FileButtonLine } from "shared-ui-components/lines/fileButtonLineComponent"; +import { TextLineComponent } from "shared-ui-components/lines/textLineComponent"; +import { ButtonLineComponent } from "shared-ui-components/lines/buttonLineComponent"; +import { LoadSceneAsync } from "core/Loading/sceneLoader"; +import { EngineStore } from "core/Engines/engineStore"; +import type { Nullable } from "core/types"; +import type { Scene } from "core/scene"; +import type { Mesh } from "core/Meshes/mesh"; +import { SPSMeshSourceBlock } from "core/Particles/Node/Blocks"; +import type { Observer } from "core/Misc/observable"; + +export class SPSMeshSourcePropertyTabComponent extends React.Component { + private _onValueChangedObserver: Nullable> = null; + + constructor(props: IPropertyComponentProps) { + super(props); + this.state = { isLoading: false }; + } + + override componentDidMount(): void { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + this._onValueChangedObserver = block.onValueChangedObservable.add(() => { + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + }); + } + + private _getNodeScene(): Nullable { + return (this.props.nodeData as any).__spsMeshScene || null; + } + + private _setNodeScene(scene: Nullable) { + const nodeData = this.props.nodeData as any; + if (nodeData.__spsMeshScene) { + nodeData.__spsMeshScene.dispose(); + } + nodeData.__spsMeshScene = scene || null; + } + + async loadMesh(file: File) { + if (!EngineStore.LastCreatedEngine) { + return; + } + this.setState({ isLoading: true }); + const scene = await LoadSceneAsync(file, EngineStore.LastCreatedEngine); + this.setState({ isLoading: false }); + + if (!scene) { + return; + } + + this._setNodeScene(scene); + + const meshes = scene.meshes.filter((m) => !!m.name && m.getTotalVertices() > 0) as Mesh[]; + if (meshes.length) { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + block.setCustomMesh(meshes[0]); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + } + + this.forceUpdate(); + } + + removeData() { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + block.clearCustomMesh(); + this._setNodeScene(null); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + } + + applyMesh(mesh: Nullable) { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + block.setCustomMesh(mesh ?? null); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + } + + override componentWillUnmount(): void { + const scene = this._getNodeScene(); + if (scene) { + scene.dispose(); + } + (this.props.nodeData as any).__spsMeshScene = null; + const block = this.props.nodeData.data as SPSMeshSourceBlock; + if (this._onValueChangedObserver) { + block.onValueChangedObservable.remove(this._onValueChangedObserver); + this._onValueChangedObserver = null; + } + } + + override render() { + const block = this.props.nodeData.data as SPSMeshSourceBlock; + const scene = this._getNodeScene(); + + const meshes = scene ? (scene.meshes.filter((m) => !!m.name && m.getTotalVertices() > 0) as Mesh[]) : []; + const meshOptions = [{ label: "None", value: -1 }]; + meshOptions.push( + ...meshes.map((mesh, index) => ({ + label: mesh.name, + value: index, + })) + ); + + const selectedMeshIndex = block.hasCustomMesh ? meshes.findIndex((m) => m.name === block.customMeshName) : -1; + + return ( +
+ + + {block.hasCustomMesh ? ( + + ) : ( + + )} + {this.state.isLoading && } + {!this.state.isLoading && await this.loadMesh(file)} />} + {scene && meshOptions.length > 1 && ( + selectedMeshIndex} + onSelect={(value) => { + const index = value as number; + if (index === -1) { + this.applyMesh(null); + return; + } + this.applyMesh(meshes[index]); + this.forceUpdate(); + }} + /> + )} + {block.hasCustomMesh && ( + this.removeData()} /> + )} + +
+ ); + } +} + diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx new file mode 100644 index 00000000000..51de414aa97 --- /dev/null +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/spsNodeMaterialPropertyComponent.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; +import { LineContainerComponent } from "shared-ui-components/lines/lineContainerComponent"; +import { GeneralPropertyTabComponent, GenericPropertyTabComponent } from "./genericNodePropertyComponent"; +import type { IPropertyComponentProps } from "shared-ui-components/nodeGraphSystem/interfaces/propertyComponentProps"; +import { FileButtonLine } from "shared-ui-components/lines/fileButtonLineComponent"; +import { TextLineComponent } from "shared-ui-components/lines/textLineComponent"; +import { ButtonLineComponent } from "shared-ui-components/lines/buttonLineComponent"; +import type { Nullable } from "core/types"; +import type { Observer } from "core/Misc/observable"; +import { SPSNodeMaterialBlock } from "core/Particles/Node/Blocks"; + +export class SPSNodeMaterialPropertyTabComponent extends React.Component { + private _onValueChangedObserver: Nullable> = null; + + constructor(props: IPropertyComponentProps) { + super(props); + this.state = { isLoading: false }; + } + + override componentDidMount(): void { + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + this._onValueChangedObserver = block.onValueChangedObservable.add(() => { + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + }); + } + + override componentWillUnmount(): void { + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + if (this._onValueChangedObserver) { + block.onValueChangedObservable.remove(this._onValueChangedObserver); + this._onValueChangedObserver = null; + } + } + + async loadMaterial(file: File) { + this.setState({ isLoading: true }); + const text = await file.text(); + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + block.setSerializedMaterial(text, file.name); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.props.stateManager.onUpdateRequiredObservable.notifyObservers(block); + this.setState({ isLoading: false }); + this.forceUpdate(); + } + + removeMaterial() { + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + block.clearMaterial(); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.props.stateManager.onUpdateRequiredObservable.notifyObservers(block); + this.forceUpdate(); + } + + override render() { + const block = this.props.nodeData.data as SPSNodeMaterialBlock; + + return ( +
+ + + + + {block.hasCustomMaterial && } + {this.state.isLoading && } + {!this.state.isLoading && await this.loadMaterial(file)} accept=".json" />} + {block.hasCustomMaterial && this.removeMaterial()} />} + +
+ ); + } +} + diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts index 00bb8d70faf..80ee2cb0d07 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts @@ -37,10 +37,20 @@ 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; DisplayLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutDisplayManager; DisplayLedger.RegisteredControls["BasicConditionBlock"] = ConditionDisplayManager; DisplayLedger.RegisteredControls["ParticleTriggerBlock"] = TriggerDisplayManager; + DisplayLedger.RegisteredControls["SpsParticlePropsGetBlock"] = InputDisplayManager; + DisplayLedger.RegisteredControls["SpsParticlePropsSetBlock"] = InputDisplayManager; }; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 0290e5e97d5..4ce78a4ac1e 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -5,6 +5,8 @@ import { TextureSourcePropertyTabComponent } from "./properties/textureSourceNod import { DebugPropertyTabComponent } from "./properties/debugNodePropertyComponent"; import { TeleportOutPropertyTabComponent } from "./properties/teleportOutNodePropertyComponent"; import { MeshShapePropertyTabComponent } from "./properties/meshShapeNodePropertyComponent"; +import { SPSMeshSourcePropertyTabComponent } from "./properties/spsMeshSourceNodePropertyComponent"; +import { SPSNodeMaterialPropertyTabComponent } from "./properties/spsNodeMaterialPropertyComponent"; export const RegisterToPropertyTabManagers = () => { PropertyLedger.DefaultControl = GenericPropertyComponent; @@ -13,4 +15,14 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleDebugBlock"] = DebugPropertyTabComponent; PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; + + PropertyLedger.RegisteredControls["SPSMeshSourceBlock"] = SPSMeshSourcePropertyTabComponent; + PropertyLedger.RegisteredControls["SPSParticleConfigBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSCreateBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSSystemBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSInitBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSUpdateBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SpsParticlePropsSetBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SpsParticlePropsGetBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SPSNodeMaterialBlock"] = SPSNodeMaterialPropertyTabComponent; }; 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, +}