Spawning in different sublevels

I’m setting up a flight sim, with different airports set up as different sublevels. I’m running into a bit of a chicken-and-egg problem.
Here’s my setup:
I have an OriginShiftComponent on my aircraft pawn.
I have one or more player starts associated with each airport, and each playerstart has a GlobeAnchor attached to it. The player start GlobeAnchors are configured with the appropriate LLA coordinates corresponding to the airport they are associated with. PlayerStarts are not children of the sublevel.
I am using JSBSim, and have modified the UJSBSimMovementComponent to talk directly to the CesiumGeoReference (instead of syncing CesiumGeoReference tp the built-in unreal GeoReferenceSystem)

Problem:
A sub-level isn’t streamed in until the OriginShiftComponent moves near that sub-level, but if we want to spawn the pawn at that sublevel, we must know the unreal position of the player starts. As I understand it, in order to get the correct unreal position of a player start, I must translate the PlayerStart’s LLA into Unreal coordinates. In the case where I want to spawn at an airport belonging to a sublevel that isn’t loaded, I must spawn the aircraft at the player start, then on the next tick, OriginShiftComponent will detect that it should trigger the airport’s sublevel to begin loading. One or more frames later, the sublevel will finish streaming in and update the GeoReference. But by this time, the aircraft simulation has already killed itself because it detects that something about the parameters I’m sending it is invalid (usually altitude is negative).
The problem, as far as I can tell, is that the values that I’m getting from the GeoReference won’t give me 100% accurate translations between Unreal and ECEF coordinates until it’s streamed in the new sublevel and updated the GeoReference. Is that correct or am I misunderstanding?

Second problem: I could defer starting the aircraft simulation until the sublevel has finished streaming in and updated the GeoReference using the OnGeoreferenceUpdated delegate. But then I run into the second problem: if the playerstart that’s been chosen happens to be in the sublevel that is currently loaded, OnGeoreferenceUpdated won’t be triggered. So I need to figure out which sublevel is supposed to be loaded, so that I can either immediately move to the next step [if target sublevel is active] or wait for OnGeoreferenceUpdated if target sublevel isn’t active. There’s no easy way to do that right now, as far as I can find, so the road I’m about to start down is to duplicate the code in UCesiumOriginShiftComponent::TickComponent so that I can trigger it when I first spawn my aircraft, rather than waiting for tick.

It would be helpful to move the logic of UCesiumOriginShiftComponent::TickComponent into a method that could be called separately. Ideally, that logic woudl be split up into separate pieces as well - one to query whether we need to load a new sublevel, and one to actually perform the origin shift / sublevel load.

Third problem:
After activating a new sublevel, there is no notification that level streaming has completed. This seems like it would be pretty simple to add, so I’m wondering if I’m approaching this incorrectly. Any reason not to add this (and then use that notification to know when I’m ready to spawn my aircraft)?

What is the recommended way to handle my usage case? Have I going horribly astray somewhere? :slight_smile:

Hi @dividebylife, welcome to the community!

Sorry for the delayed response, and thank you for the detailed write-up.

The problem, as far as I can tell, is that the values that I’m getting from the GeoReference won’t give me 100% accurate translations between Unreal and ECEF coordinates until it’s streamed in the new sublevel and updated the GeoReference. Is that correct or am I misunderstanding?

The CesiumGeoreference is giving you coordinates relative to its geolocated origin at that point in time. At the first tick, it will probably different from the origin of your sublevel, which means that yes, the Unreal coordinates will not be 1-to-1 between frames; the point of reference will have changed.

So I need to figure out which sublevel is supposed to be loaded, so that I can either immediately move to the next step [if target sublevel is active] or wait for OnGeoreferenceUpdated if target sublevel isn’t active.

You can query which sublevel is currently loaded here:

Alternatively, you could do a brute force check for the Enabled property of each sublevel:

Or, even more manually, query the distance between the sublevel and your current position, to anticipate which sublevel will be enabled. But hopefully you won’t have to do that. :smile:

Also, you can manually enable / disable sublevels using Set Target Sub Level, or the Set Enabled Blueprint, in case that is useful for you later on.

After activating a new sublevel, there is no notification that level streaming has completed. This seems like it would be pretty simple to add, so I’m wondering if I’m approaching this incorrectly. Any reason not to add this (and then use that notification to know when I’m ready to spawn my aircraft)?

That’s a good point. It would be helpful to have an “On Sublevel Loaded” event in the API.

I know that Unreal has an event you can bind on a level instance. I don’t see a reason why it wouldn’t work even with Cesium’s sub-level system, but please correct me if I’m wrong.

image

I hope these pointers can help you find a workable solution!

