Skip to content

Instantly share code, notes, and snippets.

@AThilenius
Last active September 30, 2024 14:00
Show Gist options
  • Select an option

  • Save AThilenius/528b981accc2cc62de3deb49f5fff3ed to your computer and use it in GitHub Desktop.

Select an option

Save AThilenius/528b981accc2cc62de3deb49f5fff3ed to your computer and use it in GitHub Desktop.

How Unity's ECS Unity.Transforms works.

A breakdown of how Unity's new ECS-based hierarchical transform system works.

Hierarchy

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 a PreviousParent are picked up and a PreviousParent component is added.
  • Entities that have a PreviousParent but not a Parent are removed from the Child list of the PreviousParent.
  • Entities that have both a Parent, LocalToWorld, LocalToParent and a PreviousParent already, and where Parent has changed are enumerated:
    1. All additions to Child are collected from the new value of Parent
    2. All removals from Child are collected from the value of PreviousParent (if any).
    3. 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.
    4. The PreviousParent is set to the same value as Parent for all
    5. Any Entity in the 'additions' HashMap that doesn't yet have a Child component has that added (also done by an beforehand collect followed by a flush).
    6. 'Removed' children are removed from unique parents, they are they re-added based on the HashMap collected in step 1.
  • Entities with a Child that do not have a LocalToWorld have all their children collected, and for each child, the Parent, PreviousParent, LocalToParent and Child are 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 (assuming LocalToParent is the main flag for hierarchy membership).

Transform

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's Parent.
  • 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 LocalToWorld but do not have a Parent are only updated when one of the contributing transformation components changes (aka this update is diff based).
  • All Entities that have a LocalToParent are updated exactly like LocalToWorld, except that this update is not diff based. This is an important note: 100% of all entities with both transform-contributing components and a LocalToParent component, will be recalculated each system run, always.
  • Finally, in a separate LocalToParentSystem, Entities without a Parent but with a Child array and a LocalToWorld are used to kick off a recursive, (single-task per chunk) update of children LocalToWorld transforms. These are computed simply as LocalToWorld = Parent.LocalToWorld * LocalToParent. Again, this is done for 100% of entities in a hierarchy, every system run.

Transform-contributing Components

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.

Conclusion

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment