Actor bounds is invalid in World Partition Level

In WorldPartitionActorDesc::Init, it looks like calculating wrong editorbounds when child actor use absolute location type.

Would I modify code like below?

    RuntimeBounds.Init();
    EditorBounds.Init();
    
    if (!bIsDefaultActorDesc)
    {
        ActorTransform = InActor->GetActorTransform();
        ActorTransformRelative = InActor->GetRootComponent() ? InActor->GetRootComponent()->GetRelativeTransform() : ActorTransform;

#if 1 // Fix attached actor with abolute transform has incorrect actor bounds in Editor
	if (auto RootComponent = InActor->GetRootComponent())
	{
		if (USceneComponent* AttachParent = RootComponent->GetAttachParent())
		{
			FTransform const ParentToWorld = AttachParent->GetSocketTransform(RootComponent->GetAttachSocketName());
			FVector NewLocation = ActorTransform.GetLocation();
			FQuat NewRotationQuat = ActorTransform.GetRotation();
			FVector NewScale = ActorTransform.GetScale3D();
			if (FTransform::AnyHasNegativeScale(RootComponent->GetRelativeScale3D(), ParentToWorld.GetScale3D()))
			{
				FTransform const WorldTransform = FTransform(ActorTransformRelative.GetRotation(), NewLocation, RootComponent->GetRelativeScale3D() * ParentToWorld.GetScale3D());
				FTransform const RelativeTransform = WorldTransform.GetRelativeTransform(ParentToWorld);

				NewLocation = RelativeTransform.GetLocation();
				NewRotationQuat = RelativeTransform.GetRotation();
				NewScale = RelativeTransform.GetScale3D();
			}
			else
			{
				NewLocation = ParentToWorld.InverseTransformPosition(NewLocation);
				NewRotationQuat = ParentToWorld.GetRotation().Inverse() * NewRotationQuat;
				NewScale = NewScale / ParentToWorld.GetScale3D();
			}

			ActorTransformRelative = FTransform(NewRotationQuat, NewLocation, NewScale);
		}
	}
#endif

        InActor->GetStreamingBounds(RuntimeBounds, EditorBounds);
        FixupStreamingBounds();

        RuntimeBoundsRelative = FWorldPartitionRelativeBounds(RuntimeBounds).InverseTransformBy(ActorTransform);
        EditorBoundsRelative = FWorldPartitionRelativeBounds(EditorBounds).InverseTransformBy(ActorTransform);

        bHasValidRelativeBounds = true;
    }
    



Steps to Reproduce

  1. Create a World Partition Level.
  2. Place a Parent Actor: Set its location to a very large coordinate (e.g., 100,000,100,000,100,000). A large value is required to expose the precision/culling issue.
  3. Place a Child Actor: Attach a new actor as a child of the parent actor created in Step 2 at the same location.
  4. Change Transform Rule: Set the child actor’s transform location type to “Absolute”.
  5. Unload and Reload: In the World Partition window, unload the region containing these actors, then use “Load Region from Selection” to reload them.
  6. Verify Issue: Observe that the child actor fails to load correctly because its calculated actor bounds have been projected to an incorrect location.

Hi James,

thanks for debugging the issue. I made it a bit more robust by using the Parent Actor computed later in the Init() and moving it earlier (so this matches the actor used in UpdateActorToWorld)

        if (!bIsDefaultActorDesc)
	{
		const AActor* AttachParentActor = InActor->GetAttachParentActor();
		if (AttachParentActor)
		{
			while (AttachParentActor->GetParentActor())
			{
				AttachParentActor = AttachParentActor->GetParentActor();
			}
			ParentActor = AttachParentActor->GetActorGuid();
		}
 
		ActorTransform = InActor->GetActorTransform();
 
		const USceneComponent* RootComponent = InActor->GetRootComponent();
		if (RootComponent &&
			(RootComponent->IsUsingAbsoluteLocation() ||
			 RootComponent->IsUsingAbsoluteRotation() ||
			 RootComponent->IsUsingAbsoluteScale()))
		{
			// Root uses one or more absolute loc/rot/scale flags, so the matching relative fields are stored in world space.
			// Derive the relative transform from world transforms.
			ActorTransformRelative = AttachParentActor ? ActorTransform.GetRelativeTransform(AttachParentActor->GetActorTransform()) : ActorTransform;
		}
		else
		{
			ActorTransformRelative = RootComponent ? RootComponent->GetRelativeTransform() : ActorTransform;
		}
 
		InActor->GetStreamingBounds(RuntimeBounds, EditorBounds);
		FixupStreamingBounds();
 
		RuntimeBoundsRelative = FWorldPartitionRelativeBounds(RuntimeBounds).InverseTransformBy(ActorTransform);
		EditorBoundsRelative = FWorldPartitionRelativeBounds(EditorBounds).InverseTransformBy(ActorTransform);
 
		bHasValidRelativeBounds = true;
	}

