A runnable example building the thing a spawn system actually leans on:
named socket empties parented into an asset, so a module dropped onto a
socket with an identity local transform lands exactly where the artist put
it, oriented the way the naming convention promises. Every clause in that
sentence is a matrix identity, and every one of them drifts silently — a
skipped matrix_parent_inverse, a basis built with the cross product the
wrong way round, a transform frozen on the parent after the sockets were
placed.
The asset, for reuse: Drone.Survey, an 0.86 m span survey quadcopter —
a lofted fuselage with a vented deck and canopy face, four canted booms with
finned motor cans and nav lights, and a two-rail landing gear. Seven mount
pads carry seven sockets, and four interchangeable modules seat on them:
| Socket | Mount normal | Module |
|---|---|---|
SKT_Rotor.FL/FR/RL/RR |
8° canted outward | Mod.Rotor (twisted, tapered blades) |
SKT_Camera |
belly, −Z | Mod.CamPod (gimbal ball + lens) |
SKT_Mast |
dorsal, +Z | Mod.Mast (antenna, dish, beacon) |
SKT_Battery |
rear, tilted down | Mod.Battery (cell block + gauge) |
Origin is the skid contact plane centre (z == 0 is where it rests, +X is
forward), transforms are identity by construction, datablocks live under
Drone.Survey.* and SKT_*, and accents are per-face material slots rather
than extra objects. To use it: instantiate a module, parent it to the socket,
zero the local transform. That is the whole API — and it only works because
the socket matrices hold.
Socket convention (encoded in the SKT_ prefix):
- +Z — the outward mount normal, matching the mount pad's own face normal.
- +Y — the module's up reference: chassis +Z orthogonalised against the socket normal, falling back to chassis +X when the normal is parallel to chassis up (the belly and dorsal mounts take that branch).
- +X —
Y × Z, so the basis is right-handed (det == +1).
Pipeline arc neighbours: pivot and origin discipline in
prop-origin-transform, parent-inverse under
animation in parent-inverse-orrery, tile-grid
boundary contracts in modular-kit-snap, collision
packaging in collision-hull-proxy.
What it witnesses (all closed form or independently re-derived):
- Socket matrices. All 7 evaluated
matrix_worldvalues equalroot.matrix_world @ authored_localwithin 1e-6 (measured 1.788e-07); every basis is orthonormal (Gram error 3.576e-07) and right-handed (det − 1= 2.980e-07). - Orientation vs geometry. Each socket's world +Z equals the Newell normal of its mount pad's mount face — computed from raw vertex coordinates, and the pad mesh is built by a quaternion swing from +Z while the socket basis is built by explicit Gram-Schmidt, so the two derivations are independent (normal deviation 1.794e-07). The socket origin sits on that face's centroid (6.687e-08), and the up axis matches the documented fallback rule (1.943e-07).
- Seating. Every module's mount origin coincides with its socket origin
(offset exactly 0.0) with its mount axis on the socket +Z
(
dot − 1= 1.735e-07), and carries a strictly identity local matrix — seated by the socket, not nudged into place. - Rigid invariance. Re-posing the chassis root to a second arbitrary
transform moves every socket to
root.matrix_world @ authored_local(2.384e-07) and every module stays on its socket (0.0). - Transform apply. Freezing loc/rot/scale on the root leaves every socket's world matrix put (2.235e-07) — see the hazard below for what it does to the local ones.
- Reuse hygiene. 14 chassis parts at identity scale,
Drone.Survey.*/SKT_*names, no default datablocks,min z == 0.00e+00.
- Applying a transform on an Empty root pushes it down into the children.
An Empty has no object data to bake into, so
transform_applyon the root resets 7/7 children'smatrix_parent_inverseto identity and hands each child the root's scale locally (measured 0.35 for an applied 1.35). World matrices survive to float32; local transforms do not. A spawn system that readsmatrix_local, or an exporter that trusts "transforms are applied", reads different numbers after an artist freezes the rig. Both halves are asserted, so the behaviour cannot change under us silently. - Only the root may be selected during the apply. A child left selected gets the parent transform applied twice — measured 2.335 m of socket drift on this asset.
bm.normal_update()does not fix winding. It recomputes normals from the existing winding. The lofted fuselage wound its side quads inward and rendered as a flat unshaded white panel rather than an obvious hole;bmesh.ops.recalc_face_normalsis what fixes it, andPart.finishnow always calls it.- Bevel offsets near half the local edge length collapse. The skid rails
at a 5 mm offset on a 9.9 mm octagon edge produced 3 zero-area faces each —
the collapse
degenerate-bevel-weldwitnesses. 2.2 mm is clean. Note thatexamples/gallery_asset_quality.pyraisesValueError: zero length vectors have no valid angleon such a mesh rather than reporting it.
What each check catches on failure (every one probed, with the measured
error): sockets parented without the parent-inverse — worst jump 0.690128 m
(exit 3); a left-handed basis from z.cross(y) — det == −1.000000 (exit 3);
a rotor pad built flat instead of canted — 1.395e-01 off the Newell normal
(exit 4); an 11° roll on the basis — 1.917e-01 off the up rule (exit 4); a
module nudged 4 mm off its socket (exit 5); a default Cube datablock name
(exit 6); a socket left unparented so it ignores the re-pose — 1.417e+00
(exit 7); a child selected during the apply — 1.252e+00 to 2.244e+00 of
socket drift (exit 8).
Version witness: check output is byte-identical on Blender 4.5.11 LTS and 5.1.2 — same counts, same measured deviations to every printed digit, and the same framing numbers on the render path.
Render as proof: the drone on the dark stage with every module seated, the
orange mount pads reading as the socket signature. The falsification variant
(--falsify) parents the sockets without matrix_parent_inverse: the pads
are left bare, the rotors and mast vanish from the airframe, and the camera
pod and battery hang in space near the top of frame. A broken matrix is not a
subtle pixel difference here — the asset comes apart.
blender --background --python socket_attach_points.py --
blender --background --python socket_attach_points.py -- --output drone.png
blender --background --python socket_attach_points.py -- --falsify adrift.png
blender --background --python socket_attach_points.py -- --probeExits non-zero on failure. The blender-smoke workflow runs the check on
Blender 4.5 LTS and 5.1. The --output render path additionally gates framing
via examples/gallery_framing.py (fill 0.881x, margins
0.066/0.053/0.122/0.106, no edge touched) and the asset floors via
examples/gallery_asset_quality.py (32 materials, edge90 0.027, no
default names). The --falsify render is a diagnostic, not a gallery hero, so
it takes a documented framing deviation — its whole point is that the modules
leave the frame.