NullEngine has no GPU: scene.render() updates matrices, culling, animations and observables but draws nothing, so scenes run in Node.js 2,131 for tests, server-side logic and asset processing. This checks that six books fit a shelf:
const BABYLON = require('babylonjs'); // the UMD package works in Node too
const pages = [312, 428, 256, 198, 344, 176]; // BookNest's six books
const engine = new BABYLON.NullEngine({ renderWidth: 1280, renderHeight: 720 });
const scene = new BABYLON.Scene(engine);
new BABYLON.FreeCamera('camera', new BABYLON.Vector3(0, 1.5, -4), scene);
let x = -1.2; // left end of a 2.4 m shelf
for (const [i, n] of pages.entries()) {
const width = n / 4000 + 0.02; // spine thickness from page count
const book = BABYLON.MeshBuilder.CreateBox(`book${i + 1}`, { width, height: 0.3 }, scene);
book.position.set(x + width / 2, 1.15, 0);
x += width + 0.01;
}
scene.render();
console.log(`books use ${(x + 1.2).toFixed(3)} m of the shelf`);
console.log(`active meshes after render: ${scene.getActiveMeshes().length}`);Output
BJS - [15:42:56]: Babylon.js v9.28.0 - Null engine books use 0.609 m of the shelf active meshes after render: 6 (node:18804) Warning: `--localstorage-file` was provided without a valid path (Use `node --trace-warnings ...` to show where the warning was created)
It ran on Node 25 after npm 2,036 install babylonjs. The harmless warning appears as soon as the package is required: it touches localStorage, which Node 25 now provides, with no storage file given. Anything needing a real canvas, such as camera.attachControl or a DynamicTexture, fails here, and loading files over HTTP needs an XMLHttpRequest polyfill (the documentation uses the xhr2 package).
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 320px; display: block; touch-action: none; }
#log { max-width: 600px; margin: 0; padding: 6px 8px; font-family: monospace; }
</style>
<canvas id="view"></canvas>
<pre id="log"></pre>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
const log = (line) => { document.getElementById('log').textContent += line + '\n'; };
// 1. Headless: the same code would run in Node.js with require('babylonjs')
const pages = [312, 428, 256, 198, 344, 176]; // BookNest's six books
const nullEngine = new BABYLON.NullEngine({ renderWidth: 1280, renderHeight: 720 });
const headless = new BABYLON.Scene(nullEngine);
new BABYLON.FreeCamera('camera', new BABYLON.Vector3(0, 1.5, -4), headless);
let x = -1.2; // left end of a 2.4 m shelf
for (const [i, n] of pages.entries()) {
const width = n / 4000 + 0.02; // spine thickness from page count
const book = BABYLON.MeshBuilder.CreateBox(`book${i + 1}`, { width, height: 0.3 }, headless);
book.position.set(x + width / 2, 1.15, 0);
x += width + 0.01;
}
headless.render(); // matrices and culling, no drawing
log(`NullEngine: books use ${(x + 1.2).toFixed(3)} m of the 2.4 m shelf`);
log(`NullEngine: active meshes after render: ${headless.getActiveMeshes().length}`);
const layout = headless.meshes.map((m) => ({ name: m.name, x: m.position.x, width: m.getBoundingInfo().boundingBox.extendSize.x * 2 }));
headless.dispose();
nullEngine.dispose();
// 2. On screen: a WebGL engine draws the layout the headless scene computed
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.15, 1.25, 2.7, new BABYLON.Vector3(0, 1.1, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene);
const shelf = BABYLON.MeshBuilder.CreateBox('shelf', { width: 2.4, height: 0.04, depth: 0.4 }, scene);
shelf.position.y = 0.98;
shelf.material = new BABYLON.StandardMaterial('walnut', scene);
shelf.material.diffuseColor = BABYLON.Color3.FromHexString('#6d4c33');
const COLORS = ['#1f5f8b', '#5b3f99', '#e09a10', '#3f7d3a', '#b5452f', '#2a9d8f'];
layout.forEach(({ name, x, width }, i) => {
const book = BABYLON.MeshBuilder.CreateBox(name, { width, height: 0.3, depth: 0.25 }, scene);
book.position.set(x, 1.15, 0);
book.material = new BABYLON.StandardMaterial(name, scene);
book.material.diffuseColor = BABYLON.Color3.FromHexString(COLORS[i]);
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>