Welding
Weld is a second mode for a parented assembly, alongside the default attached mode covered
on Parenting. Where an attached assembly is a group of
kinematic children following one parent, a welded assembly is genuinely one physics body.
<!-- screenshot: editor/working/welding.png -->
Physics caveat. Weld changes what is actually simulated, not just how the Inspector displays the assembly. Read the mechanics below before enabling it on anything you plan to throw dice at.
Turning it on#
Weld children is a checkbox in the parent's RIGIDBODY section, and it is offered
only when the entity actually has children (TableEditModeShell.tsx, the Weld children
toggle inside the Inspector's RIGIDBODY collapsible) — there is nothing
to weld otherwise. It writes physics.weldChildren on the parent
(packages/shared/src/objectComponents.ts schema; the flag itself lives on ObjectPhysics).
What weld changes, mechanically#
Enabling weld (TabletopRuntime.applyWeldState, TabletopRuntime.ts):
- The parent's
collisioncomponent becomescompound. - Every descendant keeps its
collisioncomponent but loses itsrigidbody— a descendant with its own body would be simulated separately and contribute to the compound, which has no defined meaning, so the body is removed. - Ammo then treats every descendant's collision shape as a child shape of the parent's single compound shape: one body, with combined mass and inertia.
The practical difference from attached mode: an attached assembly is kinematic, so it shoves
loose pieces around but is never pushed back by them (see
Parenting). A
welded assembly is one dynamic body — a die thrown at it bounces off and nudges it, the
same as it would nudge any other loose piece.
⚠ Weld and authored trigger volumes cannot coexist, and that is an engine constraint rather than a policy. Building a compound adopts every rigidbody-less descendant collision under the parent — which is exactly what a trigger volume is, so a volume inside a welded assembly would silently become solid and start blocking pieces. Volumes are therefore torn down before a weld and rebuilt on unweld, with a warning logged once. A model whose trigger volumes carry game rules should not be welded into an assembly.
The compound shape rebuilds, debounced to once per frame#
The compound shape has to be rebuilt whenever a child is added, removed, moved or rescaled.
That rebuild is queued and debounced to once per frame — queueWeldRebuild /
flushWeldRebuilds (TabletopRuntime.ts) — and explicitly never per drag
frame: the queue collects a parentId and the actual applyWeldState call happens once
from update(), however many times the parent's edge changed within that frame. Forcing a
rebuild is also necessary on every weld/unweld transition even when the collision type
stays compound, because PlayCanvas's own collision.type setter is a no-op when the
assigned value is unchanged (recreateCollisionShape, TabletopRuntime.ts, calls
the engine's recreatePhysicalShapes directly to force it).
⚠ Do not read a performance claim into "debounced." The debounce bounds how often a rebuild happens, not how expensive one is. The actual compound-rebuild cost in this Ammo build is unmeasured — this page states the mechanism, not a performance verdict. Do not extrapolate one from the other.
Unweld restores each child's own body — and its authored physics#
Turning weld off is not just "give the rigidbody back." applyWeldState's unweld branch
(TabletopRuntime.ts) does three things per child, in order:
- Clears the engine's private
_compoundParentreference before the parent's shape stops being a compound — leaving it set would make the next shape rebuild on the child throw inside the engine (there is no public "leave this compound" API, so this is the documented-by-source workaround). - Re-adds a
rigidbodycomponent if the child does not have one. - Re-applies the child's authored
physics(applyPhysicsOverrideToObject) — mass, friction, restitution, damping, body type, collision shape. Skipping this step would leave every unwelded child with onlycreateRigidbodyConfig's generic per-kind defaults until the scene reloaded, silently discarding whatever you had tuned before welding.
Nested weld is rejected, with a reason#
Welding a member of an already-welded assembly is refused outright rather than
half-supported (TabletopRuntime.ts):
"Cannot weld
<label>: it is already part of a welded assembly."
Why: PlayCanvas reassigns a child's _compoundParent to the nearest compound ancestor
when shapes are rebuilt, which would silently pull the inner sub-assembly out of the outer one
while grab escalation and isWeldedMember still reported the outer parent — physics and the
UI would disagree about which assembly a piece belongs to. Rather than ship that, the runtime
deletes the just-set weldChildren flag and refuses.
Where that reason goes today. The rejection is written through the same log path every
system message uses — this.log("System", …), which appends a TableEvent and calls
callbacks.onEvent. On the live table that reaches the activity log. In Edit Mode it
currently does not reach anywhere: TableEditModeShell.tsx wires the canvas's
onEvent to a no-op (onEvent={() => undefined}), so the reason string above is generated
but discarded while authoring. See Escalations below — this is a real gap between what
the feature intends ("surface the reason") and what Edit Mode currently does with it, not a
documentation choice.
One consequence worth knowing while that gap stands: because the Inspector's Weld children
checkbox reads from the editor's own scene draft (object.physics?.weldChildren), not
from the runtime's post-rejection state, toggling weld on a nested assembly can leave the
checkbox showing on in the Inspector even though the runtime rejected it and the assembly
is not actually welded. Toggling it off and back on, or reselecting the entity, will not
reconcile this on its own — reload the scene if you suspect this has happened. Verify a weld
took effect by checking ENTITY (ADVANCED) — a genuinely welded child's Live Entity carries
no rigidbody component in its chip list.
The one-line decision rule#
Parenting gives a child a parent transform (and makes it kinematic). Weld merges children into the parent's single compound rigidbody. Joints connect two independently-dynamic bodies with an Ammo constraint.
If you are choosing between these three for an assembly you're building: reach for parenting first (it's the default and the cheapest), reach for weld when you need the assembly to actually receive physical impacts as one body, and reach for a Fixed Joint when the pieces must each keep being their own independently-simulated dynamic body.
Unvalidated behavior#
Unvalidated. The physics-jitter pass on a parented/welded assembly and any multi-peer weld/unweld round trip (does a peer that joins mid-weld see the same compound shape as the host?) have not been walked in a browser. Everything above is derived from reading
TabletopRuntime.tsdirectly. Treat behavior under real network conditions or a live physics stress test as code-derived, not human-confirmed.
See also#
- Parenting — the default
attachedmode, and why a parented child is kinematic. - The Scene Model
- Glossary
- Mod scripting API —
TableJointStateandTableObjectState.parentId, how a weld appears to a mod.
