runRenderLoop

The Render Loop with runRenderLoop

engine.runRenderLoop(fn) calls fn from requestAnimationFrame (Game Loop), pausing in hidden tabs, and scene.render() runs one frame: animations, camera, culling, drawing. Put per-frame logic in observables, scaled by engine.getDeltaTime() (milliseconds since the last frame) so speed ignores the frame rate:

Frame-rate-independent motion in onBeforeRenderObservableJavaScript
scene.onBeforeRenderObservable.add(() => {
  book.rotation.y += (Math.PI / 2) * engine.getDeltaTime() / 1000;   // 90 degrees a second
});
engine.runRenderLoop(() => scene.render());
setTimeout(() => engine.stopRenderLoop(), 2000);      // no argument: remove every loop

In headless runs those two seconds took 120 frames (engine.getFps() 60) and turned the book 179 to 180 degrees; the first frame's getDeltaTime() is 0. stopRenderLoop(fn) removes one function. For fixed-step physics, the engine options deterministicLockstep and timeStep fire scene.onBeforeStepObservable at a fixed rate. Three.js 30,815 's equivalent is setAnimationLoop() (setAnimationLoop). Since you call scene.render() yourself, you can skip idle frames.

A book turning 90 degrees a second with getDeltaTime, and a button to stop the render loopHTMLLive
<!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; }
  #bar { max-width: 600px; display: flex; gap: 12px; align-items: center; padding: 6px 8px; }
  #info { font-family: monospace; }
  button { font: inherit; padding: 4px 12px; }
</style>
<canvas id="view"></canvas>
<div id="bar"><button id="toggle">Stop loop</button><span id="info"></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('#cfd8dcff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.1, 4, new BABYLON.Vector3(0, 0.2, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene);

const book = BABYLON.MeshBuilder.CreateBox('book', { width: 1, height: 1.5, depth: 0.25 }, scene);
book.material = new BABYLON.StandardMaterial('cover', scene);
book.material.diffuseColor = BABYLON.Color3.FromHexString('#3f7d3a');
const base = BABYLON.MeshBuilder.CreateCylinder('base', { height: 0.1, diameter: 1.6 }, scene);
base.position.y = -0.8;

// Per-frame logic in an observable, scaled by elapsed time so speed ignores the frame rate
scene.onBeforeRenderObservable.add(() => {
  book.rotation.y += (Math.PI / 2) * engine.getDeltaTime() / 1000;   // 90 degrees a second
});

const info = document.getElementById('info');
scene.onAfterRenderObservable.add(() => {
  const degrees = (book.rotation.y * 180 / Math.PI) % 360;
  info.textContent = `frame ${scene.getFrameId()}  fps ${engine.getFps().toFixed(0)}  ` +
    `dt ${engine.getDeltaTime().toFixed(1)} ms  angle ${degrees.toFixed(0)}°`;
});

const renderFrame = () => scene.render();
let running = true;
engine.runRenderLoop(renderFrame);                   // called from requestAnimationFrame

document.getElementById('toggle').addEventListener('click', (event) => {
  running = !running;
  if (running) engine.runRenderLoop(renderFrame);
  else engine.stopRenderLoop(renderFrame);          // removes just this function
  event.target.textContent = running ? 'Stop loop' : 'Start loop';
});
addEventListener('resize', () => engine.resize());
</script>