VisibilityId for Components in Level Instances

We use Umbra to precompute visibility for our maps which we can trigger from the editor or from a commandlet. This requires unique visibilityIds for all scene components. To compute these,

we iterate through all the static mesh components etc., generate a unique id and assign it to the Component’s VisibilityId

Component->VisibilityId = VisibilityId;

Component->MarkRenderStateDirty();

Component->MarkPackageDirty();

And this mostly works in editor, however for components that live in level instances, the RF_Transient flag is set on the package and the change is never ‘saved’.

What is the correct protocol/API to modify the VisibilityId values for components in Level Instances in a commandlet or even in editor for that matter since the results are lost there too upon exit?

For example one idea is to cache all the Visibility Ids in an array in ULevelStreaming and then restore them all to the scene components on level load. But this seems like a pretty nasty hack.

[Attachment Removed]

Steps to Reproduce
The error is that VisibilityId used in precomputed visibility doesn’t work for components in LevelInstances because their packages are RF_Transient.

[Attachment Removed]

Hi,

Thanks for reaching out. Saving component changes on level instances is a common issue for which there is no standardised solution as far as I know. One way to handle this is to store any property changes on the owning level of the level instance. It’s also possible to add the visibility IDs to the UMapBuildDataRegistry and update the serialization logic to make sure they are serialized. You should be able to call UMapBuildDataRegistry::Get(Component) from anywhere the primitive component is available.

I’m not sure if you have tried this, but the editor also has a way to commit changes after editing a level instance (see this link). If this works in your case, you can have a look at the commit logic in LevelInstanceSubSystem.cpp.

I hope that helps, but let me know if you have any further questions.

Thanks,

Sam

[Attachment Removed]

Hi Sam,

Thanks for your reply.

I am not familiar with UMapBuildDataRegistry.

Is this a storage system that can persist across sessions, it seems to be used while building lightmaps?

Can a Component’s data from an LI be stored there in a stable way?

The link you referenced, which I had seen, indicates that any changes made to an LI propagates to all other instances.

One of the exceptions being the transformation matrix itself. These are persisted for each LI and it seems to be stored on FLoadLevelInstanceParams which is handled in

ULevelStreamingLevelInstance* ULevelStreamingLevelInstance::LoadInstance(ILevelInstanceInterface* LevelInstance)

So I would seem I could attach an array of VisibilityIds there.

However, I do not know how to get a stable ‘id’ for the PrimitiveComponents in the LI. Maybe you have a suggestion.

Is the order of components from

for (const UActorComponent* Component : LevelActor->GetComponents())

stable? If so I could use an index.

Also, what is this value used for?

Level->PrecomputedVisibilityHandler.GetId();

[Attachment Removed]

Hi,

>> I am not familiar with UMapBuildDataRegistry.

As you already mentioned, UMapBuildDataRegistry is a type of Unreal Engine’s Data Registries for storing precomputed lightmap data and persists across sessions. It can be extended to allow for additional data like visibility ids, provided its serialization logic also needs to be updated to handle it. This is just one solution (that has worked in some games), but it may not be the best one for your project.

>> I do not know how to get a stable ‘id’ for the PrimitiveComponents in the LI. Maybe you have a suggestion.

I did some more digging into this topic and found a few related threads (with answers from Epic engineers) that can be of interest. As there is a lot of information contained in them, I’m providing the links below:

[Content removed]

[Content removed]

[Content removed]

[Content removed]

Some less relevant topics, but may still contain some interesting info:

[Content removed]

[Content removed]

>> Also, what is this value used for? Level->PrecomputedVisibilityHandler.GetId();

The source code (in Level.h) mentions that this Id is the “Id used by the renderer to know when cached visibility data is valid.” This is data set for the entire level (FPrecomputedVisibilityHandler handles operations on precomputed visibility data for a level), not per component.

I hope this helps, but let me know if you have more questions.

Best,

Sam

[Attachment Removed]

Hi Sam,

Thanks a gain for your reply.

There appears to be no good way to get an id for an actor + component in the level instance.

There are suggestions to use FGuid but that only exists in editor and I loathe to incur the overhead of adding it always to all Actors.

W.r.t. components, I considered using the iterator index but they are stored in a TSet so order is not guaranteed.

It looks like there would need to be a pretty significant rework of the LevelInstance concept to support storing this.

It looks like you are also just trying to scrape through the code and the posts for answers and I appreciate your efforts. Its a shame the epic engineers themselves don’t seem to answer the questions here to give some more definitive answers on what the intent of the design of the engine is. It looks like we have all just found an amazing alien artifacts and we are trying to get the best out of it but the aliens are not revealing what the plan is or how things are intended to work.

[Attachment Removed]

Hi there,

