Skip to content

Commit 84dc1dd

Browse files
Add Babylon.js tile fade plugin and example (#1745)
* Add Babylon.js tile fade plugin and example Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Refactor tiles fade plugins to a shared base Share tile fade lifecycle between Three.js and Babylon.js and remove unrelated direct dependencies. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix Babylon tile fade material selection Skip geometryless meshes before resolving materials and select MultiMaterial leaves from the mesh's submeshes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Fix Babylon fade compatibility Keep Babylon 8 peer compatibility by disabling tile fading when the required material plugin API is unavailable. Add lifecycle coverage, document the fallback, and remove the obsolete Three.js fade manager shim. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Simplify Babylon fade ownership and skip unsupported tiles Remove mesh ownership tracking and warn instead of throwing for unsupported materials and instances. Bypass fading for the whole affected tile while preserving normal visibility timing, with documentation and regression coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent eb6ea84 commit 84dc1dd

20 files changed

Lines changed: 1308 additions & 363 deletions

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ npm install 3d-tiles-renderer --save
3131
| Package | Reference |
3232
| --- | --- |
3333
| `3d-tiles-renderer/three/plugins` | [Plugin Guide](./src/three/plugins/README.md) · [API Reference](./src/three/plugins/API.md) |
34+
| `3d-tiles-renderer/babylonjs/plugins` | [Plugin Guide](./src/babylonjs/plugins/README.md) |
3435
| `3d-tiles-renderer/core/plugins` | [API Reference](./src/core/plugins/API.md) |
3536

3637
# Examples

‎TESTCASES.md‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,47 @@ Verify that the total geometries in the render stats display is still "220".
339339
#### expected
340340

341341
Verify correct depth is returned when clicking the tiles.
342+
343+
## Verify tile fade transitions
344+
345+
Run the shared steps below separately for Three.js and Babylon.js. The Babylon.js
346+
example requires Babylon.js 9.26.1 or later, which includes
347+
`DitheredTileFadeMaterialPlugin`.
348+
349+
#### steps
350+
351+
1. Run `npm start`.
352+
1. Open `three/fadingTiles.html` or `babylonjs/fadingTiles.html` and wait for
353+
the terrain to load.
354+
1. Disable `useFade` and zoom in and out to observe immediate LOD changes.
355+
1. Enable `useFade`, set `fadeDuration` to 2 seconds, and zoom in to trigger
356+
a slow dither transition.
357+
1. Reverse the zoom direction before the transition completes.
358+
1. Stop moving and confirm the fading counter returns to zero
359+
(`fadingGroundTiles` in Three.js; `fadingTiles` in Babylon.js).
360+
1. Trigger another transition and disable `useFade` while it is in progress.
361+
1. Repeat with the other renderer.
362+
363+
#### expected
364+
365+
With fading enabled, both renderers retain the outgoing LOD during the opaque
366+
dither transition, handle reversal without disappearing tiles, and settle with
367+
only the selected LOD visible. Disabling fading completes any pending transition
368+
on the next update, and the fading counter returns to zero.
369+
370+
Three.js uses a 4x4 pattern and Babylon.js uses an 8x8 pattern, so their lifecycle
371+
should match without requiring identical stipple pixels.
372+
373+
#### Babylon.js variants and scope
374+
375+
Repeat the shared steps with `babylonjs/fadingTiles.html?url=/data/tileset.json`
376+
for bundled B3DM content, and with
377+
`babylonjs/fadingTiles.html?webgpu&url=/data/tileset.json` to require WebGPU.
378+
WebGPU must identify itself as the active backend or report an explicit block;
379+
it must never silently fall back to WebGL.
380+
381+
The Babylon.js scope is one active camera and ordinary non-instanced
382+
B3DM/glTF/GLB meshes using opaque Standard/PBR materials, including
383+
MultiMaterial leaves and shared/frozen materials. Babylon batching,
384+
hardware/thin instances, multi-camera traversal, and standalone
385+
shadow/depth/picking/outline/custom passes are deferred.

‎example/babylonjs/fadingTiles.html‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html>
3+
4+
<head>
5+
<title>Babylon.js Dither Fade Tiles</title>
6+
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
7+
<link rel="stylesheet" href="../styles.css">
8+
</head>
9+
10+
<body>
11+
<div id="info">
12+
Babylon.js tile LOD transitions using opaque ordered dithering.
13+
<span id="backend" data-testid="backend">initializing</span>
14+
</div>
15+
<canvas id="renderCanvas"></canvas>
16+
<script type="module" src="./fadingTiles.js"></script>
17+
</body>
18+
19+
</html>

‎example/babylonjs/fadingTiles.js‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { Engine } from '@babylonjs/core/Engines/engine';
2+
import { WebGPUEngine } from '@babylonjs/core/Engines/webgpuEngine';
3+
import { Scene } from '@babylonjs/core/scene';
4+
import { ArcRotateCamera } from '@babylonjs/core/Cameras/arcRotateCamera';
5+
import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight';
6+
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
7+
import { Color4 } from '@babylonjs/core/Maths/math.color';
8+
import { TilesRenderer } from '3d-tiles-renderer/babylonjs';
9+
import { TilesFadePlugin } from '3d-tiles-renderer/babylonjs/plugins';
10+
import GUI from 'lil-gui';
11+
12+
const DEFAULT_TILESET_URL = 'https://raw.githubusercontent.com/NASA-AMMOS/3DTilesSampleData/master/msl-dingo-gap/0528_0260184_to_s64o256_colorize/0528_0260184_to_s64o256_colorize/0528_0260184_to_s64o256_colorize_tileset.json';
13+
const searchParams = new URLSearchParams( location.search );
14+
const TILESET_URL = searchParams.get( 'url' ) || DEFAULT_TILESET_URL;
15+
const usesBundledTiles = TILESET_URL.startsWith( '/data/' );
16+
const canvas = document.getElementById( 'renderCanvas' );
17+
const requestedWebGPU = searchParams.has( 'webgpu' );
18+
const backendElement = document.getElementById( 'backend' );
19+
20+
async function createEngine() {
21+
22+
if ( requestedWebGPU ) {
23+
24+
if ( ! navigator.gpu ) {
25+
26+
throw new Error( 'WebGPU was requested but navigator.gpu is unavailable.' );
27+
28+
}
29+
30+
const engine = new WebGPUEngine( canvas, {
31+
antialias: false,
32+
enableAllFeatures: true,
33+
setMaximumLimits: true,
34+
enableGPUDebugMarkers: false,
35+
useLargeWorldRendering: false,
36+
} );
37+
engine.enableOfflineSupport = false;
38+
engine.useReverseDepthBuffer = false;
39+
engine.compatibilityMode = true;
40+
await engine.initAsync();
41+
engine.renderEvenInBackground = true;
42+
engine.getCaps().parallelShaderCompile = undefined;
43+
return engine;
44+
45+
}
46+
47+
return new Engine( canvas, false, {
48+
useLargeWorldRendering: true,
49+
preserveDrawingBuffer: true,
50+
} );
51+
52+
}
53+
54+
async function init() {
55+
56+
const engine = await createEngine();
57+
engine.setHardwareScalingLevel( 1 / window.devicePixelRatio );
58+
backendElement.textContent = requestedWebGPU ? 'WebGPU' : 'WebGL2';
59+
60+
const scene = new Scene( engine );
61+
scene.clearColor = new Color4( 0.05, 0.05, 0.05, 1 );
62+
scene.useRightHandedSystem = true;
63+
64+
const camera = new ArcRotateCamera(
65+
'camera',
66+
- Math.PI / 2,
67+
Math.PI / 2.5,
68+
50,
69+
Vector3.Zero(),
70+
scene,
71+
);
72+
camera.attachControl( canvas, true );
73+
camera.minZ = 0.1;
74+
camera.maxZ = 1000;
75+
if ( usesBundledTiles ) {
76+
77+
camera.setTarget( new Vector3( 216, - 77, 32 ) );
78+
camera.radius = 400;
79+
80+
}
81+
82+
const light = new HemisphericLight( 'light', new Vector3( 0, 1, 0 ), scene );
83+
light.intensity = 1.5;
84+
85+
const tiles = new TilesRenderer( TILESET_URL, scene );
86+
tiles.group.rotation.x = Math.PI / 2;
87+
const fadePlugin = new TilesFadePlugin();
88+
tiles.registerPlugin( fadePlugin );
89+
90+
const params = {
91+
useFade: true,
92+
fadeRootTiles: false,
93+
fadeDuration: Number( searchParams.get( 'fadeDuration' ) ?? 0.5 ),
94+
errorTarget: Number( searchParams.get( 'errorTarget' ) ?? 6 ),
95+
renderScale: 1,
96+
fadingTiles: '0 tiles',
97+
visibleTiles: '0 tiles',
98+
};
99+
100+
const gui = new GUI();
101+
const fadeFolder = gui.addFolder( 'fade' );
102+
fadeFolder.add( params, 'useFade' );
103+
fadeFolder.add( params, 'fadeRootTiles' );
104+
fadeFolder.add( params, 'fadeDuration', 0, 5 ).listen();
105+
fadeFolder.add( params, 'errorTarget', 0, 1000 );
106+
fadeFolder.add( params, 'renderScale', 0.1, 1, 0.05 ).onChange( value => {
107+
108+
engine.setHardwareScalingLevel( 1 / ( value * window.devicePixelRatio ) );
109+
110+
} );
111+
fadeFolder.add( params, 'fadingTiles' ).listen().disable();
112+
fadeFolder.add( params, 'visibleTiles' ).listen().disable();
113+
gui.open();
114+
115+
scene.onBeforeRenderObservable.add( () => {
116+
117+
fadePlugin.fadeRootTiles = params.fadeRootTiles;
118+
fadePlugin.fadeDuration = params.useFade ? params.fadeDuration * 1000 : 0;
119+
tiles.errorTarget = params.errorTarget;
120+
tiles.update();
121+
122+
params.fadingTiles = fadePlugin.fadingTiles + ' tiles';
123+
params.visibleTiles = tiles.visibleTiles.size + ' tiles';
124+
125+
} );
126+
127+
engine.runRenderLoop( () => scene.render() );
128+
window.addEventListener( 'resize', () => engine.resize() );
129+
130+
}
131+
132+
init().catch( error => {
133+
134+
backendElement.textContent = `blocked: ${ error.message }`;
135+
backendElement.dataset.error = error.message;
136+
console.error( error );
137+
138+
} );

‎package-lock.json‎

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
"types": "./src/babylonjs/renderer/index.d.ts",
5050
"import": "./build/index.babylonjs.js"
5151
},
52+
"./babylonjs/plugins": {
53+
"types": "./src/babylonjs/plugins/index.d.ts",
54+
"import": "./build/index.babylonjs-plugins.js"
55+
},
5256
"./core/plugins": {
5357
"types": "./src/core/plugins/index.d.ts",
5458
"import": "./build/index.core-plugins.js"
@@ -89,8 +93,8 @@
8993
"@babel/preset-modules": "^0.1.6",
9094
"@babel/preset-react": "^7.26.3",
9195
"@babel/preset-typescript": "^7.26.0",
92-
"@babylonjs/core": "^8.47.2",
93-
"@babylonjs/loaders": "^8.47.2",
96+
"@babylonjs/core": "^9.26.1",
97+
"@babylonjs/loaders": "^9.26.1",
9498
"@eslint/js": "^9.0.0",
9599
"@react-three/drei": "^10.0.0",
96100
"@react-three/fiber": "^9.0.0",

‎src/babylonjs/plugins/README.md‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Babylon.js plugins
2+
3+
`TilesFadePlugin` adds opaque ordered-dither transitions to ordinary B3DM,
4+
glTF, and GLB tiles:
5+
6+
Fading requires the `DitheredTileFadeMaterialPlugin` API included in
7+
`@babylonjs/core` 9.26.1 or later. With an older supported Babylon.js version,
8+
the plugin warns once when registered and leaves normal tile rendering
9+
unchanged.
10+
11+
```js
12+
import { TilesRenderer } from '3d-tiles-renderer/babylonjs';
13+
import { TilesFadePlugin } from '3d-tiles-renderer/babylonjs/plugins';
14+
15+
const tiles = new TilesRenderer( url, scene );
16+
tiles.registerPlugin( new TilesFadePlugin( { fadeDuration: 250 } ) );
17+
```
18+
19+
The initial Babylon.js integration supports one active camera and non-instanced
20+
meshes using opaque `StandardMaterial` or PBR materials, including
21+
`MultiMaterial` leaves and shared or frozen materials. Hardware and thin
22+
instances, batching, and standalone shadow, depth, picking, outline, or custom
23+
passes are not included.
24+
25+
If a tile contains transparent or unsupported materials, or unsupported instances,
26+
the plugin logs a warning and renders the entire tile without fading. Its normal
27+
visibility timing is preserved, and supported tiles continue to fade.
28+
29+
The default `maximumFadeOutTiles` value is 50. As in the Three.js
30+
implementation, exceeding it completes fades early only while the camera moves
31+
more than 0.1 world units or rotates more than 0.25 radians in one frame; it is
32+
not an unconditional cap.
33+
34+
Babylon.js uses the 8x8 pattern provided by
35+
`DitheredTileFadeMaterialPlugin`; the Three.js plugin uses a 4x4 pattern, so
36+
their transition timing and lifecycle match without requiring identical
37+
dither pixels. Both engine plugins share the same tile fade lifecycle and
38+
options; their camera and material integration remains engine-specific.

0 commit comments

Comments
 (0)