This blog post was written in reference to Unreal Engine 5.8
Overview
In Mass because you’re dealing with Entities are not Actors directly it requires having
a Fragment pointing to its Actor if needed, the engine provides a common Fragment for pointing
to an Actor with FMassActorFragment.
Actor LOD Handling
The engine also provides another system for having a data-oriented LOD Actor swapping/spawning system
that you can hook into using FMassRepresentationFragment(added via a Visualization Trait) which provides the Actor(if applicable) as its
current representation but can automatically change to another LOD supporting Instanced Meshes for both
Static Meshes and Skinned Meshes.
These properties are configured from a Visualization Trait. Note that the shared base UMassVisualizationTrait is
soft-deprecated in 5.8(it shows in the editor as “DEPRECATED Visualization”), so pick UMassMovableVisualizationTrait
for anything that moves or UMassStationaryVisualizationTrait for static placements.
There are distance-only variants too, but UMassDistanceVisualizationTrait is also deprecated, so use UMassStationaryDistanceVisualizationTrait
if you want that path. It also requires the LOD Trait applied in that same
Entity which handles connecting the Entity to the LOD system. It currently is intended as a single representation per Trait,
how that Fragment is used at runtime is dependent on the systems and framework that is using it(basically on you the developer).
The Visualization Trait’s support:
- High Resolution Actor: Intended as the full Actor’s functionality.
- Low Resolution Actor: Intended as the lower processing version of the Actor, could be the same Actor just executing at a lower tick-rate with fewer features enabled.
- Instanced Static Mesh: Intended as just a visual representation with no collision.
- Instanced Skinned Mesh: Intended as just a visual representation with no collision.
You are able to create representation configurations at runtime and create your own Fragments as well or modify existing configurations
to make new Fragments. The system that does this is through a UMassRepresentationSubsystem that registers representation
descriptions and uses handles to pass that information around.
For example, you can setup a Representation Trait on an Entity. In the data asset you can also have somewhere else multiple materials for a Static Mesh that are used to swap that information for the LOD system at runtime(say you want a different LOD 3 representation material after it’s spawned). In this case you would need to do these steps:
- Get the Mass Representation Subsystem.
- Call
::FindOrAddStaticMeshDesc()and provide a runtime createdFStaticMeshInstanceVisualizationDesc, whose Meshes array holds the individualFMassStaticMeshInstanceVisualizationMeshDescentries (prepare for a lot of long names in Mass). - Cache the outputted
FStaticMeshInstanceVisualizationDescHandlefrom the::FindOrAddStaticMeshDesc(). - Later in a Processor’s
::Execute()function while iterating Entities, get theFMassRepresentationFragmentfor that Entity on the specific LOD you want and set itsStaticMeshDescHandleto the handle you created in the earlier step.
Managing LOD Swapping
There are multiple Traits that manage LOD swapping, but for any basic functionality it requires a combination of two; a LOD Trait and a Visualization Trait. They branch from those two categories below:
For managing how an Actor’s LOD swaps or spawning is handled, you have to inherit from UMassRepresentationActorManagement
which is a UObject that is never actually instanced, the Trait writes your class into
FMassRepresentationParameters::RepresentationActorManagementClass, and ::ComputeCachedValues() caches that class’s
Class Default Object on the const shared Fragment as CachedRepresentationActorManagement.
Processors then use that CDO directly.
This is why every overridable function is const and why you must not store per-Entity mutable
state on it) where it has multiple virtual functions
that can be overridden. You have to specify this within the Trait for the Representation Actor Management Class to use, you can
also specify the subsystem in the Trait, but I haven’t found a need to do that yet unless you are implementing a completely
different LOD swapping framework.
You also have to inherit from these Processors for connecting it to the system because the base classes have their auto
registration disabled. UMassRepresentationProcessor(which UMassVisualizationProcessor derives from),
UMassVisualizationLODProcessor and UMassLODCollectorProcessor each set bAutoRegisterWithProcessingPhases = false
in their own constructor, which is what the engine calls a Dynamically Registered Processor: one that isn’t
automatically added to the processing graph.
You can see an example of this in the engine with the Mass Crowd’s module, where UMassCrowdVisualizationProcessor,
UMassCrowdVisualizationLODProcessor and UMassCrowdLODCollectorProcessor each inherit from one of those base
Processors and set bAutoRegisterWithProcessingPhases = true in their constructors. Otherwise, leave the flag alone
and dynamically register your Processor instance at runtime, which achieves the same effect.
These are the Processors to inherit from and enable auto register in their constructors for:
UMassLODCollectorProcessorUMassVisualizationLODProcessorUMassVisualizationProcessor
When swapping between an Instanced Mesh to a real Actor(High or Low res), the Actor Manager will call its virtual ::SetActorEnabled()
function for both directions. On an initial spawn ::OnPostActorSpawn() runs first: ::GetOrSpawnActor() only queues a
spawn request with the UMassActorSpawnerSubsystem and returns nullptr on that pass, and the subsystem fires the
post-spawn delegate when it later processes the request.
::SetActorEnabled() only gets called on a subsequent Processor pass once the request has succeeded and
::GetOrSpawnActor() actually returns the spawned Actor.
It is recommended to override both of these, and it will handle many cases. There are other overridable events within
the class, so I encourage you to take a look and see what works for your needs.
LOD Tags
When Mass swap’s LOD’s between High/Medium/Low/Off, it swaps Tags on the Entity to track it, with each tag representing each corresponding LOD:
- High:
FMassHighLODTag - Medium:
FMassMediumLODTag - Low:
FMassLowLODTag - Off:
FMassOffLODTag
You may see considerations in Processors to account for this by using AddTagRequirement<FMassOffLODTag>(EMassFragmentPresence::None),
this is so the Processor doesn’t execute on Entities too far away to visualize anything.
This can help with optimizing for performance by removing unnecessary CPU cycles.
World Partition
Regarding World Partition, the system currently checks if the runtime cell is loaded and collision is enabled before swapping from an Instanced Mesh to Actor to enforce valid state for each cell.