From 83256c3c2b336b43c239c340eead29a34483228f Mon Sep 17 00:00:00 2001 From: David Catuhe Date: Mon, 14 Jul 2025 15:00:33 -0700 Subject: [PATCH] Add support for Mesh Shape --- .../Particles/Node/Blocks/Emitters/index.ts | 1 + .../Node/Blocks/Emitters/meshShapeBlock.ts | 266 ++++++++++++++++++ .../nodeParticleEditor/src/blockTools.ts | 3 + .../components/nodeList/nodeListComponent.tsx | 3 +- .../meshShapeNodePropertyComponent.tsx | 127 +++++++++ .../graphSystem/registerToPropertyLedger.ts | 2 + 6 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 packages/dev/core/src/Particles/Node/Blocks/Emitters/meshShapeBlock.ts create mode 100644 packages/tools/nodeParticleEditor/src/graphSystem/properties/meshShapeNodePropertyComponent.tsx diff --git a/packages/dev/core/src/Particles/Node/Blocks/Emitters/index.ts b/packages/dev/core/src/Particles/Node/Blocks/Emitters/index.ts index 2c9cda638a1..1365d4f9964 100644 --- a/packages/dev/core/src/Particles/Node/Blocks/Emitters/index.ts +++ b/packages/dev/core/src/Particles/Node/Blocks/Emitters/index.ts @@ -3,5 +3,6 @@ export * from "./sphereShapeBlock"; export * from "./pointShapeBlock"; export * from "./customShapeBlock"; export * from "./cylinderShapeBlock"; +export * from "./meshShapeBlock"; export * from "./setupSpriteSheetBlock"; export * from "./createParticleBlock"; diff --git a/packages/dev/core/src/Particles/Node/Blocks/Emitters/meshShapeBlock.ts b/packages/dev/core/src/Particles/Node/Blocks/Emitters/meshShapeBlock.ts new file mode 100644 index 00000000000..7b660c97b66 --- /dev/null +++ b/packages/dev/core/src/Particles/Node/Blocks/Emitters/meshShapeBlock.ts @@ -0,0 +1,266 @@ +import { RegisterClass } from "../../../../Misc/typeStore"; +import type { Mesh } from "../../../../Meshes/mesh"; +import { VertexData } from "../../../../Meshes/mesh.vertexData"; +import type { FloatArray, IndicesArray, Nullable } from "../../../../types"; +import { PropertyTypeForEdition, editableInPropertyPage } from "core/Decorators/nodeDecorator"; +import { NodeParticleBlock } from "../../nodeParticleBlock"; +import { NodeParticleBlockConnectionPointTypes } from "../../Enums/nodeParticleBlockConnectionPointTypes"; +import type { NodeParticleBuildState } from "../../nodeParticleBuildState"; +import type { NodeParticleConnectionPoint } from "../../nodeParticleBlockConnectionPoint"; +import type { Particle } from "core/Particles/particle"; +import { TmpVectors, Vector3, Vector4 } from "core/Maths/math.vector"; +import { RandomRange } from "core/Maths/math.scalar.functions"; + +/** + * Defines a block used to generate particle shape from mesh geometry data + */ +export class MeshShapeBlock extends NodeParticleBlock { + private _mesh: Nullable; + private _cachedVertexData: Nullable = null; + private _indices: Nullable = null; + private _positions: Nullable = null; + private _normals: Nullable = null; + private _colors: Nullable = null; + private _storedNormal = Vector3.Zero(); + + /** + * Gets or sets a boolean indicating that this block should serialize its cached data + */ + @editableInPropertyPage("Serialize cached data", PropertyTypeForEdition.Boolean, "ADVANCED", { embedded: true, notifiers: { rebuild: true } }) + public serializedCachedData = false; + + /** + * Gets or sets a boolean indicating if the mesh normals should be used for particle direction + */ + @editableInPropertyPage("Use normals for direction", PropertyTypeForEdition.Boolean, "ADVANCED", { embedded: true, notifiers: { rebuild: true } }) + public useMeshNormalsForDirection = false; + + /** + * Gets or sets a boolean indicating if the mesh colors should be used for particle color + */ + @editableInPropertyPage("Use vertex color for color", PropertyTypeForEdition.Boolean, "ADVANCED", { embedded: true, notifiers: { rebuild: true } }) + public useMeshColorForColor = false; + + /** + * Gets or sets the mesh to use to get vertex data + */ + public get mesh() { + return this._mesh; + } + + public set mesh(value: Nullable) { + this._mesh = value; + } + + /** + * Create a new MeshShapeBlock + * @param name defines the block name + */ + public constructor(name: string) { + super(name); + + this.registerInput("particle", NodeParticleBlockConnectionPointTypes.Particle); + this.registerInput("direction1", NodeParticleBlockConnectionPointTypes.Vector3, true, new Vector3(0, 1.0, 0)); + this.registerInput("direction2", NodeParticleBlockConnectionPointTypes.Vector3, true, new Vector3(0, 1.0, 0)); + this.registerOutput("output", NodeParticleBlockConnectionPointTypes.Particle); + } + + /** + * Gets the current class name + * @returns the class name + */ + public override getClassName() { + return "MeshShapeBlock"; + } + + /** + * Gets a boolean indicating if the block is using cached data + */ + public get isUsingCachedData() { + return !this.mesh && !!this._cachedVertexData; + } + + /** + * Gets the particle component + */ + public get particle(): NodeParticleConnectionPoint { + return this._inputs[0]; + } + + /** + * Gets the direction1 input component + */ + public get direction1(): NodeParticleConnectionPoint { + return this._inputs[1]; + } + + /** + * Gets the direction2 input component + */ + public get direction2(): NodeParticleConnectionPoint { + return this._inputs[2]; + } + + /** + * Gets the output component + */ + public get output(): NodeParticleConnectionPoint { + return this._outputs[0]; + } + + /** + * Remove stored data + */ + public cleanData() { + this._mesh = null; + this._cachedVertexData = null; + } + + /** + * Builds the block + * @param state defines the build state + */ + public override _build(state: NodeParticleBuildState) { + const system = this.particle.getConnectedValue(state); + + if (!this._mesh && !this._cachedVertexData) { + this.output._storedValue = system; + return; + } + + if (this._mesh) { + this._cachedVertexData = VertexData.ExtractFromMesh(this._mesh, false, true); + } + + if (!this._cachedVertexData) { + this.output._storedValue = system; + return; + } + + this._indices = this._cachedVertexData.indices; + this._positions = this._cachedVertexData.positions; + this._normals = this._cachedVertexData.normals; + this._colors = this._cachedVertexData.colors; + + system._directionCreation.process = (particle: Particle) => { + state.particleContext = particle; + state.systemContext = system; + + if (this.useMeshNormalsForDirection && this._normals) { + if (state.isEmitterTransformNode) { + Vector3.TransformNormalToRef(this._storedNormal, state.emitterWorldMatrix!, particle.direction); + } else { + particle.direction.copyFrom(this._storedNormal); + } + return; + } + + const direction1 = this.direction1.getConnectedValue(state) as Vector3; + const direction2 = this.direction2.getConnectedValue(state) as Vector3; + + const randX = RandomRange(direction1.x, direction2.x); + const randY = RandomRange(direction1.y, direction2.y); + const randZ = RandomRange(direction1.z, direction2.z); + + if (state.isEmitterTransformNode) { + Vector3.TransformNormalFromFloatsToRef(randX, randY, randZ, state.emitterWorldMatrix!, particle.direction); + } else { + particle.direction.copyFromFloats(randX, randY, randZ); + } + }; + + system._positionCreation.process = (particle: Particle) => { + if (!this._indices || !this._positions) { + return; + } + + const randomFaceIndex = 3 * ((Math.random() * (this._indices.length / 3)) | 0); + const bu = Math.random(); + const bv = Math.random() * (1.0 - bu); + const bw = 1.0 - bu - bv; + + const faceIndexA = this._indices[randomFaceIndex]; + const faceIndexB = this._indices[randomFaceIndex + 1]; + const faceIndexC = this._indices[randomFaceIndex + 2]; + const vertexA = TmpVectors.Vector3[0]; + const vertexB = TmpVectors.Vector3[1]; + const vertexC = TmpVectors.Vector3[2]; + const randomVertex = TmpVectors.Vector3[3]; + + Vector3.FromArrayToRef(this._positions, faceIndexA * 3, vertexA); + Vector3.FromArrayToRef(this._positions, faceIndexB * 3, vertexB); + Vector3.FromArrayToRef(this._positions, faceIndexC * 3, vertexC); + + randomVertex.x = bu * vertexA.x + bv * vertexB.x + bw * vertexC.x; + randomVertex.y = bu * vertexA.y + bv * vertexB.y + bw * vertexC.y; + randomVertex.z = bu * vertexA.z + bv * vertexB.z + bw * vertexC.z; + + if (state.isEmitterTransformNode) { + Vector3.TransformCoordinatesFromFloatsToRef(randomVertex.x, randomVertex.y, randomVertex.z, state.emitterWorldMatrix!, particle.position); + } else { + particle.position.copyFromFloats(randomVertex.x, randomVertex.y, randomVertex.z); + } + + if (this.useMeshNormalsForDirection && this._normals) { + Vector3.FromArrayToRef(this._normals, faceIndexA * 3, vertexA); + Vector3.FromArrayToRef(this._normals, faceIndexB * 3, vertexB); + Vector3.FromArrayToRef(this._normals, faceIndexC * 3, vertexC); + + this._storedNormal.x = bu * vertexA.x + bv * vertexB.x + bw * vertexC.x; + this._storedNormal.y = bu * vertexA.y + bv * vertexB.y + bw * vertexC.y; + this._storedNormal.z = bu * vertexA.z + bv * vertexB.z + bw * vertexC.z; + } + + if (this.useMeshColorForColor && this._colors) { + Vector4.FromArrayToRef(this._colors, faceIndexA * 4, TmpVectors.Vector4[0]); + Vector4.FromArrayToRef(this._colors, faceIndexB * 4, TmpVectors.Vector4[1]); + Vector4.FromArrayToRef(this._colors, faceIndexC * 4, TmpVectors.Vector4[2]); + + particle.color.copyFromFloats( + bu * TmpVectors.Vector4[0].x + bv * TmpVectors.Vector4[1].x + bw * TmpVectors.Vector4[2].x, + bu * TmpVectors.Vector4[0].y + bv * TmpVectors.Vector4[1].y + bw * TmpVectors.Vector4[2].y, + bu * TmpVectors.Vector4[0].z + bv * TmpVectors.Vector4[1].z + bw * TmpVectors.Vector4[2].z, + bu * TmpVectors.Vector4[0].w + bv * TmpVectors.Vector4[1].w + bw * TmpVectors.Vector4[2].w + ); + } + }; + + this.output._storedValue = system; + } + + /** + * Serializes this block in a JSON representation + * @returns the serialized block object + */ + public override serialize(): any { + const serializationObject = super.serialize(); + serializationObject.serializedCachedData = this.serializedCachedData; + + if (this.serializedCachedData) { + if (this._mesh) { + serializationObject.cachedVertexData = VertexData.ExtractFromMesh(this._mesh, false, true).serialize(); + } else if (this._cachedVertexData) { + serializationObject.cachedVertexData = this._cachedVertexData.serialize(); + } + } + + serializationObject.useMeshNormalsForDirection = this.useMeshNormalsForDirection; + serializationObject.useMeshColorForColor = this.useMeshColorForColor; + + return serializationObject; + } + + public override _deserialize(serializationObject: any) { + super._deserialize(serializationObject); + + if (serializationObject.cachedVertexData) { + this._cachedVertexData = VertexData.Parse(serializationObject.cachedVertexData); + } + + this.serializedCachedData = !!serializationObject.serializedCachedData; + this.useMeshNormalsForDirection = !!serializationObject.useMeshNormalsForDirection; + this.useMeshColorForColor = !!serializationObject.useMeshColorForColor; + } +} + +RegisterClass("BABYLON.MeshShapeBlock", MeshShapeBlock); diff --git a/packages/tools/nodeParticleEditor/src/blockTools.ts b/packages/tools/nodeParticleEditor/src/blockTools.ts index af488390aa0..8bfdcd95bfb 100644 --- a/packages/tools/nodeParticleEditor/src/blockTools.ts +++ b/packages/tools/nodeParticleEditor/src/blockTools.ts @@ -33,6 +33,7 @@ import { SphereShapeBlock } from "core/Particles/Node/Blocks/Emitters/sphereShap import { PointShapeBlock } from "core/Particles/Node/Blocks/Emitters/pointShapeBlock"; import { CustomShapeBlock } from "core/Particles/Node/Blocks/Emitters/customShapeBlock"; import { CylinderShapeBlock } from "core/Particles/Node/Blocks/Emitters/cylinderShapeBlock"; +import { MeshShapeBlock } from "core/Particles/Node/Blocks/Emitters/meshShapeBlock"; /** * Static class for BlockTools @@ -139,6 +140,8 @@ export class BlockTools { return new CustomShapeBlock("Custom shape"); case "CylinderShapeBlock": return new CylinderShapeBlock("Cylinder shape"); + case "MeshShapeBlock": + return new MeshShapeBlock("Mesh shape"); case "PositionBlock": { const block = new ParticleInputBlock("Position"); block.contextualValue = NodeParticleContextualSources.Position; diff --git a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx index fa2dffa2405..398e48f1fc5 100644 --- a/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx +++ b/packages/tools/nodeParticleEditor/src/components/nodeList/nodeListComponent.tsx @@ -31,6 +31,7 @@ export class NodeListComponent extends React.Component { + constructor(props: IPropertyComponentProps) { + super(props); + + this.state = { isLoading: false }; + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + async loadMesh(file: File) { + this.setState({ isLoading: true }); + const scene = await SceneLoader.LoadAsync("file:", file, EngineStore.LastCreatedEngine); + + if (!scene) { + return; + } + + this.setState({ isLoading: false }); + + const nodeData = this.props.nodeData as any; + + if (nodeData.__scene) { + nodeData.__scene.dispose(); + } + nodeData.__scene = scene; + + const meshes = scene.meshes.filter((m) => !!m.name && m.getTotalVertices() > 0); + + if (meshes.length) { + const block = this.props.nodeData.data as MeshShapeBlock; + block.mesh = meshes[0] as Mesh; + + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + } + + this.forceUpdate(); + } + + removeData() { + const block = this.props.nodeData.data as MeshShapeBlock; + block.cleanData(); + this.forceUpdate(); + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + } + + override render() { + const scene = (this.props.nodeData as any).__scene as Nullable; + const meshOptions = [{ label: "None", value: -1 }]; + let meshes: AbstractMesh[] = []; + + if (scene) { + meshes = scene.meshes.filter((m) => !!m.name && m.getTotalVertices() > 0); + meshes.sort((a, b) => a.name.localeCompare(b.name)); + + meshes.sort((a, b) => a.name.localeCompare(b.name)); + + meshOptions.push( + ...meshes.map((v, i) => { + return { label: v.name, value: i }; + }) + ); + } + const block = this.props.nodeData.data as MeshShapeBlock; + + return ( +
+ + + {this.state.isLoading && } + {!this.state.isLoading && await this.loadMesh(file)} accept=".glb, .babylon" />} + {scene && ( + { + switch (value) { + case -1: + block.mesh = null; + break; + default: + block.mesh = meshes[value as number] as Mesh; + } + + this.props.stateManager.onRebuildRequiredObservable.notifyObservers(); + this.forceUpdate(); + }} + extractValue={() => { + if (!block.mesh) { + return -1; + } + + const meshIndex = meshes.indexOf(block.mesh); + + if (meshIndex > -1) { + return meshIndex; + } + + return -1; + }} + /> + )} + {!scene && !!block.mesh && } + {!scene && !!block.isUsingCachedData && } + {!this.state.isLoading && (!!block.mesh || !!block.isUsingCachedData) && this.removeData()} />} + +
+ ); + } +} diff --git a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts index 60ffa6f8712..0290e5e97d5 100644 --- a/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts +++ b/packages/tools/nodeParticleEditor/src/graphSystem/registerToPropertyLedger.ts @@ -4,6 +4,7 @@ import { InputPropertyTabComponent } from "./properties/inputNodePropertyCompone import { TextureSourcePropertyTabComponent } from "./properties/textureSourceNodePropertyComponent"; import { DebugPropertyTabComponent } from "./properties/debugNodePropertyComponent"; import { TeleportOutPropertyTabComponent } from "./properties/teleportOutNodePropertyComponent"; +import { MeshShapePropertyTabComponent } from "./properties/meshShapeNodePropertyComponent"; export const RegisterToPropertyTabManagers = () => { PropertyLedger.DefaultControl = GenericPropertyComponent; @@ -11,4 +12,5 @@ export const RegisterToPropertyTabManagers = () => { PropertyLedger.RegisteredControls["ParticleTextureSourceBlock"] = TextureSourcePropertyTabComponent; PropertyLedger.RegisteredControls["ParticleDebugBlock"] = DebugPropertyTabComponent; PropertyLedger.RegisteredControls["ParticleTeleportOutBlock"] = TeleportOutPropertyTabComponent; + PropertyLedger.RegisteredControls["MeshShapeBlock"] = MeshShapePropertyTabComponent; };