Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- Fixed the "Data attribution" credit link and the credit lightbox not being usable with a keyboard. Both the link and the lightbox close button are now focusable and can be activated with `Enter` or `Space`, the lightbox is exposed as a modal dialog and can be dismissed with `Escape`, and focus is moved into the lightbox when it opens and restored when it closes. [#13670](https://github.com/CesiumGS/cesium/issues/13670)
- Fixed feature ID textures ignoring the wrap mode declared by the glTF sampler. Forcing nearest filtering no longer replaces `wrapS` and `wrapT` with `CLAMP_TO_EDGE`. [#11574](https://github.com/CesiumGS/cesium/issues/11574)
- Fixed vertical exaggeration for models and tilesets with existing scale factors, so they now exaggerate proportionally to the rest of the scene. [#13518](https://github.com/CesiumGS/cesium/pull/13518)
- Changed 3D tileset traversal to have more robust replacement refinement behavior for vector data tilesets. [#13686](https://github.com/CesiumGS/cesium/issues/13686)

## 1.144 - 2026-08-01

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"asset": {
"version": "1.1"
},
"geometricError": 10,
"root": {
"boundingVolume": {
"region": [
-1.3197209591796106,
0.6988424218,
-1.3196390408203893,
0.6989055782,
0,
88
]
},
"geometricError": 10,
"refine": "REPLACE",
"children": [
{
"boundingVolume": {
"region": [
-1.3197209591796106,
0.6988424218,
-1.3196390408203893,
0.6989055782,
0,
88
]
},
"geometricError": 0,
"refine": "REPLACE",
"content": {
"uri": "ur.b3dm"
}
}
]
}
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"asset": {
"version": "1.1"
},
"extensionsUsed": ["3DTILES_content_gltf_vector"],
"geometricError": 200000,
"root": {
"boundingVolume": {
"region": [
-1.3197209591796106,
0.6988424218,
-1.3196390408203893,
0.6989055782,
0,
88
]
},
"geometricError": 100000,
"refine": "REPLACE",
"content": {
"uri": "parent.b3dm"
},
"children": [
{
"boundingVolume": {
"region": [
-1.3197209591796106,
0.6988424218,
-1.3196390408203893,
0.6989055782,
0,
88
]
},
"geometricError": 0,
"refine": "REPLACE",
"content": {
"uri": "ll.b3dm"
}
},
{
"boundingVolume": {
"region": [
-1.3197209591796106,
0.6988424218,
-1.3196390408203893,
0.6989055782,
0,
88
]
},
"geometricError": 10,
"refine": "REPLACE",
"content": {
"uri": "external.json"
}
}
]
}
}
Binary file not shown.
18 changes: 15 additions & 3 deletions packages/engine/Source/Scene/Cesium3DTilesetBaseTraversal.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,13 @@ function executeTraversal(root, frameState) {
function executeEmptyTraversal(root, frameState) {
const { canTraverse, updateTile, loadTile, touchTile } =
Cesium3DTilesetTraversal;
// Vector tilesets opt into relaxed empty-tile refinement so empty regions reached
// through implicit or external placeholders do not block their content siblings.
// Note: it's likely we don't need to limit this behavior to vector tilesets. However,
// we are being cautious of breaking changes to existing tilesets.
const isVectorTileset = root.tileset.hasExtension(
"3DTILES_content_gltf_vector",
);
Comment on lines +250 to +256

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll want to confirm and likely extend this behavior to all 3D Tiles 2.0 tilesets, when we have them. Could you open a dedicated issue?

let allDescendantsLoaded = true;
const stack = emptyTraversal.stack;
stack.push(root);
Expand All @@ -264,9 +271,14 @@ function executeEmptyTraversal(root, frameState) {
// Only traverse if the tile is empty - traversal stops at descendants with content
const traverse = !tile.hasRenderableContent && canTraverse(tile);

// Traversal stops but the tile does not have content yet
// There will be holes if the parent tries to refine to its children, so don't refine
if (!traverse && !tile.contentAvailable) {
// For vector tilesets only unloaded renderable content blocks refinement, so an empty
// tile at its resolved level of detail does not hold back its content siblings,
// including across implicit or external placeholders. All other tilesets keep the
// original behavior of blocking whenever traversal stops without content available.
Comment on lines +274 to +277

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC, this is "unconditional refinement" as described in https://github.com/CesiumGS/glTF/tree/3d-tiles-2.0/extensions/2.1/Vendor/3DTILES_tileset#unconditional-refinement. Maybe we should link to this directly for context.

const blocksRefinement = isVectorTileset
? tile.hasRenderableContent
: !traverse;
if (blocksRefinement && !tile.contentAvailable) {
allDescendantsLoaded = false;
}

Expand Down
54 changes: 54 additions & 0 deletions packages/engine/Specs/Scene/Cesium3DTilesetSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ describe(
const tilesetReplacement3Url =
"Data/Cesium3DTiles/Tilesets/TilesetReplacement3/tileset.json";

// Content root with one content child and one child pointing to an external
// tileset whose root is empty (an empty region reached through a placeholder).
const tilesetEmptyExternalUrl =
"Data/Cesium3DTiles/Tilesets/TilesetEmptyExternal/tileset.json";

// 3 level tree with mix of additive and replacement refinement
const tilesetRefinementMix =
"Data/Cesium3DTiles/Tilesets/TilesetRefinementMix/tileset.json";
Expand Down Expand Up @@ -1447,6 +1452,55 @@ describe(
expect(statistics.numberOfCommands).toEqual(3);
});

it("replacement refinement - refines past an empty tile reached through a placeholder", async function () {
// A vector tileset (opts in via 3DTILES_content_gltf_vector) with a content parent,
// one content child, and one child pointing to an external tileset whose root is
// empty (content only deeper). The empty region is reached through a placeholder
// tile (hasRenderableContent === false, hasEmptyContent === false), which the
// empty-content short-circuit does not cover. The empty branch must not block the
// content sibling from refining.
//
// C (parent.b3dm)
// C T (external tileset ref)
// (ll.b3dm) E (empty external root)
// C (ur.b3dm, deeper, not needed at this view)
//
// Viewed from far enough that the empty external root is at its resolved LOD (its
// deeper content is not needed), while the parent still needs to refine.
viewRootOnly();
const tileset = await Cesium3DTilesTester.loadTileset(
scene,
tilesetEmptyExternalUrl,
);
tileset.skipLevelOfDetail = false;
const root = tileset.root;

// Wait for the external tileset json to load, so its child becomes a placeholder
// (hasRenderableContent === false), then for the content sibling to finish loading
await pollToPromise(() => {
scene.renderForSpecs();
return root.children.some((child) => !child.hasRenderableContent);
});
await Cesium3DTilesTester.waitForTilesLoaded(scene, tileset);
scene.renderForSpecs();

const statistics = tileset._statistics;
const contentChild = root.children.find(
(child) => child.hasRenderableContent,
);
const externalChild = root.children.find(
(child) => !child.hasRenderableContent,
);

// The parent refined instead of being held back by the empty external branch
expect(isSelected(tileset, root)).toBe(false);
// The content sibling renders
expect(isSelected(tileset, contentChild)).toBe(true);
// The empty branch stays empty - its deeper content is not over-refined
expect(isSelected(tileset, externalChild)).toBe(false);
expect(statistics.numberOfCommands).toEqual(1);
});

it("replacement and additive refinement", async function () {
// A
// A R (not rendered)
Expand Down