PlayCanvas 62,178 (github.com/playcanvas/engine (https://github.com/playcanvas/engine 16,941 ), MIT, 2.22.4) is a game engine "built on WebGL2 & WebGPU" with glTF streaming, Draco 7,495 and Basis compression, ammo.js 4,573 physics and first-class 3D Gaussian splat rendering. Its distinguishing feature is the PlayCanvas Editor, a collaborative browser-based scene editor: the engine is free, while the hosted Editor has a Free plan (unlimited public projects, 1 GB), Personal at $15 a month and Organization at $50 per seat a month for private projects (prices from playcanvas.com/plans in September 2026). Choose it when a team wants a Unity-like editor in the browser; Babylon.js Versus PlayCanvas compares it with Babylon.js 89,603 .
<!doctype html>
<style>
body { margin: 0; background: #f5f1e8; font: 13px system-ui, sans-serif; color: #263238; }
canvas { display: block; max-width: 100%; }
.ui { padding: 4px 8px; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/" } }
</script>
<div class="ui">Team size <input type="range" id="team" min="1" max="10" value="3"> <b id="n">3</b> people (monthly cost; prices from playcanvas.com/plans, September 2026)</div>
<script type="module">
import * as THREE from 'three';
const PLANS = [
{ name: 'Engine (MIT)', cost: () => 0, note: 'free, self-hosted', color: '#3f7d3a' },
{ name: 'Editor Free', cost: () => 0, note: 'public projects, 1 GB', color: '#2a9d8f' },
{ name: 'Personal', cost: () => 15, note: '$15 / month', color: '#e09a10' },
{ name: 'Organization', cost: (n) => 50 * n, note: '$50 per seat', color: '#b5452f' },
];
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(600, 300);
document.body.prepend(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color('#fbf8f2');
const camera = new THREE.PerspectiveCamera(35, 2, 0.1, 50);
camera.position.set(0.8, 2.4, 6.4);
camera.lookAt(0, 1, 0);
scene.add(new THREE.HemisphereLight('#ffffff', '#8d6e63', 1.3));
const sun = new THREE.DirectionalLight('#ffffff', 2);
sun.position.set(3, 5, 4);
scene.add(sun);
const base = new THREE.Mesh(new THREE.BoxGeometry(5, 0.05, 1.4), new THREE.MeshStandardMaterial({ color: '#e0d8c8' }));
scene.add(base);
// A redrawable text sprite
function textSprite(width = 1.6) {
const canvas = Object.assign(document.createElement('canvas'), { width: 512, height: 128 });
const g = canvas.getContext('2d');
const map = new THREE.CanvasTexture(canvas);
map.colorSpace = THREE.SRGBColorSpace;
const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map }));
sprite.scale.set(width, width / 4, 1);
sprite.draw = (lines) => {
g.clearRect(0, 0, 512, 128);
g.textAlign = 'center'; g.fillStyle = '#263238';
lines.forEach(([text, size], i) => { g.font = `bold ${size}px system-ui, sans-serif`; g.fillText(text, 256, 50 + i * 50); });
map.needsUpdate = true;
};
scene.add(sprite);
return sprite;
}
const bars = PLANS.map((plan, i) => {
const bar = new THREE.Mesh(new THREE.BoxGeometry(0.7, 1, 0.7), new THREE.MeshStandardMaterial({ color: plan.color, roughness: 0.5 }));
bar.position.x = (i - 1.5) * 1.2;
scene.add(bar);
const name = textSprite();
name.position.set(bar.position.x, -0.3, 0.8);
name.draw([[plan.name, 40], [plan.note, 30]]);
return { plan, bar, value: textSprite(1.3), h: 0.02 };
});
const slider = document.getElementById('team');
renderer.setAnimationLoop(() => {
const n = +slider.value;
document.getElementById('n').textContent = n;
for (const b of bars) {
const cost = b.plan.cost(n);
const target = Math.max(0.03, cost / 200 * 2.6); // $200 → 2.6 m
b.h += (target - b.h) * 0.1;
b.bar.scale.y = b.h;
b.bar.position.y = b.h / 2;
b.value.position.set(b.bar.position.x, b.h + 0.3, 0);
const text = cost ? `$${cost}` : 'free';
if (b.value.last !== text) { b.value.last = text; b.value.draw([[text, 64]]); }
}
renderer.render(scene, camera);
});
</script>