and later in the method, removed the loop I moved in the previous snippet:

if (!bIsDefaultActorDesc)
{
	FolderPath = InActor->GetFolderPath();
	FolderGuid = InActor->GetFolderGuid();
 
// -> removed from here!
 
	const ActorsReferencesUtils::FGetActorReferencesParams Params = ActorsReferencesUtils::FGetActorReferencesParams(const_cast<AActor*>(InActor));
	TArray<ActorsReferencesUtils::FActorReference> ActorReferences = ActorsReferencesUtils::GetActorReferences(Params);
 
    ...
}

I confirmed the fix on my side, it was causing other issues in cooked content as well (actors getting cooked into the wrong cells for example). If it fixes your issue too, I will make the fix available either in a 5.8 hotfix or 5.8.1.

Here’s a better documented version with additional subtle guards

if (!bIsDefaultActorDesc)
{
	const AActor* AttachParentActor = InActor->GetAttachParentActor();
	if (AttachParentActor)
	{
		while (AttachParentActor->GetParentActor())
		{
			AttachParentActor = AttachParentActor->GetParentActor();
		}
		ParentActor = AttachParentActor->GetActorGuid();
	}
 
	ActorTransform = InActor->GetActorTransform();
 
	const USceneComponent* RootComponent = InActor->GetRootComponent();
	// We want to set ActorTransformRelative so that ActorTransformRelative * ParentTransform == ActorTransform where the parent is not necessarily just the attachment parent
	// but rather the child component hierarchy parent: ParentTransform = ParentActor.IsValid() ? ParentActorDesc->GetActorTransform() : FTransform::Identity
	// RootComponent->GetRelativeTransform() (cheap field read) already equals that value if and only if:
	// - there is a root component, AND
	//	- there is no attach parent (ParentTransform is identity), OR
	//	 - none of the absolute loc/rot/scale flags are set (otherwise those channels hold world-space values), AND
	//     the root is attached to AttachParentActor's root component itself - not a sub-component, and without child-actor
	//     hops, AND the attach socket is NAME_None (a socket would fold its offset into GetRelativeTransform()'s frame).
	// Otherwise GetRelativeTransform() is in a different frame than ParentTransform, so derive it from world transforms.
	const bool bRelativeFieldsInParentFrame =
		RootComponent &&
		(!AttachParentActor ||
			(!RootComponent->IsUsingAbsoluteLocation() &&
			 !RootComponent->IsUsingAbsoluteRotation() &&
			 !RootComponent->IsUsingAbsoluteScale() &&
			  RootComponent->GetAttachParent() == AttachParentActor->GetRootComponent() &&
			  RootComponent->GetAttachSocketName() == NAME_None)
		);
 
	if (bRelativeFieldsInParentFrame)
	{
		ActorTransformRelative = RootComponent->GetRelativeTransform();
	}
	else
	{
		// GetRelativeTransform() is not in ParentActor's frame here (absolute channels, a non-default socket, or
		// attachment to a non-root / child-actor component), so derive the relative transform from world transforms.
		ActorTransformRelative = AttachParentActor ? ActorTransform.GetRelativeTransform(AttachParentActor->GetActorTransform()) : ActorTransform;
	}
 
	InActor->GetStreamingBounds(RuntimeBounds, EditorBounds);
	FixupStreamingBounds();
 
	RuntimeBoundsRelative = FWorldPartitionRelativeBounds(RuntimeBounds).InverseTransformBy(ActorTransform);
	EditorBoundsRelative = FWorldPartitionRelativeBounds(EditorBounds).InverseTransformBy(ActorTransform);
 
	bHasValidRelativeBounds = true;
}

Thanks!