Proposal: Add an easier way to refresh loaded 3D Tiles content

I am building a web client that uses CesiumJS, where I load multiple Cesium3DTileset instances into the viewer. I have a need to reload tile content without reloading the entire tileset.

Today, this can be done by listening to tileLoad / tileUnload, keeping an external set of loaded tiles, and then manually setting expireDate on those tiles.

It would be cleaner if Cesium3DTileset exposed a supported API for expiring currently loaded tile content. Therefore, I am considering creating a PR that adds a Cesium3DTileset.expireLoadedTiles() function, but I wanted to get some feedback before starting the implementation.

The basic API could expire all currently loaded tile content:

tileset.expireLoadedTiles();

A more advanced version could also support expiring only loaded tiles that intersect a bounding sphere:

tileset.expireLoadedTiles({
  boundingSphere: removedTileset.boundingSphere,
});

This could be useful when one tileset is removed and nearby or overlapping tiles in another tileset need to be refreshed.

The intention would be that Cesium only marks already-loaded tile content as expired, causing it to be re-requested through the normal tile expiration path. It would not reload the root tileset.json or rebuild the tileset hierarchy.

Does this seem like an API that would be useful for CesiumJS? Would this fit with how Cesium wants to expose tile cache/expiration behavior, or would there be a better approach?

It could be helpful to have some higher-level, abstract information about the exact use case or intended usage pattern. There already is the concept of “tile expiration” in CesiumJS, but I think that it’s not very versatile and widely used or supported. The main reason for asking: A more generic concept is currently in the process of being developed in the context of Add `3DTILES_content_conditional` by javagl · Pull Request #834 · CesiumGS/3d-tiles · GitHub . The original goal there was to have time-dynamic tiles - i.e. the case where the actual tile that is displayed depends on the current system time. This has then be generalized to “conditional content”, where the condition for a certain tile to be displayed can depend on ~“an arbitrary condition” (where this ‘condition’ could be one that depends on time as well…)

Thanks, that makes sense. At a higher level, my use case is that the tile content returned by the server depends on application/viewer state outside the tileset itself. The tileset JSON and hierarchy stay the same, but already-loaded tile content may need to be refreshed when that external state changes.

I have two concrete cases:

  1. I switch between client-side clipping and server-side clipping depending on camera distance. When switching to server-side clipping, I want to refresh the currently loaded tile content without recreating the whole Cesium3DTileset.

  2. I have base-layer tiles and more detailed 3D tiles datasets. When a more detailed dataset is added or removed, only loaded base-layer tiles overlapping that area need to be refreshed, because the server output for those tiles depends on whether the foundation dataset is present.

So the more general need is:

Refresh already-loaded tile content when external application state changes, optionally limited to loaded tiles intersecting a spatial region, without recreating the whole Cesium3DTileset.

This could apply to server-side clipping/cropping, user-selected filters, dataset version changes, quality modes, permissions, or other request/server-side parameters.

The 3DTILES_content_conditional work sounds related, but my current need is not necessarily time-dynamic content encoded in the tileset. It is more about letting the application tell CesiumJS that the content for some already-loaded tiles is stale and should be requested again.

It does sound related, and like the 3DTILES_content_conditional support could help. It’s not clear yet whether it’s a “good fit”, or whether it would be an attempt to shoehorn something into a certain pattern for lack of better options (or just lack of better ideas - maybe someone else will also chime in here…)

The core of the description is

Refresh already-loaded tile content when external application state changes, optionally limited to loaded tiles intersecting a spatial region, without recreating the whole Cesium3DTileset.

And the linked extension draft does support some of that. The goal there is indeed to “switch” between different contents, based on “keys” (which essentially are the application state here). There is no specific mechanism for limiting that to a certain area, and the point of server/client side clipping sounds very specific and I’m not sure how that plays into that. But trying to draft what could be done there:

The different contents must be part of the tileset (i.e. the options in terms of the content must be known beforehand). They could be put into some content.json that looks like this:

