Both engines load the same glTF clips and blend them by weight. Three.js 30,815 (The Animation System) separates data from playback and makes you advance time; Babylon 89,603 's scene advances everything in render(), in frames:
| Concept | Babylon.js | Three.js |
|---|---|---|
| One animated property | Animation (keys by frame) | KeyframeTrack (times in seconds) |
| A clip of many properties | AnimationGroup | AnimationClip |
| A playing instance | Animatable, or the group itself | AnimationAction from mixer.clipAction() |
| Advancing time | Automatic in scene.render() | mixer.update(delta) every frame |
| Weights and crossfades | weight, enableBlending | setEffectiveWeight(), crossFadeTo() |
Three.js's explicit update() makes fixed-step or server-side animation simpler.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 310px; 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, 1.2, 6, new BABYLON.Vector3(0, 0.7, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.5), scene);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
// The shared data: an Animation is Babylon's KeyframeTrack, with keys in frames, not seconds
const hop = new BABYLON.Animation('hop', 'position.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT,
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
hop.setKeys([{ frame: 0, value: 0.5 }, { frame: 30, value: 1.4 }, { frame: 60, value: 0.5 }]);
const spin = new BABYLON.Animation('spin', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT,
BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE);
spin.setKeys([{ frame: 0, value: 0 }, { frame: 60, value: Math.PI * 2 }]);
const makeBook = (x, hex, text) => {
const book = BABYLON.MeshBuilder.CreateBox('book', { width: 0.6, height: 0.9, depth: 0.15 }, scene);
book.position.set(x, 0.5, 0);
book.material = new BABYLON.StandardMaterial(hex, scene);
book.material.diffuseColor = BABYLON.Color3.FromHexString(hex);
const base = new BABYLON.TransformNode('base', scene);
base.position.x = x;
const label = new BABYLON.GUI.TextBlock('', text);
Object.assign(label, { color: '#333', fontSize: 11, resizeToFit: true });
ui.addControl(label);
label.linkWithMesh(base);
label.linkOffsetY = 40;
return book;
};
// 1. Babylon: the scene advances every Animatable inside scene.render()
const auto = makeBook(-2.1, '#1f5f8b', 'beginDirectAnimation\nadvanced by scene.render()');
scene.beginDirectAnimation(auto, [hop], 0, 60, true);
// 2. Three.js style: you own the clock and call update(delta), here with animation.evaluate()
const manual = makeBook(0, '#3f7d3a', 'evaluate(frame) by hand\nlike mixer.update(delta)');
let frame = 0;
scene.onBeforeRenderObservable.add(() => {
frame = (frame + engine.getDeltaTime() / 1000 * 60) % 60; // seconds -> frames
manual.position.y = hop.evaluate(frame);
});
// 3. Weights: two animatables on one target blend; ramp the weights for a crossfade
const blended = makeBook(2.1, '#b5452f', 'weight 1 -> 0 and 0 -> 1\nlike crossFadeTo()');
const a = scene.beginDirectAnimation(blended, [hop], 0, 60, true);
const b = scene.beginDirectAnimation(blended, [spin], 0, 60, true);
scene.onBeforeRenderObservable.add(() => {
const w = 0.5 + 0.5 * Math.sin(performance.now() / 1200);
a.weight = w; // hop fades out as spin fades in
b.weight = 1 - w;
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>