Sync Ammo.js Physics with Svelte Lifecycle

This article explains how to efficiently integrate the Ammo.js physics engine with the Svelte component lifecycle. You will learn how to initialize the WebAssembly-based physics world, run a performant simulation loop using Svelte’s native lifecycle hooks, synchronize rigid body data with your visual rendering library, and properly clean up memory to prevent leaks.

1. Initialize Ammo.js inside onMount

Because Ammo.js relies on WebAssembly (WASM), it must be loaded asynchronously in the browser. Svelte’s onMount hook is the ideal place to initialize the library, as it ensures the code only runs on the client-side after the DOM is ready.

<script>
  import { onMount, onDestroy } from 'svelte';

  let physicsWorld;
  let animationFrameId;
  let lastTime = performance.now();

  onMount(async () => {
    // Wait for Ammo WebAssembly to load
    const AmmoLib = await Ammo();
    
    // Set up collision configuration and dispatchers
    const collisionConfiguration = new AmmoLib.btDefaultCollisionConfiguration();
    const dispatcher = new AmmoLib.btCollisionDispatcher(collisionConfiguration);
    const overlappingPairCache = new AmmoLib.btDbvtBroadphase();
    const solver = new AmmoLib.btSequentialImpulseConstraintSolver();
    
    // Create the physics world
    physicsWorld = new AmmoLib.btDiscreteDynamicsWorld(
      dispatcher,
      overlappingPairCache,
      solver,
      collisionConfiguration
    );
    physicsWorld.setGravity(new AmmoLib.btVector3(0, -9.81, 0));

    // Start the render/physics loop
    startLoop();
  });
</script>

2. Implement the Simulation Loop

To keep the physics simulation running smoothly, step the physics world inside a requestAnimationFrame loop. This loop calculates the time delta between frames and updates the Ammo.js world.

function startLoop() {
  const tick = () => {
    const now = performance.now();
    const deltaTime = (now - lastTime) / 1000; // Convert to seconds
    lastTime = now;

    if (physicsWorld) {
      // Step simulation (deltaTime, maxSubSteps, fixedTimeStep)
      physicsWorld.stepSimulation(deltaTime, 10);
      
      // Update visual meshes here
      updateVisuals();
    }

    animationFrameId = requestAnimationFrame(tick);
  };

  animationFrameId = requestAnimationFrame(tick);
}

3. Synchronize Physics Bodies with Visual Meshes

Ammo.js calculates physics in its own memory space. To reflect these updates in a WebGL renderer (like Three.js) integrated with Svelte, you must extract the transform from the rigid body’s motion state and apply it to your visual objects during every tick.

let rigidBodies = []; // Array of { mesh, body } pairings

function updateVisuals() {
  const transform = new Ammo.btTransform();
  
  for (let i = 0; i < rigidBodies.length; i++) {
    const { mesh, body } = rigidBodies[i];
    const motionState = body.getMotionState();
    
    if (motionState) {
      motionState.getWorldTransform(transform);
      const origin = transform.getOrigin();
      const rotation = transform.getRotation();

      // Update the visual mesh position and rotation
      mesh.position.set(origin.x(), origin.y(), origin.z());
      mesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());
    }
  }
}

4. Prevent Memory Leaks using onDestroy

Ammo.js is a C++ library ported to WebAssembly, meaning it does not benefit from automatic JavaScript garbage collection. You must manually destroy instantiated objects. Use Svelte’s onDestroy lifecycle hook to cancel the animation frame and free up WebAssembly memory.

<script>
  // ... previous setup code ...

  onDestroy(() => {
    // Stop the loop
    if (animationFrameId) {
      cancelAnimationFrame(animationFrameId);
    }

    // Clean up rigid bodies and physics world
    if (physicsWorld) {
      // Iterate backwards to safely remove bodies
      for (let i = rigidBodies.length - 1; i >= 0; i--) {
        const { body } = rigidBodies[i];
        physicsWorld.removeRigidBody(body);
        Ammo.destroy(body);
      }

      // Destroy core world components
      Ammo.destroy(physicsWorld);
      // Destroy solvers, dispatchers, and configurations initially created
    }
  });
</script>