Browse Source

Merge pull request #8131 from Popov72/nme-particle

NME: Add particle fragment editing support
David Catuhe 5 years ago
parent
commit
bbce6df946
36 changed files with 1333 additions and 236 deletions
  1. 1 1
      dist/preview release/what's new.md
  2. 13 1
      nodeEditor/public/index.js
  3. 26 1
      nodeEditor/src/blockTools.ts
  4. 15 0
      nodeEditor/src/components/nodeList/nodeListComponent.tsx
  5. 1 1
      nodeEditor/src/components/preview/previewAreaComponent.tsx
  6. 199 71
      nodeEditor/src/components/preview/previewManager.ts
  7. 40 21
      nodeEditor/src/components/preview/previewMeshControlComponent.tsx
  8. 0 9
      nodeEditor/src/components/preview/previewMeshType.ts
  9. 19 0
      nodeEditor/src/components/preview/previewType.ts
  10. 20 3
      nodeEditor/src/components/propertyTab/propertyTabComponent.tsx
  11. 24 10
      nodeEditor/src/diagram/display/inputDisplayManager.ts
  12. 4 3
      nodeEditor/src/diagram/display/textureDisplayManager.ts
  13. 1 0
      nodeEditor/src/diagram/displayLedger.ts
  14. 3 2
      nodeEditor/src/diagram/properties/texturePropertyTabComponent.tsx
  15. 1 0
      nodeEditor/src/diagram/propertyLedger.ts
  16. 7 6
      nodeEditor/src/globalState.ts
  17. 4 3
      nodeEditor/src/nodeEditor.ts
  18. 1 1
      nodeEditor/src/sharedComponents/color3LineComponent.tsx
  19. 1 1
      nodeEditor/src/sharedComponents/color4LineComponent.tsx
  20. 9 2
      nodeEditor/src/sharedComponents/colorPickerComponent.tsx
  21. 13 6
      src/Materials/Node/Blocks/Dual/textureBlock.ts
  22. 50 8
      src/Materials/Node/Blocks/Input/inputBlock.ts
  23. 4 0
      src/Materials/Node/Blocks/Particle/index.ts
  24. 95 0
      src/Materials/Node/Blocks/Particle/particleBlendMultiplyBlock.ts
  25. 97 0
      src/Materials/Node/Blocks/Particle/particleRampGradientBlock.ts
  26. 223 0
      src/Materials/Node/Blocks/Particle/particleTextureBlock.ts
  27. 1 0
      src/Materials/Node/Blocks/index.ts
  28. 2 0
      src/Materials/Node/Enums/nodeMaterialModes.ts
  29. 159 6
      src/Materials/Node/nodeMaterial.ts
  30. 1 1
      src/Materials/Node/nodeMaterialBlock.ts
  31. 8 2
      src/Materials/Node/nodeMaterialBuildStateSharedData.ts
  32. 40 0
      src/Particles/IParticleSystem.ts
  33. 135 43
      src/Particles/gpuParticleSystem.ts
  34. 97 25
      src/Particles/particleSystem.ts
  35. 17 7
      src/Particles/particleSystemComponent.ts
  36. 2 2
      src/Shaders/gpuRenderParticles.fragment.fx

+ 1 - 1
dist/preview release/what's new.md