Correctly spawning multiple aircraft pawns in a multiplayer environment ended up being quite a bit more complicated than it first seemed. I won’t go through all the twists and turns, but here are some of the highlights:

  • in our case, we want clients to be authoritative over their own movement (we’re not worried about cheating since it’s not a game).
    – This ended up being pretty convenient, since you can’t run physics reliably without having the actual environment loaded for that area (i.e. can’t run physics on the server for multiple clients without having all of their sublevels loaded, which cesium doesn’t allow). To make cesium work in multiplayer with server-authoritative positions, you’d need to customize cesium to support having multiple sublevels loaded, which is obviously a fairly large customization.
  • because each client can be in a different part of the world, there is no single cesium sublevel that can be loaded. Each participant must track which cesium sublevel should be loaded for their own simulation, and handle activating it independently of other clients or the server. Activating a cesium sublevel on my client has no impact on other clients.
  • because each participant can have a different sublevel loaded, we cannot send positions/directions/normals over the network in Unreal units. We chose to send them using ECEF values, but you could use any coordinate system that works for the entire globe.
  • The process for activating a cesium sublevel takes many frames to complete. But spawning is normally a “synchronous” operation, so we had to add support for deferring spawning the pawn until the sublevel finishes loading.
    – Unforunately, I’m still struggling to figure out how to tell when the sublevel’s geometry is there. The first time I spawn after loading a new sublevel, the plane falls through the ground, because although the sublevel itself is loaded, cesium still needs to load in the tilesets. But I haven’t been able to figure out how to tell when the system is finished creating the collision for the tileset.

Yeah, I ended up having to bind to the FWorldDelegates::LevelAddedToWorld method to tell when a new cesium sublevel is being activated. But as mentioned above, that’s still too early [for spawning, anyway] because there’s no collision in the level yet.

Are there any events that are triggered (or somewhere in the code I could add a new delegate) to know when the ground collision has been fully loaded?

I tried subscribing to the OnTilesetLoaded delegates, then doing a trace downwards from my desired playerstart to see if the ground is there yet. The delegate gets triggered multiple times after activating a new sublevel, but the ground didn’t appear to be there anytime the delegate was triggered.

Of course, we could add a static mesh that is positioned so that it overlaps with where the ground will end up, but we’re really hoping to avoid having to do that because doing this for hundreds of sublevels is going to be a lot of work.

(BTW, it might be more helpful if OnTilesetLoaded passed in a pointer to the tileset that was loaded)

Hi @dividebylife,

To make cesium work in multiplayer with server-authoritative positions, you’d need to customize cesium to support having multiple sublevels loaded, which is obviously a fairly large customization.

That’s not just a “fairly large customization.” In fact, it doesn’t make any sense at all to do that. Let me explain.

When a Cesium sublevel is active, the CesiumGeoreference origin is changed to the sub-level’s origin. In other words, the entire Unreal world coordinate system moves to a new location on the globe. It makes no sense for the Unreal coordinate system to be in two locations on the globe simultaneously. Also, Unreal does not have any support for multiple sets of world coordinates simultaneously. So two Cesium sublevels active at once is just fundamentally impossible given how they work.

However, its totally fine to have multiple normal, non-Cesium sublevels active at once. And you’re welcome to put Cesium objects in these, too. You just don’t get that origin rebasing behavior that you get with Cesium Sublevels. Which means that your coordinate values will be very large for objects far from the origin (generally not a huge problem given that Unreal uses double-precision coordinates since UE 5.0), and +Z will not be “up” (perhaps more of a problem, depending on your application).

Long story short: if you want multiple sublevels active at once, don’t use Cesium sublevels, because the entire point of Cesium sublevels is that only one can be active at a time, and it makes no sense to change this. Use regular level instances instead.

We chose to send them using ECEF values, but you could use any coordinate system that works for the entire globe.

Yeah, that works. Or, you can make sure that your world, across all clients, uses a single Unreal coordinate system (i.e., single CesiumGeoreference, no Cesium sub-levels).

I tried subscribing to the OnTilesetLoaded delegates, then doing a trace downwards from my desired playerstart to see if the ground is there yet. The delegate gets triggered multiple times after activating a new sublevel, but the ground didn’t appear to be there anytime the delegate was triggered.

OnTilesetLoaded doesn’t promise that the entire tileset is fully loaded (that would require downloading petabytes of data for large tilesets). It only promises that the tileset is fully loaded for the current view. So perhaps your desired PlayerStart position isn’t visible from the intial view, and so the tiles in that area are not being loaded at all?

BTW, it might be more helpful if OnTilesetLoaded passed in a pointer to the tileset that was loaded

