Constraints

Compound Shapes and Constraints

A PhysicsShapeContainer holds child shapes with their own offsets, so a concave object becomes a few convex parts. BookNest's display table, createDisplayTable(scene, x, z) in l2-physics.js, merges a top and four legs into one render mesh and adds each part to a container with shape.addChild(new BABYLON.PhysicsShapeBox(center, BABYLON.Quaternion.Identity(), size, scene)). new BABYLON.Debug.PhysicsViewer(scene).showBody(body) draws shapes as Havok 516,349 sees them.

A constraint joins two bodies through a pivot and axes in each body's local space: BallAndSocket, Distance, Hinge, Slider, Prismatic, Lock and Spring (each ...Constraint), and the general Physics6DoFConstraint with per-axis limits (a door that stops at 100 degrees). BookNest's hanging sign:

A BookNest sign swinging on a hingeJavaScript
const box = (name, width, height, y, mass) => {    // a board just inside the front window
  const mesh = BABYLON.MeshBuilder.CreateBox(name, { width, height, depth: 0.04 }, scene);
  mesh.position.set(0, y, -2.5);
  return new BABYLON.PhysicsAggregate(mesh, BABYLON.PhysicsShapeType.BOX, { mass }, scene);
};
const [bracket, sign] = [box('bracket', 0.9, 0.05, 2.6, 0), box('sign', 0.8, 0.3, 2.4, 1.5)];
const x = new BABYLON.Vector3(1, 0, 0);            // the hinge axis in both bodies
bracket.body.addConstraint(sign.body, new BABYLON.HingeConstraint(
  new BABYLON.Vector3(0, -0.03, 0), new BABYLON.Vector3(0, 0.17, 0), x, x, scene));  // pivots
sign.body.setAngularDamping(0.3);                  // air drag
sign.body.applyImpulse(new BABYLON.Vector3(0, 0, 0.9), sign.transformNode.position);
const turns = [];
for (let frame = 1, last = 0; frame <= 240; frame++) {
  scene.render();
  const spin = sign.body.getAngularVelocity().x;   // changes sign at each end of a swing
  const tilt = sign.transformNode.rotationQuaternion.toEulerAngles().x;
  if (spin * last < 0) turns.push(`${frame}: ${BABYLON.Tools.ToDegrees(tilt).toFixed(1)}`);
  last = spin;
}
console.log(turns.join(' '));
Output
14: -23.1 43: 21.5 71: -20.1 99: 18.8 127: -17.6 155: 16.4 183: -15.3 211: 14.2 239: -13.1

The swing reverses every 28 frames, a 0.93 s period, as the pendulum formula predicts for this board, and loses about 1.2 degrees per swing. Constrained pairs do not collide unless you set isCollisionsEnabled.

A hanging shop sign swinging on a hinge constraint, beside a display table whose compound shape is drawn in wireframeHTMLLive
<!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; outline: none; }
  #info { max-width: 600px; margin: 0; padding: 4px 8px; font-family: monospace; white-space: pre-wrap; }
