NASA-AMMOS / NASA-AMMOS/3DTilesRendererJS
ImageOverlayPlugin: tile splitting stalls over flat quantized-mesh terrain because split children inherit the terrain tile's geometricError
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 2.5k
- Forks
- 416
- Avg merge
- 17h 52m
- Merged PRs (30d)
- 37
Description
Description
With enableTileSplitting (default on), ImageOverlayPlugin extends overlay refinement past the geometry leaves by creating virtual split children. Those children are created with:
i.e. geometricError: tile.geometricError * 0.5. This couples overlay refinement to the terrain mesh error, which has no relationship to the overlay's remaining image resolution.
Over flat ground, quantized-mesh terrain (Cesium World Terrain) reports honest, tiny mesh errors — at the repro location below the deepest real tiles (depth 14) have geometricError ≈ 0.21–0.6 m. At ~100 m altitude that's at or below errorTarget = 2, so traversal never (or barely) elects to refine into the split children at all. The overlay's shouldSplit() still returns true (calculateLevel(range) < tiling.maxLevel, src) — the splits are created, but traversal's SSE test discards them because their inherited error is centimeter-scale.
Net effect: imagery is capped at the terrain leaf's level and stays visibly blurry at street-level altitudes, even though the overlay has many more native levels available. In my measurements (details below): visible imagery capped at level 14 of maxLevel 20, with 0 of 80 created split tiles ever loaded.
To confirm the mechanism I registered a small plugin that only floors each terrain tile's geometricError to the standard level ladder (~77067 / 2^level m) in processTileModel, changing nothing else. With that single change the splits recurse correctly (nested splits, 6 levels deep) and visible imagery reaches levels 19–20 — so the split machinery itself works; it's only the inherited geometricError that prevents it from ever being selected.
| stock v0.4.28 | with terrain geometricError floored to level ladder |
|
|---|---|---|
| split tiles loaded | 0 (80 created, all at chain depth 1) | recursion to chain depth 6, virtualChildCount 0→4 as expected |
| imagery level on visible tiles | ≤ 14 | up to 20 (= overlay tiling.maxLevel) |
Driving errorTarget down instead doesn't realistically help: reaching imagery level 19 here would need errorTarget ≈ 0.1 px, which explodes geometry refinement globally (related: the errorTarget = 0.25 workaround mentioned at the end of #1334).
This is adjacent to the level-mapping discussion in #1278 but narrower and concrete: the existing tile-splitting feature is effectively non-functional over flat quantized-mesh terrain because of the inherited error value. A possible direction, in line with the texel-size ideas in #1278: floor the split child's geometricError at the overlay's projected texel size for the child's image level (the same world-space-pixel notion EllipsoidProjectionTilesPlugin uses for image-tile geometric error), e.g. max(parentError * 0.5, texelError(level + 1)), so splits keep being selected exactly while the overlay still has native resolution to show.
(While debugging I initially suspected the alreadySplit gate at L613, since split children are created without internal.virtualChildCount — but preprocessNode pre-seeds the defaults before any content can load, so the gate behaves correctly and nested expansion works once traversal refines. Verified against v0.4.28 and current master (2d21dc5).)
Reproduction steps
- Load Cesium World Terrain (ion asset 1, quantized mesh) with a
CesiumIonOverlay(e.g. Bing Aerial, ion asset 2) viaImageOverlayPlugin; leaveenableTileSplittingon anderrorTargetat 2. - Place the camera ~100 m above flat ground — e.g. Dubai, lat 25.118 / lon 55.133, looking down at ~45°.
- Let streaming settle, then observe the ground imagery stays blurry (~level 14). Walking the tile tree shows the
image_overlay_tile_splitchildren exist but never load; the imagery level on visible tiles never approaches the overlay'smaxLevel. - (Mechanism check) Floor terrain tiles'
geometricErrorto the level ladder in aprocessTileModelhook → splits now recurse and imagery reaches level 19–20 in the same scene.
Code
import { TilesRenderer } from '3d-tiles-renderer';
import { CesiumIonAuthPlugin } from '3d-tiles-renderer/core/plugins';
import { ImageOverlayPlugin, CesiumIonOverlay, QuantizedMeshPlugin } from '3d-tiles-renderer/plugins';
const tiles = new TilesRenderer();
tiles.registerPlugin( new CesiumIonAuthPlugin( {
apiToken: ION_TOKEN,
assetId: '1', // Cesium World Terrain
assetTypeHandler: ( type, tilesBase ) => {
if ( type === 'TERRAIN' && tilesBase.getPluginByName( 'QUANTIZED_MESH_PLUGIN' ) === null ) {
tilesBase.registerPlugin( new QuantizedMeshPlugin( { useRecommendedSettings: true } ) );
}
},
} ) );
tiles.registerPlugin( new ImageOverlayPlugin( {
renderer,
overlays: [ new CesiumIonOverlay( { assetId: 2, apiToken: ION_TOKEN } ) ], // Bing Aerial
} ) );
tiles.errorTarget = 2;
// camera ~100 m above flat ground (Dubai): lat 25.118, lon 55.133, tilted ~45deg
// --- diagnostics used for the numbers above (run after streaming settles) ---
const splitRe = /image_overlay_tile_split/;
const chains = {};
( function walk( tile, chain ) {
const uri = tile.content?.uri ?? '';
const c = splitRe.test( uri ) ? chain + 1 : 0;
if ( c ) {
chains[ c ] = chains[ c ] || { n: 0, loaded: 0 };
chains[ c ].n ++;
if ( tile.internal?.loadingState === 4 /* LOADED */ ) chains[ c ].loaded ++;
}
tile.children.forEach( ch => walk( ch, c ) );
} )( tiles.root, 0 );
const plugin = tiles.getPluginByName( 'IMAGE_OVERLAY_PLUGIN' );
const overlay = plugin.overlays[ 0 ];
const info = plugin.overlayInfo.get( overlay );
const levels = {};
tiles.visibleTiles.forEach( tile => {
const ti = info.tileInfo.get( tile );
if ( ti?.range ) {
const lvl = overlay.calculateLevel( ti.range );
levels[ lvl ] = ( levels[ lvl ] || 0 ) + 1;
}
} );
console.log( { chains, levels, maxLevel: overlay.tiling.maxLevel } );
// --- the "mechanism check" plugin that makes splitting work ---
const LEVEL0_ERROR = ( 6378137 * 2 * Math.PI * 0.25 ) / ( 65 * 2 );
tiles.registerPlugin( {
name: 'TERRAIN_ERROR_FLOOR',
priority: - 500,
processTileModel( scene, tile ) {
const match = /(\d+)\/\d+\/\d+\.terrain/.exec( tile.content?.uri ?? '' );
if ( match ) tile.geometricError = Math.max( tile.geometricError, LEVEL0_ERROR / 2 ** Number( match[ 1 ] ) );
},
} );
Live example
Not possible to share a runnable fiddle since the repro needs a Cesium ion token — the snippet above is complete apart from ION_TOKEN and standard three.js scene boilerplate (the globe fiddle template from the issue form works as a base).
Screenshots & Tile Set
Measured numbers in the table above (Dubai 25.118 / 55.133, camera 100 m, 45° tilt, errorTarget 2, ~750 px viewport height):
- stock: terrain leaves at depth 14 with
geometricError0.21–0.6 m; imagery levels on visible tiles{ …, 13: 3, 14: 4 }, nothing deeper; 80image_overlay_tile_splittiles in the tree, all chain depth 1, 0 loaded. - with the geometricError floor only: split chains to depth 6; imagery levels on visible tiles include
{ 18: 3, 19: 18, 20: 8 }.
Tile sets: Cesium ion assets 1 (Cesium World Terrain) + 2 (Bing Maps Aerial).
Library Version
v0.4.28 (also checked the relevant source on master @ 2d21dc5 — unchanged)
Three.js Version
r184
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/three/plugins/images/ImageOverlayPlugin.js at the split-child creation around lines 761-769 and compare it with shouldSplit() around lines 1629-1633 and traversal's SSE decision. Reproduce with Cesium World Terrain and Bing Aerial at the Dubai coordinates using the provided snippet; done means split children are selected and imagery can reach the overlay's maxLevel without lowering errorTarget globally.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, three.js
- Domain
- computer-graphics
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100