Makes sense to me! We’d welcome a pull request for that if you’re up for it.

Not with that attitude :joy: Just kidding. I agree that having multiple sets of world coordinates would be impossible, yes…but I can imagine a scheme where the server loads a union of all sublevels that the clients are in, and moves the origin of the coordinate system to be centered between all of them…or something. Anyway, I’m not advocating for that - our project uses client-authoritative positions, so having each client load whatever cesium sublevel they want and just converting all positions to some absolute coordinate system before transmission is fine.

Follow-up question, though - If I’m thinking about this correctly, we also need to transform any direction vectors as well, don’t we (velocity, acceleration, etc.)?

It should literally be like a 2 line change. Add the parameter to the delegate declaration, and change the line that calls Broadcast() on the delegate to include “this”.

OK, final question: is there any way for us to tell when spawning an aircraft at a recently loaded sublevel, that the tiles that are in view of the spawn location are fully streamed in, fully settled, whatever LOD they’re going to show at that location, etc?

but I can imagine a scheme where the server loads a union of all sublevels that the clients are in, and moves the origin of the coordinate system to be centered between all of them…or something.

Yep, but that would require the objects in the sub-level to be compatible with origin rebasing. A lot of the point of Cesium sub-levels is that they can hold objects that can’t handle origin rebasing.

If your objects can deal with the world being rebased, you could simply use the CesiumOriginShift component to keep the origin near the viewer at all times. No need for sublevels at all.

If I’m thinking about this correctly, we also need to transform any direction vectors as well, don’t we (velocity, acceleration, etc.)?

Yeah, I would think so.

is there any way for us to tell when spawning an aircraft at a recently loaded sublevel, that the tiles that are in view of the spawn location are fully streamed in, fully settled, whatever LOD they’re going to show at that location, etc?

You can determine if tiles are fully loaded for the current view with that OnTilesetLoaded event, or by checking the LoadProgress property.

But I think maybe you’re looking for a way to check if some other view is completely loaded? Well, only tiles that are in view will ever be loaded (with a few exceptions that aren’t really relevant here), so the first step is to put a camera at the new location to trigger it to load. You can use the Blueprint API on CesiumCameraManager to create virtual cameras for this purpose. Once that’s in place, and the LoadProgress goes to 100, you can be sure all of the tiles necessary to render that other view have been loaded.

What we are doing is spawning an aircraft at one of 15 airports around the globe. Each airport is set up as a separate sublevel. The steps we’re following to spawn a new aircraft are:

  • if our playercontroller currently has a pawn, disable the pawn’s origin shifter component by calling SetMode(ECesiumOriginShiftMode::Disabled). This is to prevent cesium from reactivating the sublevel we’re currently in.
  • if cesium sublevel associated with the desired airport is not the active cesium sublevel, activate it by calling SetTargetSubLevel(). (otherwise, skip the next step)
  • wait for the FWorldDelegates::LevelAddedToWorld delegate to be triggered for the cesium sublevel (this appears to be the best way to know when the sublevel has finished loading?)
  • if any tilesets have a load progress less than 100, subscribe to their OnTilesetLoaded delegate. (otherwise, skip the next step)
  • wait for OnTilesetLoaded to be triggered. check all tilesets again, to see if any are still loading.
  • once we find that no tilesets are still loading, drop the menu and spawn the plane on the runway

When we do this, it looks like the tiles are still streaming in. Everything looks very blurry at first, or not visible at all. Then we often (but not always) fall through the ground even if we put a static mesh under the runway, parented to the cesium sublevel. The only thing way we’ve been able to reliably spawn is to put static meshes under the runways, but NOT parented to the cesium sublevel. Instead we give them globe anchors and manually enter coordinates to make them overlap with the relevant runway. But this is a tedious, error prone process, not to mention still confused about why the process we followed didn’t work as expected, so I’d love to understand what we’re doing wrong.

In my last post, I said:

Well, only tiles that are in view will ever be loaded (with a few exceptions that aren’t really relevant here), so the first step is to put a camera at the new location to trigger it to load.

You haven’t done that. You’re spawning the plane only as the last step, so that’s the step where tiles will start streaming in. The earlier check of OnTilesetLoaded doesn’t help because that’s reporting the load status for some other view (or perhaps no view at all, if there are no active cameras), not the one centered on the plane at the new airport.

1 Like

I guess I’m a little confused. Are you suggesting we spawn a new ACameraActor at that location? Judging from ACesium3DTileset::GetCameras(), it seems like only player cameras and scene captures would be checked for loading the tileset (outside of editor).

You can use the API on the CesiumCameraManager Actor to create a virtual camera at a new location.