← Index · Prev · Next →

06 — Physics: bodies, constraints, triggers

All in subsystems/physics/, constraints.ts, collisions.ts. Havok V2. EnableHavokPhysics(scene) must run before Load.

Bodies (BuildPhysics)

One or more COLLIDER components and/or one RIGIDBODY → one node-attached PhysicsBody: collider-only = static/trigger; rigidbody-only = dynamic + auto-fit box; both = shape from collider(s), dynamics from body.

Multiple colliders (compound body)

An entity may carry more than one enabled COLLIDER component. Export writes each as its own manifest row; at load time the component registry's physics handler (loader/componentRegistry.ts) collects them all and BuildPhysics builds a PhysicsShapeContainer (Havok compound shape): each collider becomes a child shape with its own center/size/rotation, material, and trigger flag. One PhysicsBody, one entity.body; each collider still gets its own RegisterAttachment row (same body ref).

Authoring tips (the N-panel shows a compound-body notice when count > 1):

Single-collider paths below are unchanged.

Collider fields

BlenderManifestRuntime
Make Invisible (default off)makeInvisible: trueHideEntityNode in the registry's physics handler — mesh hidden at load; physics collider unchanged (same helper as viewport-hidden entities)
Show Preview (default on; viewport only)Cyan wireframe on selected objects (viewport/collider_preview.py)
Is TriggerisTriggerOverlap-only volume; optional eventMessages[] for Event Messages
Auto-Fit to Bounds (default on)autoFit: trueShape sized from owned-mesh bounds at load
Apply Object Scale (default on)applyObjectScaleLocal scale baked into authored dimensions (see manual path)

Use Make Invisible for collision-only props (invisible trigger volumes, invisible blocking meshes) without toggling the outliner eye icon — the mesh still exports in the glb and stays addressable; only runtime rendering is off until a behavior sets entity.node.isVisible = true.

Motion types

Manifest bodyTypeHavok PhysicsMotionTypeRole
STATICSTATICNever moves; still collides (terrain, walls).
DYNAMICDYNAMICFully simulated — forces, collisions, mass.
ANIMATEDANIMATEDDriven by animation or code; pushes dynamic bodies and constraints but is not pushed by collisions. Use for elevators, moving platforms, or behaviors that set node transforms each frame.

Mass applies only to DYNAMIC bodies. Dynamic bodies may also enable Start Asleep.

Physics step vs OnUpdate

Havok steps after onBeforeRenderObservable callbacks for that frame — so your OnUpdate runs, then the physics engine integrates, then Babylon draws. That means:

Full frame order (particles, input, draw, MSDF): 02 — Runtime Basics · runtime-loop.html.

Center of mass

For Dynamic rigid bodies only. The collision shape defines contact; center of mass defines how the body tips under gravity and impulses. After the shape is built, ApplyMassProperties calls Havok setMassProperties with the authored mass and an optional centerOfMass override in the entity's local space.

BlenderManifestRuntime
Show Preview (default on; viewport only)Amber cross on selected RigidBody objects (viewport/cog_preview.py; draws on top, scales with mesh bounds)
Auto-Fit Center of Mass (default on)centerOfMassAutoFit: trueComputeLocalBounds(node).center — same owned-mesh AABB rule as collider auto-fit
Center of Mass (when auto-fit off)centerOfMassAutoFit: false, centerOfMass: [x,z,−y]Custom offset, axis-converted at export like collider center
*(field omitted — older levels)*Mass only; Havok derives CoM from the collision shape geometry

The viewport preview draws in Blender space (auto-fit → compute_local_bounds center; manual → cog_center) so what you see matches ResolveCenterOfMass after export axis conversion. Gizmo arms are ~10% of the longest bounds axis. Because the CoM usually sits inside the mesh, the overlay skips depth testing (collider wireframes use depth test — they sit on the surface). Collider wireframes use cyan; CoM uses amber so the two overlays are easy to tell apart.

