Spawn unlocated tileset

Hello,

I would like to spawn an instance of a tileset that does not have a transform in the tileset.json, and position it with user-supplied long/lat/altitude values.

I’ve seen the related case Spawn 3DTileset with correct geolocation - #5 by aaa1a when the tileset does have geolocation transform with ECEF coordinates.

The use case is much like as described at the top of that case for data that is generic and could be spawned more than once.

So far, I can spawn a tileset and it comes in at the origin as expected. In CesiumJS I might do something like:

var cart3 = Cesium.Cartesian3.fromRadians(overrideLong, overrideLat, height);
var m1 = Cesium.Transforms.eastNorthUpToFixedFrame(cart3);
tileset.modelMatrix = m1;

to define the matrix for the tileset.

Is there a straightforward way to alter the tileset transform in the georeference context that is equivalent?

Using C++ would be fine if required - double precision may be important.

Would there be any additional considerations when spawning multiple tileset instances, when these are a long way apart? For example, if we want to geolocate them 300km apart and fly a DynamicPawn between them?

Thank you!

Hi @JNWood,

I haven’t tested this out myself, but you can try adding a Globe Anchor component when you spawn the tileset, and setting the location through the Globe Anchor component.

If you try this out, please let me know how it goes!

As for your second question, if you are building scenes in different areas of the world, you may want to try using sublevels to assist with georeferencing.

-Alex

Hi,

thanks for the suggestion.

I haven’t managed to make that work, either in the editor or by using a Blueprint to spawn a tileset then add the GlobeAnchor component with location set by a call to ‘InaccurateMoveToLong/Lat/Height’.

I do notice this message:

Mobility of /Game/UEDPIE_0_level_1_unlocated.level_1_unlocated:PersistentLevel.Cesium3DTileset_1 : Tileset has to be ‘Movable’ if you’d like to move.

Which indicates that maybe things aren’t connecting property for the BP version?

With a manual instance created in the editor and adding an Anchor component, I can see the long/lat/height and ECEF components look correct, but the tileset geometry is still floating in a void (coordinate origin?). The tools to snap orientation to ellipsoid normal or ESU do not have any effect.

Thanks,

Josh

Hi @JNWood,

It looks like it’s likely not currently possible to use a GlobeAnchor component to move a tileset. It would be a good addition to the plugin, though. I created an issue to track this feature request - Support geolocating tilesets in Unreal · Issue #813 · CesiumGS/cesium-unreal · GitHub

If you have additional information about your use case, please add it to the issue!

One other approach you could try is using a second Georeference. The original georeference should be set to the location where you would like to place your tileset. Add a second georeference for your unlocated tileset only, and set the georeference’s Origin Placement parameter to True Origin. This should place your desired position on the globe and your tileset’s origin at Unreal’s 0,0,0 coordinate (though you may need to rotate the tileset using the Rotation tool).
I believe this would only work with one unlocated tileset, though - I don’t think it would work if you had multiple unlocated tilesets that needed to be in different places.

-Alex

Hi @JNWood,

I just updated the GitHub issue with instructions for setting the world location of a tileset that doesn’t inherently have a location. Let me know if it works for you:

Kevin

1 Like

Hi Kevin,

thanks for the update.

Following the instructions does indeed work during editing. The combination of a ‘true origin’ geoReference and the globeAnchor did the trick. I had tried using both but not together. I can bring in arbitrary tilesets and assign a working location, which is great.

This is great progress, for my use case I need to be able to do this at runtime (possibly more than once).

If I attempt to produce the same structure at runtime from C++, I get the runtime warning that I am attempting to move static geometry. Message of this kind:

Mobility of /Game/UEDPIE_0_level_1_unlocated.level_1_unlocated:PersistentLevel.Cesium3DTileset_0 : Tileset has to be ‘Movable’ if you’d like to move.

Which sounds like the rootComponent of the Cesium3DTileset has mobility ‘Static’ (as seen in the editor) and prevents altering the position (tooltip says this object can’t be moved in game). Is this likely to be the problem?

Inspecting the actors and their components, the properties all look the same as a working instance created manually in the editor.

Would altering the component order to make the anchor the root help, if possible?

Or would it be better to create a subclass of Cesium3DTileset and set the anchor component and georeference up in the constructor?

Thanks for your help,

Josh