"dynamicContents": [
  {
    "uri": "content-example-A.glb",
    "keys": {
      "exampleKey": "keyA"
    }
  },
  {
    "uri": "content-example-B.glb",
    "keys": {
      "exampleKey": "keyB"
    }
  }
]

The “condition” for displaying either content can then be controlled by the application like this:

tileset.conditionalContentUriCondition = (keys) => {​
  return keys.exampleKey === "keyA";
}

In this case (a dummy example), it would always display the first content - changing the keyA to keyB (i.e. in the application) would switch to the second content.

I’m not 100% sure if this helps. But in general, the process of loading and unloading tiles and their content is pretty deeply hidden in the implementation, and there might not be a trivial way of just “replacing” loaded content based on an application state.

The linked 3DTILES_content_conditionalproposal does sound very interesting. But when I read through everything there, it sounds like it is more of a way for a 3d tiles dataset to support different contents based on some condition, or rather key (the condition can be setup on the consumer side). Also, when you say the options in terms of the content must be known beforehand, it feels like this does not match what I would like to do.

My current use case will use the same content of the tile. When an application state changes, the server will then use the existing tile content to perform some actions on the content before it is being delivered to a client. For my original example, the server will perform some clipping of the actual tile, but theoretically, it could do other things as well. This is something that happens in runtime. And this is why I was looking for a way to make CesiumJS re-request certain tiles.

We could potentially still use 3DTILES_content_conditional to achieve this, for example by specifying the same uri for two different keys, and then just switch between the keys when we want to trigger a refresh. But right now, that feels like we would use this for something else than its intended idea.

So just to iterate back to what I currently look for: A way to refresh tiles, so the client re-fetches them so the server have a chance to perform its actions based on some new application state. My initial idea was to create a helper function that would set the expireDateon a tile, thus triggering a re-fetch of it. However, maybe my initial idea was very specific to solve my use case, and there might be better solutions.

No, that sounds like a valid point. (The fact that I brought up the ‘conditional content’ may be a form of the saying “When the only tool that you have is a hammer, then every problem looks like a nail”). With the clarification - namely, that the same data (i.e. ~“data from the same URL”) is supposed to be modified on the server side, then the ‘conditional content’ is indeed not the best fit.

You mentioned the “application state”, but it was (and admittedly, still is) not entirely clear for me where this state resides. An attempt to confirm my current understanding:

  • The user is clicking some button on the client
  • The server is informed that the button was clicked
  • This will (roughly speaking) modify the content data of one specific tile
  • The client has to reload that exact tile, in order to receive the modified content

If that it correct, then it sounds like it involves a very specific interplay between the client and the server. Specifically, for the last point: How is the client supposed to know which tiles have to be reloaded? It sounds like it could be possible to model something like this with the expireDate functionality. Very roughly: Maybe the server can inform the client about the set of tiles which are “expired”. But some of the details may require more thought and further iterations…

Thank you for taking the time to discuss this. This is exactly what I was looking for by coming here.

I have purposefully been a little vague regarding my use case because I wanted to have a more general discussion of a way to re-request specific tiles. But just to help the discussion, I will try to explain it a little better. Your 4 points is pretty much what is happening, but my exact use case is a little more involved.

  1. A user have the ability to build up an area of the world using multiple 3d tiles datasets. These 3d tiles datasets have different sizes (area wise), and different locations in the world, but can also have the same locations in the world.
  2. Lets say a user first places a bigger area dataset in the world. By doing this, we feed the tileset.json url to CesiumJS which then fetches the tiles to our custom server. Since the scene only contain this dataset, all tiles are fetched normally.
  3. Then lets say the user places a second, smaller area dataset on top of the bigger dataset (they are covering the same location in the world, so the user only selects the dataset, the user don’t actually places them in a specific place in the world). Then we also pass the second dataset’s tileset.json to CesiumJS, which fetches the tiles from our server.
  4. The issue now is that since the two datasets have tiles in the same place, we get z-fighting. However, our server have the ability to solve this. But I need a way for the client to tell CesiumJS to re-request tiles for the bigger dataset (the one that we have already fetched tiles from). The tiles that should be re-requested are the ones that are also covered by the second smaller dataset.
  5. When CesiumJS re-requests the tiles for the bigger dataset, the server can now see that there is a smaller dataset on top of a bigger dataset, and when that specific tile of the bigger dataset is fetched, it clips a hole in it. This makes it so the smaller dataset can be fully visualized in CesiumJS without any z-fighting.