We had to deal with a similar issue on a different game. Which is why I suggested storing the visibility ids in UMapBuildDataRegistry for Sam to pass on, as this is what we did. While I feel your pain on this subject (having had to deal with all sorts of Level Instance issues myself), unfortunately what your are after is currently an unsupported feature, so it isn’t really intended to work at all. So the best we can really do is give advice for what has worked for other studios / games. From memory, what we did was to use the StaticMeshComponent->LODData[0].MapBuildDataId as our unique FGuid. If you look at UStaticMeshComponent::UpdateMapBuildDataId, you can see this takes into account the level instance actor Guid already, and is updated on component register, so it should be stable after this point. Since LODData is serialized per component, you can use the LOD0 MapBuildDataId as your unique component identifier. This also avoids you having to add more FGuid variables to either actors or components.

Where you then store the visibility ids is really up to you. We used UMapBuildDataRegistry, as it made sense to group this with our baked lighting data. You could instead add a custom UPROPERTY to levels to store visibility ids directly in the level. You would have to ensure that you always store the visibility ids in your Persistent level though, to avoid issues with LevelInstance levels being transient. Then, instead of looping over all your primitive components to restore the VisibilityId on map load, you can instead set the VisibilityId in your components OnRegister method by looking it up from the persistent level or UMapBuildDataRegistry using LODData[0].MapBuildDataId. Just ensure you call Super() first in OnRegister to ensure the MapBuildDataId is set correctly. Or, if you add this directly to UStaticMeshComponent::OnRegister, ensure you add it after the call to UpdateMapBuildDataId. This ensures the VisibilityId is set as soon as possible, and doesn’t require manually looping over all components on load.

Please let me know if you have any follow up questions, and if necessary I can elevate them to a subject matter expert at Epic.

Regards,

Lance Chaney

[Attachment Removed]

Hi Lance,

Thanks for your feedback. The approach you described sounds like it might be feasible. Here is where I have ended up.

I create a parallel asset to the Persistent Level. This asset acts like a lookup for the LevelInstance components.

I am relying on the FSoftObjectPath. These are very long strings large parts of which are repeated for the actors in the each level instance as well as the components. To mitigate the data size of the lookup, I crack the path into just the actor part and the component part and I use the LevelInstance Hash for the rest. This combination seems to be a stable Id.

To avoid some of the overhead of all these strings, I use two dictionaries which map strings to Ids. One is for actors and the other is for components.

The data structures I ended up with are:

// Cache visibility ids for level instances on the precomputed data for the persistent level so we can patch them in when they load.
UCLASS(notplaceable, MinimalApi)
class UVisibilityCacheData: public UAssetUserData
{
	GENERATED_UCLASS_BODY()
 
public:
	// List of unique names found on components (there are many cases where components have the exact same name).
	UPROPERTY()
	TMap<FString, int32>	ComponentNameDictionary;// Maps ComponentName to Unique Id at the Actor level
 
	// List of unique names found on actors (there are many cases where actors have the exact same name).
	UPROPERTY()
	TMap<FString, int32>	ActorNameDictionary;	// Maps ActorName to Unique Id at the LevelInstance level
 
	UPROPERTY()
	TArray<FLevelInstanceVisibilityInfo> LevelInstanceVisibilityIds;
 
	// Called at runtime on Level load to fixup components whose visibility Id is not saved, e.g. level instances
	// Can either update the cache (default) from the world, or patch the cache to the world.
	MY_API static UVisibilityCacheData* GetOrCreateMapOcclusionData(ULevel* PersistantLevel, bool bAllowCreate);
	bool SyncLevelVisibilityCache(UWorld *World, ULevel* Level, bool bPatch);
        MY_API UPackage* CacheTransientVisibilityIds(UWorld* World);
};
 
// Actor level cache for the levelinstance
USTRUCT()
struct FLevelInstanceVisibilityInfo
{
	GENERATED_USTRUCT_BODY()
 
	UPROPERTY()
	uint64	LevelInstanceIdHash = 0;
 
	UPROPERTY()
	TArray<FActorInfo>	ActorInfos;
};
 
// Component level cache for the actor
USTRUCT()
struct FActorInfo
{
	GENERATED_USTRUCT_BODY()
 
	UPROPERTY()
	int32 ActorNameId;
 
	UPROPERTY()
	TMap<int32, int32> ComponentVisibility;	// Dictionary 
 
	static const FString GetIdentifier(const AActor* Actor);
	static const FString GetComponentIdentifier(const UActorComponent* Component, const FString& ActorIdentifier) const;
};
 
 
// Strips the SoftObjectPath to remove the Package path.
const FString FActorInfo::GetActorIdentifier(const AActor* Actor)
{
	const TSoftObjectPtr<AActor> SoftObjectPtr(Actor);
	const FSoftObjectPath SoftObjectPath = SoftObjectPtr.GetUniqueID();
	const FString ActorSubPath = SoftObjectPath.GetSubPathString();
	int32 Count = 0;
	ActorSubPath.FindChar(L'.', Count);
	const FString ActorIdentifier = ActorSubPath.RightChop(Count + 1);
	return ActorIdentifier;
}
 