@@ -6,7 +6,7 @@
 - Added sprite editor to the Inspector ([Deltakosh](https://github.com/deltakosh))
 - Added the `ShadowDepthWrapper` class to support accurate shadow generation for custom as well as node material shaders. [Doc](https://doc.babylonjs.com/babylon101/shadows#custom-shadow-map-shaders) ([Popov72](https://github.com/Popov72))
 - Added Babylon.js Texture [tools](https://www.babylonjs.com/tools/ibl) to prefilter HDR files ([Sebavan](https://github.com/sebavan/))
-- Added editing of PBR materials and Post processes in the node material editor ([Popov72](https://github.com/Popov72))
+- Added editing of PBR materials, Post processes and Particle fragment shaders in the node material editor ([Popov72](https://github.com/Popov72))
 - Added Curve editor to view selected entity's animations in the Inspector ([pixelspace](https://github.com/devpixelspace))
 - Added support in `ShadowGenerator` for fast fake soft transparent shadows ([Popov72](https://github.com/Popov72))
 

+ 13 - 1
nodeEditor/public/index.js

@@ -123,7 +123,19 @@ if (BABYLON.Engine.isSupported()) {
 
     // Set to default
     if (!location.hash) {
-        nodeMaterial.setToDefault();
+        const mode = BABYLON.DataStorage.ReadNumber("Mode", BABYLON.NodeMaterialModes.Material);
+        
+        switch (mode) {
+            case BABYLON.NodeMaterialModes.Material:
+                nodeMaterial.setToDefault();
+                break;
+            case BABYLON.NodeMaterialModes.PostProcess:
+                nodeMaterial.setToDefaultPostProcess();
+                break;
+            case BABYLON.NodeMaterialModes.Particle:
+                nodeMaterial.setToDefaultParticle();
+                break;
+        }
         nodeMaterial.build(true);
         showEditor();
     }

+ 26 - 1
nodeEditor/src/blockTools.ts

@@ -72,6 +72,10 @@ import { ClearCoatBlock } from 'babylonjs/Materials/Node/Blocks/PBR/clearCoatBlo
 import { RefractionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/refractionBlock';
 import { SubSurfaceBlock } from 'babylonjs/Materials/Node/Blocks/PBR/subSurfaceBlock';
 import { CurrentScreenBlock } from 'babylonjs/Materials/Node/Blocks/Dual/currentScreenBlock';
+import { ParticleTextureBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleTextureBlock';
+import { ParticleRampGradientBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleRampGradientBlock';
+import { ParticleBlendMultiplyBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleBlendMultiplyBlock';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
 
 export class BlockTools {
     public static GetBlockFromString(data: string, scene: Scene, nodeMaterial: NodeMaterial) {
@@ -111,7 +115,7 @@ export class BlockTools {
             case "VectorSplitterBlock":
                 return new VectorSplitterBlock("VectorSplitter");
             case "TextureBlock":
-                return new TextureBlock("Texture");
+                return new TextureBlock("Texture", nodeMaterial.mode === NodeMaterialModes.Particle);
             case "ReflectionTextureBlock":
                 return new ReflectionTextureBlock("Reflection texture");
             case "LightBlock":
@@ -467,6 +471,27 @@ export class BlockTools {
                 return new SubSurfaceBlock("SubSurface");
             case "CurrentScreenBlock":
                 return new CurrentScreenBlock("CurrentScreen");
+            case "ParticleUVBlock": {
+                let uv = new InputBlock("uv");
+                uv.setAsAttribute("particle_uv");
+                return uv;
+            }
+            case "ParticleTextureBlock":
+                return new ParticleTextureBlock("ParticleTexture");
+            case "ParticleColorBlock": {
+                let color = new InputBlock("Color");
+                color.setAsAttribute("particle_color");
+                return color;
+            }
+            case "ParticleTextureMaskBlock": {
+                let u = new InputBlock("TextureMask");
+                u.setAsAttribute("particle_texturemask");
+                return u;
+            }
+            case "ParticleRampGradientBlock":
+                return new ParticleRampGradientBlock("ParticleRampGradient");
+            case "ParticleBlendMultiplyBlock":
+                return new ParticleBlendMultiplyBlock("ParticleBlendMultiply");
         }
 
         return null;

+ 15 - 0
nodeEditor/src/components/nodeList/nodeListComponent.tsx

@@ -133,6 +133,12 @@ export class NodeListComponent extends React.Component<INodeListComponentProps,
         "SubSurfaceBlock": "PBR SubSurface block",
         "Position2DBlock": "A Vector2 representing the position of each vertex of the screen quad",
         "CurrentScreenBlock": "The screen buffer used as input for the post process",
+        "ParticleUVBlock": "The particle uv texture coordinate",
+        "ParticleTextureBlock": "The particle texture",
+        "ParticleColorBlock": "The particle color",
+        "ParticleTextureMaskBlock": "The particle texture mask",
+        "ParticleRampGradientBlock": "The particle ramp gradient block",
+        "ParticleBlendMultiplyBlock": "The particle blend multiply block",
     };
 
     constructor(props: INodeListComponentProps) {
@@ -169,6 +175,7 @@ export class NodeListComponent extends React.Component<INodeListComponentProps,
             Mesh: ["InstancesBlock", "PositionBlock", "UVBlock", "ColorBlock", "NormalBlock", "PerturbNormalBlock", "NormalBlendBlock" , "TangentBlock", "MatrixIndicesBlock", "MatrixWeightsBlock", "WorldPositionBlock", "WorldNormalBlock", "WorldTangentBlock", "FrontFacingBlock"],
             Noises: ["RandomNumberBlock", "SimplexPerlin3DBlock", "WorleyNoise3DBlock"],
             Output_Nodes: ["VertexOutputBlock", "FragmentOutputBlock", "DiscardBlock"],
+            Particle: ["ParticleBlendMultiplyBlock", "ParticleColorBlock", "ParticleRampGradientBlock", "ParticleTextureBlock", "ParticleTextureMaskBlock", "ParticleUVBlock"],
             PBR: ["PBRMetallicRoughnessBlock", "AmbientOcclusionBlock", "AnisotropyBlock", "ClearCoatBlock", "ReflectionBlock", "ReflectivityBlock", "RefractionBlock", "SheenBlock", "SubSurfaceBlock"],
             PostProcess: ["Position2DBlock", "CurrentScreenBlock"],
             Range: ["ClampBlock", "RemapBlock", "NormalizeBlock"],
@@ -179,10 +186,18 @@ export class NodeListComponent extends React.Component<INodeListComponentProps,
         switch (this.props.globalState.mode) {
             case NodeMaterialModes.Material:
                 delete allBlocks["PostProcess"];
+                delete allBlocks["Particle"];
                 break;
             case NodeMaterialModes.PostProcess:
                 delete allBlocks["Animation"];
                 delete allBlocks["Mesh"];
+                delete allBlocks["Particle"];
+                break;
+            case NodeMaterialModes.Particle:
+                delete allBlocks["Animation"];
+                delete allBlocks["Mesh"];
+                delete allBlocks["PostProcess"];
+                allBlocks.Output_Nodes.splice(allBlocks.Output_Nodes.indexOf("VertexOutputBlock"), 1);
                 break;
         }
 

+ 1 - 1
nodeEditor/src/components/preview/previewAreaComponent.tsx

@@ -62,7 +62,7 @@ export class PreviewAreaComponent extends React.Component<IPreviewAreaComponentP
                         </div>
                     }
                 </div>
-                { this.props.globalState.mode !== NodeMaterialModes.PostProcess && <>
+                { this.props.globalState.mode === NodeMaterialModes.Material && <>
                     <div id="preview-config-bar">
                         <div
                             title="Render without back face culling"

+ 199 - 71
nodeEditor/src/components/preview/previewManager.ts

@@ -8,7 +8,7 @@ import { Mesh } from 'babylonjs/Meshes/mesh';
 import { Vector3 } from 'babylonjs/Maths/math.vector';
 import { HemisphericLight } from 'babylonjs/Lights/hemisphericLight';
 import { ArcRotateCamera } from 'babylonjs/Cameras/arcRotateCamera';
-import { PreviewMeshType } from './previewMeshType';
+import { PreviewType } from './previewType';
 import { Animation } from 'babylonjs/Animations/animation';
 import { SceneLoader } from 'babylonjs/Loading/sceneLoader';
 import { TransformNode } from 'babylonjs/Meshes/transformNode';
@@ -17,11 +17,17 @@ import { FramingBehavior } from 'babylonjs/Behaviors/Cameras/framingBehavior';
 import { DirectionalLight } from 'babylonjs/Lights/directionalLight';
 import { LogEntry } from '../log/logComponent';
 import { PointerEventTypes } from 'babylonjs/Events/pointerEvents';
-import { Color3 } from 'babylonjs/Maths/math.color';
+import { Color3, Color4 } from 'babylonjs/Maths/math.color';
 import { PostProcess } from 'babylonjs/PostProcesses/postProcess';
 import { Constants } from 'babylonjs/Engines/constants';
 import { CurrentScreenBlock } from 'babylonjs/Materials/Node/Blocks/Dual/currentScreenBlock';
 import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+import { ParticleSystem } from 'babylonjs/Particles/particleSystem';
+import { IParticleSystem } from 'babylonjs/Particles/IParticleSystem';
+import { ParticleHelper } from 'babylonjs/Particles/particleHelper';
+import { Texture } from 'babylonjs/Materials/Textures/texture';
+import { ParticleTextureBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleTextureBlock';
+import { FileTools } from 'babylonjs/Misc/fileTools';
 
 export class PreviewManager {
     private _nodeMaterial: NodeMaterial;
@@ -42,6 +48,7 @@ export class PreviewManager {
     private _currentType: number;
     private _lightParent: TransformNode;
     private _postprocess: Nullable<PostProcess>;
+    private _particleSystem: Nullable<IParticleSystem>;
 
     public constructor(targetCanvas: HTMLCanvasElement, globalState: GlobalState) {
         this._nodeMaterial = globalState.nodeMaterial;
@@ -104,9 +111,7 @@ export class PreviewManager {
             this._scene.render();
         });
 
-   //     let cameraLastRotation = 0;
         let lastOffsetX: number | undefined = undefined;
-     //   const lightRotationParallaxSpeed = 0.5;
         const lightRotationSpeed = 0.01;
 
         this._scene.onPointerObservable.add((evt) => {
@@ -193,35 +198,47 @@ export class PreviewManager {
         }
     }
 
-    private _prepareMeshes() {
-        if (this._globalState.mode !== NodeMaterialModes.PostProcess) {
-            this._prepareLights();
+    private _prepareScene() {
+        this._camera.useFramingBehavior = this._globalState.mode === NodeMaterialModes.Material;
 
-            // Framing
-            this._camera.useFramingBehavior = true;
+        switch (this._globalState.mode) {
+            case NodeMaterialModes.Material: {
+                this._prepareLights();
 
-            var framingBehavior = this._camera.getBehaviorByName("Framing") as FramingBehavior;
+                var framingBehavior = this._camera.getBehaviorByName("Framing") as FramingBehavior;
 
-            setTimeout(() => { // Let the behavior activate first
-                framingBehavior.framingTime = 0;
-                framingBehavior.elevationReturnTime = -1;
+                setTimeout(() => { // Let the behavior activate first
+                    framingBehavior.framingTime = 0;
+                    framingBehavior.elevationReturnTime = -1;
 
-                if (this._scene.meshes.length) {
-                    var worldExtends = this._scene.getWorldExtends();
-                    this._camera.lowerRadiusLimit = null;
-                    this._camera.upperRadiusLimit = null;
-                    framingBehavior.zoomOnBoundingInfo(worldExtends.min, worldExtends.max);
-                }
+                    if (this._scene.meshes.length) {
+                        var worldExtends = this._scene.getWorldExtends();
+                        this._camera.lowerRadiusLimit = null;
+                        this._camera.upperRadiusLimit = null;
+                        framingBehavior.zoomOnBoundingInfo(worldExtends.min, worldExtends.max);
+                    }
 
-                this._camera.pinchPrecision = 200 / this._camera.radius;
-                this._camera.upperRadiusLimit = 5 * this._camera.radius;
-            });
+                    this._camera.pinchPrecision = 200 / this._camera.radius;
+                    this._camera.upperRadiusLimit = 5 * this._camera.radius;
+                });
 
-            this._camera.wheelDeltaPercentage = 0.01;
-            this._camera.pinchDeltaPercentage = 0.01;
+                this._camera.wheelDeltaPercentage = 0.01;
+                this._camera.pinchDeltaPercentage = 0.01;
 
-            // Animations
-            this._handleAnimations();
+                // Animations
+                this._handleAnimations();
+                break;
+            }
+            case NodeMaterialModes.PostProcess: {
+                this._camera.radius = 4;
+                this._camera.upperRadiusLimit = 10;
+                break;
+            }
+            case NodeMaterialModes.Particle: {
+                this._camera.radius = this._globalState.previewType === PreviewType.Explosion ? 50 : this._globalState.previewType === PreviewType.DefaultParticleSystem ? 6 : 20;
+                this._camera.upperRadiusLimit = 5000;
+                break;
+            }
         }
 
         // Material
@@ -230,9 +247,8 @@ export class PreviewManager {
     }
 
     private _refreshPreviewMesh() {
-
-        if (this._currentType !== this._globalState.previewMeshType || this._currentType === PreviewMeshType.Custom) {
-            this._currentType = this._globalState.previewMeshType;
+        if (this._currentType !== this._globalState.previewType || this._currentType === PreviewType.Custom) {
+            this._currentType = this._globalState.previewType;
             if (this._meshes && this._meshes.length) {
                 for (var mesh of this._meshes) {
                     mesh.dispose();
@@ -247,55 +263,144 @@ export class PreviewManager {
 
             this._engine.releaseEffects();
 
+            if (this._particleSystem) {
+                this._particleSystem.onBeforeDrawParticlesObservable.clear();
+                this._particleSystem.onDisposeObservable.clear();
+                this._particleSystem.stop();
+                this._particleSystem.dispose();
+                this._particleSystem = null;
+            }
+
             SceneLoader.ShowLoadingScreen = false;
 
             this._globalState.onIsLoadingChanged.notifyObservers(true);
 
-            if (this._globalState.mode !== NodeMaterialModes.PostProcess) {
-                switch (this._globalState.previewMeshType) {
-                    case PreviewMeshType.Box:
+            if (this._globalState.mode === NodeMaterialModes.Material) {
+                switch (this._globalState.previewType) {
+                    case PreviewType.Box:
                         SceneLoader.AppendAsync("https://models.babylonjs.com/", "roundedCube.glb", this._scene).then(() => {
                             this._meshes.push(...this._scene.meshes);
-                            this._prepareMeshes();
+                            this._prepareScene();
                         });
                         return;
-                    case PreviewMeshType.Sphere:
+                    case PreviewType.Sphere:
                         this._meshes.push(Mesh.CreateSphere("dummy-sphere", 32, 2, this._scene));
                         break;
-                    case PreviewMeshType.Torus:
+                    case PreviewType.Torus:
                         this._meshes.push(Mesh.CreateTorus("dummy-torus", 2, 0.5, 32, this._scene));
                         break;
-                    case PreviewMeshType.Cylinder:
+                    case PreviewType.Cylinder:
                         SceneLoader.AppendAsync("https://models.babylonjs.com/", "roundedCylinder.glb", this._scene).then(() => {
                             this._meshes.push(...this._scene.meshes);
-                            this._prepareMeshes();
+                            this._prepareScene();
                         });
                         return;
-                    case PreviewMeshType.Plane:
+                    case PreviewType.Plane:
                         let plane = Mesh.CreateGround("dummy-plane", 2, 2, 128, this._scene);
                         plane.scaling.y = -1;
                         plane.rotation.x = Math.PI;
                         this._meshes.push(plane);
                         break;
-                    case PreviewMeshType.ShaderBall:
+                    case PreviewType.ShaderBall:
                         SceneLoader.AppendAsync("https://models.babylonjs.com/", "shaderBall.glb", this._scene).then(() => {
                             this._meshes.push(...this._scene.meshes);
-                            this._prepareMeshes();
+                            this._prepareScene();
                         });
                         return;
-                    case PreviewMeshType.Custom:
-                        SceneLoader.AppendAsync("file:", this._globalState.previewMeshFile, this._scene).then(() => {
+                    case PreviewType.Custom:
+                        SceneLoader.AppendAsync("file:", this._globalState.previewFile, this._scene).then(() => {
                             this._meshes.push(...this._scene.meshes);
-                            this._prepareMeshes();
+                            this._prepareScene();
+                        });
+                        return;
+                }
+            } else if (this._globalState.mode === NodeMaterialModes.Particle) {
+                switch (this._globalState.previewType) {
+                    case PreviewType.DefaultParticleSystem:
+                        this._particleSystem = ParticleHelper.CreateDefault(new Vector3(0, 0, 0), 500, this._scene);
+                        this._particleSystem.start();
+                        break;
+                    case PreviewType.Bubbles:
+                        this._particleSystem = new ParticleSystem("particles", 4000, this._scene);
+                        this._particleSystem.particleTexture = new Texture("https://assets.babylonjs.com/particles/textures/explosion/Flare.png", this._scene);
+                        this._particleSystem.minSize = 0.1;
+                        this._particleSystem.maxSize = 1.0;
+                        this._particleSystem.minLifeTime = 0.5;
+                        this._particleSystem.maxLifeTime = 5.0;
+                        this._particleSystem.minEmitPower = 0.5;
+                        this._particleSystem.maxEmitPower = 3.0;
+                        this._particleSystem.createBoxEmitter(new Vector3(-1, 1, -1), new Vector3(1, 1, 1), new Vector3(-0.1, -0.1, -0.1), new Vector3(0.1, 0.1, 0.1));
+                        this._particleSystem.emitRate = 100;
+                        this._particleSystem.blendMode = ParticleSystem.BLENDMODE_ONEONE;
+                        this._particleSystem.color1 = new Color4(1, 1, 0, 1);
+                        this._particleSystem.color2 = new Color4(1, 0.5, 0, 1);
+                        this._particleSystem.gravity = new Vector3(0, -1.0, 0);
+                        this._particleSystem.start();
+                        break;
+                    case PreviewType.Explosion:
+                        this._loadParticleSystem(this._globalState.previewType, 1);
+                        return;
+                    case PreviewType.Fire:
+                    case PreviewType.Rain:
+                    case PreviewType.Smoke:
+                        this._loadParticleSystem(this._globalState.previewType);
+                        return;
+                    case PreviewType.Custom:
+                        FileTools.ReadFile(this._globalState.previewFile, (json) =>  {
+                            this._particleSystem = ParticleSystem.Parse(JSON.parse(json), this._scene, "");
+                            this._particleSystem.start();
+                            this._prepareScene();
+                        }, undefined, false, (error) => {
+                            console.log(error);
                         });
                         return;
                 }
             }
 
-            this._prepareMeshes();
+            this._prepareScene();
         }
     }
 
+    private _loadParticleSystem(particleNumber: number, systemIndex = 0, prepareScene = true) {
+        let name = "";
+
+        switch (particleNumber) {
+            case PreviewType.Explosion:
+                name = "explosion";
+                break;
+            case PreviewType.Fire:
+                name = "fire";
+                break;
+            case PreviewType.Rain:
+                name = "rain";
+                break;
+            case PreviewType.Smoke:
+                name = "smoke";
+                break;
+        }
+
+        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();
+                } else {
+                    set.systems[i].dispose();
+                }
+            }
+            if (prepareScene) {
+                this._prepareScene();
+            } else {
+                let serializationObject = this._nodeMaterial.serialize();
+                this._updatePreview(serializationObject);
+            }
+        });
+    }
+
     private _forceCompilationAsync(material: NodeMaterial, mesh: AbstractMesh): Promise<void> {
         return material.forceCompilationAsync(mesh);
     }
@@ -312,42 +417,65 @@ export class PreviewManager {
                 this._postprocess = null;
             }
 
-            if (this._globalState.mode === NodeMaterialModes.PostProcess) {
-                this._globalState.onIsLoadingChanged.notifyObservers(false);
+            switch (this._globalState.mode) {
+                case NodeMaterialModes.PostProcess: {
+                    this._globalState.onIsLoadingChanged.notifyObservers(false);
 
-                this._postprocess = tempMaterial.createPostProcess(this._camera, 1.0, Constants.TEXTURE_NEAREST_SAMPLINGMODE, this._engine);
+                    this._postprocess = tempMaterial.createPostProcess(this._camera, 1.0, Constants.TEXTURE_NEAREST_SAMPLINGMODE, this._engine);
 
-                const currentScreen = tempMaterial.getBlockByPredicate((block) => block instanceof CurrentScreenBlock);
-                if (currentScreen) {
-                    this._postprocess!.onApplyObservable.add((effect) => {
-                        effect.setTexture("textureSampler", (currentScreen as CurrentScreenBlock).texture);
-                    });
-                }
-
-                if (this._material) {
-                    this._material.dispose();
-                }
-                this._material = tempMaterial;
-            } else if (this._meshes.length) {
-                let tasks = this._meshes.map((m) => this._forceCompilationAsync(tempMaterial, m));
-
-                Promise.all(tasks).then(() => {
-                    for (var mesh of this._meshes) {
-                        mesh.material = tempMaterial;
+                    const currentScreen = tempMaterial.getBlockByPredicate((block) => block instanceof CurrentScreenBlock);
+                    if (currentScreen) {
+                        this._postprocess!.onApplyObservable.add((effect) => {
+                            effect.setTexture("textureSampler", (currentScreen as CurrentScreenBlock).texture);
+                        });
                     }
 
                     if (this._material) {
                         this._material.dispose();
                     }
-
                     this._material = tempMaterial;
+                    break;
+                }
+
+                case NodeMaterialModes.Particle: {
                     this._globalState.onIsLoadingChanged.notifyObservers(false);
-                }).catch((reason) => {
-                    this._globalState.onLogRequiredObservable.notifyObservers(new LogEntry("Shader compilation error:\r\n" + reason, true));
-                    this._globalState.onIsLoadingChanged.notifyObservers(false);
-                });
-            } else {
-                this._material = tempMaterial;
+
+                    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!);
+                    break;
+                }
+
+                default: {
+                    if (this._meshes.length) {
+                        let tasks = this._meshes.map((m) => this._forceCompilationAsync(tempMaterial, m));
+
+                        Promise.all(tasks).then(() => {
+                            for (var mesh of this._meshes) {
+                                mesh.material = tempMaterial;
+                            }
+
+                            if (this._material) {
+                                this._material.dispose();
+                            }
+
+                            this._material = tempMaterial;
+                            this._globalState.onIsLoadingChanged.notifyObservers(false);
+                        }).catch((reason) => {
+                            this._globalState.onLogRequiredObservable.notifyObservers(new LogEntry("Shader compilation error:\r\n" + reason, true));
+                            this._globalState.onIsLoadingChanged.notifyObservers(false);
+                        });
+                    } else {
+                        this._material = tempMaterial;
+                    }
+                    break;
+                }
             }
         } catch (err) {
             // Ignore the error

+ 40 - 21
nodeEditor/src/components/preview/previewMeshControlComponent.tsx

@@ -2,7 +2,7 @@
 import * as React from "react";
 import { GlobalState } from '../../globalState';
 import { Color3, Color4 } from 'babylonjs/Maths/math.color';
-import { PreviewMeshType } from './previewMeshType';
+import { PreviewType } from './previewType';
 import { DataStorage } from 'babylonjs/Misc/dataStorage';
 import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
 import { Observer } from 'babylonjs/Misc/observable';
@@ -38,15 +38,15 @@ export class PreviewMeshControlComponent extends React.Component<IPreviewMeshCon
         this.props.globalState.onResetRequiredObservable.remove(this._onResetRequiredObserver);
     }
 
-    changeMeshType(newOne: PreviewMeshType) {
-        if (this.props.globalState.previewMeshType === newOne) {
+    changeMeshType(newOne: PreviewType) {
+        if (this.props.globalState.previewType === newOne) {
             return;
         }
 
-        this.props.globalState.previewMeshType = newOne;
+        this.props.globalState.previewType = newOne;
         this.props.globalState.onPreviewCommandActivated.notifyObservers(false);
 
-        DataStorage.WriteNumber("PreviewMeshType", newOne);
+        DataStorage.WriteNumber("PreviewType", newOne);
 
         this.forceUpdate();
     }
@@ -56,10 +56,10 @@ export class PreviewMeshControlComponent extends React.Component<IPreviewMeshCon
         if (files && files.length) {
             let file = files[0];
 
-            this.props.globalState.previewMeshFile = file;
-            this.props.globalState.previewMeshType = PreviewMeshType.Custom;
+            this.props.globalState.previewFile = file;
+            this.props.globalState.previewType = PreviewType.Custom;
             this.props.globalState.onPreviewCommandActivated.notifyObservers(false);
-            this.props.globalState.listOfCustomPreviewMeshFiles = [file];
+            this.props.globalState.listOfCustomPreviewFiles = [file];
             this.forceUpdate();
         }
         if (this.filePickerRef.current) {
@@ -96,28 +96,45 @@ export class PreviewMeshControlComponent extends React.Component<IPreviewMeshCon
     render() {
 
         var meshTypeOptions = [
-            { label: "Cube", value: PreviewMeshType.Box },
-            { label: "Cylinder", value: PreviewMeshType.Cylinder },
-            { label: "Plane", value: PreviewMeshType.Plane },
-            { label: "Shader ball", value: PreviewMeshType.ShaderBall },
-            { label: "Sphere", value: PreviewMeshType.Sphere },
-            { label: "Load...", value: PreviewMeshType.Custom + 1 }
+            { label: "Cube", value: PreviewType.Box },
+            { label: "Cylinder", value: PreviewType.Cylinder },
+            { label: "Plane", value: PreviewType.Plane },
+            { label: "Shader ball", value: PreviewType.ShaderBall },
+            { label: "Sphere", value: PreviewType.Sphere },
+            { label: "Load...", value: PreviewType.Custom + 1 }
         ];
 
-        if (this.props.globalState.listOfCustomPreviewMeshFiles.length > 0) {
+        var particleTypeOptions = [
+            { label: "Default", value: PreviewType.DefaultParticleSystem },
+            { label: "Bubbles", value: PreviewType.Bubbles },
+            { label: "Explosion", value: PreviewType.Explosion },
+            { label: "Fire", value: PreviewType.Fire },
+            { label: "Rain", value: PreviewType.Rain },
+            { label: "Smoke", value: PreviewType.Smoke },
+            { label: "Load...", value: PreviewType.Custom + 1 }
+        ];
+
+        if (this.props.globalState.listOfCustomPreviewFiles.length > 0) {
             meshTypeOptions.splice(0, 0, {
-                label: "Custom", value: PreviewMeshType.Custom
+                label: "Custom", value: PreviewType.Custom
+            });
+
+            particleTypeOptions.splice(0, 0, {
+                label: "Custom", value: PreviewType.Custom
             });
         }
 
+        var options = this.props.globalState.mode === NodeMaterialModes.Particle ? particleTypeOptions : meshTypeOptions;
+        var accept = this.props.globalState.mode === NodeMaterialModes.Particle ? ".json" : ".gltf, .glb, .babylon, .obj";
+
         return (
             <div id="preview-mesh-bar">
-                { this.props.globalState.mode !== NodeMaterialModes.PostProcess && <>
-                    <OptionsLineComponent label="" options={meshTypeOptions} target={this.props.globalState}
-                                propertyName="previewMeshType"
+                { (this.props.globalState.mode === NodeMaterialModes.Material || this.props.globalState.mode === NodeMaterialModes.Particle) && <>
+                    <OptionsLineComponent label="" options={options} target={this.props.globalState}
+                                propertyName="previewType"
                                 noDirectUpdate={true}
                                 onSelect={(value: any) => {
-                                    if (value !== PreviewMeshType.Custom + 1) {
+                                    if (value !== PreviewType.Custom + 1) {
                                         this.changeMeshType(value);
                                     } else {
                                         this.filePickerRef.current?.click();
@@ -126,8 +143,10 @@ export class PreviewMeshControlComponent extends React.Component<IPreviewMeshCon
                     <div style={{
                         display: "none"
                     }} title="Preview with a custom mesh" >
-                        <input ref={this.filePickerRef} id="file-picker" type="file" onChange={(evt) => this.useCustomMesh(evt)} accept=".gltf, .glb, .babylon, .obj"/>
+                        <input ref={this.filePickerRef} id="file-picker" type="file" onChange={(evt) => this.useCustomMesh(evt)} accept={accept}/>
                     </div>
+                </> }
+                { this.props.globalState.mode === NodeMaterialModes.Material && <>
                     <div
                         title="Turn-table animation"
                         onClick={() => this.changeAnimation()} className="button" id="play-button">

+ 0 - 9
nodeEditor/src/components/preview/previewMeshType.ts

@@ -1,9 +0,0 @@
-export enum PreviewMeshType {
-    Sphere,
-    Box, 
-    Torus,
-    Cylinder,
-    Plane,
-    ShaderBall,
-    Custom
-}

+ 19 - 0
nodeEditor/src/components/preview/previewType.ts

@@ -0,0 +1,19 @@
+export enum PreviewType {
+    // Meshes
+    Sphere,
+    Box,
+    Torus,
+    Cylinder,
+    Plane,
+    ShaderBall,
+
+    // Particle systems
+    DefaultParticleSystem,
+    Bubbles,
+    Smoke,
+    Rain,
+    Explosion,
+    Fire,
+
+    Custom,
+}

+ 20 - 3
nodeEditor/src/components/propertyTab/propertyTabComponent.tsx

@@ -33,6 +33,7 @@ import { NodePort } from '../../diagram/nodePort';
 import { isFramePortData } from '../../diagram/graphCanvas';
 import { OptionsLineComponent } from '../../sharedComponents/optionsLineComponent';
 import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
+import { PreviewType } from '../preview/previewType';
 require("./propertyTab.scss");
 
 interface IPropertyTabComponentProps {
@@ -263,19 +264,29 @@ export class PropertyTabComponent extends React.Component<IPropertyTabComponentP
             this._modeSelect.current?.setValue(value);
         }
 
-        this.props.globalState.mode = value as NodeMaterialModes;
-
         if (loadDefault) {
             switch (value) {
                 case NodeMaterialModes.Material:
+                    this.props.globalState.previewType = PreviewType.Sphere;
                     this.props.globalState.nodeMaterial!.setToDefault();
                     break;
                 case NodeMaterialModes.PostProcess:
                     this.props.globalState.nodeMaterial!.setToDefaultPostProcess();
                     break;
+                case NodeMaterialModes.Particle:
+                    this.props.globalState.previewType = PreviewType.Bubbles;
+                    this.props.globalState.nodeMaterial!.setToDefaultParticle();
+                    break;
             }
+
+            this.props.globalState.listOfCustomPreviewFiles = [];
+            (this.props.globalState.previewFile as any) = undefined;
+
+            DataStorage.WriteNumber("PreviewType", this.props.globalState.previewType);
         }
 
+        this.props.globalState.mode = value as NodeMaterialModes;
+
         this.props.globalState.onResetRequiredObservable.notifyObservers();
     }
 
@@ -314,6 +325,12 @@ export class PropertyTabComponent extends React.Component<IPropertyTabComponentP
 
         let gridSize = DataStorage.ReadNumber("GridSize", 20);
 
+        const modeList = [
+            { label: "Material", value: NodeMaterialModes.Material },
+            { label: "Post Process", value: NodeMaterialModes.PostProcess },
+            { label: "Particle", value: NodeMaterialModes.Particle },
+        ];
+
         return (
             <div id="propertyTab">
                 <div id="header">
@@ -324,7 +341,7 @@ export class PropertyTabComponent extends React.Component<IPropertyTabComponentP
                 </div>
                 <div>
                     <LineContainerComponent title="GENERAL">
-                        <OptionsLineComponent ref={this._modeSelect} label="Mode" target={this} getSelection={(target) => this.props.globalState.mode} options={[{ label: "Material", value: NodeMaterialModes.Material }, { label: "Post Process", value: NodeMaterialModes.PostProcess }]} onSelect={(value) => this.changeMode(value)} />
+                        <OptionsLineComponent ref={this._modeSelect} label="Mode" target={this} getSelection={(target) => this.props.globalState.mode} options={modeList} onSelect={(value) => this.changeMode(value)} />
                         <TextLineComponent label="Version" value={Engine.Version}/>
                         <TextLineComponent label="Help" value="doc.babylonjs.com" underline={true} onLink={() => window.open('https://doc.babylonjs.com/how_to/node_material', '_blank')}/>
                         <ButtonLineComponent label="Reset to default" onClick={() => {

+ 24 - 10
nodeEditor/src/diagram/display/inputDisplayManager.ts

@@ -9,16 +9,30 @@ import { Color3 } from 'babylonjs/Maths/math.color';
 import { BlockTools } from '../../blockTools';
 import { StringTools } from '../../stringTools';
 
+const inputNameToAttributeValue: { [name: string] : string } = {
+    "position2d" : "position",
+    "particle_uv" : "uv",
+    "particle_color" : "color",
+    "particle_texturemask": "textureMask",
+};
+
+const inputNameToAttributeName: { [name: string] : string } = {
+    "position2d" : "postprocess",
+    "particle_uv" : "particle",
+    "particle_color" : "particle",
+    "particle_texturemask": "particle",
+};
+
 export class InputDisplayManager implements IDisplayManager {
     public getHeaderClass(block: NodeMaterialBlock) {
         let inputBlock = block as InputBlock;
 
         if (inputBlock.isConstant) {
-            return "constant"
+            return "constant";
         }
 
         if (inputBlock.visibleInInspector) {
-            return "inspector"
+            return "inspector";
         }
 
         return "";
@@ -40,10 +54,10 @@ export class InputDisplayManager implements IDisplayManager {
     }
 
     public getBackgroundColor(block: NodeMaterialBlock): string {
-        let color = "";        
+        let color = "";
         let inputBlock = block as InputBlock;
 
-        switch (inputBlock.type) {                    
+        switch (inputBlock.type) {
             case NodeMaterialBlockConnectionPointTypes.Color3:
             case NodeMaterialBlockConnectionPointTypes.Color4: {
                 if (inputBlock.value) {
@@ -64,8 +78,8 @@ export class InputDisplayManager implements IDisplayManager {
         let inputBlock = block as InputBlock;
 
         if (inputBlock.isAttribute) {
-            const attrVal = inputBlock.name === 'position2d' ? 'position' : inputBlock.name;
-            const attrName = inputBlock.name === 'position2d' ? 'postprocess' : 'mesh';
+            const attrVal = inputNameToAttributeValue[inputBlock.name] ?? inputBlock.name;
+            const attrName = inputNameToAttributeName[inputBlock.name] ?? 'mesh';
             value = attrName + "." + attrVal;
         } else if (inputBlock.isSystemValue) {
             switch (inputBlock.systemValue) {
@@ -107,17 +121,17 @@ export class InputDisplayManager implements IDisplayManager {
                     }
                     break;
                 case NodeMaterialBlockConnectionPointTypes.Vector2:
-                    let vec2Value = inputBlock.value as Vector2
+                    let vec2Value = inputBlock.value as Vector2;
                     value = `(${vec2Value.x.toFixed(2)}, ${vec2Value.y.toFixed(2)})`;
                     break;
                 case NodeMaterialBlockConnectionPointTypes.Vector3:
-                    let vec3Value = inputBlock.value as Vector3
+                    let vec3Value = inputBlock.value as Vector3;
                     value = `(${vec3Value.x.toFixed(2)}, ${vec3Value.y.toFixed(2)}, ${vec3Value.z.toFixed(2)})`;
                     break;
                 case NodeMaterialBlockConnectionPointTypes.Vector4:
-                    let vec4Value = inputBlock.value as Vector4
+                    let vec4Value = inputBlock.value as Vector4;
                     value = `(${vec4Value.x.toFixed(2)}, ${vec4Value.y.toFixed(2)}, ${vec4Value.z.toFixed(2)}, ${vec4Value.w.toFixed(2)})`;
-                    break;                        
+                    break;
             }
         }
 

+ 4 - 3
nodeEditor/src/diagram/display/textureDisplayManager.ts

@@ -5,6 +5,7 @@ import { RefractionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/refractionB
 import { ReflectionTextureBlock } from 'babylonjs/Materials/Node/Blocks/Dual/reflectionTextureBlock';
 import { TextureLineComponent } from '../../sharedComponents/textureLineComponent';
 import { CurrentScreenBlock } from 'babylonjs/Materials/Node/Blocks/Dual/currentScreenBlock';
+import { ParticleTextureBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleTextureBlock';
 
 export class TextureDisplayManager implements IDisplayManager {
     private _previewCanvas: HTMLCanvasElement;
@@ -26,12 +27,12 @@ export class TextureDisplayManager implements IDisplayManager {
         return "#323232";
     }
 
-    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {       
-        const textureBlock = block as TextureBlock | ReflectionTextureBlock | RefractionBlock;
+    public updatePreviewContent(block: NodeMaterialBlock, contentArea: HTMLDivElement): void {
+        const textureBlock = block as TextureBlock | ReflectionTextureBlock | RefractionBlock | CurrentScreenBlock;
 
         if (!this._previewCanvas) {
             contentArea.classList.add("texture-block");
-            if (block instanceof TextureBlock || block instanceof CurrentScreenBlock) {
+            if (block instanceof TextureBlock || block instanceof CurrentScreenBlock || block instanceof ParticleTextureBlock) {
                 contentArea.classList.add("regular-texture-block");
             }
 

+ 1 - 0
nodeEditor/src/diagram/displayLedger.ts

@@ -23,4 +23,5 @@ DisplayLedger.RegisteredControls["ReflectionTextureBlock"] = TextureDisplayManag
 DisplayLedger.RegisteredControls["ReflectionBlock"] = TextureDisplayManager;
 DisplayLedger.RegisteredControls["RefractionBlock"] = TextureDisplayManager;
 DisplayLedger.RegisteredControls["CurrentScreenBlock"] = TextureDisplayManager;
+DisplayLedger.RegisteredControls["ParticleTextureBlock"] = TextureDisplayManager;
 DisplayLedger.RegisteredControls["DiscardBlock"] = DiscardDisplayManager;

+ 3 - 2
nodeEditor/src/diagram/properties/texturePropertyTabComponent.tsx

@@ -18,12 +18,13 @@ import { ReflectionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/reflectionB
 import { RefractionBlock } from 'babylonjs/Materials/Node/Blocks/PBR/refractionBlock';
 import { TextureBlock } from 'babylonjs/Materials/Node/Blocks/Dual/textureBlock';
 import { CurrentScreenBlock } from 'babylonjs/Materials/Node/Blocks/Dual/currentScreenBlock';
+import { ParticleTextureBlock } from 'babylonjs/Materials/Node/Blocks/Particle/particleTextureBlock';
 import { GeneralPropertyTabComponent, GenericPropertyTabComponent } from './genericNodePropertyComponent';
 import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
 
 type ReflectionTexture = ReflectionTextureBlock | ReflectionBlock | RefractionBlock;
 
-type AnyTexture = TextureBlock | ReflectionTexture | CurrentScreenBlock;
+type AnyTexture = TextureBlock | ReflectionTexture | CurrentScreenBlock | ParticleTextureBlock;
 
 export class TexturePropertyTabComponent extends React.Component<IPropertyComponentProps, {isEmbedded: boolean, loadAsCubeTexture: boolean}> {
 
@@ -153,7 +154,7 @@ export class TexturePropertyTabComponent extends React.Component<IPropertyCompon
         url = url.replace(/\?nocache=\d+/, "");
 
         let isInReflectionMode = this.textureBlock instanceof ReflectionTextureBlock || this.textureBlock instanceof ReflectionBlock || this.textureBlock instanceof RefractionBlock;
-        let isFrozenTexture = this.textureBlock instanceof CurrentScreenBlock;
+        let isFrozenTexture = this.textureBlock instanceof CurrentScreenBlock || this.textureBlock instanceof ParticleTextureBlock;
 
         var reflectionModeOptions: {label: string, value: number}[] = [
             {

+ 1 - 0
nodeEditor/src/diagram/propertyLedger.ts

@@ -22,4 +22,5 @@ PropertyLedger.RegisteredControls["ReflectionTextureBlock"] = TexturePropertyTab
 PropertyLedger.RegisteredControls["ReflectionBlock"] = TexturePropertyTabComponent;
 PropertyLedger.RegisteredControls["RefractionBlock"] = TexturePropertyTabComponent;
 PropertyLedger.RegisteredControls["CurrentScreenBlock"] = TexturePropertyTabComponent;
+PropertyLedger.RegisteredControls["ParticleTextureBlock"] = TexturePropertyTabComponent;
 PropertyLedger.RegisteredControls["TrigonometryBlock"] = TrigonometryPropertyTabComponent;

+ 7 - 6
nodeEditor/src/globalState.ts

@@ -3,7 +3,7 @@ import { Nullable } from "babylonjs/types";
 import { Observable } from 'babylonjs/Misc/observable';
 import { LogEntry } from './components/log/logComponent';
 import { NodeMaterialBlock } from 'babylonjs/Materials/Node/nodeMaterialBlock';
-import { PreviewMeshType } from './components/preview/previewMeshType';
+import { PreviewType } from './components/preview/previewType';
 import { DataStorage } from 'babylonjs/Misc/dataStorage';
 import { Color4 } from 'babylonjs/Maths/math.color';
 import { GraphNode } from './diagram/graphNode';
@@ -44,9 +44,9 @@ export class GlobalState {
     onGetNodeFromBlock: (block: NodeMaterialBlock) => GraphNode;
     onGridSizeChanged = new Observable<void>();
     onExposePortOnFrameObservable = new Observable<GraphNode>();
-    previewMeshType: PreviewMeshType;
-    previewMeshFile: File;
-    listOfCustomPreviewMeshFiles: File[] = [];
+    previewType: PreviewType;
+    previewFile: File;
+    listOfCustomPreviewFiles: File[] = [];
     rotatePreview: boolean;
     backgroundColor: Color4;
     backFaceCulling: boolean;
@@ -66,6 +66,7 @@ export class GlobalState {
 
     /** Sets the mode */
     public set mode(m: NodeMaterialModes) {
+        DataStorage.WriteNumber("Mode", m);
         this._mode = m;
         this.onPreviewCommandActivated.notifyObservers(true);
     }
@@ -73,14 +74,14 @@ export class GlobalState {
     customSave?: {label: string, action: (data: string) => Promise<void>};
 
     public constructor() {
-        this.previewMeshType = DataStorage.ReadNumber("PreviewMeshType", PreviewMeshType.Box);
+        this.previewType = DataStorage.ReadNumber("PreviewType", PreviewType.Box);
         this.backFaceCulling = DataStorage.ReadBoolean("BackFaceCulling", true);
         this.depthPrePass = DataStorage.ReadBoolean("DepthPrePass", false);
         this.hemisphericLight = DataStorage.ReadBoolean("HemisphericLight", true);
         this.directionalLight0 = DataStorage.ReadBoolean("DirectionalLight0", false);
         this.directionalLight1 = DataStorage.ReadBoolean("DirectionalLight1", false);
         this.controlCamera = DataStorage.ReadBoolean("ControlCamera", true);
-        this._mode = NodeMaterialModes.Material;
+        this._mode = DataStorage.ReadNumber("Mode", NodeMaterialModes.Material);
 
         let r = DataStorage.ReadNumber("BackgroundColorR", 0.12549019607843137);
         let g = DataStorage.ReadNumber("BackgroundColorG", 0.09803921568627451);

+ 4 - 3
nodeEditor/src/nodeEditor.ts

@@ -6,8 +6,9 @@ import { NodeMaterial } from "babylonjs/Materials/Node/nodeMaterial"
 import { Popup } from "../src/sharedComponents/popup"
 import { SerializationTools } from './serializationTools';
 import { Observable } from 'babylonjs/Misc/observable';
-import { PreviewMeshType } from './components/preview/previewMeshType';
+import { PreviewType } from './components/preview/previewType';
 import { DataStorage } from 'babylonjs/Misc/dataStorage';
+import { NodeMaterialModes } from 'babylonjs/Materials/Node/Enums/nodeMaterialModes';
 /**
  * Interface used to specify creation options for the node editor
  */
@@ -82,8 +83,8 @@ export class NodeEditor {
             };
         }
         window.addEventListener('beforeunload', () => {
-            if(DataStorage.ReadNumber("PreviewMeshType", PreviewMeshType.Box) === PreviewMeshType.Custom){
-                DataStorage.WriteNumber("PreviewMeshType", PreviewMeshType.Box)
+            if(DataStorage.ReadNumber("PreviewType", PreviewType.Box) === PreviewType.Custom){
+                DataStorage.WriteNumber("PreviewType", globalState.mode === NodeMaterialModes.Material ? PreviewType.Box : PreviewType.Bubbles);
             }
         });
     }

+ 1 - 1
nodeEditor/src/sharedComponents/color3LineComponent.tsx

@@ -148,7 +148,7 @@ export class Color3LineComponent extends React.Component<IColor3LineComponentPro
                         {this.props.label}
                     </div>
                     <div className="color3">
-                        <ColorPickerLineComponent value={this.state.color} disableAlpha={true} onColorChanged={color => {
+                        <ColorPickerLineComponent value={this.state.color} disableAlpha={true} globalState={this.props.globalState} onColorChanged={color => {
                                 this.onChange(color);
                             }} />  
                     </div>

+ 1 - 1
nodeEditor/src/sharedComponents/color4LineComponent.tsx

@@ -159,7 +159,7 @@ export class Color4LineComponent extends React.Component<IColor4LineComponentPro
                         {this.props.label}
                     </div>
                     <div className="color3">
-                        <ColorPickerLineComponent value={this.state.color} onColorChanged={color => {
+                        <ColorPickerLineComponent globalState={this.props.globalState} value={this.state.color} onColorChanged={color => {
                                 this.onChange(color);
                             }} />  
                     </div>

+ 9 - 2
nodeEditor/src/sharedComponents/colorPickerComponent.tsx

@@ -1,10 +1,12 @@
 import * as React from "react";
 import { Color4, Color3 } from 'babylonjs/Maths/math.color';
 import { SketchPicker } from 'react-color';
+import { GlobalState } from '../globalState';
 
 export interface IColorPickerComponentProps {
     value: Color4 | Color3;
     onColorChanged: (newOne: string) => void;
+    globalState: GlobalState;
     disableAlpha?: boolean;
 }
 
@@ -71,6 +73,11 @@ export class ColorPickerLineComponent extends React.Component<IColorPickerCompon
         this.syncPositions();
     }
 
+    setPickerState(enabled: boolean) {
+        this.setState({ pickerEnabled: enabled });
+        this.props.globalState.blockKeyboardEvents = enabled;
+    }
+
     render() {
         var color = this.state.color;
 
@@ -78,13 +85,13 @@ export class ColorPickerLineComponent extends React.Component<IColorPickerCompon
             <div className="color-picker">
                 <div className="color-rect"  ref={this._floatHostRef} 
                     style={{background: this.state.hex}} 
-                    onClick={() => this.setState({pickerEnabled: true})}>
+                    onClick={() => this.setPickerState(true)}>
 
                 </div>
                 {
                     this.state.pickerEnabled &&
                     <>
-                        <div className="color-picker-cover" onClick={() => this.setState({pickerEnabled: false})}></div>
+                        <div className="color-picker-cover" onClick={() => this.setPickerState(false)}></div>
                         <div className="color-picker-float" ref={this._floatRef}>
                             <SketchPicker color={color} 
                                 disableAlpha={this.props.disableAlpha}

+ 13 - 6
src/Materials/Node/Blocks/Dual/textureBlock.ts

@@ -30,6 +30,7 @@ export class TextureBlock extends NodeMaterialBlock {
     private _textureInfoName: string;
     private _mainUVName: string;
     private _mainUVDefineName: string;
+    private _fragmentOnly: boolean;
 
     /**
      * Gets or sets the texture associated with the node
@@ -50,9 +51,11 @@ export class TextureBlock extends NodeMaterialBlock {
      * Create a new TextureBlock
      * @param name defines the block name
      */
-    public constructor(name: string) {
+    public constructor(name: string, fragmentOnly = false) {
         super(name, NodeMaterialBlockTargets.VertexAndFragment);
 
+        this._fragmentOnly = fragmentOnly;
+
         this.registerInput("uv", NodeMaterialBlockConnectionPointTypes.Vector2, false, NodeMaterialBlockTargets.VertexAndFragment);
 
         this.registerOutput("rgba", NodeMaterialBlockConnectionPointTypes.Color4, NodeMaterialBlockTargets.Neutral);
@@ -65,7 +68,7 @@ export class TextureBlock extends NodeMaterialBlock {
         this._inputs[0].acceptedConnectionPointTypes.push(NodeMaterialBlockConnectionPointTypes.Vector3);
         this._inputs[0].acceptedConnectionPointTypes.push(NodeMaterialBlockConnectionPointTypes.Vector4);
 
-        this._inputs[0]._prioritizeVertex = true;
+        this._inputs[0]._prioritizeVertex = !fragmentOnly;
     }
 
     /**
@@ -173,11 +176,13 @@ export class TextureBlock extends NodeMaterialBlock {
                     uvInput.connectTo(this);
                 }
             } else {
-                let uvInput = material.getInputBlockByPredicate((b) => b.isAttribute && b.name === "uv");
+                const attributeName = material.mode === NodeMaterialModes.Particle ? "particle_uv" : "uv";
+
+                let uvInput = material.getInputBlockByPredicate((b) => b.isAttribute && b.name === attributeName);
 
                 if (!uvInput) {
                     uvInput = new InputBlock("uv");
-                    uvInput.setAsAttribute();
+                    uvInput.setAsAttribute(attributeName);
                 }
                 uvInput.output.connectTo(this.uv);
             }
@@ -295,7 +300,7 @@ export class TextureBlock extends NodeMaterialBlock {
             return;
         }
 
-        if (this.uv.ownerBlock.target === NodeMaterialBlockTargets.Fragment) {
+        if (this.uv.ownerBlock.target === NodeMaterialBlockTargets.Fragment || this._fragmentOnly) {
             state.compilationString += `vec4 ${this._tempTextureRead} = texture2D(${this._samplerName}, ${uvInput.associatedVariableName});\r\n`;
             return;
         }
@@ -339,7 +344,7 @@ export class TextureBlock extends NodeMaterialBlock {
     protected _buildBlock(state: NodeMaterialBuildState) {
         super._buildBlock(state);
 
-        if (state.target === NodeMaterialBlockTargets.Vertex) {
+        if (state.target === NodeMaterialBlockTargets.Vertex || this._fragmentOnly) {
             this._tempTextureRead = state._getFreeVariableName("tempTextureRead");
         }
 
@@ -418,6 +423,7 @@ export class TextureBlock extends NodeMaterialBlock {
 
         serializationObject.convertToGammaSpace = this.convertToGammaSpace;
         serializationObject.convertToLinearSpace = this.convertToLinearSpace;
+        serializationObject.fragmentOnly = this._fragmentOnly;
         if (this.texture) {
             serializationObject.texture = this.texture.serialize();
         }
@@ -430,6 +436,7 @@ export class TextureBlock extends NodeMaterialBlock {
 
         this.convertToGammaSpace = serializationObject.convertToGammaSpace;
         this.convertToLinearSpace = !!serializationObject.convertToLinearSpace;
+        this._fragmentOnly = !!serializationObject.fragmentOnly;
 
         if (serializationObject.texture && !NodeMaterial.IgnoreTexturesAtLoadTime) {
             rootUrl = serializationObject.texture.url.indexOf("data:") === 0 ? "" : rootUrl;

+ 50 - 8
src/Materials/Node/Blocks/Input/inputBlock.ts

@@ -15,6 +15,23 @@ import { AnimatedInputBlockTypes } from './animatedInputBlockTypes';
 import { Observable } from '../../../../Misc/observable';
 import { MaterialHelper } from '../../../../Materials/materialHelper';
 
+const remapAttributeName: { [name: string]: string }  = {
+    "position2d": "position",
+    "particle_uv": "vUV",
+    "particle_color": "vColor",
+    "particle_texturemask": "textureMask",
+};
+
+const attributeInFragmentOnly: { [name: string]: boolean }  = {
+    "particle_uv": true,
+    "particle_color": true,
+    "particle_texturemask": true,
+};
+
+const attributeAsUniform: { [name: string]: boolean }  = {
+    "particle_texturemask": true,
+};
+
 /**
  * Block used to expose an input value
  */
@@ -96,6 +113,7 @@ export class InputBlock extends NodeMaterialBlock {
                     case "uv":
                     case "uv2":
                     case "position2d":
+                    case "particle_uv":
                         this._type = NodeMaterialBlockConnectionPointTypes.Vector2;
                         return this._type;
                     case "matricesIndices":
@@ -107,6 +125,8 @@ export class InputBlock extends NodeMaterialBlock {
                         this._type = NodeMaterialBlockConnectionPointTypes.Vector4;
                         return this._type;
                     case "color":
+                    case "particle_color":
+                    case "particle_texturemask":
                         this._type = NodeMaterialBlockConnectionPointTypes.Color4;
                         return this._type;
                 }
@@ -393,6 +413,11 @@ export class InputBlock extends NodeMaterialBlock {
         return "";
     }
 
+    /** @hidden */
+    public get _noContextSwitch(): boolean {
+        return attributeInFragmentOnly[this.name];
+    }
+
     private _emit(state: NodeMaterialBuildState, define?: string) {
         // Uniforms
         if (this.isUniform) {
@@ -444,10 +469,18 @@ export class InputBlock extends NodeMaterialBlock {
 
         // Attribute
         if (this.isAttribute) {
-            this.associatedVariableName = this.name === 'position2d' ? 'position' : this.name;
+            this.associatedVariableName = remapAttributeName[this.name] ?? this.name;
 
             if (this.target === NodeMaterialBlockTargets.Vertex && state._vertexState) { // Attribute for fragment need to be carried over by varyings
-                this._emit(state._vertexState, define);
+                if (attributeInFragmentOnly[this.name]) {
+                    if (attributeAsUniform[this.name]) {
+                        state._emitUniformFromString(this.associatedVariableName, state._getGLType(this.type), define);
+                    } else {
+                        state._emitVaryingFromString(this.associatedVariableName, state._getGLType(this.type), define);
+                    }
+                } else {
+                    this._emit(state._vertexState, define);
+                }
                 return;
             }
 
@@ -456,12 +489,21 @@ export class InputBlock extends NodeMaterialBlock {
             }
 
             state.attributes.push(this.associatedVariableName);
-            if (define) {
-                state._attributeDeclaration += this._emitDefine(define);
-            }
-            state._attributeDeclaration += `attribute ${state._getGLType(this.type)} ${this.associatedVariableName};\r\n`;
-            if (define) {
-                state._attributeDeclaration += `#endif\r\n`;
+
+            if (attributeInFragmentOnly[this.name]) {
+                if (attributeAsUniform[this.name]) {
+                    state._emitUniformFromString(this.associatedVariableName, state._getGLType(this.type), define);
+                } else {
+                    state._emitVaryingFromString(this.associatedVariableName, state._getGLType(this.type), define);
+                }
+            } else {
+                if (define) {
+                    state._attributeDeclaration += this._emitDefine(define);
+                }
+                state._attributeDeclaration += `attribute ${state._getGLType(this.type)} ${this.associatedVariableName};\r\n`;
+                if (define) {
+                    state._attributeDeclaration += `#endif\r\n`;
+                }
             }
         }
     }

+ 4 - 0
src/Materials/Node/Blocks/Particle/index.ts

@@ -0,0 +1,4 @@
+
+export * from "./particleTextureBlock";
+export * from "./particleRampGradientBlock";
+export * from "./particleBlendMultiplyBlock";

+ 95 - 0
src/Materials/Node/Blocks/Particle/particleBlendMultiplyBlock.ts

@@ -0,0 +1,95 @@
+import { NodeMaterialBlock } from '../../nodeMaterialBlock';
+import { NodeMaterialBlockConnectionPointTypes } from '../../Enums/nodeMaterialBlockConnectionPointTypes';
+import { NodeMaterialBuildState } from '../../nodeMaterialBuildState';
+import { NodeMaterialBlockTargets } from '../../Enums/nodeMaterialBlockTargets';
+import { NodeMaterialConnectionPoint } from '../../nodeMaterialBlockConnectionPoint';
+import { _TypeStore } from '../../../../Misc/typeStore';
+
+/**
+  * Block used for the particle blend multiply section
+  */
+export class ParticleBlendMultiplyBlock extends NodeMaterialBlock {
+
+    /**
+     * Create a new ParticleBlendMultiplyBlock
+     * @param name defines the block name
+     */
+    public constructor(name: string) {
+        super(name, NodeMaterialBlockTargets.Fragment);
+
+        this._isUnique = true;
+
+        this.registerInput("color", NodeMaterialBlockConnectionPointTypes.Color4, false, NodeMaterialBlockTargets.Fragment);
+        this.registerInput("alphaTexture", NodeMaterialBlockConnectionPointTypes.Float, false, NodeMaterialBlockTargets.Fragment);
+        this.registerInput("alphaColor", NodeMaterialBlockConnectionPointTypes.Float, false, NodeMaterialBlockTargets.Fragment);
+
+        this.registerOutput("blendColor", NodeMaterialBlockConnectionPointTypes.Color4, NodeMaterialBlockTargets.Fragment);
+    }
+
+    /**
+     * Gets the current class name
+     * @returns the class name
+     */
+    public getClassName() {
+        return "ParticleBlendMultiplyBlock";
+    }
+
+    /**
+     * Gets the color input component
+     */
+    public get color(): NodeMaterialConnectionPoint {
+        return this._inputs[0];
+    }
+
+    /**
+     * Gets the alphaTexture input component
+     */
+    public get alphaTexture(): NodeMaterialConnectionPoint {
+        return this._inputs[1];
+    }
+
+    /**
+     * Gets the alphaColor input component
+     */
+    public get alphaColor(): NodeMaterialConnectionPoint {
+        return this._inputs[2];
+    }
+
+    /**
+     * Gets the blendColor output component
+     */
+    public get blendColor(): NodeMaterialConnectionPoint {
+        return this._outputs[0];
+    }
+
+    /**
+     * Initialize the block and prepare the context for build
+     * @param state defines the state that will be used for the build
+     */
+    public initialize(state: NodeMaterialBuildState) {
+        state._excludeVariableName("sourceAlpha");
+    }
+
+    protected _buildBlock(state: NodeMaterialBuildState) {
+        super._buildBlock(state);
+
+        if (state.target === NodeMaterialBlockTargets.Vertex) {
+            return;
+        }
+
+        state.compilationString += `
+            #ifdef BLENDMULTIPLYMODE
+                ${this._declareOutput(this.blendColor, state)};
+                float sourceAlpha = ${this.alphaColor.associatedVariableName} * ${this.alphaTexture.associatedVariableName};
+                ${this.blendColor.associatedVariableName}.rgb = ${this.color.associatedVariableName}.rgb * sourceAlpha + vec3(1.0) * (1.0 - sourceAlpha);
+                ${this.blendColor.associatedVariableName}.a = ${this.color.associatedVariableName}.a;
+            #else
+                ${this._declareOutput(this.blendColor, state)} = ${this.color.associatedVariableName};
+            #endif
+        `;
+
+        return this;
+    }
+}
+
+_TypeStore.RegisteredTypes["BABYLON.ParticleBlendMultiplyBlock"] = ParticleBlendMultiplyBlock;

+ 97 - 0
src/Materials/Node/Blocks/Particle/particleRampGradientBlock.ts

@@ -0,0 +1,97 @@
+import { NodeMaterialBlock } from '../../nodeMaterialBlock';
+import { NodeMaterialBlockConnectionPointTypes } from '../../Enums/nodeMaterialBlockConnectionPointTypes';
+import { NodeMaterialBuildState } from '../../nodeMaterialBuildState';
+import { NodeMaterialBlockTargets } from '../../Enums/nodeMaterialBlockTargets';
+import { NodeMaterialConnectionPoint } from '../../nodeMaterialBlockConnectionPoint';
+import { _TypeStore } from '../../../../Misc/typeStore';
+
+/**
+ * Block used for the particle ramp gradient section
+ */
+export class ParticleRampGradientBlock extends NodeMaterialBlock {
+
+    /**
+     * Create a new ParticleRampGradientBlock
+     * @param name defines the block name
+     */
+    public constructor(name: string) {
+        super(name, NodeMaterialBlockTargets.Fragment);
+
+        this._isUnique = true;
+
+        this.registerInput("color", NodeMaterialBlockConnectionPointTypes.Color4, false, NodeMaterialBlockTargets.Fragment);
+
+        this.registerOutput("rampColor", NodeMaterialBlockConnectionPointTypes.Color4, NodeMaterialBlockTargets.Fragment);
+    }
+
+    /**
+     * Gets the current class name
+     * @returns the class name
+     */
+    public getClassName() {
+        return "ParticleRampGradientBlock";
+    }
+
+    /**
+     * Gets the color input component
+     */
+    public get color(): NodeMaterialConnectionPoint {
+        return this._inputs[0];
+    }
+
+    /**
+     * Gets the rampColor output component
+     */
+    public get rampColor(): NodeMaterialConnectionPoint {
+        return this._outputs[0];
+    }
+
+    /**
+     * Initialize the block and prepare the context for build
+     * @param state defines the state that will be used for the build
+     */
+    public initialize(state: NodeMaterialBuildState) {
+        state._excludeVariableName("remapRanges");
+        state._excludeVariableName("rampSampler");
+        state._excludeVariableName("baseColor");
+        state._excludeVariableName("alpha");
+        state._excludeVariableName("remappedColorIndex");
+        state._excludeVariableName("rampColor");
+        state._excludeVariableName("finalAlpha");
+    }
+
+    protected _buildBlock(state: NodeMaterialBuildState) {
+        super._buildBlock(state);
+
+        if (state.target === NodeMaterialBlockTargets.Vertex) {
+            return;
+        }
+
+        state._emit2DSampler("rampSampler");
+        state._emitVaryingFromString("remapRanges", "vec4", "RAMPGRADIENT");
+
+        state.compilationString += `
+            #ifdef RAMPGRADIENT
+                vec4 baseColor = ${this.color.associatedVariableName};
+                float alpha = ${this.color.associatedVariableName}.a;
+
+                float remappedColorIndex = clamp((alpha - remapRanges.x) / remapRanges.y, 0.0, 1.0);
+
+                vec4 rampColor = texture2D(rampSampler, vec2(1.0 - remappedColorIndex, 0.));
+                baseColor.rgb *= rampColor.rgb;
+
+                // Remapped alpha
+                float finalAlpha = baseColor.a;
+                baseColor.a = clamp((alpha * rampColor.a - remapRanges.z) / remapRanges.w, 0.0, 1.0);
+
+                ${this._declareOutput(this.rampColor, state)} = baseColor;
+            #else
+                ${this._declareOutput(this.rampColor, state)} = ${this.color.associatedVariableName};
+            #endif
+        `;
+
+        return this;
+    }
+}
+
+_TypeStore.RegisteredTypes["BABYLON.ParticleRampGradientBlock"] = ParticleRampGradientBlock;

+ 223 - 0
src/Materials/Node/Blocks/Particle/particleTextureBlock.ts

@@ -0,0 +1,223 @@
+import { NodeMaterialBlock } from '../../nodeMaterialBlock';
+import { NodeMaterialBlockConnectionPointTypes } from '../../Enums/nodeMaterialBlockConnectionPointTypes';
+import { NodeMaterialBuildState } from '../../nodeMaterialBuildState';
+import { NodeMaterialBlockTargets } from '../../Enums/nodeMaterialBlockTargets';
+import { NodeMaterialConnectionPoint } from '../../nodeMaterialBlockConnectionPoint';
+import { AbstractMesh } from '../../../../Meshes/abstractMesh';
+import { NodeMaterialDefines } from '../../nodeMaterial';
+import { InputBlock } from '../Input/inputBlock';
+import { BaseTexture } from '../../../Textures/baseTexture';
+import { Nullable } from '../../../../types';
+import { _TypeStore } from '../../../../Misc/typeStore';
+import { Texture } from '../../../Textures/texture';
+import { Scene } from '../../../../scene';
+
+declare type NodeMaterial = import("../../nodeMaterial").NodeMaterial;
+
+/**
+ * Base block used for the particle texture
+ */
+export class ParticleTextureBlock extends NodeMaterialBlock {
+
+    private _samplerName = "diffuseSampler";
+    private _linearDefineName: string;
+    private _gammaDefineName: string;
+    private _tempTextureRead: string;
+
+    /**
+     * Gets or sets the texture associated with the node
+     */
+    public texture: Nullable<BaseTexture>;
+
+    /**
+     * Gets or sets a boolean indicating if content needs to be converted to gamma space
+     */
+    public convertToGammaSpace = false;
+
+    /**
+     * Gets or sets a boolean indicating if content needs to be converted to linear space
+     */
+    public convertToLinearSpace = false;
+
+    /**
+     * Create a new ParticleTextureBlock
+     * @param name defines the block name
+     */
+    public constructor(name: string) {
+        super(name, NodeMaterialBlockTargets.Fragment);
+
+        this._isUnique = true;
+
+        this.registerInput("uv", NodeMaterialBlockConnectionPointTypes.Vector2, false, NodeMaterialBlockTargets.VertexAndFragment);
+
+        this.registerOutput("rgba", NodeMaterialBlockConnectionPointTypes.Color4, NodeMaterialBlockTargets.Neutral);
+        this.registerOutput("rgb", NodeMaterialBlockConnectionPointTypes.Color3, NodeMaterialBlockTargets.Neutral);
+        this.registerOutput("r", NodeMaterialBlockConnectionPointTypes.Float, NodeMaterialBlockTargets.Neutral);
+        this.registerOutput("g", NodeMaterialBlockConnectionPointTypes.Float, NodeMaterialBlockTargets.Neutral);
+        this.registerOutput("b", NodeMaterialBlockConnectionPointTypes.Float, NodeMaterialBlockTargets.Neutral);
+        this.registerOutput("a", NodeMaterialBlockConnectionPointTypes.Float, NodeMaterialBlockTargets.Neutral);
+
+        this._inputs[0].acceptedConnectionPointTypes.push(NodeMaterialBlockConnectionPointTypes.Vector3);
+        this._inputs[0].acceptedConnectionPointTypes.push(NodeMaterialBlockConnectionPointTypes.Vector4);
+    }
+
+    /**
+     * Gets the current class name
+     * @returns the class name
+     */
+    public getClassName() {
+        return "ParticleTextureBlock";
+    }
+
+    /**
+     * Gets the uv input component
+     */
+    public get uv(): NodeMaterialConnectionPoint {
+        return this._inputs[0];
+    }
+
+    /**
+     * Gets the rgba output component
+     */
+    public get rgba(): NodeMaterialConnectionPoint {
+        return this._outputs[0];
+    }
+
+    /**
+     * Gets the rgb output component
+     */
+    public get rgb(): NodeMaterialConnectionPoint {
+        return this._outputs[1];
+    }
+
+    /**
+     * Gets the r output component
+     */
+    public get r(): NodeMaterialConnectionPoint {
+        return this._outputs[2];
+    }
+
+    /**
+     * Gets the g output component
+     */
+    public get g(): NodeMaterialConnectionPoint {
+        return this._outputs[3];
+    }
+
+    /**
+     * Gets the b output component
+     */
+    public get b(): NodeMaterialConnectionPoint {
+        return this._outputs[4];
+    }
+
+    /**
+     * Gets the a output component
+     */
+    public get a(): NodeMaterialConnectionPoint {
+        return this._outputs[5];
+    }
+
+    /**
+     * Initialize the block and prepare the context for build
+     * @param state defines the state that will be used for the build
+     */
+    public initialize(state: NodeMaterialBuildState) {
+        state._excludeVariableName("diffuseSampler");
+    }
+
+    public autoConfigure(material: NodeMaterial) {
+        if (!this.uv.isConnected) {
+            let uvInput = material.getInputBlockByPredicate((b) => b.isAttribute && b.name === "particle_uv");
+
+            if (!uvInput) {
+                uvInput = new InputBlock("uv");
+                uvInput.setAsAttribute("particle_uv");
+            }
+            uvInput.output.connectTo(this.uv);
+        }
+    }
+
+    public prepareDefines(mesh: AbstractMesh, nodeMaterial: NodeMaterial, defines: NodeMaterialDefines) {
+        defines.setValue(this._linearDefineName, this.convertToGammaSpace, true);
+        defines.setValue(this._gammaDefineName, this.convertToLinearSpace, true);
+    }
+
+    public isReady() {
+        if (this.texture && !this.texture.isReadyOrNotBlocking()) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private _writeOutput(state: NodeMaterialBuildState, output: NodeMaterialConnectionPoint, swizzle: string) {
+        state.compilationString += `${this._declareOutput(output, state)} = ${this._tempTextureRead}.${swizzle};\r\n`;
+
+        state.compilationString += `#ifdef ${this._linearDefineName}\r\n`;
+        state.compilationString += `${output.associatedVariableName} = toGammaSpace(${output.associatedVariableName});\r\n`;
+        state.compilationString += `#endif\r\n`;
+
+        state.compilationString += `#ifdef ${this._gammaDefineName}\r\n`;
+        state.compilationString += `${output.associatedVariableName} = toLinearSpace(${output.associatedVariableName});\r\n`;
+        state.compilationString += `#endif\r\n`;
+    }
+
+    protected _buildBlock(state: NodeMaterialBuildState) {
+        super._buildBlock(state);
+
+        if (state.target === NodeMaterialBlockTargets.Vertex) {
+            return;
+        }
+
+        this._tempTextureRead = state._getFreeVariableName("tempTextureRead");
+
+        state._emit2DSampler(this._samplerName);
+
+        state.sharedData.blockingBlocks.push(this);
+        state.sharedData.textureBlocks.push(this);
+        state.sharedData.blocksWithDefines.push(this);
+
+        this._linearDefineName = state._getFreeDefineName("ISLINEAR");
+        this._gammaDefineName = state._getFreeDefineName("ISGAMMA");
+
+        let comments = `//${this.name}`;
+        state._emitFunctionFromInclude("helperFunctions", comments);
+
+        state.compilationString += `vec4 ${this._tempTextureRead} = texture2D(${this._samplerName}, ${this.uv.associatedVariableName});\r\n`;
+
+        for (var output of this._outputs) {
+            if (output.hasEndpoints) {
+                this._writeOutput(state, output, output.name);
+            }
+        }
+
+        return this;
+    }
+
+    public serialize(): any {
+        let serializationObject = super.serialize();
+
+        serializationObject.convertToGammaSpace = this.convertToGammaSpace;
+        serializationObject.convertToLinearSpace = this.convertToLinearSpace;
+        if (this.texture) {
+            serializationObject.texture = this.texture.serialize();
+        }
+
+        return serializationObject;
+    }
+
+    public _deserialize(serializationObject: any, scene: Scene, rootUrl: string) {
+        super._deserialize(serializationObject, scene, rootUrl);
+
+        this.convertToGammaSpace = serializationObject.convertToGammaSpace;
+        this.convertToLinearSpace = !!serializationObject.convertToLinearSpace;
+
+        if (serializationObject.texture) {
+            rootUrl = serializationObject.texture.url.indexOf("data:") === 0 ? "" : rootUrl;
+            this.texture = Texture.Parse(serializationObject.texture, scene, rootUrl) as Texture;
+        }
+    }
+}
+
+_TypeStore.RegisteredTypes["BABYLON.ParticleTextureBlock"] = ParticleTextureBlock;

+ 1 - 0
src/Materials/Node/Blocks/index.ts

@@ -46,3 +46,4 @@ export * from "./reflectBlock";
 export * from "./refractBlock";
 export * from "./desaturateBlock";
 export * from "./PBR/index";
+export * from "./Particle/index";

+ 2 - 0
src/Materials/Node/Enums/nodeMaterialModes.ts

@@ -6,4 +6,6 @@ export enum NodeMaterialModes {
     Material = 0,
     /** For post process */
     PostProcess = 1,
+    /** For particle system */
+    Particle = 2,
 }

+ 159 - 6
src/Materials/Node/nodeMaterial.ts

@@ -29,6 +29,9 @@ import { TextureBlock } from './Blocks/Dual/textureBlock';
 import { ReflectionTextureBaseBlock } from './Blocks/Dual/reflectionTextureBaseBlock';
 import { RefractionBlock } from './Blocks/PBR/refractionBlock';
 import { CurrentScreenBlock } from './Blocks/Dual/currentScreenBlock';
+import { ParticleTextureBlock } from './Blocks/Particle/particleTextureBlock';
+import { ParticleRampGradientBlock } from './Blocks/Particle/particleRampGradientBlock';
+import { ParticleBlendMultiplyBlock } from './Blocks/Particle/particleBlendMultiplyBlock';
 import { EffectFallbacks } from '../effectFallbacks';
 import { WebRequest } from '../../Misc/webRequest';
 import { Effect } from '../effect';
@@ -40,6 +43,9 @@ import { RemapBlock } from './Blocks/remapBlock';
 import { MultiplyBlock } from './Blocks/multiplyBlock';
 import { NodeMaterialModes } from './Enums/nodeMaterialModes';
 import { Texture } from '../Textures/texture';
+import { IParticleSystem } from '../../Particles/IParticleSystem';
+import { BaseParticleSystem } from '../../Particles/baseParticleSystem';
+import { ColorSplitterBlock } from './Blocks/colorSplitterBlock';
 
 const onCreatedEffectParameters = { effect: null as unknown as Effect, subMesh: null as unknown as Nullable<SubMesh> };
 
@@ -597,7 +603,9 @@ export class NodeMaterial extends PushMaterial {
         this._buildWasSuccessful = false;
         var engine = this.getScene().getEngine();
 
-        if (this._vertexOutputNodes.length === 0) {
+        const allowEmptyVertexProgram = this._mode === NodeMaterialModes.Particle;
+
+        if (this._vertexOutputNodes.length === 0 && !allowEmptyVertexProgram) {
             throw "You must define at least one vertexOutputNode";
         }
 
@@ -621,6 +629,7 @@ export class NodeMaterial extends PushMaterial {
         this._sharedData.emitComments = this._options.emitComments;
         this._sharedData.verbose = verbose;
         this._sharedData.scene = this.getScene();
+        this._sharedData.allowEmptyVertexProgram = allowEmptyVertexProgram;
 
         // Initialize blocks
         let vertexNodes: NodeMaterialBlock[] = [];
@@ -749,8 +758,7 @@ export class NodeMaterial extends PushMaterial {
 
         this._processDefines(dummyMesh, defines);
 
-        Effect.ShadersStore[tempName + "VertexShader"] = this._vertexCompilationState._builtCompilationString;
-        Effect.ShadersStore[tempName + "PixelShader"] = this._fragmentCompilationState._builtCompilationString;
+        Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString, this._vertexCompilationState._builtCompilationString);
 
         const postProcess = new PostProcess(
             this.name + "PostProcess", tempName, this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers,
@@ -772,8 +780,7 @@ export class NodeMaterial extends PushMaterial {
             const result = this._processDefines(dummyMesh, defines);
 
             if (result) {
-                Effect.ShadersStore[tempName + "VertexShader"] = this._vertexCompilationState._builtCompilationString;
-                Effect.ShadersStore[tempName + "PixelShader"] = this._fragmentCompilationState._builtCompilationString;
+                Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString, this._vertexCompilationState._builtCompilationString);
 
                 postProcess.updateEffect(defines.toString(), this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, { maxSimultaneousLights: this.maxSimultaneousLights }, undefined, undefined, tempName, tempName);
             }
@@ -807,6 +814,110 @@ export class NodeMaterial extends PushMaterial {
         return postProcess;
     }
 
+    private _createEffectForParticles(particleSystem: IParticleSystem, blendMode: number, onCompiled?: (effect: Effect) => void, onError?: (effect: Effect, errors: string) => void, effect?: Effect, defines?: NodeMaterialDefines, dummyMesh?: Nullable<AbstractMesh>) {
+        let tempName = this.name + this._buildId + "_" + blendMode;
+
+        if (!defines) {
+            defines = new NodeMaterialDefines();
+        }
+
+        if (!dummyMesh) {
+            dummyMesh = this.getScene().getMeshByName(this.name + "Particle");
+            if (!dummyMesh) {
+                dummyMesh = new AbstractMesh(this.name + "Particle", this.getScene());
+            }
+        }
+
+        let buildId = this._buildId;
+
+        let particleSystemDefines: Array<string> = [];
+        let particleSystemDefinesJoined = "";
+
+        if (!effect) {
+            const result = this._processDefines(dummyMesh, defines);
+
+            Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString);
+
+            particleSystem.fillDefines(particleSystemDefines, blendMode);
+
+            particleSystemDefinesJoined = particleSystemDefines.join("\n");
+
+            effect = this.getScene().getEngine().createEffectForParticles(tempName, this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, defines.toString() + "\n" + particleSystemDefinesJoined, result?.fallbacks, onCompiled, onError, particleSystem);
+
+            particleSystem.setCustomEffect(effect, blendMode);
+        }
+
+        effect.onBindObservable.add((effect) => {
+            if (buildId !== this._buildId) {
+                delete Effect.ShadersStore[tempName + "PixelShader"];
+
+                tempName = this.name + this._buildId + "_" + blendMode;
+
+                defines!.markAsUnprocessed();
+
+                buildId = this._buildId;
+            }
+
+            particleSystemDefines.length = 0;
+
+            particleSystem.fillDefines(particleSystemDefines, blendMode);
+
+            const particleSystemDefinesJoinedCurrent = particleSystemDefines.join("\n");
+
+            if (particleSystemDefinesJoinedCurrent !== particleSystemDefinesJoined) {
+                defines!.markAsUnprocessed();
+                particleSystemDefinesJoined = particleSystemDefinesJoinedCurrent;
+            }
+
+            const result = this._processDefines(dummyMesh!, defines!);
+
+            if (result) {
+                Effect.RegisterShader(tempName, this._fragmentCompilationState._builtCompilationString);
+
+                effect = this.getScene().getEngine().createEffectForParticles(tempName, this._fragmentCompilationState.uniforms, this._fragmentCompilationState.samplers, defines!.toString() + "\n" + particleSystemDefinesJoined, result?.fallbacks, onCompiled, onError, particleSystem);
+                particleSystem.setCustomEffect(effect, blendMode);
+                this._createEffectForParticles(particleSystem, blendMode, onCompiled, onError, effect, defines, dummyMesh); // add the effect.onBindObservable observer
+                return;
+            }
+
+            // Animated blocks
+            if (this._sharedData.animatedInputs) {
+                const scene = this.getScene();
+
+                let frameId = scene.getFrameId();
+
+                if (this._animationFrame !== frameId) {
+                    for (var input of this._sharedData.animatedInputs) {
+                        input.animate(scene);
+                    }
+
+                    this._animationFrame = frameId;
+                }
+            }
+
+            // Bindable blocks
+            for (var block of this._sharedData.bindableBlocks) {
+                block.bind(effect, this);
+            }
+
+            // Connection points
+            for (var inputBlock of this._sharedData.inputBlocks) {
+                inputBlock._transmit(effect, this.getScene());
+            }
+        });
+    }
+
+    /**
+     * Create the effect to be used as the custom effect for a particle system
+     * @param particleSystem Particle system to create the effect for
+     * @param onCompiled defines a function to call when the effect creation is successful
+     * @param onError defines a function to call when the effect creation has failed
+     */
+    public createEffectForParticles(particleSystem: IParticleSystem, onCompiled?: (effect: Effect) => void, onError?: (effect: Effect, errors: string) => void) {
+        this._createEffectForParticles(particleSystem, BaseParticleSystem.BLENDMODE_ONEONE, onCompiled, onError);
+        this._createEffectForParticles(particleSystem, BaseParticleSystem.BLENDMODE_MULTIPLY, onCompiled, onError);
+    }
+
     private _processDefines(mesh: AbstractMesh, defines: NodeMaterialDefines, useInstances = false): Nullable<{
         lightDisposed: boolean,
         uniformBuffers: string[],
@@ -1080,7 +1191,7 @@ export class NodeMaterial extends PushMaterial {
      * Gets the list of texture blocks
      * @returns an array of texture blocks
      */
-    public getTextureBlocks(): (TextureBlock | ReflectionTextureBaseBlock | RefractionBlock | CurrentScreenBlock)[] {
+    public getTextureBlocks(): (TextureBlock | ReflectionTextureBaseBlock | RefractionBlock | CurrentScreenBlock | ParticleTextureBlock)[] {
         if (!this._sharedData) {
             return [];
         }
@@ -1268,6 +1379,48 @@ export class NodeMaterial extends PushMaterial {
     }
 
     /**
+     * Clear the current material and set it to a default state for particle
+     */
+    public setToDefaultParticle() {
+        this.clear();
+
+        this.editorData = null;
+
+        // Pixel
+        const uv = new InputBlock("uv");
+        uv.setAsAttribute("particle_uv");
+
+        const texture = new ParticleTextureBlock("ParticleTexture");
+        uv.connectTo(texture);
+
+        const color = new InputBlock("Color");
+        color.setAsAttribute("particle_color");
+
+        const multiply = new MultiplyBlock("texture * color");
+        texture.connectTo(multiply);
+        color.connectTo(multiply);
+
+        const rampGradient = new ParticleRampGradientBlock("ParticleRampGradient");
+        multiply.connectTo(rampGradient);
+
+        const cSplitter = new ColorSplitterBlock("ColorSplitter");
+        color.connectTo(cSplitter);
+
+        const blendMultiply = new ParticleBlendMultiplyBlock("ParticleBlendMultiply");
+        rampGradient.connectTo(blendMultiply);
+        texture.connectTo(blendMultiply, { "output": "a" });
+        cSplitter.connectTo(blendMultiply, { "output": "a" });
+
+        const fragmentOutput = new FragmentOutputBlock("FragmentOutput");
+        blendMultiply.connectTo(fragmentOutput);
+
+        // Add to nodes
+        this.addOutputNode(fragmentOutput);
+
+        this._mode = NodeMaterialModes.Particle;
+    }
+
+    /**
      * Loads the current Node Material from a url pointing to a file save by the Node Material Editor
      * @param url defines the url to load from
      * @returns a promise that will fullfil when the material is fully loaded

+ 1 - 1
src/Materials/Node/nodeMaterialBlock.ts

@@ -425,7 +425,7 @@ export class NodeMaterialBlock {
             (this.target !== NodeMaterialBlockTargets.VertexAndFragment && otherBlockWasGeneratedInVertexShader)
             )) { // context switch! We need a varying
             if ((!block.isInput && state.target !== block._buildTarget) // block was already emitted by vertex shader
-                || (block.isInput && (block as InputBlock).isAttribute) // block is an attribute
+                || (block.isInput && (block as InputBlock).isAttribute && !(block as InputBlock)._noContextSwitch) // block is an attribute
             ) {
                 let connectedPoint = input.connectedPoint!;
                 if (state._vertexState._emitVaryingFromString("v_" + connectedPoint.associatedVariableName, state._getGLType(connectedPoint.type))) {

+ 8 - 2
src/Materials/Node/nodeMaterialBuildStateSharedData.ts

@@ -5,6 +5,7 @@ import { TextureBlock } from './Blocks/Dual/textureBlock';
 import { ReflectionTextureBaseBlock } from './Blocks/Dual/reflectionTextureBaseBlock';
 import { RefractionBlock } from './Blocks/PBR/refractionBlock';
 import { CurrentScreenBlock } from './Blocks/Dual/currentScreenBlock';
+import { ParticleTextureBlock } from './Blocks/Particle/particleTextureBlock';
 import { Scene } from '../../scene';
 
 /**
@@ -34,7 +35,7 @@ export class NodeMaterialBuildStateSharedData {
     /**
      * Input blocks
      */
-    public textureBlocks = new Array<TextureBlock | ReflectionTextureBaseBlock | RefractionBlock | CurrentScreenBlock>();
+    public textureBlocks = new Array<TextureBlock | ReflectionTextureBaseBlock | RefractionBlock | CurrentScreenBlock | ParticleTextureBlock>();
 
     /**
      * Bindable blocks (Blocks that need to set data to the effect)
@@ -110,6 +111,11 @@ export class NodeMaterialBuildStateSharedData {
         notConnectedNonOptionalInputs: new Array<NodeMaterialConnectionPoint>()
     };
 
+    /**
+     * Is vertex program allowed to be empty?
+     */
+    public allowEmptyVertexProgram: boolean = false;
+
     /** Creates a new shared data */
     public constructor() {
         // Exclude usual attributes from free variable names
@@ -153,7 +159,7 @@ export class NodeMaterialBuildStateSharedData {
     public emitErrors() {
         let errorMessage = "";
 
-        if (!this.checks.emitVertex) {
+        if (!this.checks.emitVertex && !this.allowEmptyVertexProgram) {
             errorMessage += "NodeMaterial does not have a vertex output. You need to at least add a block that generates a glPosition value.\r\n";
         }
         if (!this.checks.emitFragment) {

+ 40 - 0
src/Particles/IParticleSystem.ts

@@ -7,6 +7,8 @@ import { Texture } from "../Materials/Textures/texture";
 import { BoxParticleEmitter, IParticleEmitterType, PointParticleEmitter, HemisphericParticleEmitter, SphereParticleEmitter, SphereDirectedParticleEmitter, CylinderParticleEmitter, ConeParticleEmitter } from "../Particles/EmitterTypes/index";
 import { Scene } from "../scene";
 import { ColorGradient, FactorGradient, Color3Gradient } from "../Misc/gradients";
+import { Effect } from "../Materials/effect";
+import { Observable } from "../Misc/observable";
 
 declare type Animation = import("../Animations/animation").Animation;
 
@@ -283,6 +285,10 @@ export interface IParticleSystem {
      */
     dispose(disposeTexture?: boolean): void;
     /**
+    * An event triggered when the system is disposed
+    */
+    onDisposeObservable: Observable<IParticleSystem>;
+    /**
      * Clones the particle system.
      * @param name The name of the cloned object
      * @param newEmitter The new emitter to use
@@ -335,6 +341,40 @@ export interface IParticleSystem {
      * @returns a string containing the class name
      */
     getClassName(): string;
+    /**
+     * Gets the custom effect used to render the particles
+     * @param blendMode Blend mode for which the effect should be retrieved
+     * @returns The effect
+     */
+    getCustomEffect(blendMode: number): Nullable<Effect>;
+    /**
+     * Sets the custom effect used to render the particles
+     * @param effect The effect to set
+     * @param blendMode Blend mode for which the effect should be set
+     */
+    setCustomEffect(effect: Nullable<Effect>, blendMode: number): void;
+
+    /**
+     * Fill the defines array according to the current settings of the particle system
+     * @param defines Array to be updated
+     * @param blendMode blend mode to take into account when updating the array
+     */
+    fillDefines(defines: Array<string>, blendMode: number): void;
+    /**
+     * Fill the uniforms, attributes and samplers arrays according to the current settings of the particle system
+     * @param uniforms Uniforms array to fill
+     * @param attributes Attributes array to fill
+     * @param samplers Samplers array to fill
+     */
+    fillUniformsAttributesAndSamplerNames(uniforms: Array<string>, attributes: Array<string>, samplers: Array<string>): void;
+    /**
+     * Observable that will be called just before the particles are drawn
+     */
+    onBeforeDrawParticlesObservable: Observable<Nullable<Effect>>;
+    /**
+     * Gets the name of the particle vertex shader
+     */
+    vertexShaderName: string;
 
     /**
      * Adds a new color gradient

+ 135 - 43
src/Particles/gpuParticleSystem.ts

@@ -74,6 +74,7 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
 
     private _randomTextureSize: number;
     private _actualFrame = 0;
+    private _customEffect: { [blendMode: number] : Nullable<Effect> };
 
     private readonly _rawTextureWidth = 256;
 
@@ -90,7 +91,7 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
     /**
     * An event triggered when the system is disposed.
     */
-    public onDisposeObservable = new Observable<GPUParticleSystem>();
+    public onDisposeObservable = new Observable<IParticleSystem>();
 
     /**
      * Gets the maximum number of particles active at the same time.
@@ -135,7 +136,7 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
             return false;
         }
 
-        if (!this.emitter || !this._updateEffect.isReady() || !this._imageProcessingConfiguration.isReady() || !this._renderEffect.isReady() || !this.particleTexture || !this.particleTexture.isReady()) {
+        if (!this.emitter || !this._updateEffect.isReady() || !this._imageProcessingConfiguration.isReady() || !this._getEffect().isReady() || !this.particleTexture || !this.particleTexture.isReady()) {
             return false;
         }
 
@@ -223,6 +224,45 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
         return "GPUParticleSystem";
     }
 
+    /**
+     * Gets the custom effect used to render the particles
+     * @param blendMode Blend mode for which the effect should be retrieved
+     * @returns The effect
+     */
+    public getCustomEffect(blendMode: number = 0): Nullable<Effect> {
+        return this._customEffect[blendMode] ?? this._customEffect[0];
+    }
+
+    /**
+     * Sets the custom effect used to render the particles
+     * @param effect The effect to set
+     * @param blendMode Blend mode for which the effect should be set
+     */
+    public setCustomEffect(effect: Nullable<Effect>, blendMode: number = 0) {
+        this._customEffect[blendMode] = effect;
+    }
+
+    /** @hidden */
+    protected _onBeforeDrawParticlesObservable: Nullable<Observable<Nullable<Effect>>> = null;
+
+    /**
+     * Observable that will be called just before the particles are drawn
+     */
+    public get onBeforeDrawParticlesObservable(): Observable<Nullable<Effect>> {
+        if (!this._onBeforeDrawParticlesObservable) {
+            this._onBeforeDrawParticlesObservable = new Observable<Nullable<Effect>>();
+        }
+
+        return this._onBeforeDrawParticlesObservable;
+    }
+
+    /**
+     * Gets the name of the particle vertex shader
+     */
+    public get vertexShaderName(): string {
+        return "gpuRenderParticles";
+    }
+
     private _colorGradientsTexture: RawTexture;
 
     protected _removeGradientAndTexture(gradient: number, gradients: Nullable<IValueGradient[]>, texture: RawTexture): BaseParticleSystem {
@@ -660,16 +700,19 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
      * @param options The options used to create the system
      * @param scene The scene the particle system belongs to
      * @param isAnimationSheetEnabled Must be true if using a spritesheet to animate the particles texture
+     * @param customEffect a custom effect used to change the way particles are rendered by default
      */
     constructor(name: string, options: Partial<{
         capacity: number,
         randomTextureSize: number
-    }>, scene: Scene, isAnimationSheetEnabled: boolean = false) {
+    }>, scene: Scene, isAnimationSheetEnabled: boolean = false, customEffect: Nullable<Effect> = null) {
         super(name);
         this._scene = scene || EngineStore.LastCreatedScene;
 
         this.uniqueId = this._scene.getUniqueId();
 
+        this._customEffect = { 0: customEffect };
+
         // Setup the default processing configuration to the scene.
         this._attachImageProcessingConfiguration(null);
 
@@ -862,7 +905,7 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
         renderVertexBuffers["offset"] = spriteSource.createVertexBuffer("offset", 0, 2);
         renderVertexBuffers["uv"] = spriteSource.createVertexBuffer("uv", 2, 2);
 
-        let vao = this._engine.recordVertexArrayObject(renderVertexBuffers, null, this._renderEffect);
+        let vao = this._engine.recordVertexArrayObject(renderVertexBuffers, null, this._getEffect());
         this._engine.bindArrayBuffer(null);
 
         return vao;
@@ -1105,45 +1148,52 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
         this._updateEffect = new Effect("gpuUpdateParticles", this._updateEffectOptions, this._scene.getEngine());
     }
 
-    /** @hidden */
-    public _recreateRenderEffect() {
-        let defines = "";
+    private _getEffect(): Effect {
+        return this.getCustomEffect() ?? this._renderEffect;
+    }
+
+    /**
+     * Fill the defines array according to the current settings of the particle system
+     * @param defines Array to be updated
+     * @param blendMode blend mode to take into account when updating the array
+     */
+    public fillDefines(defines: Array<string>, blendMode: number = 0) {
         if (this._scene.clipPlane) {
-            defines = "\n#define CLIPPLANE";
+            defines.push("#define CLIPPLANE");
         }
         if (this._scene.clipPlane2) {
-            defines = "\n#define CLIPPLANE2";
+            defines.push("#define CLIPPLANE2");
         }
         if (this._scene.clipPlane3) {
-            defines = "\n#define CLIPPLANE3";
+            defines.push("#define CLIPPLANE3");
         }
         if (this._scene.clipPlane4) {
-            defines = "\n#define CLIPPLANE4";
+            defines.push("#define CLIPPLANE4");
         }
         if (this._scene.clipPlane5) {
-            defines = "\n#define CLIPPLANE5";
+            defines.push("#define CLIPPLANE5");
         }
         if (this._scene.clipPlane6) {
-            defines = "\n#define CLIPPLANE6";
+            defines.push("#define CLIPPLANE6");
         }
 
         if (this.blendMode === ParticleSystem.BLENDMODE_MULTIPLY) {
-            defines = "\n#define BLENDMULTIPLYMODE";
+            defines.push("#define BLENDMULTIPLYMODE");
         }
 
         if (this.isLocal) {
-            defines += "\n#define LOCAL";
+            defines.push("#define LOCAL");
         }
 
         if (this._isBillboardBased) {
-            defines += "\n#define BILLBOARD";
+            defines.push("#define BILLBOARD");
 
             switch (this.billboardMode) {
                 case ParticleSystem.BILLBOARDMODE_Y:
-                    defines += "\n#define BILLBOARDY";
+                    defines.push("#define BILLBOARDY");
                     break;
                 case ParticleSystem.BILLBOARDMODE_STRETCHED:
-                    defines += "\n#define BILLBOARDSTRETCHED";
+                    defines.push("#define BILLBOARDSTRETCHED");
                     break;
                 case ParticleSystem.BILLBOARDMODE_ALL:
                 default:
@@ -1152,34 +1202,68 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
         }
 
         if (this._colorGradientsTexture) {
-            defines += "\n#define COLORGRADIENTS";
+            defines.push("#define COLORGRADIENTS");
         }
 
         if (this.isAnimationSheetEnabled) {
-            defines += "\n#define ANIMATESHEET";
+            defines.push("#define ANIMATESHEET");
         }
 
         if (this._imageProcessingConfiguration) {
             this._imageProcessingConfiguration.prepareDefines(this._imageProcessingConfigurationDefines);
-            defines += "\n" + this._imageProcessingConfigurationDefines.toString();
+            defines.push("" + this._imageProcessingConfigurationDefines.toString());
         }
+    }
 
-        if (this._renderEffect && this._renderEffect.defines === defines) {
-            return;
-        }
+    /**
+     * Fill the uniforms, attributes and samplers arrays according to the current settings of the particle system
+     * @param uniforms Uniforms array to fill
+     * @param attributes Attributes array to fill
+     * @param samplers Samplers array to fill
+     */
+    public fillUniformsAttributesAndSamplerNames(uniforms: Array<string>, attributes: Array<string>, samplers: Array<string>) {
+        attributes.push("position", "age", "life", "size", "color", "offset", "uv", "direction", "initialDirection", "angle", "cellIndex");
+
+        uniforms.push("emitterWM", "worldOffset", "view", "projection", "colorDead", "invView", "vClipPlane", "vClipPlane2", "vClipPlane3", "vClipPlane4", "vClipPlane5", "vClipPlane6", "sheetInfos", "translationPivot", "eyePosition");
 
-        var uniforms = ["emitterWM", "worldOffset", "view", "projection", "colorDead", "invView", "vClipPlane", "vClipPlane2", "vClipPlane3", "vClipPlane4", "vClipPlane5", "vClipPlane6", "sheetInfos", "translationPivot", "eyePosition"];
-        var samplers = ["textureSampler", "colorGradientSampler"];
+        samplers.push("diffuseSampler", "colorGradientSampler");
 
-        if (ImageProcessingConfiguration) {
+        if (this._imageProcessingConfiguration) {
             ImageProcessingConfiguration.PrepareUniforms(uniforms, this._imageProcessingConfigurationDefines);
             ImageProcessingConfiguration.PrepareSamplers(samplers, this._imageProcessingConfigurationDefines);
         }
+    }
+
+    /** @hidden */
+    public _recreateRenderEffect(): Effect {
+        const customEffect = this.getCustomEffect();
+
+        if (customEffect) {
+            return customEffect;
+        }
+
+        let defines: Array<string> = [];
+
+        this.fillDefines(defines);
+
+        var join = defines.join("\n");
+
+        if (this._renderEffect && this._renderEffect.defines === join) {
+            return this._renderEffect;
+        }
+
+        var attributes: Array<string> = [];
+        var uniforms: Array<string> = [];
+        var samplers: Array<string> = [];
+
+        this.fillUniformsAttributesAndSamplerNames(uniforms, attributes, samplers);
 
         this._renderEffect = new Effect("gpuRenderParticles",
-            ["position", "age", "life", "size", "color", "offset", "uv", "direction", "initialDirection", "angle", "cellIndex"],
+            attributes,
             uniforms,
-            samplers, this._scene.getEngine(), defines);
+            samplers, this._scene.getEngine(), join);
+
+        return this._renderEffect;
     }
 
     /**
@@ -1397,42 +1481,44 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
 
         if (!preWarm) {
             // Enable render effect
-            this._engine.enableEffect(this._renderEffect);
+            const effect = this._getEffect();
+
+            this._engine.enableEffect(effect);
             let viewMatrix = this._scene.getViewMatrix();
-            this._renderEffect.setMatrix("view", viewMatrix);
-            this._renderEffect.setMatrix("projection", this._scene.getProjectionMatrix());
-            this._renderEffect.setTexture("textureSampler", this.particleTexture);
-            this._renderEffect.setVector2("translationPivot", this.translationPivot);
-            this._renderEffect.setVector3("worldOffset", this.worldOffset);
+            effect.setMatrix("view", viewMatrix);
+            effect.setMatrix("projection", this._scene.getProjectionMatrix());
+            effect.setTexture("diffuseSampler", this.particleTexture);
+            effect.setVector2("translationPivot", this.translationPivot);
+            effect.setVector3("worldOffset", this.worldOffset);
             if (this.isLocal) {
-                this._renderEffect.setMatrix("emitterWM", emitterWM);
+                effect.setMatrix("emitterWM", emitterWM);
             }
             if (this._colorGradientsTexture) {
-                this._renderEffect.setTexture("colorGradientSampler", this._colorGradientsTexture);
+                effect.setTexture("colorGradientSampler", this._colorGradientsTexture);
             } else {
-                this._renderEffect.setDirectColor4("colorDead", this.colorDead);
+                effect.setDirectColor4("colorDead", this.colorDead);
             }
 
             if (this._isAnimationSheetEnabled && this.particleTexture) {
                 let baseSize = this.particleTexture.getBaseSize();
-                this._renderEffect.setFloat3("sheetInfos", this.spriteCellWidth / baseSize.width, this.spriteCellHeight / baseSize.height, baseSize.width / this.spriteCellWidth);
+                effect.setFloat3("sheetInfos", this.spriteCellWidth / baseSize.width, this.spriteCellHeight / baseSize.height, baseSize.width / this.spriteCellWidth);
             }
 
             if (this._isBillboardBased) {
                 var camera = this._scene.activeCamera!;
-                this._renderEffect.setVector3("eyePosition", camera.globalPosition);
+                effect.setVector3("eyePosition", camera.globalPosition);
             }
 
             if (this._scene.clipPlane || this._scene.clipPlane2 || this._scene.clipPlane3 || this._scene.clipPlane4 || this._scene.clipPlane5 || this._scene.clipPlane6) {
                 var invView = viewMatrix.clone();
                 invView.invert();
-                this._renderEffect.setMatrix("invView", invView);
-                MaterialHelper.BindClipPlane(this._renderEffect, this._scene);
+                effect.setMatrix("invView", invView);
+                MaterialHelper.BindClipPlane(effect, this._scene);
             }
 
             // image processing
             if (this._imageProcessingConfiguration && !this._imageProcessingConfiguration.applyByPostProcess) {
-                this._imageProcessingConfiguration.bind(this._renderEffect);
+                this._imageProcessingConfiguration.bind(effect);
             }
 
             // Draw order
@@ -1458,6 +1544,10 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
             // Bind source VAO
             this._engine.bindVertexArrayObject(this._renderVAO[this._targetIndex], null);
 
+            if (this._onBeforeDrawParticlesObservable) {
+                this._onBeforeDrawParticlesObservable.notifyObservers(effect);
+            }
+
             // Render
             this._engine.drawArraysType(Material.TriangleFanDrawMode, 0, 4, this._currentActiveCount);
             this._engine.setAlphaMode(Constants.ALPHA_DISABLE);
@@ -1588,7 +1678,9 @@ export class GPUParticleSystem extends BaseParticleSystem implements IDisposable
      * @returns the cloned particle system
      */
     public clone(name: string, newEmitter: any): GPUParticleSystem {
+        var custom = { ...this._customEffect };
         var result = new GPUParticleSystem(name, { capacity: this._capacity, randomTextureSize: this._randomTextureSize }, this._scene);
+        result._customEffect = custom;
 
         DeepCopier.DeepCopy(this, result, ["particles", "customShader", "noiseTexture", "particleTexture", "onDisposeObservable"]);
 

+ 97 - 25
src/Particles/particleSystem.ts

@@ -81,9 +81,9 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
     /**
     * An event triggered when the system is disposed
     */
-    public onDisposeObservable = new Observable<ParticleSystem>();
+    public onDisposeObservable = new Observable<IParticleSystem>();
 
-    private _onDisposeObserver: Nullable<Observer<ParticleSystem>>;
+    private _onDisposeObserver: Nullable<Observer<IParticleSystem>>;
     /**
      * Sets a callback that will be triggered when the system is disposed
      */
@@ -105,7 +105,7 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
     private _spriteBuffer: Nullable<Buffer>;
     private _indexBuffer: Nullable<DataBuffer>;
     private _effect: Effect;
-    private _customEffect: Nullable<Effect>;
+    private _customEffect: { [blendMode: number] : Nullable<Effect> };
     private _cachedDefines: string;
     private _scaledColorStep = new Color4(0, 0, 0, 0);
     private _colorDiff = new Color4(0, 0, 0, 0);
@@ -214,6 +214,45 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
     }
 
     /**
+     * Gets the custom effect used to render the particles
+     * @param blendMode Blend mode for which the effect should be retrieved
+     * @returns The effect
+     */
+    public getCustomEffect(blendMode: number = 0): Nullable<Effect> {
+        return this._customEffect[blendMode] ?? this._customEffect[0];
+    }
+
+    /**
+     * Sets the custom effect used to render the particles
+     * @param effect The effect to set
+     * @param blendMode Blend mode for which the effect should be set
+     */
+    public setCustomEffect(effect: Nullable<Effect>, blendMode: number = 0) {
+        this._customEffect[blendMode] = effect;
+    }
+
+    /** @hidden */
+    private _onBeforeDrawParticlesObservable: Nullable<Observable<Nullable<Effect>>> = null;
+
+    /**
+     * Observable that will be called just before the particles are drawn
+     */
+    public get onBeforeDrawParticlesObservable(): Observable<Nullable<Effect>> {
+        if (!this._onBeforeDrawParticlesObservable) {
+            this._onBeforeDrawParticlesObservable = new Observable<Nullable<Effect>>();
+        }
+
+        return this._onBeforeDrawParticlesObservable;
+    }
+
+    /**
+     * Gets the name of the particle vertex shader
+     */
+    public get vertexShaderName(): string {
+        return "particles";
+    }
+
+    /**
      * Instantiates a particle system.
      * Particles are often small sprites used to simulate hard-to-reproduce phenomena like fire, smoke, water, or abstract visual effects like magic glitter and faery dust.
      * @param name The name of the particle system
@@ -238,7 +277,7 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
         // Setup the default processing configuration to the scene.
         this._attachImageProcessingConfiguration(null);
 
-        this._customEffect = customEffect;
+        this._customEffect = { 0: customEffect };
 
         this._scene.particleSystems.push(this);
 
@@ -1564,14 +1603,12 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
         return effectCreationOption;
     }
 
-    /** @hidden */
-    private _getEffect(blendMode: number): Effect {
-        if (this._customEffect) {
-            return this._customEffect;
-        }
-
-        var defines = [];
-
+    /**
+     * Fill the defines array according to the current settings of the particle system
+     * @param defines Array to be updated
+     * @param blendMode blend mode to take into account when updating the array
+     */
+    public fillDefines(defines: Array<string>, blendMode: number) {
         if (this._scene.clipPlane) {
             defines.push("#define CLIPPLANE");
         }
@@ -1628,21 +1665,49 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
             this._imageProcessingConfiguration.prepareDefines(this._imageProcessingConfigurationDefines);
             defines.push(this._imageProcessingConfigurationDefines.toString());
         }
+    }
+
+    /**
+     * Fill the uniforms, attributes and samplers arrays according to the current settings of the particle system
+     * @param uniforms Uniforms array to fill
+     * @param attributes Attributes array to fill
+     * @param samplers Samplers array to fill
+     */
+    public fillUniformsAttributesAndSamplerNames(uniforms: Array<string>, attributes: Array<string>, samplers: Array<string>) {
+        attributes.push(...ParticleSystem._GetAttributeNamesOrOptions(this._isAnimationSheetEnabled, this._isBillboardBased && this.billboardMode !== ParticleSystem.BILLBOARDMODE_STRETCHED, this._useRampGradients));
+
+        uniforms.push(...ParticleSystem._GetEffectCreationOptions(this._isAnimationSheetEnabled));
+
+        samplers.push("diffuseSampler", "rampSampler");
+
+        if (this._imageProcessingConfiguration) {
+            ImageProcessingConfiguration.PrepareUniforms(uniforms, this._imageProcessingConfigurationDefines);
+            ImageProcessingConfiguration.PrepareSamplers(samplers, this._imageProcessingConfigurationDefines);
+        }
+    }
+
+    /** @hidden */
+    private _getEffect(blendMode: number): Effect {
+        const customEffect = this.getCustomEffect(blendMode);
+
+        if (customEffect) {
+            return customEffect;
+        }
+
+        var defines: Array<string> = [];
+
+        this.fillDefines(defines, blendMode);
 
         // Effect
         var join = defines.join("\n");
         if (this._cachedDefines !== join) {
             this._cachedDefines = join;
 
-            var attributesNamesOrOptions = ParticleSystem._GetAttributeNamesOrOptions(this._isAnimationSheetEnabled, this._isBillboardBased && this.billboardMode !== ParticleSystem.BILLBOARDMODE_STRETCHED, this._useRampGradients);
-            var effectCreationOption = ParticleSystem._GetEffectCreationOptions(this._isAnimationSheetEnabled);
-
-            var samplers = ["diffuseSampler", "rampSampler"];
+            var attributesNamesOrOptions: Array<string> = [];
+            var effectCreationOption: Array<string> = [];
+            var samplers: Array<string> = [];
 
-            if (ImageProcessingConfiguration) {
-                ImageProcessingConfiguration.PrepareUniforms(effectCreationOption, this._imageProcessingConfigurationDefines);
-                ImageProcessingConfiguration.PrepareSamplers(samplers, this._imageProcessingConfigurationDefines);
-            }
+            this.fillUniformsAttributesAndSamplerNames(effectCreationOption, attributesNamesOrOptions, samplers);
 
             this._effect = this._scene.getEngine().createEffect(
                 "particles",
@@ -1867,6 +1932,10 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
                 break;
         }
 
+        if (this._onBeforeDrawParticlesObservable) {
+            this._onBeforeDrawParticlesObservable.notifyObservers(effect);
+        }
+
         if (this._useInstancing) {
             engine.drawArraysType(Material.TriangleFanDrawMode, 0, 4, this._particles.length);
         } else {
@@ -1958,6 +2027,10 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
             (<AbstractMesh>this.emitter).dispose(true);
         }
 
+        if (this._onBeforeDrawParticlesObservable) {
+            this._onBeforeDrawParticlesObservable.clear();
+        }
+
         // Remove from scene
         var index = this._scene.particleSystems.indexOf(this);
         if (index > -1) {
@@ -1981,17 +2054,16 @@ export class ParticleSystem extends BaseParticleSystem implements IDisposable, I
      * @returns the cloned particle system
      */
     public clone(name: string, newEmitter: any): ParticleSystem {
-        var custom: Nullable<Effect> = null;
+        var custom = { ...this._customEffect };
         var program: any = null;
         if (this.customShader != null) {
             program = this.customShader;
             var defines: string = (program.shaderOptions.defines.length > 0) ? program.shaderOptions.defines.join("\n") : "";
-            custom = this._scene.getEngine().createEffectForParticles(program.shaderPath.fragmentElement, program.shaderOptions.uniforms, program.shaderOptions.samplers, defines);
-        } else if (this._customEffect) {
-            custom = this._customEffect;
+            custom[0] = this._scene.getEngine().createEffectForParticles(program.shaderPath.fragmentElement, program.shaderOptions.uniforms, program.shaderOptions.samplers, defines);
         }
-        var result = new ParticleSystem(name, this._capacity, this._scene, custom);
+        var result = new ParticleSystem(name, this._capacity, this._scene, custom[0]);
         result.customShader = program;
+        result._customEffect = custom;
 
         DeepCopier.DeepCopy(this, result, ["particles", "customShader", "noiseTexture", "particleTexture", "onDisposeObservable"]);
 

+ 17 - 7
src/Particles/particleSystemComponent.ts

@@ -44,7 +44,8 @@ declare module "../Engines/engine" {
     export interface Engine {
         /**
          * Create an effect to use with particle systems.
-         * Please note that some parameters like animation sheets or not being billboard are not supported in this configuration
+         * Please note that some parameters like animation sheets or not being billboard are not supported in this configuration, except if you pass
+         * the particle system for which you want to create a custom effect in the last parameter
          * @param fragmentName defines the base name of the effect (The name of file without .fragment.fx)
          * @param uniformsNames defines a list of attribute names
          * @param samplers defines an array of string used to represent textures
@@ -52,18 +53,27 @@ declare module "../Engines/engine" {
          * @param fallbacks defines the list of potential fallbacks to use if shader conmpilation fails
          * @param onCompiled defines a function to call when the effect creation is successful
          * @param onError defines a function to call when the effect creation has failed
+         * @param particleSystem the particle system you want to create the effect for
          * @returns the new Effect
          */
         createEffectForParticles(fragmentName: string, uniformsNames: string[], samplers: string[], defines: string, fallbacks?: EffectFallbacks,
-            onCompiled?: (effect: Effect) => void, onError?: (effect: Effect, errors: string) => void): Effect;
+            onCompiled?: (effect: Effect) => void, onError?: (effect: Effect, errors: string) => void, particleSystem?: IParticleSystem): Effect;
     }
 }
 
 Engine.prototype.createEffectForParticles = function(fragmentName: string, uniformsNames: string[] = [], samplers: string[] = [], defines = "", fallbacks?: EffectFallbacks,
-    onCompiled?: (effect: Effect) => void, onError?: (effect: Effect, errors: string) => void): Effect {
+    onCompiled?: (effect: Effect) => void, onError?: (effect: Effect, errors: string) => void, particleSystem?: IParticleSystem): Effect {
 
-    var attributesNamesOrOptions = ParticleSystem._GetAttributeNamesOrOptions();
-    var effectCreationOption = ParticleSystem._GetEffectCreationOptions();
+    var attributesNamesOrOptions: Array<string> = [];
+    var effectCreationOption: Array<string> = [];
+    var allSamplers: Array<string> = [];
+
+    if (particleSystem) {
+        particleSystem.fillUniformsAttributesAndSamplerNames(effectCreationOption, attributesNamesOrOptions, allSamplers);
+    } else {
+        attributesNamesOrOptions = ParticleSystem._GetAttributeNamesOrOptions();
+        effectCreationOption = ParticleSystem._GetEffectCreationOptions();
+    }
 
     if (defines.indexOf(" BILLBOARD") === -1) {
         defines += "\n#define BILLBOARD\n";
@@ -75,12 +85,12 @@ Engine.prototype.createEffectForParticles = function(fragmentName: string, unifo
 
     return this.createEffect(
         {
-            vertex: "particles",
+            vertex: particleSystem?.vertexShaderName ?? "particles",
             fragmentElement: fragmentName
         },
         attributesNamesOrOptions,
         effectCreationOption.concat(uniformsNames),
-        samplers, defines, fallbacks, onCompiled, onError);
+        allSamplers.concat(samplers), defines, fallbacks, onCompiled, onError);
 };
 
 declare module "../Meshes/mesh" {

+ 2 - 2
src/Shaders/gpuRenderParticles.fragment.fx

@@ -1,6 +1,6 @@
 #version 300 es
 
-uniform sampler2D textureSampler;
+uniform sampler2D diffuseSampler;
 
 in vec2 vUV;
 in vec4 vColor;
@@ -17,7 +17,7 @@ out vec4 outFragColor;
 
 void main() {
 	#include<clipPlaneFragment> 
-	vec4 textureColor = texture(textureSampler, vUV);
+	vec4 textureColor = texture(diffuseSampler, vUV);
   	outFragColor = textureColor * vColor;
 
 	#ifdef BLENDMULTIPLYMODE