</style>
<canvas id="view"></canvas>
<pre id="info"></pre>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script>
// ---- BookNest's bookstore: the helpers the book keeps in bookstore.js ----
const ROOM = { width: 8, depth: 6, height: 3, wall: 0.15 };
const BOOKS = [
  { id: 1, title: 'The Quiet Harbor', author: 'Mara Ellison', color: '#1f5f8b', pages: 312, price: 14.99, rating: 4.6, inStock: true },
  { id: 2, title: 'Patterns of the Deep Web', author: 'Tomas Reyes', color: '#5b3f99', pages: 428, price: 32.5, rating: 4.4, inStock: true },
  { id: 3, title: 'Salt and Saffron', author: 'Priya Nair', color: '#e09a10', pages: 256, price: 24, rating: 4.8, inStock: false },
  { id: 4, title: 'Small Steps to Big Summits', author: 'Dana Whitfield', color: '#3f7d3a', pages: 198, price: 18.5, rating: 4.3, inStock: true },
  { id: 5, title: "The Clockmaker's Paradox", author: 'Julian Voss', color: '#b5452f', pages: 344, price: 16.75, rating: 4.7, inStock: true },
  { id: 6, title: 'Gardens in Glass', author: 'Lena Hart', color: '#2a9d8f', pages: 176, price: 21.3, rating: 4.5, inStock: true },
];
function createScene(engine) {
  const scene = new BABYLON.Scene(engine);
  scene.clearColor = BABYLON.Color4.FromHexString('#cfd8dcff');
  const sky = new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0.3, 1, -0.4), scene);
  sky.groundColor = new BABYLON.Color3(0.35, 0.32, 0.3);
  return scene;
}
function paint(scene, name, hex) {                  // a matte color
  const material = new BABYLON.StandardMaterial(name, scene);
  material.diffuseColor = BABYLON.Color3.FromHexString(hex);
  material.specularColor = BABYLON.Color3.Black();
  return material;
}
function createRoom(scene) {                        // floor and three walls, front open
  const room = new BABYLON.TransformNode('room', scene);
  const floor = BABYLON.MeshBuilder.CreateGround('floor', { width: ROOM.width, height: ROOM.depth }, scene);
  floor.material = paint(scene, 'oak', '#b08d6a');
  floor.parent = room;
  const plaster = paint(scene, 'plaster', '#fff3e0');
  for (const [name, width, x, z] of [['backWall', 8, 0, 3], ['leftWall', 6, -4, 0], ['rightWall', 6, 4, 0]]) {
    const wall = BABYLON.MeshBuilder.CreateBox(name, { width, height: ROOM.height, depth: ROOM.wall }, scene);
    wall.position.set(x, ROOM.height / 2, z);
    wall.rotation.y = x ? Math.PI / 2 : 0;
    wall.material = plaster;
    wall.parent = room;
  }
  return room;
}
function createBookcase(scene, name, x) {           // 8 boxes merged into 1 mesh
  const [w, h, d, t] = [3, 2.2, 0.35, 0.04];
  const parts = [[t, h, d, -w / 2, h / 2, 0], [t, h, d, w / 2, h / 2, 0], [w, h, t, 0, h / 2, d / 2 - t / 2]];
  for (let i = 0; i < 5; i++) parts.push([w, t, d, 0, 0.1 + i * 0.5, 0]);
  const bookcase = BABYLON.Mesh.MergeMeshes(parts.map(([width, height, depth, px, py, pz]) => {
    const part = BABYLON.MeshBuilder.CreateBox('part', { width, height, depth }, scene);
    part.position.set(px, py, pz);
    return part;
  }), true);
  bookcase.name = name;
  bookcase.material = paint(scene, 'walnut', '#6d4c33');
  bookcase.position.set(x, 0, ROOM.depth / 2 - ROOM.wall / 2 - d / 2);   // against the back wall
  return bookcase;
}
function drawCover(scene, book) {                   // cover art painted on a canvas
  const tex = new BABYLON.DynamicTexture('cover' + book.id, { width: 256, height: 384 }, scene);
  const c = tex.getContext();
  c.fillStyle = book.color; c.fillRect(0, 0, 256, 384);
  c.fillStyle = 'rgba(0,0,0,0.4)'; c.fillRect(0, 0, 18, 384);         // the dark left edge
  c.strokeStyle = '#fff8e1'; c.lineWidth = 3; c.strokeRect(34, 24, 202, 336);
  c.fillStyle = '#fff8e1'; c.textAlign = 'center'; c.font = 'bold 30px Georgia';
  const lines = [''];
  for (const word of book.title.split(' ')) {
    const test = (lines[lines.length - 1] + ' ' + word).trim();
    if (c.measureText(test).width > 180) lines.push(word); else lines[lines.length - 1] = test;
  }
  lines.forEach((line, i) => c.fillText(line, 135, 110 + i * 38));
  c.font = '20px Georgia'; c.fillText(book.author, 135, 330);
  tex.update();
  return tex;
}
function placeBooks(scene, bookcase, shelf = 2) {   // six books, spines out, covers on +x
  const strip = new BABYLON.Vector4(0, 0, 0.07, 1);
  const faceUV = [strip, strip, new BABYLON.Vector4(0, 0, 1, 1), strip, strip, strip];
  let x = -0.35;                                    // left end of the row, local to the bookcase
  return BOOKS.map((record) => {
    const width = record.pages / 4000 + 0.02;       // spine thickness from page count
    const book = BABYLON.MeshBuilder.CreateBox(`book${record.id}`, { width, height: 0.3, depth: 0.22, faceUV, wrap: true }, scene);
    book.parent = bookcase;
    book.position.set(x + width / 2, 0.12 + shelf * 0.5 + 0.15, -0.02);
    x += width + 0.012;
    book.material = new BABYLON.StandardMaterial(record.title, scene);
    book.material.diffuseTexture = drawCover(scene, record);
    book.material.specularColor = new BABYLON.Color3(0.15, 0.15, 0.15);
    book.metadata = record;                         // the record, for picking
    return book;
  });
}
// The book cuts the storefront with CSG2 (Manifold WebAssembly); this sandbox cannot download
// the .wasm, so the older pure-JavaScript BABYLON.CSG makes the same cuts here.
function createFrontWall(scene) {
  const box = (width, height, depth, x, y) => {
    const mesh = BABYLON.MeshBuilder.CreateBox('cut', { width, height, depth }, scene);
    mesh.position.set(x, y, -ROOM.depth / 2);
    return mesh;
  };
  const solid = box(ROOM.width, ROOM.height, ROOM.wall, 0, ROOM.height / 2);
  const windowHole = box(3.2, 1.6, 1, -1.4, 1.6), doorHole = box(1.1, 2.2, 1, 2.3, 1.1);
  const wall = BABYLON.CSG.FromMesh(solid).subtract(BABYLON.CSG.FromMesh(windowHole))
    .subtract(BABYLON.CSG.FromMesh(doorHole)).toMesh('frontWall', scene.getMaterialByName('plaster'), scene);
  [solid, windowHole, doorHole].forEach((mesh) => mesh.dispose());
  return wall;
}
// ---- end of the bookstore helpers ----