const FString FActorInfo::GetActorComponentIdentifier(const UActorComponent* Component, const FString& ActorIdentifier)
{
	const TSoftObjectPtr<UActorComponent> SoftObjectPtr(Component);
	const FSoftObjectPath SoftObjectPath = SoftObjectPtr.GetUniqueID();
	const FString ActorComponentSubPath = SoftObjectPath.GetSubPathString();
	int32 Count = 0;
	ActorComponentSubPath.FindChar(L'.', Count);
	const FString ComponentIdentifier = ActorComponentSubPath.RightChop(Count + ActorIdentifier.Len() + 2);
	return ComponentIdentifier;
}

}

[Attachment Removed]

Thanks for the suggestions w.r.t. PackageShortName.

I looked at this, but PackageShortName is blank for me. It is also private so I dont think it was meant to be acessed.

[Attachment Removed]

To follow up, I think this code does produce unique ids at least in editor and they seem to be consistent between editor sessions:

const FString ActorPackageName = FPackageName::GetShortName(Component->GetOwner()->GetPackage()->GetName());

For the relevant actors it produces names like:

BPEBMDBHE1ZAIMUQXTHVU1

For some of the actors (which are generally hidden in the outliner) it produces the ‘long package name form’.

[Attachment Removed]

I am hitting another mystery, since i need to patch the VisibilityIds back into the Level instances’ scenecomponents I am using

OnLevelStreamingStateChanged(UWorld* InWorld

, const ULevelStreaming* InLevelStreaming

, ULevel* InLevelIfLoaded

, ELevelStreamingState InPreviousState

, ELevelStreamingState InNewState)

And waiting for InNewState == ELevelStreamingState::LoadedVisible

However even though the meshes are totally visible in editor, when that even it called I only have a small subset of the actors actually present in the level instance. I am at a loss as to what else to do to ‘wait’ for the correct state to be completed.

Is there something special I need to do in Editor for this?

[Attachment Removed]

Hi Sam,

Thanks for your answer. I can get the level instance pointer, and I receive the InNewState == ELevelStreamingState::LoadedVisible

but not all the actors that are visible in the viewport in the editor are actually present when I iterate. Perhaps I am not using the correct method to iterate over the actors. The code I am using is as follows:

ULevel* LoadedLevel = LevelInstance->GetLoadedLevel();

for (AActor* LevelActor : LoadedLevel->Actors) // Iterate over all the actors in the level

{

// Patch visibilityId from the cache for all the components on this actor.

}

[Attachment Removed]

Hi,

To answer your question, the problem is likely that while the level is loaded and visible (ELevelStreamingState::LoadedVisible), the actors and their components might not have finished their full initialization/registration, especially within the confines of the editor (which often has preloaded the actors) or on the first frame of visibility. The LoadedVisible state only guarantees that the level package is loaded and marked visible, not that all actors are fully initialized and registered. It does not strictly guarantee that every single actor in that level has completed its PostLoad or BeginPlay (which doesn’t happen in the editor for all actors right away) or that they have finished registering with the scene. You can try adding a small delay to defer the patching of visibility ids to the next tick with SetTimerForNextTick() or hook into the actor’s PostActorCreated() or PostLoad() methods.

On a sidenote, this article does a better job at what I was trying to say in my previous post.

I hope that helps.

Regards,

Sam

[Attachment Removed]

Hi Sam,

Thanks for your reply, I appreciate your efforts. I had already seen that article. It still does not seem to be a proper fix for the problem, at least in my case it does not work reliably. Btw, do you work for Epic or are you affiliated indirectly?

[Attachment Removed]

Hi,

thanks for sharing your solution, this looks like a good approach.

Something else that may be of interest is the PackageShortName which is a property of the FLevelInstanceID struct (in LevelInstanceTypes.h). From the docs:

PackageShortName allows distinguising between instanced LevelInstances of the same source level. Loading /Game/Path/WorldA.WorldA as /Game/Path/WorldA_LevelInstance1.WorldA & /Game/Path/WorldA_LevelInstance2.WorldA with the source WorldA containing one of many LevelInstance actors. Those actors would end up with the same hash. We use PackageShortName (WorldA_LevelInstance1 & WorldA_LevelInstance2) to distinguish them.

Regards,

Sam

[Attachment Removed]

Hi,

yes, you’re right, my apologies. I saw this being used to construct a unique hash in FLevelInstanceID::FLevelInstanceID() (inside LevelInstanceSubsystem.cpp).

Regards,

Sam

[Attachment Removed]

Hi,

referring to [this [Content removed] you could cast the ULevelStreaming* object into a ULevelInstanceLevelStreaming*. If that cast result is non-null, then you can get the level instance pointer with ULevelInstanceLevelStreaming::GetLevelInstance().

Let me know if that helps.

Regards,

Sam

[Attachment Removed]

Hi,

we’re part of the Epic Pro Support Partnership program. We provide first line support, but can also escalate cases to Epic if needed. That said, I’m not sure how to help further, so if you would like me to pass this on to a subject matter expert at Epic, please let me know.

Thanks,

Sam

[Attachment Removed]