Fit CoM to Bounds snapshots the mesh AABB center into the custom field and turns off auto-fit (same workflow as Fit to Bounds on colliders). Auto-fit CoM tracks visible mesh geometry, which can differ from a hand-tuned collider — useful when tipping behavior should follow visuals, not collider volume.

Start asleep

Dynamic rigid bodies may set startAsleep: true (Start Asleep in Blender). The loader passes this to PhysicsBody's startsAsleep constructor argument (and PhysicsAggregate.startAsleep on auto-fit mesh paths). Bodies begin with physics calculations skipped until a collision or applied force wakes them — a performance hint for scenes where bodies are at rest on load, not a guarantee (nearby awake bodies can wake them). The engine also puts resting bodies to sleep automatically when appropriate.

Three shape paths for one collider (each its own builder); two or more colliders always use the compound container (BuildCompoundBodyBuildColliderShape per row):

The owned-meshes rule <a name="owned-meshes"></a>

Two kinds of "children" look identical in the scene graph: glTF material submeshes (_primitive0…, no GUID) and author-parented child entities (have a GUID). The rule lives once in core/meshOwnership.ts (CollectOwnedChildMeshes / CollectOwnedMeshes): a descendant is owned only if no node on its path up to the owner carries bjs_id — so a collider spans its own multi-material geometry and any parented meshes that are not separate entities, but never a child entity's mesh. Physics (OwnedColliderMeshes, all three shape paths, and the hasGeometry check — fixed v0.29.1) and reflection probes (capture/exclusion lists) both route through it.

When several prefab instances share the same glTF mesh data (typical after duplicating a linked prefab), Babylon loads the first copy as a normal Mesh and later copies as InstancedMesh. Those instances still count as owned geometry — physics clones/bakes from sourceMesh when building hulls and bounds (geometry.ts).

Parent with a collider, child without: A child does not inherit a COLLIDER component — physics is built only from components on that object's manifest row. If the child has no components and no GUID, it is not a separate entity; its mesh is owned geometry and is folded into the parent's auto-fit bounds (and into Fit to Bounds / the viewport collider preview on the parent). Collision at the child's location comes from the parent's PhysicsBody, not a child COLLIDER attachment. To exclude the child from the parent's collider, make it its own entity (Assign GUID or add any component such as Tag) and leave Collider off. To collide only with the parent's own mesh, put the collider on the mesh object or use manual center/size instead of auto-fit. A child with only RIGIDBODY (no COLLIDER) still gets an auto-fit box at runtime.

Why right-handed import <a name="right-handed"></a>

Babylon's default left-handed glTF path parents content under a __root__ carrying a reflection (negative-determinant) transform. Havok places bodies by decomposing world matrices, and a reflection decomposes like a 180° rotation → mis-oriented colliders (the original kit-breaking bug). LevelLoader sets useRightHandedSystem = true *before* the append, so no mirror exists and one node-attached body path is sound. NeutralizeGltfRoot only warns if a mirrored root reappears.

Constraints (BuildConstraints) <a name="constraints"></a>

CONSTRAINT components become joints in FinalizeLevel (both bodies must exist):

constraintTypeHavok joint
FIXEDLockConstraint
BALLBallAndSocketConstraint
HINGE / SLIDER / SPRING / CUSTOMPhysics6DoFConstraint

For 6DoF types, the constraint frame's X = the authored axis; Y/Z follow from a perpendicular pair (ComputeConstraintFrame). Preset mapping:

