A node material's mode decides what its graph shades: meshes (Material, 0), the whole frame (PostProcess, 1, through a CurrentScreenBlock), particle sprites (Particle, 2, with ParticleTextureBlock), or a texture (ProceduralTexture, 3); newer modes serve Gaussian splats and the Smart Filter Editor. Each mode has a starter graph and a factory returning an ordinary engine object (both ran here):
const grade = new BABYLON.NodeMaterial('grade', scene);
grade.setToDefaultPostProcess(); // samples the rendered frame
grade.build();
const pass = grade.createPostProcess(camera); // a PostProcess on this camera
const sparks = new BABYLON.NodeMaterial('sparks', scene);
sparks.setToDefaultParticle();
sparks.build();
sparks.createEffectForParticles(dust); // replaces the particle shaderThe Node Particle Editor (The Node Particle Editor) goes further, building whole particle systems, not only their shaders, as graphs.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 340px; display: block; touch-action: none; }
#bar { max-width: 600px; padding: 6px 8px; }
</style>
<canvas id="view"></canvas>
<div id="bar"><label><input type="checkbox" id="grade" checked> node post-process (warm grade)</label></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#cfd8dcff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.3, 6, new BABYLON.Vector3(0, 1.1, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.5), scene);
const paint = (hex) => {
const m = new BABYLON.StandardMaterial(hex, scene);
m.diffuseColor = BABYLON.Color3.FromHexString(hex);
m.specularColor = BABYLON.Color3.Black();
return m;
};
BABYLON.MeshBuilder.CreateGround('floor', { width: 8, height: 6 }, scene).material = paint('#b08d6a');
const wall = BABYLON.MeshBuilder.CreatePlane('wall', { width: 8, height: 4 }, scene);
wall.position.set(0, 2, 1.2);
wall.material = paint('#fff3e0');
['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'].forEach((hex, i) => {
const book = BABYLON.MeshBuilder.CreateBox('book' + i, { width: 0.5, height: 1.4, depth: 0.2 }, scene);
book.position.set((i - 2.5) * 0.62, 0.7, 0.6);
book.material = paint(hex);
});
// 1. PostProcess mode: the graph shades the whole rendered frame through a CurrentScreenBlock
const grade = new BABYLON.NodeMaterial('grade', scene);
const position = new BABYLON.InputBlock('position');
position.setAsAttribute('position2d'); // the full-screen quad
const one = new BABYLON.InputBlock('one');
one.isConstant = true;
one.value = 1;
const position3D = new BABYLON.VectorMergerBlock('position3D');
position.connectTo(position3D);
one.connectTo(position3D, { input: 'w' });
const vertexOut = new BABYLON.VertexOutputBlock('vertexOut');
position3D.connectTo(vertexOut);
const uv = new BABYLON.RemapBlock('uv'); // -1..1 to 0..1
position.connectTo(uv);
const screen = new BABYLON.CurrentScreenBlock('currentScreen'); // the frame rendered so far
uv.connectTo(screen);
const tint = new BABYLON.InputBlock('tint');
tint.value = new BABYLON.Color3(1.12, 0.95, 0.72); // warm, slightly faded
const graded = new BABYLON.MultiplyBlock('graded');
screen.rgb.connectTo(graded.left);
tint.output.connectTo(graded.right);
const fragmentOut = new BABYLON.FragmentOutputBlock('fragmentOut');
graded.output.connectTo(fragmentOut.rgb);
grade.addOutputNode(vertexOut);
grade.addOutputNode(fragmentOut);
grade.mode = BABYLON.NodeMaterialModes.PostProcess;
grade.build();
let pass = grade.createPostProcess(camera); // an ordinary PostProcess on this camera
// 2. Particle mode: the default particle graph (texture x color) replaces the particle shader
const dot = new BABYLON.DynamicTexture('dot', { width: 64, height: 64 }, scene);
const g = dot.getContext().createRadialGradient(32, 32, 0, 32, 32, 32);
g.addColorStop(0, 'rgba(255,255,255,1)'); g.addColorStop(1, 'rgba(255,255,255,0)');
dot.getContext().fillStyle = g;
dot.getContext().fillRect(0, 0, 64, 64);
dot.update();
dot.hasAlpha = true;
const dust = new BABYLON.ParticleSystem('dust', 600, scene);
dust.particleTexture = dot;
dust.emitter = new BABYLON.Vector3(0, 1.4, 0);
dust.minEmitBox = new BABYLON.Vector3(-3, -1.2, -1.5);
dust.maxEmitBox = new BABYLON.Vector3(3, 1.5, 0.5);
dust.color1 = new BABYLON.Color4(1, 0.95, 0.8, 0.9);
dust.color2 = new BABYLON.Color4(1, 0.85, 0.6, 0.6);
dust.colorDead = new BABYLON.Color4(1, 0.9, 0.7, 0);
Object.assign(dust, { minSize: 0.03, maxSize: 0.08, minLifeTime: 3, maxLifeTime: 6, emitRate: 120,
gravity: new BABYLON.Vector3(0, -0.02, 0), minEmitPower: 0.02, maxEmitPower: 0.08 });
dust.preWarmCycles = 200; // start with the air already full
const sparks = new BABYLON.NodeMaterial('sparks', scene);
sparks.setToDefaultParticle();
sparks.build();
sparks.createEffectForParticles(dust);
dust.start();
document.getElementById('grade').addEventListener('change', (event) => {
if (event.target.checked) pass = grade.createPostProcess(camera);
else pass.dispose();
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>