b3d-physics

Enables Jolt physics on the scene. Add as a child of <tosi-b3d> to opt in to rigid-body simulation. Other components (like the mesh exploder) auto-detect the physics engine and use it when available.

Jolt loads asynchronously (WASM). The component exposes a ready promise and dispatches a 'physics-ready' event when initialization completes.

Demo

import { b3d, b3dPhysics, b3dLight, b3dSkybox, b3dGround, b3dSphere, explodeMesh, label3d, button3d, toggle3d } from 'tosijs-3d'
import { elements } from 'tosijs'
const { div, p } = elements

let sphere = null
let dropSphere = null
let dropAggregate = null
let babylon = null
const physics = b3dPhysics()

function createSphere() {
  sphere = babylon.MeshBuilder.CreateSphere(
    'target', { diameter: 2, segments: 12 }, scene.scene
  )
  sphere.position.y = 4
  const mat = new babylon.StandardMaterial('mat', scene.scene)
  mat.diffuseColor = new babylon.Color3(0.8, 0.2, 0.1)
  sphere.material = mat
}

function createDropSphere() {
  dropSphere = babylon.MeshBuilder.CreateSphere(
    'dropTarget', { diameter: 1.5, segments: 12 }, scene.scene
  )
  dropSphere.position.set(4, 12, 0)
  const mat = new babylon.StandardMaterial('dropMat', scene.scene)
  mat.diffuseColor = new babylon.Color3(0.2, 0.5, 0.9)
  dropSphere.material = mat
  // Dynamic physics body — it will fall under gravity
  dropAggregate = new babylon.PhysicsAggregate(
    dropSphere, babylon.PhysicsShapeType.SPHERE,
    { mass: 2, restitution: 0.1 }, scene.scene
  )
  // Watch for impact
  let prevVelY = 0
  const checkImpact = () => {
    if (!dropSphere) { scene.scene.unregisterBeforeRender(checkImpact); return }
    const vel = new babylon.Vector3()
    dropAggregate.body.getLinearVelocityToRef(vel)
    // Detect sudden deceleration (hit something)
    if (prevVelY < -2 && Math.abs(vel.y) < Math.abs(prevVelY) * 0.5) {
      scene.scene.unregisterBeforeRender(checkImpact)
      explodeMesh(dropSphere, scene.scene, {
        fragments: 18,
        force: 8,
        tumble: 4,
        fadeStart: 0.8,
        duration: 10,
        restitution: 0.6,
      })
      dropAggregate.dispose()
      dropSphere = null
      dropAggregate = null
    }
    prevVelY = vel.y
  }
  scene.scene.registerBeforeRender(checkImpact)
}

function createObstacles(BABYLON, s) {
  const boxMat = new BABYLON.StandardMaterial('boxMat', s)
  boxMat.diffuseColor = new BABYLON.Color3(0.4, 0.5, 0.7)

  // Scattered obstacles — asymmetric layout for more interesting bounces
  const positions = [
    [-5, 1, 1], [4.5, 0.75, -2],
    [1, 1, -5], [-2, 1.25, 5],
    [-3.5, 0.5, -4], [5, 0.5, 4],
    [3, 0.75, 2], [-4, 0.4, 3],
    [0, 0.5, 6], [-6, 0.75, -1],
  ]
  const sizes = [
    [0.5, 2, 3], [1, 1.5, 1],
    [3, 2, 0.5], [0.8, 2.5, 2],
    [1.5, 1, 1.5], [1, 1, 2],
    [1.2, 1.5, 1.2], [2, 0.8, 0.8],
    [3, 1, 0.4], [0.5, 1.5, 3],
  ]
  for (let i = 0; i < positions.length; i++) {
    const [w, h, d] = sizes[i]
    const box = BABYLON.MeshBuilder.CreateBox('obstacle' + i, { width: w, height: h, depth: d }, s)
    box.position.set(positions[i][0], positions[i][1], positions[i][2])
    box.material = boxMat
    // Static physics body so fragments bounce off
    new BABYLON.PhysicsAggregate(box, BABYLON.PhysicsShapeType.BOX, { mass: 0, restitution: 0.5 }, s)
  }

  // Ramp
  const ramp = BABYLON.MeshBuilder.CreateBox('ramp', { width: 4, height: 0.15, depth: 3 }, s)
  ramp.position.set(0, 0.3, 5)
  ramp.rotation.x = -0.25
  ramp.material = boxMat
  new BABYLON.PhysicsAggregate(ramp, BABYLON.PhysicsShapeType.BOX, { mass: 0, restitution: 0.5 }, s)
}

const scene = b3d(
  {
    // Explode / Drop / show-colliders live in the dual-presence ⚙ panel, so the
    // physics playground is fully controllable from inside VR too.
    scenePanel: () => [
      label3d({ text: 'Physics' }),
      button3d({ label: 'Explode!', onClick: doExplode }),
      button3d({ label: 'Drop!', onClick: () => { if (!dropSphere) createDropSphere() } }),
      toggle3d({ label: 'show colliders', value: false, onChange: (v) => { physics.debug = v } }),
    ],
    sceneCreated(el, BABYLON) {
      babylon = BABYLON
      const camera = new BABYLON.ArcRotateCamera(
        'cam', -Math.PI / 2, Math.PI / 3, 14,
        new BABYLON.Vector3(0, 2, 0), el.scene
      )
      camera.attachControl(el.querySelector('canvas'), true)
      el.setActiveCamera(camera)
      createSphere()
    },
  },
  physics,
  b3dLight({ y: 1, intensity: 0.8 }),
  b3dSkybox({ timeOfDay: 12 }),
  b3dGround({ width: 20, height: 20, color: '#556644' }),
)

// Create obstacles once physics is ready
physics.ready.then(() => {
  createObstacles(babylon, scene.scene)
  // Also give the ground a static physics body
  const ground = scene.scene.getMeshByName('ground')
  if (ground) {
    new babylon.PhysicsAggregate(ground, babylon.PhysicsShapeType.BOX, { mass: 0, restitution: 0.3 }, scene.scene)
  }
})

function doExplode() {
  if (!sphere) return
  explodeMesh(sphere, scene.scene, {
    fragments: 24,
    force: 14,
    tumble: 6,
    fadeStart: 0.8,
    duration: 10,
    restitution: 0.5,
  })
  sphere = null
  setTimeout(createSphere, 6000)
}

preview.append(
  scene,
  div(
    { style: 'position:absolute; top:8px; right:8px; background:rgba(0,0,0,0.6); color:white; padding:8px 12px; border-radius:6px; font:12px monospace' },
    p('Fragments use Jolt physics. Controls in the ⚙ (VR too).'),
  ),
)

Attributes

Attribute Default Description
gravityX 0 Gravity X component
gravityY -9.81 Gravity Y component
gravityZ 0 Gravity Z component
debug false Show wireframe physics collider shapes