Node Material Editor

Opening a Material in the Node Material Editor

The NME, introduced in Babylon.js 4.1 89,603 , runs three ways: standalone at nme.babylonjs.com, saving to Babylon's snippet server; from the Inspector (Inspector and Tools), whose node-material pane has an Edit button; and from your page, where material.edit() downloads the editor script for your exact version (https://cdn.babylonjs.com/v9.28.0/nodeEditor/babylon.nodeEditor.js 89,603 ) and opens it in a pop-up bound to the live material, so every change shows in your scene at once:

Opening the editor on a node material from a buttonJavaScript
editButton.addEventListener('click', () => {           // pop-ups need a user gesture
  highlight.edit({ nodeEditorConfig: { backgroundColor: new BABYLON.Color4(1, 1, 1, 1) } });
});

The editor shows blocks as boxes with input ports on the left and outputs on the right, beside a preview mesh and a property pane. Ship the material as JSON (Saving Node Materials) or code, and keep edit(), which downloads about 2 MB of editor script, out of production.

A NodeMaterial on a book, its graph drawn editor-style, and the edit() button that opens the NMEHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  #view { width: 100%; max-width: 600px; height: 200px; display: block; touch-action: none; }
  #graph { width: 100%; max-width: 600px; height: 250px; display: block; background: #2b2f36; }
  #bar { max-width: 600px; display: flex; gap: 10px; align-items: center; padding: 6px 8px; }
  button { font: inherit; padding: 4px 12px; }
</style>
<canvas id="view"></canvas>
<canvas id="graph" width="600" height="250"></canvas>
<div id="bar"><button id="edit">Edit in the Node Material Editor</button><span id="note"></span></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('#f7f3ecff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 - 0.5, 1.2, 3, BABYLON.Vector3.Zero(), scene);
camera.attachControl(canvas, true);

// The smallest graph: position -> World -> ViewProjection -> vertex output; a color -> fragment output
const highlight = new BABYLON.NodeMaterial('highlight', scene);
highlight.setToDefault();
highlight.build();                                    // build() also registers the attached blocks
highlight.getBlockByName('color').value = BABYLON.Color4.FromHexString('#e09a10ff');   // a uniform
const book = BABYLON.MeshBuilder.CreateBox('book', { width: 1, height: 1.4, depth: 0.25 }, scene);
book.material = highlight;
scene.onBeforeRenderObservable.add(() => { book.rotation.y += engine.getDeltaTime() / 2000; });

// edit() downloads the editor script for this exact version and opens it in a pop-up
document.getElementById('edit').addEventListener('click', () => {    // pop-ups need a user gesture
  if (window.top !== window) {                                        // this preview is a sandboxed frame
    document.getElementById('note').textContent = 'Pop-ups are blocked here; run the page on its own.';
    return;
  }
  highlight.edit({ nodeEditorConfig: { backgroundColor: new BABYLON.Color4(1, 1, 1, 1) } });
});

// Draw the graph the way the editor shows it: blocks as boxes, inputs left, outputs right
function drawGraph(material, ctx) {
  const depth = new Map();
  const visit = (block, d) => {
    if ((depth.get(block) ?? -1) >= d) return;
    depth.set(block, d);
    for (const input of block.inputs) if (input.connectedPoint) visit(input.connectedPoint.ownerBlock, d + 1);
  };
  material.attachedBlocks.filter((b) => b.outputs.every((o) => !o.hasEndpoints)).forEach((b) => visit(b, 0));
  const maxDepth = Math.max(...depth.values());
  const rows = new Map(), boxes = new Map();
  for (const [block, d] of depth) {
    const row = rows.get(d) || 0;
    rows.set(d, row + 1);
    const ports = block.inputs.filter((p) => p.isConnected).concat(block.outputs.filter((p) => p.hasEndpoints));
    const x = 14 + (maxDepth - d) * (560 / (maxDepth + 1)), y = 16 + row * 115;
    boxes.set(block, { x, y, w: 124, h: 26 + ports.length * 14 });
  }
  const portY = (block, point) => {
    const box = boxes.get(block);
    const ports = block.inputs.filter((p) => p.isConnected).concat(block.outputs.filter((p) => p.hasEndpoints));
    return box.y + 32 + ports.indexOf(point) * 14;
  };
  ctx.lineWidth = 2;
  for (const block of boxes.keys()) for (const input of block.inputs) {
    const from = input.connectedPoint;
    if (!from) continue;
    const a = boxes.get(from.ownerBlock), b = boxes.get(block);
    const [x1, y1, x2, y2] = [a.x + a.w, portY(from.ownerBlock, from), b.x, portY(block, input)];
    ctx.strokeStyle = '#8fb3d9';
    ctx.beginPath(); ctx.moveTo(x1, y1); ctx.bezierCurveTo(x1 + 40, y1, x2 - 40, y2, x2, y2); ctx.stroke();
  }
  ctx.font = '11px sans-serif';
  for (const [block, { x, y, w, h }] of boxes) {
    ctx.fillStyle = '#454b55'; ctx.fillRect(x, y, w, h);
    ctx.fillStyle = block.isInput ? '#7a5c2e' : '#35577a'; ctx.fillRect(x, y, w, 20);
    let title = block.name;                           // shorten names that overflow the box
    while (ctx.measureText(title).width > w - 12) title = title.slice(0, -2) + '…';
    ctx.fillStyle = '#fff'; ctx.fillText(title, x + 6, y + 14);
    const ports = block.inputs.filter((p) => p.isConnected).concat(block.outputs.filter((p) => p.hasEndpoints));
    ports.forEach((p) => {
      const isInput = block.inputs.includes(p), py = portY(block, p);
      ctx.fillStyle = '#ddd';
      ctx.textAlign = isInput ? 'left' : 'right';
      ctx.fillText(p.name, isInput ? x + 8 : x + w - 8, py + 4);
      ctx.beginPath(); ctx.arc(isInput ? x : x + w, py, 3.5, 0, Math.PI * 2); ctx.fill();
      ctx.textAlign = 'left';
    });
  }
}
drawGraph(highlight, document.getElementById('graph').getContext('2d'));
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>