For reference, the C++ method I’ve tried:

void ATestSpawnTileset::SpawnSomething(FString sourceURL, ACesiumGeoreference* mainReference, FVector lla)
{
FVector Location(0.0f, 0.0f, 0.0f);
FRotator Rotation(0.0f, 0.0f, 0.0f);
FActorSpawnParameters SpawnInfo;

ACesiumGeoreference* localReference = GetWorld()->SpawnActor<ACesiumGeoreference>(Location, Rotation, SpawnInfo);
localReference->OriginPlacement = EOriginPlacement::TrueOrigin;
localReference->SetActorLabel("BasicLocalGeoreference");	//so near top of list
ACesium3DTileset* tileset = GetWorld()->SpawnActor<ACesium3DTileset>(Location, Rotation, SpawnInfo);
tileset->SetGeoreference(localReference);
tileset->SetTilesetSource(ETilesetSource::FromUrl);
tileset->SetUrl(sourceURL);
tileset->SetActorLabel(TEXT("BasicLocatedTileset1"));

UCesiumGlobeAnchorComponent* anchor = NewObject<UCesiumGlobeAnchorComponent>(tileset);	
anchor->SetGeoreference(mainReference);
UE_LOG(LogTemp, Warning, TEXT("Long %f Lat %f H %f"), lla.X, lla.Y, lla.Z);
anchor->InaccurateMoveToLongitudeLatitudeHeight(lla);
tileset->AddInstanceComponent(anchor);
anchor->RegisterComponent();

}

Hi Josh,

I think you can just change the mobility flag of the root component after spawning the tileset. Something like this:

ACesium3DTileset* tileset = GetWorld()->SpawnActor<ACesium3DTileset>(Location, Rotation, SpawnInfo);
tileset->GetRootComponent()->SetMobility(EComponentMobility::Movable);
tileset->SetGeoreference(localReference);
tileset->SetTilesetSource(ETilesetSource::FromUrl);
tileset->SetUrl(sourceURL);
tileset->SetActorLabel(TEXT("BasicLocatedTileset1"));

I can’t easily try it at the moment, but I think it will work. Let me know.

Kevin

I’m sure this seems odd, so I thought it might be useful to spill a few words about why this works. Cesium3DTilesets usually know their position on the globe inherently. In other words, after transformation, all model vertices are already in ECEF. So it doesn’t make sense to anchor them because they’re already anchored, and attempting to do so would double-do the transformation.

But some tilesets are not already placed on the globe, their vertices are just relative to some local model coordinate system. Unfortunately we can’t really tell the difference between these cases. We could look at the magnitude of the vertex positions, maybe, but that would only be a guess. But we can tell Cesium that the model uses a local coordinate system by referencing it to a CesiumGeoreference in TrueOrigin mode. This is not very intuitive at all. I would like to evolve toward a system where TrueOrigin goes away completely and there’s simply a checkbox on a tileset indicating whether the tileset has an inherent location on the globe or not.

In any case, once Cesium knows that the tileset has a local rather than global origin, it acts just like any other Unreal Actor; Cesium for Unreal doesn’t perform any extra transformations. Giving it a location of (0,0,0) puts it at the origin of the UE coordinate system, and we can move and rotate it from there. And we can attach a GlobeAnchor component to automatically assign a location and rotation based on a globe position, just like we can for other Unreal Actors.

Hopefully that makes it a little less surprising!

Kevin

Hi,

thanks for the detailed explanation, it all makes sense.

I can see in my samples files, tileset.json for the located samples have a transform matrix on the root tile, and the unlocated ones do not. I’m sure I’ll eventually need to be able to tell at runtime if a URL references a tileset that has a root transform, otherwise I’ll end up with that double-transform situation you describe.

Your suggestion to alter the mobility mode certainly makes the warnings stop. If I do the same sort of C++ for a located tileset, without the TrueOrigin georeference and added globeAnchor, that works fine (mobility is needed to be set in order to tweak the actor transform, but that is a small thing).

The unlocated example does not appear even with Mobility set (compared to the in-editor data). I’m not sure if it is just not visible, hidden by terrain, off screen or failing to load :slight_smile:

I will try a bit more logging and find samples that have simple local coordinates to reduce sources of error.

Hi,

I’ve tried to exclude sources of error and have got to the following state.

