diff --git a/example/webgpu/fadingTiles.html b/example/webgpu/fadingTiles.html
new file mode 100644
index 000000000..27174a453
--- /dev/null
+++ b/example/webgpu/fadingTiles.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Dither Fade Tiles
+
+
+
+
+ Demonstration of tiles using a dither fade to change, smoothing out the transition.
+
+
+
+
diff --git a/example/webgpu/fadingTiles.js b/example/webgpu/fadingTiles.js
new file mode 100644
index 000000000..af2ac9fc4
--- /dev/null
+++ b/example/webgpu/fadingTiles.js
@@ -0,0 +1,243 @@
+import {
+ Scene,
+ PerspectiveCamera,
+ OrthographicCamera,
+ Group,
+} from 'three';
+import { MeshBasicNodeMaterial, WebGPURenderer } from 'three/webgpu';
+import { TilesFadePlugin } from '3d-tiles-renderer/plugins';
+import { EnvironmentControls, TilesRenderer, CameraTransitionManager } from '3d-tiles-renderer';
+import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
+
+let controls, scene, renderer;
+let groundTiles, skyTiles, tilesParent, transition;
+
+const params = {
+
+ reinstantiateTiles,
+ fadeRootTiles: false,
+ useFade: true,
+ errorTarget: 6,
+ fadeDuration: 0.5,
+ renderScale: 1,
+ fadingGroundTiles: '0 tiles',
+
+ orthographic: false,
+ transitionDuration: 0.25,
+
+};
+
+init().then( () => {
+
+ render();
+
+} );
+
+async function init() {
+
+ // renderer
+ renderer = new WebGPURenderer( { antialias: true } );
+ await renderer.init();
+ renderer.setPixelRatio( window.devicePixelRatio );
+ renderer.setSize( window.innerWidth, window.innerHeight );
+ renderer.setClearColor( 0xd8cec0 );
+
+ document.body.appendChild( renderer.domElement );
+
+ // scene
+ scene = new Scene();
+
+ // set up cameras and ortho / perspective transition
+ transition = new CameraTransitionManager(
+ new PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.25, 4000 ),
+ new OrthographicCamera( - 1, 1, 1, - 1, 0, 4000 ),
+ );
+ transition.camera.position.set( 20, 10, 20 );
+ transition.camera.lookAt( 0, 0, 0 );
+ transition.autoSync = false;
+
+ transition.addEventListener( 'camera-change', ( { camera, prevCamera } ) => {
+
+ skyTiles.deleteCamera( prevCamera );
+ groundTiles.deleteCamera( prevCamera );
+
+ skyTiles.setCamera( camera );
+ groundTiles.setCamera( camera );
+
+ controls.setCamera( camera );
+
+ } );
+
+ // controls
+ controls = new EnvironmentControls( scene, transition.camera, renderer.domElement );
+ controls.minZoomDistance = 2;
+ controls.cameraRadius = 1;
+
+ // tiles parent group
+ tilesParent = new Group();
+ tilesParent.rotation.set( Math.PI / 2, 0, 0 );
+ scene.add( tilesParent );
+
+ // init tiles
+ reinstantiateTiles();
+
+ // events
+ onWindowResize();
+ window.addEventListener( 'resize', onWindowResize, false );
+
+ // gui initialization
+ const gui = new GUI();
+ const cameraFolder = gui.addFolder( 'camera' );
+ cameraFolder.add( params, 'orthographic' ).onChange( v => {
+
+ transition.fixedPoint.copy( controls.pivotPoint );
+
+ // adjust the camera before the transition begins
+ transition.syncCameras();
+ controls.adjustCamera( transition.perspectiveCamera );
+ controls.adjustCamera( transition.orthographicCamera );
+ transition.toggle();
+
+ } );
+ cameraFolder.add( params, 'transitionDuration', 0, 1.5 );
+
+ const fadeFolder = gui.addFolder( 'fade' );
+ fadeFolder.add( params, 'useFade' );
+ fadeFolder.add( params, 'fadeRootTiles' );
+ fadeFolder.add( params, 'errorTarget', 0, 1000 );
+ fadeFolder.add( params, 'fadeDuration', 0, 5 );
+ fadeFolder.add( params, 'renderScale', 0.1, 1.0, 0.05 ).onChange( v => renderer.setPixelRatio( v * window.devicePixelRatio ) );
+
+ const textController = fadeFolder.add( params, 'fadingGroundTiles' ).listen().disable();
+ textController.domElement.style.opacity = 1.0;
+
+ gui.add( params, 'reinstantiateTiles' );
+
+ gui.open();
+
+}
+
+function replaceMaterial( tiles ) {
+
+ tiles.addEventListener( 'load-model', ( { scene } ) => {
+
+ scene.traverse( c => {
+
+ if ( c.material ) {
+
+ const originalMaterial = c.material;
+ const material = new MeshBasicNodeMaterial();
+ if ( originalMaterial.map ) {
+
+ material.map = originalMaterial.map.clone();
+
+ }
+ c.originalMaterial = c.material;
+ c.material = material;
+
+ }
+
+ } );
+
+ } );
+
+ tiles.addEventListener( 'dispose-model', ( { scene } ) => {
+
+ scene.traverse( c => {
+
+ if ( c.material ) {
+
+ c.material.dispose();
+
+ }
+
+ } );
+
+ } );
+
+}
+
+function reinstantiateTiles() {
+
+ if ( groundTiles ) {
+
+ groundTiles.dispose();
+ groundTiles.group.removeFromParent();
+
+ skyTiles.dispose();
+ skyTiles.group.removeFromParent();
+
+ }
+
+ groundTiles = new TilesRenderer( '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' );
+ groundTiles.fetchOptions.mode = 'cors';
+ groundTiles.lruCache.minSize = 900;
+ groundTiles.lruCache.maxSize = 1300;
+ groundTiles.errorTarget = 12;
+ replaceMaterial( groundTiles );
+ groundTiles.registerPlugin( new TilesFadePlugin() );
+ groundTiles.setCamera( transition.camera );
+
+ skyTiles = new TilesRenderer( 'https://raw.githubusercontent.com/NASA-AMMOS/3DTilesSampleData/master/msl-dingo-gap/0528_0260184_to_s64o256_colorize/0528_0260184_to_s64o256_sky/0528_0260184_to_s64o256_sky_tileset.json' );
+ skyTiles.fetchOptions.mode = 'cors';
+ skyTiles.lruCache = groundTiles.lruCache;
+ replaceMaterial( skyTiles );
+ skyTiles.registerPlugin( new TilesFadePlugin() );
+ skyTiles.setCamera( transition.camera );
+
+
+ tilesParent.add( groundTiles.group, skyTiles.group );
+
+}
+
+function onWindowResize() {
+
+ const { perspectiveCamera, orthographicCamera } = transition;
+ const aspect = window.innerWidth / window.innerHeight;
+
+ orthographicCamera.bottom = - 40;
+ orthographicCamera.top = 40;
+ orthographicCamera.left = - 40 * aspect;
+ orthographicCamera.right = 40 * aspect;
+ orthographicCamera.updateProjectionMatrix();
+
+ perspectiveCamera.aspect = aspect;
+ perspectiveCamera.updateProjectionMatrix();
+
+ renderer.setSize( window.innerWidth, window.innerHeight );
+
+}
+
+function render() {
+
+ requestAnimationFrame( render );
+
+ controls.enabled = ! transition.animating;
+ controls.update();
+
+ transition.duration = 1000 * params.transitionDuration;
+ transition.update();
+
+ const camera = transition.camera;
+ camera.updateMatrixWorld();
+
+ const groundPlugin = groundTiles.getPluginByName( 'FADE_TILES_PLUGIN' );
+ groundPlugin.fadeRootTiles = params.fadeRootTiles;
+ groundPlugin.fadeDuration = params.useFade ? params.fadeDuration * 1000 : 0;
+ groundTiles.errorTarget = params.errorTarget;
+ groundTiles.setCamera( camera );
+ groundTiles.setResolutionFromRenderer( camera, renderer );
+ groundTiles.update();
+
+ const skyPlugin = skyTiles.getPluginByName( 'FADE_TILES_PLUGIN' );
+ skyPlugin.fadeRootTiles = params.fadeRootTiles;
+ skyPlugin.fadeDuration = params.useFade ? params.fadeDuration * 1000 : 0;
+ skyTiles.setCamera( camera );
+ skyTiles.setResolutionFromRenderer( camera, renderer );
+ skyTiles.update();
+
+ renderer.render( scene, camera );
+
+ params.fadingGroundTiles = groundPlugin.fadingTiles + ' tiles';
+
+}
diff --git a/src/three/plugins/fade/wrapFadeMaterial.js b/src/three/plugins/fade/wrapFadeMaterial.js
index 60cc8001e..732b5855f 100644
--- a/src/three/plugins/fade/wrapFadeMaterial.js
+++ b/src/three/plugins/fade/wrapFadeMaterial.js
@@ -1,4 +1,7 @@
// Adjusts the provided material to support fading in and out using a bayer pattern. Providing a "previous"
+
+import { Discard, Fn, If, output, screenCoordinate, uniform } from 'three/tsl';
+
// before compile can be used to chain shader adjustments. Returns the added uniforms used for fading.
const FADE_PARAMS = Symbol( 'FADE_PARAMS' );
export function wrapFadeMaterial( material, previousOnBeforeCompile ) {
@@ -18,6 +21,22 @@ export function wrapFadeMaterial( material, previousOnBeforeCompile ) {
material[ FADE_PARAMS ] = params;
+ if ( material.isNodeMaterial ) {
+
+ modifyNodeMaterial( material, params );
+
+ } else {
+
+ modifyMaterial( material, params, previousOnBeforeCompile );
+
+ }
+
+ return params;
+
+}
+
+function modifyMaterial( material, params, previousOnBeforeCompile ) {
+
material.defines = {
...( material.defines || {} ),
FEATURE_FADE: 0,
@@ -138,6 +157,82 @@ export function wrapFadeMaterial( material, previousOnBeforeCompile ) {
};
- return params;
+}
+
+// adapted from https://www.shadertoy.com/view/Mlt3z8
+const bayerDither2x2 = Fn( ( [ v ] ) => {
+
+ return v.y.mul( 3 ).add( v.x.mul( 2 ) ).mod( 4 );
+
+} ).setLayout( {
+ name: 'bayerDither2x2',
+ type: 'float',
+ inputs: [ { name: 'v', type: 'vec2' } ]
+} );
+
+const bayerDither4x4 = Fn( ( [ v ] ) => {
+
+ const P1 = v.mod( 2 );
+ const P2 = v.mod( 4 ).mul( 0.5 ).floor();
+ return bayerDither2x2( P1 ).mul( 4 ).add( bayerDither2x2( P2 ) );
+
+} ).setLayout( {
+ name: 'bayerDither4x4',
+ type: 'float',
+ inputs: [ { name: 'v', type: 'vec2' } ]
+} );
+
+// Define shared uniforms for fadeIn/fadeOut so that "outputNode" can be cached.
+const fadeIn = uniform( 0 ).onObjectUpdate( ( { material } ) => material.params.fadeIn.value );
+const fadeOut = uniform( 0 ).onObjectUpdate( ( { material } ) => material.params.fadeOut.value );
+
+const outputNode = Fn( () => {
+
+ const bayerValue = bayerDither4x4( screenCoordinate.xy.mod( 4 ).floor() );
+ const bayerBins = 16;
+ const dither = bayerValue.add( 0.5 ).div( bayerBins );
+
+ If( dither.greaterThanEqual( fadeIn ), () => {
+
+ Discard();
+
+ } );
+
+ If( dither.lessThan( fadeOut ), () => {
+
+ Discard();
+
+ } );
+
+ return output;
+
+} )();
+
+function modifyNodeMaterial( material, params ) {
+
+ material.params = params;
+
+ let FEATURE_FADE = 0;
+
+ material.defines = {
+
+ get FEATURE_FADE() {
+
+ return FEATURE_FADE;
+
+ },
+
+ set FEATURE_FADE( value ) {
+
+ if ( value != FEATURE_FADE ) {
+
+ FEATURE_FADE = value;
+ material.outputNode = value ? outputNode : null;
+
+ }
+
+ }
+
+ };
}