ADR 0007: Pluggable RigBackend: ORL now, GNM next
- Status: Accepted —
orlshipped in M3,gnmin M5. See ADR 0012 for how the GNM pack is produced. - Date: 2026-09-13
- Plan decision: architecture decision 6
Context
The head is currently driven by OpenRigLogic. The project is pivoting to Google's GNM parametric head, which is Apache-2.0 but exists today only as Python and .npz. Decoders will be retrained on GNM topology by another team, on a different schedule than the engine.
Decision
Put the rig stage behind RigBackend — kind, controlNames, vertexCount, init, setControls, setJointOverrides?, encode(encoder), vertsBuffer, vertsAABB, dispose. The bundle's scene.json names the backend, in an optional rig block the engine added to the exporter's schema; a bundle that declares nothing defaults to orl with its own rig_names.json, which is what every shipped bundle does.
Two implementations:
orlwraps the vendored OpenRigLogic wasm module, then int8 blendshape deltas and linear-blend skinning on the GPU. It is the rig the avatar was authored with, so it is correct by construction, and it is per-character: the pack ships inside the bundle. Its neutral is in centimetres. A branch under 4096 vertices — hair, a garment — takes a cheaper shell path instead of full skinning.gnmconsumes a baked.aosrigpack and runs as WGSL (gnm_blend.wgsl): a 383-coefficient linear blend plus LBS. Not ONNX — the model is a matrix multiply and an LBS, and pushing that through an inference runtime would be slower than the shader. 17,821 vertices, metres, 383 expression coefficients plus four gaze angles.
Both share lbs_common.wgsl.
Consequences
- Swapping
"rig": "orl"for"gnm"changes nothing else in a game. - The seam between "which rig posed this face" and "what the animation layer should send" had to be split into two blocks.
rigsays which rig;expression_space(arkit52/gnm387 /gnm6868) says what to send, and is separate because two spaces that happen to share a width are otherwise indistinguishable. Sending an ARKit vector to a GNM bundle with no trainedarkit_mapis refused loudly rather than reinterpreted. - GNM identity is baked offline, so there is no identity basis at runtime — a pack is one character.
- "The skinning differs between backends" is not a bug that can exist, because there is one LBS shader.
- Correctness is gated on reference frames exported from the Python model, and
gnmReference.tsis a CPU implementation of the same maths the GPU path is tested against. - ORL remains the shipping backend for faces whose decoders were trained on ORL topology; the GNM milestone gated only the rig stage, exactly as planned.