Note that the information the server needs to know that it should clip, and where, is available in the server before the re-requested tiles are happening. We save information about which datasets exists in the view, and the server uses this information when a tile is requested.

I have been able to make this work using the tileLoad / tileUnloadevent listeners, and then saving a reference to each tile that is loaded. And then using that to set the expireDate of the relevant tiles. But I would prefer an easier and maybe more natural way to invalidate/expire tiles than using this method.

With this said, it would be nice to come up with a way to expire tiles to support as many things as possible, not just my specific use case. My original proposal having a specific tileset.expireLoadedTiles method does solve my use case. But there might be even better things we can do that I haven’t though of yet

Having a “vague” (or rather: generic, high level) description first certainly makes sense. A common issue in (forum) support is the XY Problem, where someone describes how a solution does not work, without stating the actual problem that was supposed to be solved. The points that you now mentioned may paint a clearer picture, and while I cannot claim to know “THE” (“perfect”) solution from the tip of my head, maybe we can think through some options.

The points 4. and 5. contained some subtle details that may require clarification:

But I need a way for the client to tell CesiumJS to re-request tiles for the bigger dataset (the one that we have already fetched tiles from). The tiles that should be re-requested are the ones that are also covered by the second smaller dataset.

Why should CesiumJS re-request the tiles from the bigger data set?

The remaining description sounded like it should exactly not to that. It sounded like CesiumJS should replace this area with the tiles from the smaller data set. Specifically, that’s what you said in point 4 and 5.:

…tell CesiumJS to re-request tiles for the bigger dataset…
…the server can now see that there is a smaller dataset on top of a bigger dataset…

Is it necessary that this “clipping” happens on the server side?

The reason for asking: There is some existing functionality (and some ongoing development - I’d have to check the latest state) that could be helpful here. But the idea could be, very roughly:

  • CesiumJS requests the large tileset
  • The user adds the small tileset
  • CesiumJS could then request the “outline” of the small tileset (maybe as some GeoJSON that just contains the 2D shape of the small tileset), and convert this into a clipping region
  • Then, CesiumJS could request the small tileset as usual. It would be placed into the area that was “cut out” from the larger one

The advantage here would be that the server only has to provide the outline of the small tileset. On the CesiumJS side, the rendering would use all standard, built-in functionalities: It would just render two tilesets (with the larger one clipped with a clipping polygon). No expireDate, to complex client-server-communication or tileLoad/tileUnload-hooks…

Is there anything that would fundamentally not work with this approach?

A specific example that might be similar to what you are trying to achieve is in the “Clipping Regions” Sandcastle : The “City” is the Google 3D Tiles data set. That single building is another tileset. And the area of the building is “cut out” from the city, and replaced with the building.

Using the current functionality in CesiumJS to clip datasets is something we have investigated as well. We have tried to use Cesium.ClippingPolygon, and tileset.customShader to achieve this. But both these solutions have their own issues when it comes to corners, or more complex shapes. They also have an issue with “resolution” (cannot come up with the correct word here), where if a user zooms in close to the edges, the edges look pretty bad.

Both these problems are solved by our server clip/cut functionality.

One solution to this issue is to continue to work with the CesiumJS clipper functionality, and improve that. Because yes, this might actually be the most optimal solution. But in the current state, it is not good enough.

And since I could get my solution to work using the existing expireDate approach, it felt like it would be easier to come up with a solution for doing this in a better way, than to improve the CesiumJS clippers. Also, my example above is basically just the most simple example. In reality, we have much bigger set of datasets in the same view, and I know that pre version 1.140, there was performance issues using the ClippingPolygonCollection. The performance has been improved a lot in 1.140 and up, which is amazing. But I am unsure if it will still be enough. But with that said, the biggest issue is still that corners (90 degrees does not work) and edges when the user zooms in a lot look bad.

