No engine wins everywhere. Start from the project's most demanding requirement:
| If the project needs | Choose | Because |
|---|---|---|
| The smallest download for a viewer or effect | Three.js 30,815 (or OGL 4,660 ) | 139 KB gzipped for a scene |
| Physics, GUI, audio, XR and tools, one vendor | Babylon.js 89,603 | All first-party, one version |
| A stable API for a long-lived product | Babylon.js | No breaks between majors |
| The most examples, answers and hires | Three.js | About 40 times the npm 2,036 downloads |
| Designers building scenes in an editor | PlayCanvas 62,178 | Collaborative browser Editor |
| React 7,897 state driving the scene | R3F 32,576 , or react-babylonjs 889 | R3F is far more widely used |
| A game already made in Unity 2,152 or Godot 15,706 | Its web export | Reuses the project |
| Film-quality visuals on any device | Unreal 133 Pixel Streaming | Server GPU, costly per visitor |
| One scene in a web page and a native app | Babylon Native 922 | Same code, preview status |
Two questions settle most cases. Who builds the scenes, programmers or artists in an editor? And how much of the engine would you otherwise assemble yourself? BookNest needed a walkable room, picking, a GUI panel, physics, particles, audio and a VR mode, built in code by a small team, which is Babylon.js's home ground. A product page with one rotating book would have been better served by Three.js.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 380px; display: block; touch-action: none; outline: none; }
</style>
<canvas id="view"></canvas>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.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.95, 10.5, new BABYLON.Vector3(2.6, 0.6, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.6), scene);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
ui.idealWidth = 600;
const ENGINES = ['Three.js', 'Babylon.js', 'PlayCanvas', 'R3F / react-babylonjs', 'Unity / Godot', 'Pixel Streaming', 'Babylon Native'];
const ROWS = [
['The smallest download', 'Three.js', '139 KB gzipped for a scene'],
['Physics, GUI, audio, XR, tools', 'Babylon.js', 'all first-party, one version'],
['A stable API for years', 'Babylon.js', 'no breaks between majors'],
['The most examples and hires', 'Three.js', 'about 40x the npm downloads'],
['Designers in an editor', 'PlayCanvas', 'collaborative browser Editor'],
['React state drives the scene', 'R3F / react-babylonjs', 'R3F is far more widely used'],
['A game made in Unity/Godot', 'Unity / Godot', 'reuses the project'],
['Film-quality on any device', 'Pixel Streaming', 'server GPU, costly per visitor'],
['A web page and a native app', 'Babylon Native', 'same code, preview status'],
];
const podiums = ENGINES.map((name, i) => {
const podium = BABYLON.MeshBuilder.CreateCylinder(name, { height: 1, diameter: 0.7 }, scene);
podium.position.set(2.2 + (i % 4) * 1.55, 0, i < 4 ? 1.4 : -1.4);
podium.material = new BABYLON.StandardMaterial(name, scene);
podium.material.diffuseColor = BABYLON.Color3.FromHexString('#cfc6b8');
const block = new BABYLON.GUI.TextBlock('', name.replace(' / ', '/\n'));
Object.assign(block, { color: '#333', fontSize: 10, resizeToFit: true });
ui.addControl(block);
const foot = new BABYLON.TransformNode('foot', scene);
foot.position.copyFrom(podium.position);
block.linkWithMesh(foot);
block.linkOffsetY = 20;
return podium;
});
const list = Object.assign(new BABYLON.GUI.StackPanel(), { width: '230px', left: '6px', horizontalAlignment: 0 });
ui.addControl(list);
list.addControl(Object.assign(new BABYLON.GUI.TextBlock('', 'If the project needs…'), { height: '24px', fontSize: 13, color: '#1f5f8b', fontWeight: 'bold' }));
const reason = Object.assign(new BABYLON.GUI.TextBlock('', ''), { height: '24px', fontSize: 12, color: '#b5452f', top: '-8px', verticalAlignment: 1 });
ui.addControl(reason);
const buttons = ROWS.map(([need, choice, because], i) => {
const button = BABYLON.GUI.Button.CreateSimpleButton('need' + i, need);
Object.assign(button, { height: '30px', fontSize: 11, color: '#333', background: 'white', thickness: 1, cornerRadius: 4, paddingTop: '3px' });
button.onPointerUpObservable.add(() => choose(i));
list.addControl(button);
return button;
});
let target = null;
function choose(i) {
const [need, choice, because] = ROWS[i];
buttons.forEach((b, k) => { b.background = k === i ? '#ffe9b0' : 'white'; });
target = ENGINES.indexOf(choice);
reason.text = `${need} → ${choice}: ${because}`;
}
scene.onBeforeRenderObservable.add(() => { // the chosen podium rises, the others settle
podiums.forEach((podium, i) => {
const goal = i === target ? 2.2 : 0.5;
podium.scaling.y += (goal - podium.scaling.y) * 0.1;
podium.position.y = podium.scaling.y / 2;
podium.material.diffuseColor = BABYLON.Color3.FromHexString(i === target ? '#1f5f8b' : '#cfc6b8');
});
});
choose(1); // BookNest's case: everything first-party
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>