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; + + } + + } + + }; }