I guess that what you refer to with “resolution” might be that the shape that is cut out does not cleanly match the actual shape that the polygon has. This is tracked in “Clipping polygons precision on Terrain and 3D Tiles”

And more generally, the clipping functionality has recently gained some attention and there have been (and still are ongoing) efforts to improve it in various ways.

An “overarching” issue was raised at “Robust Clipping Polygon v2”. This links to several sub/follow-up issues, and some pull requests where some of these issues have been addressed. For example, some performance improvements have been done in

And the last one also introduces some trade-off between “performance” and “quality”.
(Other PRs are related to clipping as well, but probably not directly to your specific task).

One point is: You mentioned that you tried 1.140. But you could consider trying out more recent versions as well, to see whether your original approaches work better now.


the biggest issue is still that corners (90 degrees does not work)

That’s very specific, and I’m a bit curious: Was this really a glitch at 90 degrees (with 89° and 91° working as expected)? If so, I’d probably try to carve out a sandcastle to test this, and maybe track it in an issue as well.


All that said: I don’t want to convince you to use clipping polygons. They might also not be well-suited for your use case. But until now, it sounds like they could be a good fit (and maybe the only thing that preventing you from using them are the performance/precision issues). Eventually, and very generally speaking, what you are trying to do there is a form of “clipping”, and on that level, it’s all a matter of whether CesiumJS nicely supports this, or requires quirky workarounds.

So I cannot immediately describe such workarounds (like the one based on tileLoad or expireDate) that are guaranteed to be “better” (or “nicer”) than the one that you currently have. (And it fact, it would be undesirable to develop such workarounds, just because of some fundamental shortcomings of the existing clipping functionality…)

I’m wondering if the conversation here could be funneled into a new issue, iff this issue goes beyond the clipping improvements that are already ongoing or on the radar…?

I am currently using the latest version of CesiumJS (1.143 in time of writing this), and the precision regarding clipping polygons are seen there as well. When I said 90 degrees corners does not work, it is not just exactly 90 degrees. There is an issue regarding this (or at least mentions it) here Clipping polygons precision on Terrain and 3D Tiles. As you mention, there are several issues and potential improvements being tracked and probably worked on regarding these clipping polygons, which is great.

And like I said, if the performance and precision was perfect when using clipping polygons (or at least match or closely match what my current server clipping achieves), I would use that for my specific use case. However, as it is today, it is not.

So for my specific use case, the most practical path forward is either to wait for improvements to clipping polygons, or to contribute to them directly, but that was never really the focus of my original post.

My original intent was to explore whether there was interest in making it easier to expire individual tiles without needing to set up event listeners and maintain references to each loaded tile. I’m curious whether the current approach is considered the preferred solution, or if there’s room to simplify it.

I also want to clarify that this idea didn’t come from trying to work around a limitation. It was a genuine suggestion for improving the tile expiration API for clients. I understand that the why behind expiring tiles matters when making the case for an improvement, and I appreciate that context is needed to evaluate whether a change is worthwhile.

I’m curious whether the current approach is considered the preferred solution, or if there’s room to simplify it.

I also want to clarify that this idea didn’t come from trying to work around a limitation. […]

On the one hand, it does sound like a workaround, but on the other hand(s), 1. it’s hard to say whether there is a “better” solution without diving deeply into the code, and 2. (more importantly) it might very well be that using the concept of tile expiration is suitable here or that there are similar use-cases (that could not be solved with clipping) that could benefit from improvements and simplifications in this area.

But as mentioned above: The expireDate is a bit of an unknown area for me. It does not seem to be widely used. In fact, the only case where I’ve seen it “in practice” was in the TilesetWithExipration sample in the 3d-tiles-samples repo, and even this sample was removed at some point. Maybe, if you are willing to share some code snippets, I (or someone more familiar with this part) could provide further suggestions.