If I create a tileset and assign the ‘True Origin’ geolocation and globe anchor in the editor, it all works perfectly.

If I try to do the same from C++, the resulting objects have the same configuration but I cannot see the geometry. The final transform on the spawned tileset object has small differences but would still likely be visible.

If I reference a tileset with anchor created in the editor, I can give it a new source URL and update the Globe Anchor coordinates to achieve about the right result. This is good but limits creating multiple tilesets.

Spawning a geolocated tileset works fine with no additional work, no special georeference or globe anchor required, as expected.

Is there any other setting or construction order I am likely to be missing, that might explain the difference between construction in the editor and at runtime?

Thanks,

Josh

My C++ looks like so:

void ATestSpawnTileset::SpawnUnlocatedTileset(FString sourceURL, ACesiumGeoreference* mainReference, FVector lla)
{
	FVector Location(0.0f, 0.0f, 0.0f);
	FRotator Rotation(0.0f, 0.0f, 0.0f);
	FActorSpawnParameters SpawnInfo;
	 
	UE_LOG(LogTemp, Warning, TEXT("Spawning tileset with URL %s"), *sourceURL);

	ACesiumGeoreference* localReference = GetWorld()->SpawnActor<ACesiumGeoreference>(Location, Rotation, SpawnInfo);
	localReference->OriginPlacement = EOriginPlacement::TrueOrigin;
	localReference->SetActorLabel("BasicLocalGeoreference");	//so near top of list
	ACesium3DTileset* tileset = GetWorld()->SpawnActor<ACesium3DTileset>(Location, Rotation, SpawnInfo);
	tileset->GetRootComponent()->SetMobility(EComponentMobility::Movable);
	tileset->SetGeoreference(localReference);
	tileset->SetActorLabel(TEXT("BasicUnlocatedTileset1"));

	//assigning this before setting tileset source - after behaves the same way.
	UCesiumGlobeAnchorComponent* anchor = NewObject<UCesiumGlobeAnchorComponent>(tileset);
	anchor->SetGeoreference(mainReference);
	UE_LOG(LogTemp, Warning, TEXT("Spawning at Long %f Lat %f H %f"), lla.X, lla.Y, lla.Z);
	tileset->AddInstanceComponent(anchor);
	anchor->RegisterComponent();
	anchor->InaccurateMoveToLongitudeLatitudeHeight(lla);

	tileset->SetTilesetSource(ETilesetSource::FromUrl);
	tileset->SetUrl(sourceURL);		
}

void ATestSpawnTileset::ReplaceTileset(FString sourceURL, ACesium3DTileset* tileset, FVector longLatAlt)
{
	tileset->GetRootComponent()->SetMobility(EComponentMobility::Movable);	//should not be needed?
	tileset->SetUrl(sourceURL);
	FTransform transform{};	//identity
	tileset->SetActorTransform(transform);
	UCesiumGlobeAnchorComponent* anchor = static_cast<UCesiumGlobeAnchorComponent*>(tileset->GetComponentByClass(UCesiumGlobeAnchorComponent::StaticClass()));
	if (anchor)
	{
		anchor->InaccurateMoveToLongitudeLatitudeHeight(longLatAlt);
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("Missing globe anchor"));
	}
}

Incidentally I don’t think I can do this via BP due to readOnly preventing setting a new Georeference to ‘TrueOrigin’ mode. C++ is needed anyway, but perhaps this is another difference between editor and runtime behaviour?

  UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Cesium")
  EOriginPlacement OriginPlacement = EOriginPlacement::CartographicOrigin;

Found part of it, if I create the ‘true origin’ georeference like so:

	ACesiumGeoreference* localReference = GetWorld()->SpawnActor<ACesiumGeoreference>(Location, Rotation, SpawnInfo);
	localReference->OriginPlacement = EOriginPlacement::TrueOrigin;
	localReference->UpdateGeoreference();

then I can spawn tilesets at runtime. The UpdateGeoreference() made the difference.

This works, which is great. The next issue is that spawning multiple objects seems to have some state held between them, as if the locations are far apart, the first object spawns with good orientation (up is as expected) but a second spawns rotated. If I reverse the order of spawning, the same thing is observed (first one is correctly upright, others are not, irrespective of which particular tilesets are used).

I will investigate the ‘snap orientation’ functions on GlobeAnchor.

Thanks!