diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISolidParticleData.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISolidParticleData.ts new file mode 100644 index 00000000000..a8c1ea0633e --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/ISolidParticleData.ts @@ -0,0 +1,37 @@ +import type { Vector3 } from "core/Maths/math.vector"; +import type { Color4 } from "core/Maths/math.color"; +import type { VertexData } from "core/Meshes/mesh.vertexData"; + +/** + * Interface for solid particle mesh source data + */ +export interface ISolidParticleMeshSourceData { + customMeshName?: string; + vertexData?: VertexData; +} + +/** + * Interface for solid particle update block data + */ +export interface ISolidParticleUpdateData { + position?: () => Vector3; + velocity?: () => Vector3; + color?: () => Color4; + scaling?: () => Vector3; + rotation?: () => Vector3; +} + +/** + * Interface for solid particle create block data + */ +export interface ISolidParticleInitData { + meshData: ISolidParticleMeshSourceData | null; + count: number; + position?: () => Vector3; + velocity?: () => Vector3; + color?: () => Color4; + scaling?: () => Vector3; + rotation?: () => Vector3; + lifeTime?: () => number; + updateData?: ISolidParticleUpdateData | null; +} diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/createSolidParticleBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/createSolidParticleBlock.ts new file mode 100644 index 00000000000..aa90f653a70 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/createSolidParticleBlock.ts @@ -0,0 +1,109 @@ +import { Color4 } from "core/Maths/math.color"; +import { Vector3 } from "core/Maths/math.vector"; +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 { ISolidParticleInitData } from "./ISolidParticleData"; + +/** + * Block used to create a solid particle configuration (mesh, count, position, velocity, color, scaling, rotation) + */ +export class CreateSolidParticleBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("count", NodeParticleBlockConnectionPointTypes.Int, true, 1); + this.registerInput("lifeTime", NodeParticleBlockConnectionPointTypes.Float, true, Infinity); + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4, true); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3, true); + this.registerInput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + + this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "CreateSolidParticleBlock"; + } + + public get count(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get lifeTime(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[3]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[4]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[5]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[6]; + } + + public get mesh(): NodeParticleConnectionPoint { + return this._inputs[7]; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const meshData = this.mesh.getConnectedValue(state); + const count = this.count.getConnectedValue(state) ?? 1; + + const lifeTime = () => { + return this.lifeTime.isConnected ? this.lifeTime.getConnectedValue(state) : Infinity; + }; + + const position = () => { + return this.position.isConnected ? this.position.getConnectedValue(state) : Vector3.Zero(); + }; + const velocity = () => { + return this.velocity.isConnected ? this.velocity.getConnectedValue(state) : Vector3.Zero(); + }; + const color = () => { + return this.color.isConnected ? this.color.getConnectedValue(state) : new Color4(1, 1, 1, 1); + }; + const scaling = () => { + return this.scaling.isConnected ? this.scaling.getConnectedValue(state) : Vector3.One(); + }; + const rotation = () => { + return this.rotation.isConnected ? this.rotation.getConnectedValue(state) : Vector3.Zero(); + }; + + const particleConfig: ISolidParticleInitData = { + meshData, + count, + lifeTime, + position, + velocity, + color, + scaling, + rotation, + updateData: null, + }; + + this.solidParticle._storedValue = particleConfig; + } +} + +RegisterClass("BABYLON.CreateSolidParticleBlock", CreateSolidParticleBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/mergeSolidParticlesBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/mergeSolidParticlesBlock.ts new file mode 100644 index 00000000000..03a5178d5e6 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/mergeSolidParticlesBlock.ts @@ -0,0 +1,119 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { ISolidParticleInitData } from "./ISolidParticleData"; +import type { Observer } from "core/Misc/observable"; + +/** + * Block used to merge multiple solid particle configurations + */ +export class MergeSolidParticlesBlock extends NodeParticleBlock { + private _connectionObservers = new Map>(); + private _disconnectionObservers = new Map>(); + + public constructor(name: string) { + super(name); + this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + + this._manageExtendedInputs(0); + } + + public override getClassName() { + return "MergeSolidParticlesBlock"; + } + + private _entryCount = 1; + + private _extend() { + this._entryCount++; + this.registerInput(`solidParticle-${this._entryCount - 1}`, NodeParticleBlockConnectionPointTypes.SolidParticle, true); + this._manageExtendedInputs(this._entryCount - 1); + } + + private _shrink() { + if (this._entryCount > 1) { + this._unmanageExtendedInputs(this._entryCount - 1); + this._entryCount--; + this.unregisterInput(`solidParticle-${this._entryCount}`); + } + } + + private _manageExtendedInputs(index: number) { + const connectionObserver = this._inputs[index].onConnectionObservable.add(() => { + if (this._entryCount - 1 > index) { + return; + } + this._extend(); + }); + + const disconnectionObserver = this._inputs[index].onDisconnectionObservable.add(() => { + if (this._entryCount - 1 > index) { + return; + } + this._shrink(); + }); + + // Store observers for later removal + this._connectionObservers.set(index, connectionObserver); + this._disconnectionObservers.set(index, disconnectionObserver); + } + + private _unmanageExtendedInputs(index: number) { + const connectionObserver = this._connectionObservers.get(index); + const disconnectionObserver = this._disconnectionObservers.get(index); + + if (connectionObserver) { + this._inputs[index].onConnectionObservable.remove(connectionObserver); + this._connectionObservers.delete(index); + } + + if (disconnectionObserver) { + this._inputs[index].onDisconnectionObservable.remove(disconnectionObserver); + this._disconnectionObservers.delete(index); + } + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + // Collect all particle configurations + const particleConfigs: ISolidParticleInitData[] = []; + for (let i = 0; i < this._inputs.length; i++) { + const particleData = this._inputs[i].getConnectedValue(state) as ISolidParticleInitData | ISolidParticleInitData[]; + if (this._inputs[i].isConnected && particleData) { + // If it's already an array, flatten it + if (Array.isArray(particleData)) { + particleConfigs.push(...particleData); + } else { + particleConfigs.push(particleData); + } + } + } + + // Output array of configurations + // The actual SPS creation will happen in SolidParticleSystemBlock + this.solidParticle._storedValue = particleConfigs; + } + + 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.MergeSolidParticlesBlock", MergeSolidParticlesBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts new file mode 100644 index 00000000000..c6cd148307c --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/meshSourceBlock.ts @@ -0,0 +1,119 @@ +/* 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 { 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 type { ISolidParticleMeshSourceData } from "./ISolidParticleData"; + +/** + * Block used to provide mesh source for SPS + */ +export class MeshSourceBlock extends NodeParticleBlock { + private _customVertexData: Nullable = null; + private _customMeshName = ""; + + /** Gets an observable raised when the block data changes */ + public onValueChangedObservable = new Observable(); + + public constructor(name: string) { + super(name); + + this.registerOutput("mesh", NodeParticleBlockConnectionPointTypes.Mesh); + } + + public override getClassName() { + return "MeshSourceBlock"; + } + + 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.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.onValueChangedObservable.notifyObservers(this); + } + + /** + * Clears any assigned custom mesh data + */ + public clearCustomMesh() { + this._customVertexData = null; + this._customMeshName = ""; + this.onValueChangedObservable.notifyObservers(this); + } + + public override _build(state: NodeParticleBuildState) { + if (!this._customVertexData) { + this.mesh._storedValue = null; + return; + } + + const meshData: ISolidParticleMeshSourceData = { + vertexData: this._customVertexData, + customMeshName: this._customMeshName, + }; + + this.mesh._storedValue = meshData; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.customMeshName = this._customMeshName; + if (this._customVertexData) { + serializationObject.customVertexData = this._customVertexData.serialize(); + } + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + if (serializationObject.customVertexData) { + this._customVertexData = VertexData.Parse(serializationObject.customVertexData); + this._customMeshName = serializationObject.customMeshName || ""; + } + } +} + +RegisterClass("BABYLON.MeshSourceBlock", MeshSourceBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/solidParticleSystemBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/solidParticleSystemBlock.ts new file mode 100644 index 00000000000..66bf9e07a40 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/solidParticleSystemBlock.ts @@ -0,0 +1,198 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import type { ISolidParticleInitData } from "./ISolidParticleData"; +import { Mesh } from "core/Meshes/mesh"; +import type { SolidParticle } from "../../../solidParticle"; + +/** + * Block used to create SolidParticleSystem from merged particle configurations + */ +export class SolidParticleSystemBlock extends NodeParticleBlock { + private static _IdCounter = 0; + + @editableInPropertyPage("Billboard", PropertyTypeForEdition.Boolean, "ADVANCED", { + embedded: true, + notifiers: { rebuild: true }, + }) + public billboard = false; + + public _internalId = SolidParticleSystemBlock._IdCounter++; + + public constructor(name: string) { + super(name); + this._isSystem = true; + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerOutput("system", NodeParticleBlockConnectionPointTypes.System); + } + + public override getClassName() { + return "SolidParticleSystemBlock"; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get system(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public createSystem(state: NodeParticleBuildState): SolidParticleSystem { + state.buildId = ++this._buildId; + + this.build(state); + + if (!state.scene) { + throw new Error("Scene is not initialized in NodeParticleBuildState"); + } + + const particleData = this.solidParticle.getConnectedValue(state) as ISolidParticleInitData | ISolidParticleInitData[]; + + if (!particleData) { + throw new Error("No solid particle configuration connected to SolidParticleSystemBlock"); + } + + // Create the SPS + const sps = new SolidParticleSystem(this.name, state.scene, { + useModelMaterial: true, + }); + + const createBlocks = new Map(); + // Support both single particle config and array of configs + const particleConfigs: ISolidParticleInitData[] = Array.isArray(particleData) ? particleData : [particleData]; + + for (let i = 0; i < particleConfigs.length; i++) { + const creatData = particleConfigs[i]; + if (!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); + + 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); + if (!particleCreateData) { + continue; + } + const { lifeTime, position, velocity, color, scaling, rotation } = particleCreateData; + + state.particleContext = particle; + state.systemContext = sps; + + if (lifeTime) { + particle.lifeTime = lifeTime(); + particle.age = 0; + particle.alive = true; + } + + if (position) { + particle.position.copyFrom(position()); + } + if (velocity) { + particle.velocity.copyFrom(velocity()); + } + if (color) { + const particleColor = particle.color; + if (particleColor) { + particleColor.copyFrom(color()); + } + } + if (scaling) { + particle.scaling.copyFrom(scaling()); + } + if (rotation) { + particle.rotation.copyFrom(rotation()); + } + } + } finally { + state.particleContext = originalContext; + state.systemContext = originalSystemContext; + } + }; + + sps.updateParticle = (particle: SolidParticle) => { + if (!sps) { + return particle; + } + + const particleCreateData = createBlocks.get(particle.shapeId); + const updateData = particleCreateData?.updateData; + if (!updateData) { + 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 (updateData.position) { + particle.position.copyFrom(updateData.position()); + } + if (updateData.velocity) { + particle.velocity.copyFrom(updateData.velocity()); + } + if (updateData.color) { + particle.color?.copyFrom(updateData.color()); + } + if (updateData.scaling) { + particle.scaling.copyFrom(updateData.scaling()); + } + if (updateData.rotation) { + particle.rotation.copyFrom(updateData.rotation()); + } + } finally { + // Restore original context + state.particleContext = originalContext; + state.systemContext = originalSystemContext; + } + return particle; + }; + + sps.billboard = this.billboard; + sps.name = this.name; + + return sps; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.billboard = this.billboard; + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + this.billboard = !!serializationObject.billboard; + } +} + +RegisterClass("BABYLON.SolidParticleSystemBlock", SolidParticleSystemBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleColorBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleColorBlock.ts new file mode 100644 index 00000000000..66c96993d44 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleColorBlock.ts @@ -0,0 +1,60 @@ +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 { ISolidParticleInitData, ISolidParticleUpdateData } from "./ISolidParticleData"; +import type { Color4 } from "core/Maths/math.color"; + +/** + * Block used to update the color of a solid particle + */ +export class UpdateSolidParticleColorBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("color", NodeParticleBlockConnectionPointTypes.Color4); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "UpdateSolidParticleColorBlock"; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get color(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const inputConfig = this.solidParticle.getConnectedValue(state) as ISolidParticleInitData; + const updateData: ISolidParticleUpdateData = { ...(inputConfig.updateData || {}) }; + + if (this.color.isConnected) { + updateData.color = () => { + return this.color.getConnectedValue(state) as Color4; + }; + } + + this.output._storedValue = { ...inputConfig, updateData }; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.UpdateSolidParticleColorBlock", UpdateSolidParticleColorBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticlePositionBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticlePositionBlock.ts new file mode 100644 index 00000000000..a53480d42c0 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticlePositionBlock.ts @@ -0,0 +1,60 @@ +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 { ISolidParticleInitData, ISolidParticleUpdateData } from "./ISolidParticleData"; +import type { Vector3 } from "core/Maths/math.vector"; + +/** + * Block used to update the position of a solid particle + */ +export class UpdateSolidParticlePositionBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("position", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "UpdateSolidParticlePositionBlock"; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get position(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const inputConfig = this.solidParticle.getConnectedValue(state) as ISolidParticleInitData; + const updateData: ISolidParticleUpdateData = { ...(inputConfig.updateData || {}) }; + + if (this.position.isConnected) { + updateData.position = () => { + return this.position.getConnectedValue(state) as Vector3; + }; + } + + this.output._storedValue = { ...inputConfig, updateData }; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.UpdateSolidParticlePositionBlock", UpdateSolidParticlePositionBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleRotationBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleRotationBlock.ts new file mode 100644 index 00000000000..31b2002505d --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleRotationBlock.ts @@ -0,0 +1,60 @@ +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 { ISolidParticleInitData, ISolidParticleUpdateData } from "./ISolidParticleData"; +import type { Vector3 } from "core/Maths/math.vector"; + +/** + * Block used to update the rotation of a solid particle + */ +export class UpdateSolidParticleRotationBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("rotation", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "UpdateSolidParticleRotationBlock"; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get rotation(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const inputConfig = this.solidParticle.getConnectedValue(state) as ISolidParticleInitData; + const updateData: ISolidParticleUpdateData = { ...(inputConfig.updateData || {}) }; + + if (this.rotation.isConnected) { + updateData.rotation = () => { + return this.rotation.getConnectedValue(state) as Vector3; + }; + } + + this.output._storedValue = { ...inputConfig, updateData }; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.UpdateSolidParticleRotationBlock", UpdateSolidParticleRotationBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleScalingBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleScalingBlock.ts new file mode 100644 index 00000000000..8494aff0235 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleScalingBlock.ts @@ -0,0 +1,60 @@ +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 { ISolidParticleInitData, ISolidParticleUpdateData } from "./ISolidParticleData"; +import type { Vector3 } from "core/Maths/math.vector"; + +/** + * Block used to update the scaling of a solid particle + */ +export class UpdateSolidParticleScalingBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("scaling", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "UpdateSolidParticleScalingBlock"; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get scaling(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const inputConfig = this.solidParticle.getConnectedValue(state) as ISolidParticleInitData; + const updateData: ISolidParticleUpdateData = { ...(inputConfig.updateData || {}) }; + + if (this.scaling.isConnected) { + updateData.scaling = () => { + return this.scaling.getConnectedValue(state) as Vector3; + }; + } + + this.output._storedValue = { ...inputConfig, updateData }; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.UpdateSolidParticleScalingBlock", UpdateSolidParticleScalingBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleVelocityBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleVelocityBlock.ts new file mode 100644 index 00000000000..1b5882d09f9 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/SolidParticle/updateSolidParticleVelocityBlock.ts @@ -0,0 +1,60 @@ +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 { ISolidParticleInitData, ISolidParticleUpdateData } from "./ISolidParticleData"; +import type { Vector3 } from "core/Maths/math.vector"; + +/** + * Block used to update the velocity of a solid particle + */ +export class UpdateSolidParticleVelocityBlock extends NodeParticleBlock { + public constructor(name: string) { + super(name); + + this.registerInput("solidParticle", NodeParticleBlockConnectionPointTypes.SolidParticle); + this.registerInput("velocity", NodeParticleBlockConnectionPointTypes.Vector3); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.SolidParticle); + } + + public override getClassName() { + return "UpdateSolidParticleVelocityBlock"; + } + + public get solidParticle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + public get velocity(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + public override _build(state: NodeParticleBuildState) { + const inputConfig = this.solidParticle.getConnectedValue(state) as ISolidParticleInitData; + const updateData: ISolidParticleUpdateData = { ...(inputConfig.updateData || {}) }; + + if (this.velocity.isConnected) { + updateData.velocity = () => { + return this.velocity.getConnectedValue(state) as Vector3; + }; + } + + this.output._storedValue = { ...inputConfig, updateData }; + } + + public override serialize(): any { + const serializationObject = super.serialize(); + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + } +} + +RegisterClass("BABYLON.UpdateSolidParticleVelocityBlock", UpdateSolidParticleVelocityBlock); diff --git a/packages/dev/core/src/Particles/Node/Blocks/index.ts b/packages/dev/core/src/Particles/Node/Blocks/index.ts index d1cfdaf2586..462cfeace51 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/index.ts @@ -35,3 +35,13 @@ export * from "./Triggers/particleTriggerBlock"; export * from "./particleLocalVariableBlock"; export * from "./particleVectorLengthBlock"; export * from "./particleFresnelBlock"; +export * from "./SolidParticle/ISolidParticleData"; +export * from "./SolidParticle/meshSourceBlock"; +export * from "./SolidParticle/createSolidParticleBlock"; +export * from "./SolidParticle/mergeSolidParticlesBlock"; +export * from "./SolidParticle/solidParticleSystemBlock"; +export * from "./SolidParticle/updateSolidParticlePositionBlock"; +export * from "./SolidParticle/updateSolidParticleVelocityBlock"; +export * from "./SolidParticle/updateSolidParticleColorBlock"; +export * from "./SolidParticle/updateSolidParticleScalingBlock"; +export * from "./SolidParticle/updateSolidParticleRotationBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/particleInputBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/particleInputBlock.ts index 11a52bc29a2..ce39054fc38 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/particleInputBlock.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/particleInputBlock.ts @@ -136,6 +136,9 @@ export class ParticleInputBlock extends NodeParticleBlock { case NodeParticleContextualSources.ScaledDirection: case NodeParticleContextualSources.InitialDirection: case NodeParticleContextualSources.LocalPositionUpdated: + case NodeParticleContextualSources.SolidParticleVelocity: + case NodeParticleContextualSources.SolidParticleScaling: + case NodeParticleContextualSources.SolidParticleRotation: this._type = NodeParticleBlockConnectionPointTypes.Vector3; break; case NodeParticleContextualSources.Color: @@ -156,6 +159,7 @@ export class ParticleInputBlock extends NodeParticleBlock { case NodeParticleContextualSources.SpriteCellEnd: case NodeParticleContextualSources.SpriteCellStart: case NodeParticleContextualSources.SpriteCellIndex: + case NodeParticleContextualSources.SolidParticleIndex: this._type = NodeParticleBlockConnectionPointTypes.Int; break; } diff --git a/packages/dev/core/src/Particles/Node/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..816e6df9fcc 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes.ts @@ -28,12 +28,16 @@ export enum NodeParticleBlockConnectionPointTypes { Color4Gradient = 0x0800, /** System */ System = 0x1000, + /** Solid Particle */ + SolidParticle = 0x2000, + /** Mesh */ + Mesh = 0x8000, /** Detect type based on connection */ - AutoDetect = 0x2000, + AutoDetect = 0x20000, /** Output type that will be defined by input type */ - BasedOnInput = 0x4000, + BasedOnInput = 0x40000, /** Undefined */ - Undefined = 0x8000, + Undefined = 0x80000, /** Bitmask of all types */ - All = 0xffff, + All = 0x1ffff, } diff --git a/packages/dev/core/src/Particles/Node/Enums/nodeParticleContextualSources.ts b/packages/dev/core/src/Particles/Node/Enums/nodeParticleContextualSources.ts index 80a385f39ae..5855224e8ef 100644 --- a/packages/dev/core/src/Particles/Node/Enums/nodeParticleContextualSources.ts +++ b/packages/dev/core/src/Particles/Node/Enums/nodeParticleContextualSources.ts @@ -45,4 +45,12 @@ export enum NodeParticleContextualSources { Size = 0x0019, /** Direction Scale */ DirectionScale = 0x0020, + /** Solid Particle Index */ + SolidParticleIndex = 0x0021, + /** Solid Particle Velocity */ + SolidParticleVelocity = 0x0022, + /** Solid Particle Scaling */ + SolidParticleScaling = 0x0023, + /** Solid Particle Rotation */ + SolidParticleRotation = 0x0024, } 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 f025c4cae7b..ee98fa83e7e 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleBuildState.ts @@ -6,10 +6,12 @@ import type { ThinParticleSystem } from "core/Particles/thinParticleSystem"; import type { NodeParticleConnectionPoint } from "core/Particles/Node/nodeParticleBlockConnectionPoint"; import { Color4 } from "core/Maths/math.color"; -import { Vector2, Vector3 } from "core/Maths/math.vector"; +import { Matrix, Vector2, Vector3 } from "core/Maths/math.vector"; import { NodeParticleBlockConnectionPointTypes } from "core/Particles/Node/Enums/nodeParticleBlockConnectionPointTypes"; import { NodeParticleContextualSources } from "core/Particles/Node/Enums/nodeParticleContextualSources"; import { NodeParticleSystemSources } from "core/Particles/Node/Enums/nodeParticleSystemSources"; +import { SolidParticle } from "core/Particles/solidParticle"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; /** * Class used to store node based geometry build state @@ -34,15 +36,35 @@ export class NodeParticleBuildState { /** Gets or sets a boolean indicating that verbose mode is on */ public verbose: boolean; + private _particleContext: Nullable = null; + private _isSolidParticle: boolean = false; + /** * Gets or sets the particle context for contextual data */ - public particleContext: Nullable = null; + public get particleContext(): Nullable { + return this._particleContext; + } + + public set particleContext(value: Nullable) { + this._particleContext = value; + this._isSolidParticle = value instanceof SolidParticle; + } + + private _systemContext: Nullable = null; + private _isSolidParticleSystem: boolean = false; /** * Gets or sets the system context for contextual data */ - public systemContext: Nullable = null; + public get systemContext(): Nullable { + return this._systemContext; + } + + public set systemContext(value: Nullable) { + this._systemContext = value; + this._isSolidParticleSystem = value instanceof SolidParticleSystem; + } /** * Gets or sets the index of the gradient to use @@ -114,48 +136,115 @@ export class NodeParticleBuildState { case NodeParticleContextualSources.Position: return this.particleContext.position; case NodeParticleContextualSources.Direction: - return this.particleContext.direction; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle).direction; case NodeParticleContextualSources.DirectionScale: - return this.particleContext._directionScale; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle)._directionScale; case NodeParticleContextualSources.ScaledDirection: - this.particleContext.direction.scaleToRef(this.particleContext._directionScale, this.particleContext._scaledDirection); - return this.particleContext._scaledDirection; + if (this._isSolidParticle) { + return null; + } + const particle = this.particleContext as Particle; + particle.direction.scaleToRef(particle._directionScale, particle._scaledDirection); + return particle._scaledDirection; case NodeParticleContextualSources.Color: return this.particleContext.color; case NodeParticleContextualSources.InitialColor: - return this.particleContext.initialColor; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle).initialColor; case NodeParticleContextualSources.ColorDead: - return this.particleContext.colorDead; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle).colorDead; case NodeParticleContextualSources.Age: return this.particleContext.age; case NodeParticleContextualSources.Lifetime: return this.particleContext.lifeTime; case NodeParticleContextualSources.Angle: - return this.particleContext.angle; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle).angle; case NodeParticleContextualSources.Scale: return this.particleContext.scale; case NodeParticleContextualSources.Size: - return this.particleContext.size; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle).size; case NodeParticleContextualSources.AgeGradient: return this.particleContext.age / this.particleContext.lifeTime; case NodeParticleContextualSources.SpriteCellEnd: - return this.systemContext.endSpriteCellID; + if (this._isSolidParticleSystem) { + return null; + } + return (this.systemContext as ThinParticleSystem).endSpriteCellID; case NodeParticleContextualSources.SpriteCellIndex: - return this.particleContext.cellIndex; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle).cellIndex; case NodeParticleContextualSources.SpriteCellStart: - return this.systemContext.startSpriteCellID; + if (this._isSolidParticleSystem) { + return null; + } + return (this.systemContext as ThinParticleSystem).startSpriteCellID; + case NodeParticleContextualSources.SolidParticleIndex: + if (!this._isSolidParticle) { + return null; + } + return (this.particleContext as SolidParticle).idx; + case NodeParticleContextualSources.SolidParticleVelocity: + if (!this._isSolidParticle) { + return null; + } + return (this.particleContext as SolidParticle).velocity; + case NodeParticleContextualSources.SolidParticleScaling: + if (!this._isSolidParticle) { + return null; + } + return (this.particleContext as SolidParticle).scaling; + case NodeParticleContextualSources.SolidParticleRotation: + if (!this._isSolidParticle) { + return null; + } + return (this.particleContext as SolidParticle).rotation; case NodeParticleContextualSources.InitialDirection: - return this.particleContext._initialDirection; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle)._initialDirection; case NodeParticleContextualSources.ColorStep: - return this.particleContext.colorStep; + if (this._isSolidParticle) { + return null; + } + return (this.particleContext as Particle).colorStep; case NodeParticleContextualSources.ScaledColorStep: - this.particleContext.colorStep.scaleToRef(this.systemContext._scaledUpdateSpeed, this.systemContext._scaledColorStep); - return this.systemContext._scaledColorStep; + if (this._isSolidParticle || this._isSolidParticleSystem) { + return null; + } + const particleForColor = this.particleContext as Particle; + const systemForColor = this.systemContext as ThinParticleSystem; + particleForColor.colorStep.scaleToRef(systemForColor._scaledUpdateSpeed, systemForColor._scaledColorStep); + return systemForColor._scaledColorStep; case NodeParticleContextualSources.LocalPositionUpdated: - this.particleContext.direction.scaleToRef(this.particleContext._directionScale, this.particleContext._scaledDirection); - this.particleContext._localPosition!.addInPlace(this.particleContext._scaledDirection); - Vector3.TransformCoordinatesToRef(this.particleContext._localPosition!, this.systemContext._emitterWorldMatrix, this.particleContext.position); - return this.particleContext.position; + if (this._isSolidParticle || this._isSolidParticleSystem) { + return this.particleContext.position; + } + const particleForLocal = this.particleContext as Particle; + const systemForLocal = this.systemContext as ThinParticleSystem; + particleForLocal.direction.scaleToRef(particleForLocal._directionScale, particleForLocal._scaledDirection); + particleForLocal._localPosition!.addInPlace(particleForLocal._scaledDirection); + Vector3.TransformCoordinatesToRef(particleForLocal._localPosition!, systemForLocal._emitterWorldMatrix, particleForLocal.position); + return particleForLocal.position; } return null; @@ -168,7 +257,14 @@ export class NodeParticleBuildState { if (!this.systemContext) { return null; } - return this.systemContext._emitterWorldMatrix; + if (this._isSolidParticleSystem) { + const worldMatrix = (this.systemContext as SolidParticleSystem).mesh?.getWorldMatrix(); + if (!worldMatrix) { + return Matrix.Identity(); + } + return worldMatrix; + } + return (this.systemContext as ThinParticleSystem)._emitterWorldMatrix; } /** @@ -178,7 +274,14 @@ export class NodeParticleBuildState { if (!this.systemContext) { return null; } - return this.systemContext._emitterInverseWorldMatrix; + if (this._isSolidParticleSystem) { + const worldMatrix = (this.systemContext as SolidParticleSystem).mesh?.getWorldMatrix(); + if (!worldMatrix) { + return Matrix.Identity(); + } + return worldMatrix.invert(); + } + return (this.systemContext as ThinParticleSystem)._emitterInverseWorldMatrix; } /** @@ -189,15 +292,20 @@ export class NodeParticleBuildState { return null; } - if (!this.systemContext.emitter) { + if (this._isSolidParticleSystem) { + return (this.systemContext as SolidParticleSystem).mesh?.absolutePosition || Vector3.Zero(); + } + + const thinSystem = this.systemContext as ThinParticleSystem; + if (!thinSystem.emitter) { return null; } - if (this.systemContext.emitter instanceof Vector3) { - return this.systemContext.emitter; + if (thinSystem.emitter instanceof Vector3) { + return thinSystem.emitter; } - return (this.systemContext.emitter).absolutePosition; + return (thinSystem.emitter).absolutePosition; } /** diff --git a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts index eb05f23fc1b..cdc77c03785 100644 --- a/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts +++ b/packages/dev/core/src/Particles/Node/nodeParticleSystemSet.ts @@ -12,16 +12,26 @@ import { Constants } from "core/Engines/constants"; import { Tools } from "core/Misc/tools"; import { AbstractEngine } from "core/Engines/abstractEngine"; import { ParticleInputBlock } from "./Blocks/particleInputBlock"; +import { NodeParticleBlockConnectionPointTypes } from "./Enums/nodeParticleBlockConnectionPointTypes"; import { ParticleTextureSourceBlock } from "./Blocks/particleSourceTextureBlock"; import { NodeParticleContextualSources } from "./Enums/nodeParticleContextualSources"; import { UpdatePositionBlock } from "./Blocks/Update/updatePositionBlock"; import { ParticleMathBlock, ParticleMathBlockOperations } from "./Blocks/particleMathBlock"; -import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTeleportOutBlock"; -import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; +import { ParticleTrigonometryBlock, ParticleTrigonometryBlockOperations } from "./Blocks/particleTrigonometryBlock"; import { BoxShapeBlock } from "./Blocks/Emitters/boxShapeBlock"; import { CreateParticleBlock } from "./Blocks/Emitters/createParticleBlock"; +import { ParticleSystem } from "core/Particles/particleSystem"; +import type { ParticleTeleportOutBlock } from "./Blocks/Teleport/particleTeleportOutBlock"; +import type { ParticleTeleportInBlock } from "./Blocks/Teleport/particleTeleportInBlock"; +import type { Nullable } from "core/types"; import type { Color4 } from "core/Maths/math.color"; -import type { Nullable } from "../../types"; +import { SolidParticleSystemBlock } from "./Blocks/SolidParticle/solidParticleSystemBlock"; +import { MeshSourceBlock } from "./Blocks/SolidParticle/meshSourceBlock"; +import { CreateSolidParticleBlock } from "./Blocks/SolidParticle/createSolidParticleBlock"; +import { UpdateSolidParticlePositionBlock } from "./Blocks/SolidParticle/updateSolidParticlePositionBlock"; +import { ParticleConverterBlock } from "./Blocks/particleConverterBlock"; +import { VertexData } from "core/Meshes/mesh.vertexData"; +import { NodeParticleSystemSources } from "./Enums/nodeParticleSystemSources"; // declare NODEPARTICLEEDITOR namespace for compilation issue declare let NODEPARTICLEEDITOR: any; @@ -45,7 +55,7 @@ export interface INodeParticleEditorOptions { * PG: #ZT509U#1 */ export class NodeParticleSystemSet { - private _systemBlocks: SystemBlock[] = []; + private _systemBlocks: (SystemBlock | SolidParticleSystemBlock)[] = []; private _buildId: number = 0; /** Define the Url to load node editor script */ @@ -90,7 +100,7 @@ export class NodeParticleSystemSet { /** * Gets the system blocks */ - public get systemBlocks(): SystemBlock[] { + public get systemBlocks(): (SystemBlock | SolidParticleSystemBlock)[] { return this._systemBlocks; } @@ -269,13 +279,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 +346,45 @@ export class NodeParticleSystemSet { this._systemBlocks.push(system); } + public setToDefaultSps() { + this.clear(); + this.editorData = null; + + // Mesh source with a tiny box shape so something renders immediately + const meshSource = new MeshSourceBlock("Mesh Source"); + const defaultMeshData = VertexData.CreateBox({ size: 0.5 }); + meshSource.setCustomVertexData(defaultMeshData, "Box"); + + // Time input for movement + const timeInput = new ParticleInputBlock("Time", NodeParticleBlockConnectionPointTypes.Float); + timeInput.systemSource = NodeParticleSystemSources.Time; + + // Sin for oscillating movement (back and forth) + const sinBlock = new ParticleTrigonometryBlock("Sin"); + sinBlock.operation = ParticleTrigonometryBlockOperations.Sin; + timeInput.output.connectTo(sinBlock.input); + + // Position vector for update - uses sin for oscillating movement + const positionVector = new ParticleConverterBlock("Position"); + sinBlock.output.connectTo(positionVector.xIn); + + // Create solid particle + const createParticleBlock = new CreateSolidParticleBlock("Create Solid Particle"); + createParticleBlock.count.value = 1; + meshSource.mesh.connectTo(createParticleBlock.mesh); + + // Update position - this will loop automatically as updateParticle is called each frame + const updatePositionBlock = new UpdateSolidParticlePositionBlock("Update Position"); + createParticleBlock.solidParticle.connectTo(updatePositionBlock.solidParticle); + positionVector.xyzOut.connectTo(updatePositionBlock.position); + + // System block creates the SPS - can accept SolidParticle directly + const systemBlock = new SolidParticleSystemBlock("SPS System"); + updatePositionBlock.output.connectTo(systemBlock.solidParticle); + + this._systemBlocks.push(systemBlock); + } + /** * 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 e58d170e507..db24e45debb 100644 --- a/packages/dev/core/src/Particles/solidParticleSystem.ts +++ b/packages/dev/core/src/Particles/solidParticleSystem.ts @@ -161,7 +161,9 @@ export class SolidParticleSystem implements IDisposable { */ public updateSpeed = 0.01; /** @internal */ - protected _scaledUpdateSpeed: number; + public _scaledUpdateSpeed: number; + /** @internal */ + public _actualFrame = 0; /** * Creates a SPS (Solid Particle System) object. @@ -1184,6 +1186,7 @@ export class SolidParticleSystem implements IDisposable { // Calculate scaled update speed based on animation ratio (for FPS independence) if (this._started && !this._stopped) { this._scaledUpdateSpeed = this.updateSpeed * (this._scene?.getAnimationRatio() || 1); + this._actualFrame += this._scaledUpdateSpeed; } for (let p = start; p <= end; p++) { @@ -1563,7 +1566,9 @@ export class SolidParticleSystem implements IDisposable { */ public dispose(): void { this.stop(); - this.mesh.dispose(); + if (this.mesh) { + this.mesh.dispose(); + } this.vars = null; // drop references to internal big arrays for the GC (this._positions) = null; @@ -2104,6 +2109,7 @@ export class SolidParticleSystem implements IDisposable { this._started = true; this._stopped = false; + this._actualFrame = 0; // Register update loop if (this._scene) { 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 42351ea9892..b5f2b26e999 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -44,6 +44,15 @@ import { BasicColorUpdateBlock } from "core/Particles/Node/Blocks/Update/basicCo import { ParticleLocalVariableBlock } from "core/Particles/Node/Blocks/particleLocalVariableBlock"; import { ParticleVectorLengthBlock } from "core/Particles/Node/Blocks/particleVectorLengthBlock"; import { ParticleFresnelBlock } from "core/Particles/Node/Blocks/particleFresnelBlock"; +import { MeshSourceBlock } from "core/Particles/Node/Blocks/SolidParticle/meshSourceBlock"; +import { UpdateSolidParticlePositionBlock } from "core/Particles/Node/Blocks/SolidParticle/updateSolidParticlePositionBlock"; +import { UpdateSolidParticleVelocityBlock } from "core/Particles/Node/Blocks/SolidParticle/updateSolidParticleVelocityBlock"; +import { UpdateSolidParticleColorBlock } from "core/Particles/Node/Blocks/SolidParticle/updateSolidParticleColorBlock"; +import { UpdateSolidParticleScalingBlock } from "core/Particles/Node/Blocks/SolidParticle/updateSolidParticleScalingBlock"; +import { UpdateSolidParticleRotationBlock } from "core/Particles/Node/Blocks/SolidParticle/updateSolidParticleRotationBlock"; +import { CreateSolidParticleBlock } from "core/Particles/Node/Blocks/SolidParticle/createSolidParticleBlock"; +import { MergeSolidParticlesBlock } from "core/Particles/Node/Blocks/SolidParticle/mergeSolidParticlesBlock"; +import { SolidParticleSystemBlock } from "core/Particles/Node/Blocks/SolidParticle/solidParticleSystemBlock"; import { ParticleFloatToIntBlock } from "core/Particles/Node/Blocks/particleFloatToIntBlock"; /** @@ -157,6 +166,24 @@ export class BlockTools { return new UpdateAttractorBlock("Update attractor"); case "SystemBlock": return new SystemBlock("System"); + case "MeshSourceBlock": + return new MeshSourceBlock("Mesh Source for SPS"); + case "SolidParticleSystemBlock": + return new SolidParticleSystemBlock("SPS System"); + case "CreateSolidParticleBlock": + return new CreateSolidParticleBlock("Create Solid Particle"); + case "MergeSolidParticlesBlock": + return new MergeSolidParticlesBlock("Merge Solid Particles"); + case "UpdateSolidParticlePositionBlock": + return new UpdateSolidParticlePositionBlock("Update SPS Position"); + case "UpdateSolidParticleVelocityBlock": + return new UpdateSolidParticleVelocityBlock("Update SPS Velocity"); + case "UpdateSolidParticleColorBlock": + return new UpdateSolidParticleColorBlock("Update SPS Color"); + case "UpdateSolidParticleScalingBlock": + return new UpdateSolidParticleScalingBlock("Update SPS Scaling"); + case "UpdateSolidParticleRotationBlock": + return new UpdateSolidParticleRotationBlock("Update SPS Rotation"); case "TextureBlock": return new ParticleTextureSourceBlock("Texture"); case "BoxShapeBlock": @@ -253,6 +280,26 @@ export class BlockTools { block.contextualValue = NodeParticleContextualSources.SpriteCellIndex; return block; } + case "SolidParticleIndexBlock": { + const block = new ParticleInputBlock("Solid Particle Index"); + block.contextualValue = NodeParticleContextualSources.SolidParticleIndex; + return block; + } + case "SolidParticleVelocityBlock": { + const block = new ParticleInputBlock("Solid Particle Velocity"); + block.contextualValue = NodeParticleContextualSources.SolidParticleVelocity; + return block; + } + case "SolidParticleScalingBlock": { + const block = new ParticleInputBlock("Solid Particle Scaling"); + block.contextualValue = NodeParticleContextualSources.SolidParticleScaling; + return block; + } + case "SolidParticleRotationBlock": { + const block = new ParticleInputBlock("Solid Particle Rotation"); + block.contextualValue = NodeParticleContextualSources.SolidParticleRotation; + return block; + } case "SpriteCellStartBlock": { const block = new ParticleInputBlock("Sprite cell start"); block.contextualValue = NodeParticleContextualSources.SpriteCellStart; @@ -473,6 +520,12 @@ export class BlockTools { case NodeParticleBlockConnectionPointTypes.System: color = "#f20a2e"; break; + case NodeParticleBlockConnectionPointTypes.SolidParticle: + color = "#84995c"; + break; + case NodeParticleBlockConnectionPointTypes.Mesh: + color = "#4682b4"; + break; } return color; @@ -492,6 +545,10 @@ export class BlockTools { return NodeParticleBlockConnectionPointTypes.Color4; case "Matrix": return NodeParticleBlockConnectionPointTypes.Matrix; + case "SolidParticle": + return NodeParticleBlockConnectionPointTypes.SolidParticle; + case "Mesh": + return NodeParticleBlockConnectionPointTypes.Mesh; } return NodeParticleBlockConnectionPointTypes.AutoDetect; @@ -511,6 +568,10 @@ export class BlockTools { return "Color4"; case NodeParticleBlockConnectionPointTypes.Matrix: return "Matrix"; + case NodeParticleBlockConnectionPointTypes.SolidParticle: + return "SolidParticle"; + case NodeParticleBlockConnectionPointTypes.Mesh: + return "Mesh"; } return ""; diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts b/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts new file mode 100644 index 00000000000..cdf55679b9b --- /dev/null +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/blockDefinitions.ts @@ -0,0 +1,381 @@ +import { NodeParticleModes } from "../../nodeParticleModes"; + +export interface IBlockDefinition { + name: string; + category: string; + modes: NodeParticleModes[]; + tooltip: string; +} + +/** + * Defines all available blocks with their categories, supported modes, and tooltips + */ +export const BlockDefinitions: IBlockDefinition[] = [ + // Shapes (Particle only) + { name: "BoxShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a box shape" }, + { name: "ConeShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a cone shape" }, + { name: "SphereShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a sphere shape" }, + { name: "PointShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a point" }, + { name: "CustomShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a custom position" }, + { name: "CylinderShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a cylinder shape" }, + { name: "MeshShapeBlock", category: "Shapes", modes: [NodeParticleModes.Particle], tooltip: "Emit particles from a mesh shape" }, + + // Inputs + { name: "Float", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a float value" }, + { name: "Vector2", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a Vector2 value" }, + { name: "Vector3", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a Vector3 value" }, + { name: "Int", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a integer value" }, + { name: "TextureBlock", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Provide a texture" }, + { name: "Color4", category: "Inputs", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Input block set to a Color4 value" }, + { name: "MeshSourceBlock", category: "Inputs", modes: [NodeParticleModes.SolidParticle], tooltip: "Mesh source for SPS - load custom geometry from the inspector" }, + + // Updates (Particle only - use _updateQueueStart) + { name: "UpdateDirectionBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the direction of a particle" }, + { name: "UpdatePositionBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the position of a particle" }, + { name: "UpdateColorBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the color of a particle" }, + { name: "UpdateScaleBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the scale of a particle" }, + { name: "UpdateSizeBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the size of a particle" }, + { name: "UpdateAngleBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the angle of a particle" }, + { name: "UpdateAgeBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Update the age of a particle" }, + { + name: "BasicColorUpdateBlock", + category: "Updates", + modes: [NodeParticleModes.Particle], + tooltip: "Block used to update the color of a particle with a basic update (eg. color * delta)", + }, + { + name: "BasicPositionUpdateBlock", + category: "Updates", + modes: [NodeParticleModes.Particle], + tooltip: "Block used to update the position of a particle with a basic update (eg. direction * delta)", + }, + { name: "UpdateFlowMapBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to update particle position based on a flow map" }, + { name: "UpdateNoiseBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to update particle position based on a noise texture" }, + { name: "UpdateAttractorBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to update particle position based on an attractor" }, + { name: "AlignAngleBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to align the angle of a particle to its direction" }, + // Updates (SolidParticle only) + { name: "UpdateSolidParticlePositionBlock", category: "Updates", modes: [NodeParticleModes.SolidParticle], tooltip: "Update the position of a solid particle" }, + { name: "UpdateSolidParticleVelocityBlock", category: "Updates", modes: [NodeParticleModes.SolidParticle], tooltip: "Update the velocity of a solid particle" }, + { name: "UpdateSolidParticleColorBlock", category: "Updates", modes: [NodeParticleModes.SolidParticle], tooltip: "Update the color of a solid particle" }, + { name: "UpdateSolidParticleScalingBlock", category: "Updates", modes: [NodeParticleModes.SolidParticle], tooltip: "Update the scaling of a solid particle" }, + { name: "UpdateSolidParticleRotationBlock", category: "Updates", modes: [NodeParticleModes.SolidParticle], tooltip: "Update the rotation of a solid particle" }, + { + name: "BasicSpriteUpdateBlock", + category: "Updates", + modes: [NodeParticleModes.Particle], + tooltip: "Block used to update the sprite index of a particle with a basic update (eg. incrementing the index by 1)", + }, + { name: "UpdateSpriteCellIndexBlock", category: "Updates", modes: [NodeParticleModes.Particle], tooltip: "Block used to update the sprite cell index of a particle" }, + + // Triggers (Particle only) + { name: "TriggerBlock", category: "Triggers", modes: [NodeParticleModes.Particle], tooltip: "Block used to trigger a particle system based on a condition" }, + + // Setup + { + name: "CreateParticleBlock", + category: "Setup", + modes: [NodeParticleModes.Particle], + tooltip: "Block used to create a particle with properties such as emit power, lifetime, color, scale, and angle", + }, + { name: "SetupSpriteSheetBlock", category: "Setup", modes: [NodeParticleModes.Particle], tooltip: "Block used to setup a sprite sheet for particles" }, + { + name: "CreateSolidParticleBlock", + category: "Setup", + modes: [NodeParticleModes.SolidParticle], + tooltip: "Create a solid particle configuration: mesh, count, material, init properties", + }, + { name: "MergeSolidParticlesBlock", category: "Setup", modes: [NodeParticleModes.SolidParticle], tooltip: "Merge multiple solid particle configurations into an array" }, + + // Math - Standard + { name: "AddBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Add" }, + { name: "DivideBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Divide" }, + { name: "MaxBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Max" }, + { name: "MinBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Min" }, + { name: "MultiplyBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Multiply" }, + { name: "SubtractBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Subtract" }, + { name: "NegateBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Math block set to Negate" }, + { name: "OneMinusBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to One Minus" }, + { name: "ReciprocalBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Reciprocal" }, + { name: "SignBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Sign" }, + { name: "SqrtBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Square Root" }, + { name: "RoundBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Round" }, + { name: "FloorBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Floor" }, + { name: "CeilingBlock", category: "Math__Standard", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Ceiling" }, + { + name: "FloatToIntBlock", + category: "Math__Standard", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to convert a float value to an integer value using a specified operation (Round, Ceil, Floor, Truncate)", + }, + + // Math - Scientific + { name: "AbsBlock", category: "Math__Scientific", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Trigonometry block set to Abs" }, + { + name: "ArcCosBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Arc cos (using radians)", + }, + { + name: "ArcSinBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Arc sin (using radians)", + }, + { + name: "ArcTanBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Arc tan (using radians)", + }, + { + name: "CosBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Cos (using radians)", + }, + { + name: "ExpBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Exp (using radians)", + }, + { + name: "Exp2Block", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Exp2 (using radians)", + }, + { + name: "LogBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Log (using radians)", + }, + { + name: "SinBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Sin (using radians)", + }, + { + name: "TanBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Trigonometry block set to Tan (using radians)", + }, + { + name: "ToDegreesBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Conversion block used to convert radians to degree", + }, + { + name: "ToRadiansBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Conversion block used to convert degrees to radians", + }, + { + name: "FractBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Outputs only the fractional value of a floating point number", + }, + { + name: "VectorLengthBlock", + category: "Math__Scientific", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to get the length of a vector", + }, + + // Logical + { name: "EqualBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to Equal" }, + { name: "NotEqualBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to NotEqual" }, + { name: "LessThanBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to LessThan" }, + { name: "LessOrEqualBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to LessOrEqual" }, + { name: "GreaterThanBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to GreaterThan" }, + { name: "GreaterOrEqualBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to GreaterOrEqual" }, + { name: "XorBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to Xor" }, + { name: "OrBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to Or" }, + { name: "AndBlock", category: "Logical", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Conditional block set to And" }, + { + name: "ConditionBlock", + category: "Logical", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to evaluate a condition and return a value", + }, + + // Interpolation + { name: "LerpBlock", category: "Interpolation", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Interpolate between two values" }, + { + name: "GradientValueBlock", + category: "Interpolation", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "A gradient value block used to define a value at a specific age", + }, + { + name: "GradientBlock", + category: "Interpolation", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "A gradient block used to define a gradient of values over the lifetime of a particle", + }, + + // Misc + { + name: "ConverterBlock", + category: "Misc", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Convert between different types of values, such as Color4, Vector2, Vector3, and Float", + }, + { name: "RandomBlock", category: "Misc", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Generate a random value" }, + { + name: "DebugBlock", + category: "Misc", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Debug block used to output values of connection ports", + }, + { name: "ElbowBlock", category: "Misc", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Passthrough block mostly used to organize your graph" }, + { + name: "TeleportInBlock", + category: "Misc", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Passthrough block mostly used to organize your graph (but without visible lines). It works like a teleportation point for the graph.", + }, + { name: "TeleportOutBlock", category: "Misc", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Endpoint for a TeleportInBlock." }, + { + name: "LocalVariableBlock", + category: "Misc", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to store local values (eg. within a loop)", + }, + { name: "FresnelBlock", category: "Misc", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Block used to compute the Fresnel term" }, + + // System Nodes + { name: "SystemBlock", category: "System_Nodes", modes: [NodeParticleModes.Particle], tooltip: "Generate a particle system" }, + { + name: "SolidParticleSystemBlock", + category: "System_Nodes", + modes: [NodeParticleModes.SolidParticle], + tooltip: "Configure Solid Particle System - connect SolidParticleSystemBlock output here", + }, + { name: "TimeBlock", category: "System_Nodes", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Block used to get the current time in ms" }, + { + name: "DeltaBlock", + category: "System_Nodes", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Block used to get the delta value for animations", + }, + { + name: "EmitterPositionBlock", + category: "System_Nodes", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the coordinates of the emitter", + }, + { + name: "CameraPositionBlock", + category: "System_Nodes", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the position of the active camera", + }, + + // Contextual + { + name: "PositionBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the position of a particle", + }, + { + name: "DirectionBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle], + tooltip: "Contextual block to get the direction of a particle", + }, + { + name: "DirectionScaleBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle], + tooltip: "Contextual block to get the direction scale of a particle", + }, + { + name: "ScaledDirectionBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle], + tooltip: "Contextual block to get the scaled direction of a particle", + }, + { + name: "ColorBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the color of a particle", + }, + { name: "AgeBlock", category: "Contextual", modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], tooltip: "Contextual block to get the age of a particle" }, + { + name: "LifetimeBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the lifetime of a particle", + }, + { + name: "ScaleBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the scale of a particle", + }, + { name: "SizeBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the size of a particle" }, + { + name: "AgeGradientBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle, NodeParticleModes.SolidParticle], + tooltip: "Contextual block to get the age gradient of a particle ie. the age divided by the lifetime", + }, + { + name: "AngleBlock", + category: "Contextual", + modes: [NodeParticleModes.Particle], + tooltip: "Contextual block to get the angle of a particle", + }, + { name: "SolidParticleIndexBlock", category: "Contextual", modes: [NodeParticleModes.SolidParticle], tooltip: "Contextual block to get the index of a solid particle" }, + { name: "SolidParticleVelocityBlock", category: "Contextual", modes: [NodeParticleModes.SolidParticle], tooltip: "Contextual block to get the velocity of a solid particle" }, + { name: "SolidParticleScalingBlock", category: "Contextual", modes: [NodeParticleModes.SolidParticle], tooltip: "Contextual block to get the scaling of a solid particle" }, + { name: "SolidParticleRotationBlock", category: "Contextual", modes: [NodeParticleModes.SolidParticle], tooltip: "Contextual block to get the rotation of a solid particle" }, + { name: "InitialColorBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the initial color of a particle" }, + { name: "ColorDeadBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the expected dead color of a particle" }, + { name: "SpriteCellEndBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the end cell of a sprite sheet" }, + { name: "SpriteCellStartBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the start cell of a sprite sheet" }, + { name: "SpriteCellIndexBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the sprite cell index of a particle" }, + { name: "InitialDirectionBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the initial direction of a particle" }, + { name: "ColorStepBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the expected color step of a particle" }, + { name: "ScaledColorStepBlock", category: "Contextual", modes: [NodeParticleModes.Particle], tooltip: "Contextual block to get the expected scaled color step of a particle" }, +]; + +/** + * Groups blocks by category for a given mode + * @param mode The particle mode to filter blocks for + * @returns A record mapping category names to arrays of block definitions + */ +export function GetBlocksByMode(mode: NodeParticleModes): Record { + const result: Record = {}; + for (const block of BlockDefinitions) { + if (block.modes.includes(mode)) { + if (!result[block.category]) { + result[block.category] = []; + } + result[block.category].push(block); + } + } + // Sort blocks within each category by name + for (const category in result) { + result[category].sort((a, b) => a.name.localeCompare(b.name)); + } + return result; +} + +/** + * Gets all block names for registration (all modes combined) + * @returns An array of all block names + */ +export function GetAllBlockNames(): string[] { + return BlockDefinitions.map((block) => block.name); +} diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index c730470774b..0533a6c33bf 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -11,6 +11,7 @@ import { Tools } from "core/Misc/tools"; import addButton from "../../imgs/add.svg"; import deleteButton from "../../imgs/delete.svg"; import { NodeLedger } from "shared-ui-components/nodeGraphSystem/nodeLedger"; +import { GetBlocksByMode, GetAllBlockNames, type IBlockDefinition } from "./blockDefinitions"; import "./nodeList.scss"; @@ -20,116 +21,6 @@ interface INodeListComponentProps { export class NodeListComponent extends React.Component { private _onResetRequiredObserver: Nullable>; - - private static _Tooltips: { [key: string]: string } = { - SystemBlock: "Generate a particle system", - Float: "Input block set to a float value", - Vector2: "Input block set to a Vector2 value", - Vector3: "Input block set to a Vector3 value", - Color4: "Input block set to a Color4 value", - Int: "Input block set to a integer value", - TextureBlock: "Provide a texture", - BoxShapeBlock: "Emit particles from a box shape", - ConeShapeBlock: "Emit particles from a cone shape", - CustomShapeBlock: "Emit particles from a custom position", - CylinderShapeBlock: "Emit particles from a cylinder shape", - MeshShapeBlock: "Emit particles from a mesh shape", - PointShapeBlock: "Emit particles from a point", - SphereShapeBlock: "Emit particles from a sphere shape", - UpdateDirectionBlock: "Update the direction of a particle", - UpdatePositionBlock: "Update the position of a particle", - UpdateColorBlock: "Update the color of a particle", - UpdateScaleBlock: "Update the scale of a particle", - UpdateSizeBlock: "Update the size of a particle", - UpdateAngleBlock: "Update the angle of a particle", - UpdateAgeBlock: "Update the age of a particle", - AddBlock: "Math block set to Add", - DivideBlock: "Math block set to Divide", - MaxBlock: "Math block set to Max", - MinBlock: "Math block set to Min", - MultiplyBlock: "Math block set to Multiply", - SubtractBlock: "Math block set to Subtract", - PositionBlock: "Contextual block to get the position of a particle", - DirectionBlock: "Contextual block to get the direction of a particle", - DirectionScaleBlock: "Contextual block to get the direction scale of a particle", - ScaledDirectionBlock: "Contextual block to get the scaled direction of a particle", - ColorBlock: "Contextual block to get the color of a particle", - InitialColorBlock: "Contextual block to get the initial color of a particle", - InitialDirectionBlock: "Contextual block to get the initial direction of a particle", - ColorDeadBlock: "Contextual block to get the expected dead color of a particle", - AgeBlock: "Contextual block to get the age of a particle", - AngleBlock: "Contextual block to get the angle of a particle", - LifetimeBlock: "Contextual block to get the lifetime of a particle", - ScaleBlock: "Contextual block to get the scale of a particle", - SizeBlock: "Contextual block to get the size of a particle", - AgeGradientBlock: "Contextual block to get the age gradient of a particle ie. the age divided by the lifetime", - LerpBlock: "Interpolate between two values", - GradientValueBlock: "A gradient value block used to define a value at a specific age", - GradientBlock: "A gradient block used to define a gradient of values over the lifetime of a particle", - ConverterBlock: "Convert between different types of values, such as Color4, Vector2, Vector3, and Float", - FractBlock: "Outputs only the fractional value of a floating point number", - AbsBlock: "Trigonometry block set to Abs", - ArcCosBlock: "Trigonometry block set to Arc cos (using radians)", - ArcSinBlock: "Trigonometry block set to Arc sin (using radians)", - ArcTanBlock: "Trigonometry block set to Arc tan (using radians)", - ArcTan2Block: "Trigonometry block set to Arc tan2 (using radians)", - CosBlock: "Trigonometry block set to Cos (using radians)", - ExpBlock: "Trigonometry block set to Exp (using radians)", - Exp2Block: "Trigonometry block set to Exp2 (using radians)", - LogBlock: "Trigonometry block set to Log (using radians)", - SinBlock: "Trigonometry block set to Sin (using radians)", - TanBlock: "Trigonometry block set to Tan (using radians)", - ToDegreesBlock: "Conversion block used to convert radians to degree", - ToRadiansBlock: "Conversion block used to convert degrees to radians", - NegateBlock: "Math block set to Negate", - OneMinusBlock: "Trigonometry block set to One Minus", - ReciprocalBlock: "Trigonometry block set to Reciprocal", - SignBlock: "Trigonometry block set to Sign", - SqrtBlock: "Trigonometry block set to Square Root", - RoundBlock: "Trigonometry block set to Round", - FloorBlock: "Trigonometry block set to Floor", - CeilingBlock: "Trigonometry block set to Ceiling", - FloatToIntBlock: "Block used to convert a float value to an integer value using a specified operation (Round, Ceil, Floor, Truncate)", - RandomBlock: "Generate a random value", - DebugBlock: "Debug block used to output values of connection ports", - ElbowBlock: "Passthrough block mostly used to organize your graph", - TeleportInBlock: "Passthrough block mostly used to organize your graph (but without visible lines). It works like a teleportation point for the graph.", - TeleportOutBlock: "Endpoint for a TeleportInBlock.", - TimeBlock: "Block used to get the current time in ms", - DeltaBlock: "Block used to get the delta value for animations", - BasicPositionUpdateBlock: "Block used to update the position of a particle with a basic update (eg. direction * delta)", - BasicColorUpdateBlock: "Block used to update the color of a particle with a basic update (eg. color * delta)", - TriggerBlock: "Block used to trigger a particle system based on a condition", - SetupSpriteSheetBlock: "Block used to setup a sprite sheet for particles", - BasicUpdateSpriteBlock: "Block used to update the sprite index of a particle with a basic update (eg. incrementing the index by 1)", - UpdateSpriteCellIndexBlock: "Block used to update the sprite cell index of a particle", - SpriteCellEndBlock: "Contextual block to get the end cell of a sprite sheet", - SpriteCellStartBlock: "Contextual block to get the start cell of a sprite sheet", - SpriteCellIndexBlock: "Contextual block to get the sprite cell index of a particle", - EmitterPositionBlock: "Contextual block to get the coordinates of the emitter", - CameraPositionBlock: "Contextual block to get the position of the active camera", - UpdateFlowMapBlock: "Block used to update particle position based on a flow map", - UpdateNoiseBlock: "Block used to update particle position based on a noise texture", - UpdateAttractorBlock: "Block used to update particle position based on an attractor", - ConditionBlock: "Block used to evaluate a condition and return a value", - EqualBlock: "Conditional block set to Equal", - NotEqualBlock: "Conditional block set to NotEqual", - LessThanBlock: "Conditional block set to LessThan", - LessOrEqualBlock: "Conditional block set to LessOrEqual", - GreaterThanBlock: "Conditional block set to GreaterThan", - GreaterOrEqualBlock: "Conditional block set to GreaterOrEqual", - XorBlock: "Conditional block set to Xor", - OrBlock: "Conditional block set to Or", - AndBlock: "Conditional block set to And", - CreateParticleBlock: "Block used to create a particle with properties such as emit power, lifetime, color, scale, and angle", - AlignAngleBlock: "Block used to align the angle of a particle to its direction", - VectorLengthBlock: "Block used to get the length of a vector", - LocalVariableBlock: "Block used to store local values (eg. within a loop)", - FresnelBlock: "Block used to compute the Fresnel term", - ColorStepBlock: "Contextual block to get the expected color step of a particle", - ScaledColorStepBlock: "Contextual block to get the expected scaled color step of a particle", - }; - private _customFrameList: { [key: string]: string }; constructor(props: INodeListComponentProps) { @@ -209,111 +100,49 @@ export class NodeListComponent extends React.Component = { Custom_Frames: customFrameNames, - Shapes: ["BoxShapeBlock", "ConeShapeBlock", "SphereShapeBlock", "PointShapeBlock", "CustomShapeBlock", "CylinderShapeBlock", "MeshShapeBlock"], - Inputs: ["Float", "Vector2", "Vector3", "Int", "TextureBlock", "Color4"], - Updates: [ - "UpdateDirectionBlock", - "UpdatePositionBlock", - "UpdateColorBlock", - "UpdateScaleBlock", - "UpdateSizeBlock", - "UpdateAngleBlock", - "UpdateAgeBlock", - "BasicColorUpdateBlock", - "BasicPositionUpdateBlock", - "BasicSpriteUpdateBlock", - "UpdateSpriteCellIndexBlock", - "UpdateFlowMapBlock", - "UpdateNoiseBlock", - "UpdateAttractorBlock", - "AlignAngleBlock", - ], - Triggers: ["TriggerBlock"], - Setup: ["CreateParticleBlock", "SetupSpriteSheetBlock"], - Math__Standard: [ - "AddBlock", - "DivideBlock", - "MaxBlock", - "MinBlock", - "MultiplyBlock", - "SubtractBlock", - "NegateBlock", - "OneMinusBlock", - "ReciprocalBlock", - "SignBlock", - "SqrtBlock", - "RoundBlock", - "FloorBlock", - "CeilingBlock", - "FloatToIntBlock", - ], - Math__Scientific: [ - "AbsBlock", - "ArcCosBlock", - "ArcSinBlock", - "ArcTanBlock", - "CosBlock", - "ExpBlock", - "Exp2Block", - "LogBlock", - "SinBlock", - "TanBlock", - "ToDegreesBlock", - "ToRadiansBlock", - "FractBlock", - "VectorLengthBlock", - ], - Logical: ["EqualBlock", "NotEqualBlock", "LessThanBlock", "LessOrEqualBlock", "GreaterThanBlock", "GreaterOrEqualBlock", "XorBlock", "OrBlock", "AndBlock"], - Interpolation: ["LerpBlock", "GradientValueBlock", "GradientBlock"], - Misc: ["ConverterBlock", "RandomBlock", "DebugBlock", "ElbowBlock", "TeleportInBlock", "TeleportOutBlock", "LocalVariableBlock", "FresnelBlock"], - System_Nodes: ["SystemBlock", "TimeBlock", "DeltaBlock", "EmitterPositionBlock", "CameraPositionBlock"], - Contextual: [ - "PositionBlock", - "DirectionBlock", - "DirectionScaleBlock", - "ScaledDirectionBlock", - "ColorBlock", - "AgeBlock", - "LifetimeBlock", - "ScaleBlock", - "SizeBlock", - "AgeGradientBlock", - "AngleBlock", - "InitialColorBlock", - "ColorDeadBlock", - "SpriteCellEndBlock", - "SpriteCellStartBlock", - "SpriteCellIndexBlock", - "InitialDirectionBlock", - "ColorStepBlock", - "ScaledColorStepBlock", - ], + ...blocksByCategory, }; // Create node menu const blockMenu = []; for (const key in allBlocks) { - const blockList = allBlocks[key] - .filter((b: string) => !this.state.filter || b.toLowerCase().indexOf(this.state.filter.toLowerCase()) !== -1) - .sort((a: string, b: string) => a.localeCompare(b)) - .map((block: any) => { + const blocks = allBlocks[key]; + // Skip empty categories + if (!blocks || blocks.length === 0) { + continue; + } + + const blockList = blocks + .filter((block: string | IBlockDefinition) => { + const blockName = typeof block === "string" ? block : block.name; + return !this.state.filter || blockName.toLowerCase().indexOf(this.state.filter.toLowerCase()) !== -1; + }) + .sort((a: string | IBlockDefinition, b: string | IBlockDefinition) => { + const nameA = typeof a === "string" ? a : a.name; + const nameB = typeof b === "string" ? b : b.name; + return nameA.localeCompare(nameB); + }) + .map((block: string | IBlockDefinition) => { if (key === "Custom_Frames") { + const blockName = block as string; return ( this.removeItem(value)} /> ); } - return ; + const blockDef = block as IBlockDefinition; + return ; }); if (key === "Custom_Frames") { @@ -340,32 +169,28 @@ export class NodeListComponent extends React.Component ); } + } - // Register blocks - const ledger = NodeLedger.RegisteredNodeNames; - for (const key in allBlocks) { - const blocks = allBlocks[key] as string[]; - if (blocks.length) { - for (const block of blocks) { - if (!ledger.includes(block)) { - ledger.push(block); - } - } - } + // Register all blocks (register all blocks regardless of mode, as they may exist in saved files) + const ledger = NodeLedger.RegisteredNodeNames; + const allBlockNames = GetAllBlockNames(); + for (const blockName of allBlockNames) { + if (!ledger.includes(blockName)) { + ledger.push(blockName); } - NodeLedger.NameFormatter = (name) => { - let finalName = name; - // custom frame - if (name.endsWith("Custom")) { - const nameIndex = name.lastIndexOf("Custom"); - finalName = name.substring(0, nameIndex); - finalName += " [custom]"; - } else { - finalName = name.replace("Block", ""); - } - return finalName; - }; } + NodeLedger.NameFormatter = (name) => { + let finalName = name; + // custom frame + if (name.endsWith("Custom")) { + const nameIndex = name.lastIndexOf("Custom"); + finalName = name.substring(0, nameIndex); + finalName += " [custom]"; + } else { + finalName = name.replace("Block", ""); + } + return finalName; + }; return (
diff --git a/packages/tools/nodeParticleEditor/src/components/preview/previewManager.ts b/packages/tools/nodeParticleEditor/src/components/preview/previewManager.ts index d7dc6ada2bf..c21f5793ffe 100644 --- a/packages/tools/nodeParticleEditor/src/components/preview/previewManager.ts +++ b/packages/tools/nodeParticleEditor/src/components/preview/previewManager.ts @@ -17,6 +17,8 @@ import type { ThinParticleSystem } from "core/Particles/thinParticleSystem"; import type { ParticleSystemSet } from "core/Particles/particleSystemSet"; import { EngineStore } from "core/Engines"; import type { ParticleSystem } from "core/Particles"; +import { SolidParticleSystem } from "core/Particles/solidParticleSystem"; +import { DirectionalLight } from "core/Lights"; export class PreviewManager { private _nodeParticleSystemSet: NodeParticleSystemSet; @@ -51,6 +53,11 @@ export class PreviewManager { this._scene.ambientColor = new Color3(1, 1, 1); this._camera = new ArcRotateCamera("Camera", 0, 0.8, 4, Vector3.Zero(), this._scene); + const sunLight = new DirectionalLight("sun", new Vector3(-1, -1, -1), this._scene); + sunLight.intensity = 1.0; + sunLight.diffuse = new Color3(1, 1, 1); + sunLight.specular = new Color3(1, 1, 1); + this._camera.doNotSerialize = true; this._camera.lowerRadiusLimit = 3; this._camera.upperRadiusLimit = 10; @@ -73,6 +80,13 @@ export class PreviewManager { (this._scene.particleSystems as ThinParticleSystem[]).forEach((ps) => { totalParticleCount += ps.particles.length; }); + if (this._particleSystemSet) { + this._particleSystemSet.systems.forEach((system) => { + if (system instanceof SolidParticleSystem) { + totalParticleCount += system.nbParticles; + } + }); + } if (globalState.updateState) { globalState.updateState( "Update loop: " + sceneInstrumentation.particlesRenderTimeCounter.lastSecAverage.toFixed(2) + " ms", diff --git a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx index 7a143f2da20..133b7213602 100644 --- a/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/propertyTab/propertyTabComponent.tsx @@ -28,6 +28,8 @@ import type { LockObject } from "shared-ui-components/tabs/propertyGrids/lockObj import { TextLineComponent } from "shared-ui-components/lines/textLineComponent"; import { SliderLineComponent } from "shared-ui-components/lines/sliderLineComponent"; import { NodeParticleSystemSet } from "core/Particles"; +import { NodeParticleModes } from "../../nodeParticleModes"; +import { OptionsLine } from "shared-ui-components/lines/optionsLineComponent"; interface IPropertyTabComponentProps { globalState: GlobalState; @@ -202,6 +204,13 @@ export class PropertyTabComponent extends React.Component { + if (nodeParticleSet.editorData?.mode !== undefined && nodeParticleSet.editorData?.mode !== null) { + this.props.globalState.mode = nodeParticleSet.editorData.mode; + this.props.globalState.onResetRequiredObservable.notifyObservers(true); + } else { + this.props.globalState.mode = NodeParticleModes.Particle; + } + await nodeParticleSet.buildAsync(this.props.globalState.hostScene); this.props.globalState.onClearUndoStack.notifyObservers(); }) @@ -210,6 +219,33 @@ export class PropertyTabComponent extends React.Component + this.changeMode(value as NodeParticleModes)} + /> void; customSave?: { label: string; action: (data: string) => Promise }; + mode: NodeParticleModes = NodeParticleModes.Particle; public constructor() { this.stateManager = new StateManager(); diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts b/packages/tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts index 695951935f0..95083fedd69 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/blockNodeData.ts @@ -77,7 +77,7 @@ export class BlockNodeData implements INodeData { public isConnectedToOutput() { const block = this.data; - return block.isDebug || block.isAnAncestorOfType("SystemBlock"); + return block.isDebug || block.isAnAncestorOfType("SystemBlock") || block.isAnAncestorOfType("SolidParticleSystemBlock"); } public dispose() { diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/properties/inputNodePropertyComponent.tsx b/packages/tools/nodeParticleEditor/src/graphSystem/properties/inputNodePropertyComponent.tsx index 1277017b972..17fb6e30010 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/properties/inputNodePropertyComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/graphSystem/properties/inputNodePropertyComponent.tsx @@ -134,9 +134,12 @@ export class InputPropertyTabComponent 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 MeshSourceBlock; + this._onValueChangedObserver = block.onValueChangedObservable.add(() => { + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + }); + } + + private _getNodeScene(): Nullable { + return (this.props.nodeData as any).__spsMeshScene || null; + } + + private _getLoadedMeshes(): Mesh[] { + return (this.props.nodeData as any).__spsLoadedMeshes || []; + } + + private _setNodeScene(scene: Nullable) { + const nodeData = this.props.nodeData as any; + if (nodeData.__spsMeshScene) { + nodeData.__spsMeshScene.dispose(); + } + nodeData.__spsMeshScene = scene || null; + + // Store meshes from loaded scene + if (scene) { + nodeData.__spsLoadedMeshes = scene.meshes.filter((m) => !!m.name && m.getTotalVertices() > 0) as Mesh[]; + } else { + nodeData.__spsLoadedMeshes = []; + } + } + + async loadMeshAsync(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 MeshSourceBlock; + block.setCustomMesh(meshes[0]); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + } + + this.forceUpdate(); + } + + removeData() { + const block = this.props.nodeData.data as MeshSourceBlock; + block.clearCustomMesh(); + const nodeData = this.props.nodeData as any; + this._setNodeScene(null); + nodeData.__spsLoadedMeshes = []; + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + } + + applyMesh(mesh: Nullable) { + const block = this.props.nodeData.data as MeshSourceBlock; + block.setCustomMesh(mesh ?? null); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + } + + override componentWillUnmount(): void { + const scene = this._getNodeScene(); + if (scene) { + scene.dispose(); + } + const nodeData = this.props.nodeData as any; + nodeData.__spsMeshScene = null; + nodeData.__spsLoadedMeshes = null; + const block = this.props.nodeData.data as MeshSourceBlock; + if (this._onValueChangedObserver) { + block.onValueChangedObservable.remove(this._onValueChangedObserver); + this._onValueChangedObserver = null; + } + } + + override render() { + const block = this.props.nodeData.data as MeshSourceBlock; + const loadedMeshes = this._getLoadedMeshes(); + + const meshOptions = [{ label: "None", value: -1 }]; + meshOptions.push( + ...loadedMeshes.map((mesh, index) => ({ + label: mesh.name, + value: index, + })) + ); + + const selectedMeshIndex = block.hasCustomMesh ? loadedMeshes.findIndex((m) => m.name === block.customMeshName) : -1; + + return ( +
+ + + {block.hasCustomMesh ? : } + {this.state.isLoading && } + {!this.state.isLoading && await this.loadMeshAsync(file)} />} + {loadedMeshes.length > 0 && meshOptions.length > 1 && ( + selectedMeshIndex} + onSelect={(value) => { + const index = value as number; + if (index === -1) { + this.applyMesh(null); + return; + } + this.applyMesh(loadedMeshes[index]); + this.forceUpdate(); + }} + /> + )} + {block.hasCustomMesh && this.removeData()} />} + +
+ ); + } +} diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerNodePortDesign.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerNodePortDesign.ts index 7119ef3b215..9a31d7bac16 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerNodePortDesign.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerNodePortDesign.ts @@ -49,9 +49,18 @@ export const RegisterNodePortDesign = (stateManager: StateManager) => { "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMSIgaGVpZ2h0PSIyMSIgdmlld0JveD0iMCAwIDIxIDIxIj48Y2lyY2xlIGN4PSI3LjEiIGN5PSIxMy4wOCIgcj0iMy4yNSIgc3R5bGU9ImZpbGw6I2ZmZiIvPjxwYXRoIGQ9Ik0xMC40OSwzQTcuNTIsNy41MiwwLDAsMCwzLDEwYTUuMTMsNS4xMywwLDEsMSw2LDcuODUsNy42MSw3LjYxLDAsMCwwLDEuNTIuMTYsNy41Miw3LjUyLDAsMCwwLDAtMTVaIiBzdHlsZT0iZmlsbDojZmZmIi8+PC9zdmc+"; break; case NodeParticleBlockConnectionPointTypes.Particle: + case NodeParticleBlockConnectionPointTypes.SolidParticle: svg = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJMYXllcl81IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6Izg0OTk1Yzt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJtMTAsMjBDNC40OSwyMCwwLDE1LjUxLDAsMTBTNC40OSwwLDEwLDBzMTAsNC40OSwxMCwxMC00LjQ5LDEwLTEwLDEwWiIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI5LjE1IDEwLjQ5IDMuMzkgNy4xNyAzLjM5IDEzLjgxIDkuMTUgMTcuMTQgOS4xNSAxMC40OSIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSIxMCA5LjAyIDE1Ljc2IDUuNjkgMTAgMi4zNyA0LjI0IDUuNjkgMTAgOS4wMiIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSIxMC44NSAxMC40OSAxMC44NSAxNy4xNCAxNi42MSAxMy44MSAxNi42MSA3LjE3IDEwLjg1IDEwLjQ5Ii8+PC9zdmc+"; break; + case NodeParticleBlockConnectionPointTypes.Mesh: + svg = + "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJMYXllcl81IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6Izg0OTk1Yzt9PC9zdHlsZT48L2RlZnM+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJtMTAsMjBDNC40OSwyMCwwLDE1LjUxLDAsMTBTNC40OSwwLDEwLDBzMTAsNC40OSwxMCwxMC00LjQ5LDEwLTEwLDEwWiIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSI5LjE1IDEwLjQ5IDMuMzkgNy4xNyAzLjM5IDEzLjgxIDkuMTUgMTcuMTQgOS4xNSAxMC40OSIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSIxMCA5LjAyIDE1Ljc2IDUuNjkgMTAgMi4zNyA0LjI0IDUuNjkgMTAgOS4wMiIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMSIgcG9pbnRzPSIxMC44NSAxMC40OSAxMC44NSAxNy4xNCAxNi42MSAxMy44MSAxNi42MSA3LjE3IDEwLjg1IDEwLjQ5Ii8+PC9zdmc+"; + // Replace icon color with connection color + const connectionColor = BlockTools.GetColorFromConnectionNodeType(type); + const decoded = atob(svg); + svg = btoa(decoded.replace(/#84995c/gi, connectionColor)); + break; case NodeParticleBlockConnectionPointTypes.Texture: svg = "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIGlkPSJMYXllcl81IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDIwIDIwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjtzdHJva2Utd2lkdGg6MHB4O308L3N0eWxlPjwvZGVmcz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Im0xMCwyLjEyYy00LjM0LDAtNy44OCwzLjUzLTcuODgsNy44OHMzLjUzLDcuODgsNy44OCw3Ljg4LDcuODgtMy41Myw3Ljg4LTcuODgtMy41My03Ljg4LTcuODgtNy44OFptMCwxLjc5YzEuMTIsMCwyLjE3LjMxLDMuMDYuODQtLjY4LS4wNC0xLjM3LjEyLTEuOTcuNDctLjQ0LjI1LS44LjU5LTEuMDkuOTktLjI4LS4zOS0uNjUtLjczLTEuMDktLjk5LS42MS0uMzUtMS4yOS0uNTEtMS45Ny0uNDcuOS0uNTMsMS45NC0uODQsMy4wNi0uODRabS02LjA1LDYuMDVzMC0uMDQsMC0uMDVjLjMxLjYxLjc5LDEuMTMsMS4zOSwxLjQ4LjQ0LjI2LjkyLjQsMS40LjQ1LS4yLjQ0LS4zMS45Mi0uMzEsMS40NCwwLC43Mi4yMiwxLjM5LjU5LDEuOTUtMS44My0xLjA0LTMuMDgtMy0zLjA4LTUuMjZabTIuMjEuMDJjLS40NS0uMjYtLjc3LS42OC0uOS0xLjE4LS4xMy0uNS0uMDctMS4wMi4xOS0xLjQ3aDBjLjI2LS40NS42OC0uNzcsMS4xOC0uOS4xNy0uMDUuMzQtLjA3LjUxLS4wNy4zNCwwLC42Ny4wOS45Ni4yNi45My41MywxLjI0LDEuNzIuNzEsMi42NS0uMjYuNDUtLjY4Ljc3LTEuMTguOS0uNS4xMy0xLjAyLjA3LTEuNDctLjE5Wm0zLjg0LDUuMjNjLTEuMDcsMC0xLjk0LS44Ny0xLjk0LTEuOTRzLjg3LTEuOTQsMS45NC0xLjk0LDEuOTQuODcsMS45NCwxLjk0LS44NywxLjk0LTEuOTQsMS45NFptMi4zNy01LjA0Yy0uNS0uMTMtLjkyLS40NS0xLjE4LS45LS4yNi0uNDUtLjMzLS45Ny0uMTktMS40Ny4xMy0uNS40NS0uOTIuOS0xLjE4LjMtLjE3LjYzLS4yNi45Ni0uMjYuMTcsMCwuMzQuMDIuNTEuMDcuNS4xMy45Mi40NSwxLjE4LjloMGMuMjYuNDUuMzMuOTcuMTksMS40Ny0uMTMuNS0uNDUuOTItLjksMS4xOC0uNDUuMjYtLjk3LjMzLTEuNDcuMTlabS42LDUuMDVjLjM3LS41Ni41OS0xLjIzLjU5LTEuOTUsMC0uNTEtLjExLTEtLjMxLTEuNDQuNDktLjA1Ljk2LS4yLDEuNC0uNDUuNjEtLjM1LDEuMDktLjg2LDEuMzktMS40OCwwLC4wMiwwLC4wNCwwLC4wNiwwLDIuMjYtMS4yNCw0LjIyLTMuMDgsNS4yNloiLz48L3N2Zz4="; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts index 5826bd381f6..909d0490b72 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToDisplayLedger.ts @@ -38,6 +38,17 @@ export const RegisterToDisplayManagers = () => { DisplayLedger.RegisteredControls["UpdateFlowMapBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["UpdateNoiseBlock"] = UpdateDisplayManager; DisplayLedger.RegisteredControls["SystemBlock"] = SystemDisplayManager; + + DisplayLedger.RegisteredControls["MeshSourceBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["CreateSolidParticleBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["MergeSolidParticlesBlock"] = EmitterDisplayManager; + DisplayLedger.RegisteredControls["SolidParticleSystemBlock"] = SystemDisplayManager; + DisplayLedger.RegisteredControls["UpdateSolidParticlePositionBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["UpdateSolidParticleVelocityBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["UpdateSolidParticleColorBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["UpdateSolidParticleScalingBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["UpdateSolidParticleRotationBlock"] = UpdateDisplayManager; + DisplayLedger.RegisteredControls["ParticleDebugBlock"] = DebugDisplayManager; DisplayLedger.RegisteredControls["ParticleElbowBlock"] = ElbowDisplayManager; DisplayLedger.RegisteredControls["ParticleTeleportInBlock"] = TeleportInDisplayManager; diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 0290e5e97d5..92bef5db4c3 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -5,6 +5,7 @@ import { TextureSourcePropertyTabComponent } from "./properties/textureSourceNod import { DebugPropertyTabComponent } from "./properties/debugNodePropertyComponent"; import { TeleportOutPropertyTabComponent } from "./properties/teleportOutNodePropertyComponent"; import { MeshShapePropertyTabComponent } from "./properties/meshShapeNodePropertyComponent"; +import { MeshSourcePropertyTabComponent } from "./properties/meshSourceNodePropertyComponent"; export const RegisterToPropertyTabManagers = () => { PropertyLedger.DefaultControl = GenericPropertyComponent; @@ -13,4 +14,14 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleDebugBlock"] = DebugPropertyTabComponent; PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; + + PropertyLedger.RegisteredControls["MeshSourceBlock"] = MeshSourcePropertyTabComponent; + PropertyLedger.RegisteredControls["CreateSolidParticleBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["MergeSolidParticlesBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["SolidParticleSystemBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["UpdateSolidParticlePositionBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["UpdateSolidParticleVelocityBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["UpdateSolidParticleColorBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["UpdateSolidParticleScalingBlock"] = GenericPropertyComponent; + PropertyLedger.RegisteredControls["UpdateSolidParticleRotationBlock"] = GenericPropertyComponent; }; diff --git a/packages/tools/nodeParticleEditor/src/nodeParticleModes.ts b/packages/tools/nodeParticleEditor/src/nodeParticleModes.ts new file mode 100644 index 00000000000..bd5f03e7668 --- /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 */ + Particle = 0, + /** SPS (Solid Particle System) */ + SolidParticle = 1, +} diff --git a/packages/tools/nodeParticleEditor/src/serializationTools.ts b/packages/tools/nodeParticleEditor/src/serializationTools.ts index 36860749b76..a237bc9e3b2 100644 --- a/packages/tools/nodeParticleEditor/src/serializationTools.ts +++ b/packages/tools/nodeParticleEditor/src/serializationTools.ts @@ -3,14 +3,15 @@ import type { GlobalState } from "./globalState"; import type { Nullable } from "core/types"; import type { GraphFrame } from "shared-ui-components/nodeGraphSystem/graphFrame"; import type { NodeParticleBlock } from "core/Particles/Node/nodeParticleBlock"; +import { NodeParticleModes } from "./nodeParticleModes"; export class SerializationTools { public static UpdateLocations(particleSet: NodeParticleSystemSet, globalState: GlobalState, frame?: Nullable) { - particleSet.editorData = { - locations: [], - }; + if (!particleSet.editorData) { + particleSet.editorData = {}; + } + particleSet.editorData.locations = []; - // Store node locations const blocks: NodeParticleBlock[] = frame ? frame.nodes.map((n) => n.content.data) : particleSet.attachedBlocks; for (const block of blocks) { @@ -34,15 +35,32 @@ export class SerializationTools { const serializationObject = particleSet.serialize(selectedBlocks); + if (!serializationObject.editorData) { + serializationObject.editorData = {}; + } + serializationObject.editorData.mode = globalState.mode; + return JSON.stringify(serializationObject, undefined, 2); } public static Deserialize(serializationObject: any, globalState: GlobalState) { + const savedMode = serializationObject.editorData?.mode; + if (savedMode !== undefined && savedMode !== null) { + globalState.mode = savedMode; + } else { + globalState.mode = NodeParticleModes.Particle; + } + globalState.nodeParticleSet.parseSerializedObject(serializationObject); globalState.onIsLoadingChanged.notifyObservers(false); } public static AddFrameToParticleSystemSet(serializationObject: any, globalState: GlobalState, currentSystemSet: NodeParticleSystemSet) { + const savedMode = serializationObject.editorData?.mode; + if (savedMode !== undefined && savedMode !== null) { + globalState.mode = savedMode; + } + this.UpdateLocations(currentSystemSet, globalState); globalState.nodeParticleSet.parseSerializedObject(serializationObject, true); globalState.onImportFrameObservable.notifyObservers(serializationObject);