/*
  With Havok (not downloadable in this sandbox) the sign is two aggregates and a constraint:
    bracket.body.addConstraint(sign.body, new BABYLON.HingeConstraint(
      new BABYLON.Vector3(0, -0.03, 0), new BABYLON.Vector3(0, 0.17, 0), x, x, scene));
  and the table a PhysicsShapeContainer with a PhysicsShapeBox child per part. Here the hinge is
  integrated directly: a 1.5 kg board swinging about the x axis 0.17 m above its center.
*/
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true, {}, true);
const scene = createScene(engine);
const floor = BABYLON.MeshBuilder.CreateGround('floor', { width: 6, height: 4 }, scene);
floor.material = paint(scene, 'oak', '#b08d6a');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2 - 0.4, 1.35, 3.4, new BABYLON.Vector3(0.6, 1.7, 0), scene);
camera.attachControl(canvas, true);

// The sign: a static bracket and a board hanging from a hinge node (the constraint's pivot)
const bracket = BABYLON.MeshBuilder.CreateBox('bracket', { width: 0.9, height: 0.05, depth: 0.04 }, scene);
bracket.position.set(1, 2.6, 0);
bracket.material = paint(scene, 'iron', '#333333');
const hinge = new BABYLON.TransformNode('hinge', scene);
hinge.position.set(1, 2.57, 0);                      // 0.03 m under the bracket's center
const sign = BABYLON.MeshBuilder.CreateBox('sign', { width: 0.8, height: 0.3, depth: 0.04 }, scene);
sign.parent = hinge;
sign.position.y = -0.17;                             // the board's pivot, 0.17 m above its center
const face = new BABYLON.DynamicTexture('signFace', { width: 512, height: 192 }, scene);
face.drawText('BookNest', null, 125, 'bold 110px Georgia', '#fff8e1', '#1f5f8b');
sign.material = new BABYLON.StandardMaterial('signMat', scene);
sign.material.diffuseTexture = face;

// Pendulum physics for the hinge: I = m (h^2 / 12 + d^2), torque = -m g d sin(angle)
const m = 1.5, d = 0.17, h = 0.3, inertia = m * (h * h / 12 + d * d), damping = 0.3;
let angle = 0, spin = 0.9 * d / inertia;              // an impulse of 0.9 N s at the center
const turns = [];
let frame = 0, lastSpin = spin;
scene.onBeforeRenderObservable.add(() => {
  const dt = 1 / 60;
  spin += (-m * 9.81 * d * Math.sin(angle) / inertia - damping * spin) * dt;
  angle += spin * dt;
  hinge.rotation.x = angle;
  frame++;
  if (spin * lastSpin < 0 && turns.length < 8) turns.push(`${frame}: ${(angle * 180 / Math.PI).toFixed(1)}°`);
  lastSpin = spin;
  if (frame === 600) { angle = 0; spin = 0.9 * d / inertia; frame = 0; turns.length = 0; }   // push again
  document.getElementById('info').textContent = `swing ends (frame: angle) ${turns.join('  ')}\n` +
    `period ${(2 * Math.PI * Math.sqrt(inertia / (m * 9.81 * d))).toFixed(2)} s`;
});

// The display table: one render mesh, five child boxes in its compound shape (the wireframes)
const wire = new BABYLON.StandardMaterial('wire', scene);
wire.wireframe = true;
wire.emissiveColor = BABYLON.Color3.FromHexString('#b5452f');
wire.disableLighting = true;
const parts = [[1.2, 0.05, 0.8, 0, 0.725, 0]];
for (const [lx, lz] of [[-0.55, -0.35], [0.55, -0.35], [-0.55, 0.35], [0.55, 0.35]]) parts.push([0.06, 0.7, 0.06, lx, 0.35, lz]);
const table = BABYLON.Mesh.MergeMeshes(parts.map(([w, hh, dd, px, py, pz]) => {
  const part = BABYLON.MeshBuilder.CreateBox('part', { width: w, height: hh, depth: dd }, scene);
  part.position.set(-1.5 + px, py, 1 + pz);
  const shape = BABYLON.MeshBuilder.CreateBox('childShape', { width: w + 0.02, height: hh + 0.02, depth: dd + 0.02 }, scene);
  shape.position.copyFrom(part.position);
  shape.material = wire;                             // as Debug.PhysicsViewer draws the container
  return part;
}), true);
table.material = paint(scene, 'tableWood', '#8d6e4f');
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>