ComputeConstraintFrame derives the target-side pivot/axes from live world transforms, pinning the as-placed relative pose — nothing snaps on load. Apply Object Scale is on by default: authored pivot and linear limits are multiplied by the owner's local scale (parent scale comes from the world transform). Turn off in Blender to opt out. Angular limits stay in degrees. Motors (setAxisMotorType(VELOCITY) + target + max force) apply to HINGE/SLIDER presets only. collision (Blender Bodies Collide, default off) maps to Havok's pairwise isCollisionsEnabled on the joint — when off, the two connected bodies should not generate contact impulses against each other (overlap can still cause constraint solver fighting; keep colliders separated or use auto-fit sizes that don't intersect). Joints land in level.constraints, disposed with the level.

Collision layers (subsystems/collisionLayers.ts)

Unity-style named collision filtering: a scene-wide layer list + collision matrix (Blender Babylon Scene › Collision Layers) and a per-object Collision Layer component that picks one layer name. At load, the engine converts names to bit indices and matrix rows to Havok filter masks on every registered PhysicsShape.

Havok rule: two shapes collide (and triggers fire) only when (A.membership & B.collide) !== 0 and (B.membership & A.collide) !== 0. Layer index i → membership bit 1 << i; row i's collide mask is the OR of column bits where matrix[i][j] is true. Up to 32 named layers.

Blender settingManifestRuntime
Collision Layers panel — layer list + matrixscene.collisionLayerslayers[] + matrix[][]BuildCollisionLayerResolver → name → masks
Collision Layer component — layer picker{ "type": "COLLISION_LAYER", "layer": "Player", … }ApplyCollisionLayers walks parent tree (same propagation rules as render layers)
Apply to Owned CollidersapplyOwnedColliders (default true)When false, this entity's shapes keep Havok defaults
Apply to Child EntitiesapplyChildEntities (default false)When true, propagate until a child defines its own component

Shapes are registered during BuildPhysics (including compound PhysicsShapeContainer children). Applied in FinalizeLevel via ApplyCollisionLayers after constraints and alongside ApplyRenderLayers, before level.Begin(). Entities without the component (and no inherited layer) are unchanged — they collide with everything.

Manifest examples

"collisionLayers": {
  "layers": ["Default", "Player", "Environment"],
  "matrix": [
    [true, true, true],
    [true, false, true],
    [true, true, true]
  ]
}
{
  "type": "COLLISION_LAYER",
  "layer": "Player",
  "applyOwnedColliders": true,
  "applyChildEntities": false
}

Collision layers affect physics only — not rendering. Render layers do not affect collision.

Collision lifecycle hooks (WireCollisionEvents)

Behaviors may override Unity-style hooks: OnCollisionEnter / OnCollisionStay / OnCollisionExit (solid colliders) and OnTriggerEnter / OnTriggerExit (trigger volumes). WireCollisionEvents relays Havok plugin observables — COLLISION_STARTED / CONTINUED / FINISHED and TRIGGER_ENTERED / EXITED. Both bodies in a contact receive hook callbacks (Unity semantics). Collision callbacks are opt-in (overridden hook or collision-phase Event Message); trigger hooks need no per-body flag. No OnTriggerStay — Havok has no continued trigger event. Handles stored on level.collisionEventHandles, removed on dispose.

Usage — override a hook on any behavior whose entity has a COLLIDER (+ RIGIDBODY for solid contacts); no subscription code needed:

import { Behavior } from "@bjs/engine";
import type { CollisionContact, Entity } from "@bjs/engine";

export default class LandingDetector extends Behavior
{
  OnCollisionEnter(other: Entity, contact: CollisionContact): void
  {
    if (other.tag === "Ground" && (contact.impulse ?? 0) > 5)
    {
      this.entity.GetSound("thud")?.play();
    }
  }

  OnTriggerEnter(other: Entity): void
  {
    console.log(`${other.name} entered the zone`);
  }
}

Working demo: apps/playground/src/behaviors/CollisionProbe.ts logs all five hooks with an optional tag filter.

Event Messages (WireCollisionEvents)

Collider components may carry authored Event Messages (Blender UI rows with When / Target / Message / Only Tag). Phases: TRIGGER_ENTER, TRIGGER_EXIT, COLLISION_ENTER, COLLISION_EXIT. Same dispatcher as lifecycle hooks: tag filter against the other entity, target GUID lookup, target.SendMessage(message, otherEntity)behavior OnMessage. Manifest field: eventMessages[] on COLLIDER rows. Havok gotcha: MESH-shaped triggers never fire (validator warns). 3D GUI On Click rows reuse the same PropertyGroup but ignore the when field.