A breakdown of how Unity's new ECS-based hierarchical transform system works.
Let's first consider Unity's hierarchy implementation, which is almost entirely
separate from the transform system. The only things shared are the Parent and Child
component types. This is an important note, as it means no diff information
about the hierarchy is communicated to the larger transform system. There is
however an exception to this: Unity will only process hierarchies that already
have both a LocalToWorld and a LocalToParent component also attached. It
does nothing with these components apart from require their existence. I do not
see a reason for this however.
Unity stores the source-of-true for it's ECS hierarchy in the Parent
component, which is nothing more than an Entity handle. The ParentSystem takes
these components and 'fixes up' both a PreviousParent component as well as a
Child component (which stores the Rust-equivalent of a SmallVec<[Entity; 8]>
of Entity handles). This is done in several passes:
- Entities with a
Parent,LocalToWorld,LocalToParent, and that don't have aPreviousParentare picked up and aPreviousParentcomponent is added. - Entities that have a
PreviousParentbut not aParentare removed from theChildlist of thePreviousParent. - Entities that have both a
Parent,LocalToWorld,LocalToParentand aPreviousParentalready, and whereParenthas changed are enumerated:- All additions to
Childare collected from the new value ofParent - All removals from
Childare collected from the value ofPreviousParent(if any). - All unique parents are collected, ie those that have been 'seen' during this part of the system, including those that might no longer have children.
- The
PreviousParentis set to the same value asParentfor all - Any Entity in the 'additions' HashMap that doesn't yet have a
Childcomponent has that added (also done by an beforehand collect followed by a flush). - 'Removed' children are removed from unique parents, they are they re-added based on the HashMap collected in step 1.
- All additions to
- Entities with a
Childthat do not have aLocalToWorldhave all their children collected, and for each child, theParent,PreviousParent,LocalToParentandChildare removed from them. This is only done 1 level deep but will keep eating down in depth once per system-tick. It effectively removed all children of a deleted parent from the hierarchy (assumingLocalToParentis the main flag for hierarchy membership).
Keep in mind that the Transform is completely separate from the hierarchy. It is completely unaware when parents change, for example.
Unity creates a sort of 'data flow' that all collapses down into one of two different space-transforms, both stored as homogeneous 4x4 matrices of f32:
LocalToParent: The space-transform from the Entity, to it'sParent.LocalToWorld: The space-transform from the Entity, to 'world space`. This is the only transform-related component that the vast majority of other systems (like rendering) care about.
For static objected (including those belonging to a static hierarchy) the
LocalToWorld matrix is simply pre-computed and static, and stored as a full
4x4 float matrix.
For dynamic objects things get more complicated:
- All Entities that have a
LocalToWorldbut do not have aParentare only updated when one of the contributing transformation components changes (aka this update is diff based). - All Entities that have a
LocalToParentare updated exactly likeLocalToWorld, except that this update is not diff based. This is an important note: 100% of all entities with both transform-contributing components and aLocalToParentcomponent, will be recalculated each system run, always. - Finally, in a separate
LocalToParentSystem, Entities without aParentbut with aChildarray and aLocalToWorldare used to kick off a recursive, (single-task per chunk) update of childrenLocalToWorldtransforms. These are computed simply asLocalToWorld = Parent.LocalToWorld * LocalToParent. Again, this is done for 100% of entities in a hierarchy, every system run.
Unity breaks the tradition 'Transform' (which is normally a an Affine
transformation for many game engines, including old-Unity) into many different
components which can be mixed and matched in many combinations. The general idea
is that after several systems pass over these components, they will finally be
boiled down into either a LocalToParent or LocalToWorld homogeneous matrix
4x4. Once the composite types have been calculated, as well as a few odd-ball
other transforms, the following components will be queried as an Any and will
contribute to the final Affine transform:
- Translation: A Vector3 XYZ translation.
- Rotation: Quaternion stored 3D rotation.
- Scale: A single f32 uniform scale.
- NonUniformScale: A Vector3 XYZ non-uniform scale.
- CompositeRotation: Stored as a homogeneous mat4, built by the
CompositeRotationSystem. - CompositeScale: Stored as a homogeneous mat4, built by the
CompositeScaleSystem. - ParentScaleInverse: haven't bothered looking how this one is calculated.
The Unity Transform implementation in ECS is both clever, and a total mess. It also seems to be lacking some low-hanging fruit for optimization.