| Unity | glTF / three.js | |
|---|---|---|
| Handedness | Left-handed | Right-handed |
| Up | +Y | +Y |
| Forward axis | +Z (Unity: “the positive z-axis points forward”) | +Z (spec: front side faces +Z) |
| Asset's right is | +X | −X (spec: left side faces +X) |
| Linear units | metres ° | metres |
| Angles | degrees in the public API (e.g. eulerAngles) ° | radians |
| Quaternion order | x, y, z, w ° | x, y, z, w (W scalar) |
| Local matrix | T * R * S ° | T * R * S |
| World transform | parent_world * local ° | parent_world * local |
Positions negate X. Rotations negate Y and Z. A conversion that negates the same letter in both places is wrong, and it looks fine until something rotates.
Why: conjugating a rotation by the mirror M = diag(−1,1,1) gives
M R M, a rotation by the negated angle about the reflected axis.
In q = (sin(θ⁄2)·n, cos(θ⁄2)) the two sign changes cancel on the flipped
component and survive on the other two. (−x, y, z, −w) is the same rotation, since
q and −q always are.
Both rules are their own inverse, so one function serves both directions:
// positions / directions
Vector3 Flip(Vector3 v) => new Vector3(-v.x, v.y, v.z);
// rotations
Quaternion Flip(Quaternion q) => new Quaternion(q.x, -q.y, -q.z, q.w);
// Scale is unchanged — including a negative one, since
// M * diag(sx,sy,sz) * M == diag(sx,sy,sz) for any signs.
// Convert a whole node:
// node.position = Flip(node.position)
// node.rotation = Flip(node.rotation)
// node.scale = node.scale
Negative scale is legal and load-bearing in both systems, so it survives the conversion
untouched — but the same (−1, 1, 1) reinterpretation applied to mesh vertex
positions reverses triangle winding. glTF § 3.7.4 Instantiation:
the determinant of the node's global transform defines the winding order of that
primitive
— and its implementation note adds, Switching the winding order to clockwise
enables mirroring geometry via negative scale transforms.
Khronos's UnityGLTF also flips
the tangent W sign
when converting mesh data ((-1, 1, 1, -1)). If you are converting a whole asset and
not just a pose, budget for winding order, normals, and tangents on top of the two rules above.
Curl the fingers of one hand from +X toward
+Y. If the right thumb points along +Z
it is right-handed; if the left thumb does, left-handed. In numbers: right-handed exactly
when (X × Y) · Z > 0, with the three axes written as components in a frame you
have already established is right-handed — a drawing, or a convention you have already
settled. That qualifier is the whole test. Write any basis in its own coordinates and you get the
identity, whose triple product is +1; apply the formula that way and Unity comes out
right-handed.
Take its three semantic directions and compute (forward × up) · right. Within one
frame the sign is invariant under every rotation and flips under every reflection, so it answers
the question from any camera angle. But it is not frame-independent: in a right-handed
frame a correct body gives +1, and in a left-handed frame the same correct body gives
−1. So compare against a pose you know is correct expressed in the same
convention; if the signs differ, it is reflected, and no rotation will fix it. It is a
direction test — it catches mirroring, not wrong limb lengths.
| Symptom | Class | Look at |
|---|---|---|
| Right limb appears on the left; text and logos read backwards | Reflection | Handedness conversion missing or applied to only one of positions / rotations. No rotation can fix this. |
| Whole body faces backwards but is not mirrored | 180° yaw | Handedness flip spent on the forward axis (negating Z instead of X). |
| One bone rotates about the wrong axis; parent looks fine | Wrong basis | Rest/bind pose or sensor-to-segment offset, not handedness. The bone's local frame is not the one you think. |
| Error grows down the chain; the root is fine, fingers are wild | Multiply order | Local vs world confusion, or a correction applied on the wrong side. |
| Pose degrades over time; calibration stops holding | Accumulation | Not a convention bug. Normalization and reference-frame drift. |
For every boundary a pose crosses — sensor SDK, fusion filter, solver, engine, exporter — record:
xyzw or wxyz) and whether it is normalized.A boundary whose conventions are not written down is where the next bug is. This list is the one artefact that makes these bugs cheap instead of expensive.