m06 · Transforms · reading · 9 min
LocalTransform — why one component, and why not a matrix
The obvious design, and why it’s wrong twice
Every entity that exists in space needs position, rotation and scale. That much is forced. What isn’t forced — and what this lesson derives — is how many components that becomes and what’s stored in them.
Two designs suggest themselves immediately, and both are wrong for reasons you already have the tools to see.
Three separate components — Position, Rotation, Scale. It looks
clean: each is one concern, and a system that only reads position doesn’t
declare the others. Module 1 even seems to encourage it, since narrow
components mean narrow queries.
One 4×4 matrix — store the composed transform directly, since that’s what rendering and math ultimately want. No conversion at use time.
Take them in turn. The reasoning is the lesson.
Why not three components
Module 1 established that a
That’s three sets of
This is the mirror image of the density argument. Module 1 said don’t put data
in a component if systems don’t read it — cold data taxes every chunk. The
corollary is equally binding: data that is always read together should live
together, because splitting it multiplies the streams without reducing the
bytes. Splitting LocalTransform into three would cost access overhead and
save nothing, since every consumer wants all three anyway.
So the grouping rule is not “one concept per component.” It’s one access-pattern per component. Position, rotation and scale share an access pattern completely, so they share a component.
Why not a matrix
The second design is more tempting, because a 4×4 matrix is what the GPU and most math ultimately consume. Storing it directly seems to skip a conversion.
It fails on two counts, and the first is pure Module 1 arithmetic.
A 4×4 of floats is 64 bytes. TRS stored as position (3 floats), rotation as a quaternion (4 floats), and uniform scale (1 float) is 32 bytes — half. Be precise about where that bill lands, using Module 1’s full model: on a bare transform-only archetype both designs sit under the ~120-byte cap line and clamp at 128 entities per chunk, so a toy benchmark would show no capacity difference at all. But no real archetype stays bare — and those 32 extra bytes are 32 bytes closer to the line on every archetype in the game, because this is the single most universally-present component in the world. Past the line, they divide straight into capacity; before it, they are still doubled memory traffic on every transform read, permanently. All of that to avoid a conversion that is a handful of arithmetic operations Burst will vectorize.
The second count is worse, and it’s a correctness argument rather than a performance one. A matrix is a bad thing to interpolate. Blend two rotation matrices component-wise and you get a matrix that is not a rotation — it shears, it scales, it’s garbage. Blend two quaternions and you get a rotation, which is the entire reason quaternions are the storage form. Any system that lerps between transforms — animation, network interpolation, physics smoothing — needs the decomposed form. Storing the composed matrix means decomposing it back before you can do the operation, and decomposition is both lossy and expensive.
So: TRS is smaller and it is the form operations actually need. The matrix is
a derived representation, computed when something needs it, which is exactly
what the next lesson’s LocalToWorld turns out to be.
What LocalTransform actually is
Given all that, the component’s shape is now derivable rather than memorized:
public struct LocalTransform : IComponentData
{
public float3 Position; // 12 bytes
public quaternion Rotation; // 16 bytes
public float Scale; // 4 bytes
} // 32 bytes
Three fields, one component, decomposed form, uniform scale, float3 and
quaternion from Unity.Mathematics rather than Vector3 and Quaternion —
which Module 3 explains, since the math types are the ones Burst vectorizes.
Note the name. It is LocalTransform, not Transform, and the qualifier is
load-bearing. This component stores the entity’s transform relative to its
parent, and for an entity with no parent, relative to the world — which are
the same thing when the parent is the identity. That single naming decision is
what makes the hierarchy possible at all, and it is the subject of the next
lesson.
What this buys you
You can now explain why transform data is one component rather than three, why it stores TRS rather than the matrix that consumers ultimately want, and why uniform scale is the default rather than a limitation. Next: the word Local — what a parent actually is, and why a hierarchy converts independent entity data into a dependency graph.