|
1 | 1 | <!DOCTYPE html> |
2 | 2 | <html lang="en"> |
3 | 3 | <head> |
4 | | - <title>three.js loader - 3d tiles</title> |
| 4 | + <title>three.js loader - 3d tiles + clouds</title> |
5 | 5 | <meta charset="utf-8"> |
6 | 6 | <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"> |
7 | 7 | <link type="text/css" rel="stylesheet" href="main.css"> |
8 | 8 | <style> |
9 | 9 | body { |
10 | | - background-color: #111; |
| 10 | + background-color: #000; |
11 | 11 | color: #eee; |
12 | 12 | } |
13 | 13 |
|
|
33 | 33 |
|
34 | 34 | <body> |
35 | 35 | <div id="info"> |
36 | | - <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> 3d tiles - <a href="https://github.com/NASA-AMMOS/3DTilesRendererJS" target="_blank" rel="noopener">3d-tiles-renderer</a><br/> |
37 | | - See <a href="https://github.com/NASA-AMMOS/3DTilesRendererJS" target="_blank" rel="noopener">main project repository</a> for more information and examples on loading 3d tiles |
38 | | - <br/> |
39 | | - and other tiled data formats. <a href="https://developers.google.com/maps/documentation/tile/3d-tiles" target="_blank" rel="noopener">Google Photorealistic Tiles</a> token courtesy of <a href="https://ion.cesium.com/" target="_blank" rel="noopener">Cesium Ion</a>. |
| 36 | + <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - |
| 37 | + <a href="https://github.com/NASA-AMMOS/3DTilesRendererJS" target="_blank" rel="noopener">3d-tiles-renderer</a> + |
| 38 | + <a href="https://github.com/takram-design-engineering/three-geospatial" target="_blank" rel="noopener">three-clouds</a><br/> |
| 39 | + <a href="https://developers.google.com/maps/documentation/tile/3d-tiles" target="_blank" rel="noopener">Google Photorealistic Tiles</a> token courtesy of <a href="https://ion.cesium.com/" target="_blank" rel="noopener">Cesium Ion</a>.<br/> |
| 40 | + <label>time of day: <input id="hour" type="range" min="0" max="24" value="0" step="0.01" style="pointer-events: all; vertical-align: middle;"></label> |
40 | 41 | </div> |
41 | 42 |
|
42 | 43 | <div id="credits"> |
43 | 44 | <img src="./textures/google_on_non_white_hdpi.png" /> |
44 | 45 | <a href="https://ion.cesium.com/" target="_blank" rel="noopener"><img src="./textures/cesiumion.png" /></a> |
45 | 46 | </div> |
46 | 47 |
|
| 48 | + <!-- "three/examples/" import map entry required by 3d-tiles-renderer --> |
47 | 49 | <script type="importmap"> |
48 | 50 | { |
49 | 51 | "imports": { |
50 | 52 | "three": "../build/three.module.js", |
| 53 | + "three/addons/": "./jsm/", |
51 | 54 | "three/examples/": "./", |
52 | | - "3d-tiles-renderer": "https://cdn.jsdelivr.net/npm/3d-tiles-renderer@0.4.8/src/index.js", |
53 | | - "3d-tiles-renderer/plugins": "https://cdn.jsdelivr.net/npm/3d-tiles-renderer@0.4.8/src/plugins/index.js" |
| 55 | + "3d-tiles-renderer": "https://cdn.jsdelivr.net/npm/3d-tiles-renderer@0.4.23/build/index.js", |
| 56 | + "3d-tiles-renderer/core/plugins": "https://cdn.jsdelivr.net/npm/3d-tiles-renderer@0.4.23/build/index.core-plugins.js", |
| 57 | + "3d-tiles-renderer/three/plugins": "https://cdn.jsdelivr.net/npm/3d-tiles-renderer@0.4.23/build/index.three-plugins.js", |
| 58 | + "postprocessing": "https://cdn.jsdelivr.net/npm/postprocessing@6.39.0/build/index.js", |
| 59 | + "@takram/three-clouds": "https://cdn.jsdelivr.net/npm/@takram/three-clouds@0.7.3/build/index.js", |
| 60 | + "@takram/three-atmosphere": "https://cdn.jsdelivr.net/npm/@takram/three-atmosphere@0.17.1/build/index.js", |
| 61 | + "@takram/three-geospatial": "https://cdn.jsdelivr.net/npm/@takram/three-geospatial@0.7.1/build/index.js", |
| 62 | + "@takram/three-geospatial/shaders": "https://cdn.jsdelivr.net/npm/@takram/three-geospatial@0.7.1/build/shaders.js", |
| 63 | + "@takram/three-geospatial-effects": "https://cdn.jsdelivr.net/npm/@takram/three-geospatial-effects@0.6.1/build/index.js", |
| 64 | + "@takram/three-atmosphere/shaders/bruneton": "https://cdn.jsdelivr.net/npm/@takram/three-atmosphere@0.17.1/build/shaders/bruneton.js" |
54 | 65 | } |
55 | 66 | } |
56 | 67 | </script> |
57 | 68 |
|
58 | 69 | <script type="module"> |
59 | 70 |
|
60 | 71 | import * as THREE from 'three'; |
61 | | - import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; |
62 | | - import { TilesRenderer, GlobeControls } from '3d-tiles-renderer'; |
63 | | - import { CesiumIonAuthPlugin, GLTFExtensionsPlugin, TilesFadePlugin, UpdateOnChangePlugin } from '3d-tiles-renderer/plugins'; |
| 72 | + import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'; |
| 73 | + import { toCreasedNormals } from 'three/addons/utils/BufferGeometryUtils.js'; |
| 74 | + import { TilesRenderer, GlobeControls, CAMERA_FRAME } from '3d-tiles-renderer'; |
| 75 | + import { CesiumIonAuthPlugin } from '3d-tiles-renderer/core/plugins'; |
| 76 | + import { GLTFExtensionsPlugin, TilesFadePlugin, UpdateOnChangePlugin } from '3d-tiles-renderer/three/plugins'; |
| 77 | + |
| 78 | + import { EffectMaterial, EffectPass, NormalPass, SMAAEffect } from 'postprocessing'; |
| 79 | + import { |
| 80 | + CloudsEffect, |
| 81 | + CLOUD_SHAPE_TEXTURE_SIZE, |
| 82 | + CLOUD_SHAPE_DETAIL_TEXTURE_SIZE, |
| 83 | + DEFAULT_LOCAL_WEATHER_URL, |
| 84 | + DEFAULT_SHAPE_URL, |
| 85 | + DEFAULT_SHAPE_DETAIL_URL, |
| 86 | + DEFAULT_TURBULENCE_URL |
| 87 | + } from '@takram/three-clouds'; |
| 88 | + import { AerialPerspectiveEffect, PrecomputedTexturesGenerator, getSunDirectionECEF } from '@takram/three-atmosphere'; |
| 89 | + import { STBNLoader, DEFAULT_STBN_URL } from '@takram/three-geospatial'; |
| 90 | + import { DitheringEffect, LensFlareEffect } from '@takram/three-geospatial-effects'; |
64 | 91 |
|
65 | 92 | // Ion key provided by Cesium for use on threejs.org |
66 | 93 | // A personal Cesium Ion key can be used for development. |
67 | 94 | const ION_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiMTFiZTRmZS1mMWIxLTQ5YzYtYjA4Zi0xYTE0MjFmYzQ5OGYiLCJpZCI6MjY3NzgzLCJpYXQiOjE3MzY0NzQxMDh9.ppGPgpse1lq7QeNyljX7THUyK5w1x_4HksSHSlhe5sY'; |
68 | 95 |
|
69 | 96 | let camera, scene, renderer; |
70 | 97 | let tiles, controls; |
| 98 | + let clouds, aerialPerspective; |
| 99 | + let _prevTime = 0, _deltaTime = 0; |
71 | 100 |
|
72 | 101 | init(); |
73 | 102 |
|
74 | | - function init() { |
| 103 | + async function init() { |
75 | 104 |
|
76 | 105 | // camera |
77 | | - camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 100 ); |
78 | | - camera.position.set( - 1, 1, 1 ).normalize().multiplyScalar( 10 ); |
79 | | - camera.position.set( - 8000000, 10000000, - 14720000 ); |
80 | | - camera.lookAt( 0, 0, 0 ); |
| 106 | + camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 10, 1e6 ); |
81 | 107 |
|
82 | 108 | // scene |
83 | 109 | scene = new THREE.Scene(); |
84 | 110 |
|
85 | 111 | // renderer |
86 | | - renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } ); |
| 112 | + renderer = new THREE.WebGLRenderer( { outputBufferType: THREE.HalfFloatType } ); |
87 | 113 | renderer.setPixelRatio( window.devicePixelRatio ); |
88 | 114 | renderer.setSize( window.innerWidth, window.innerHeight ); |
89 | | - renderer.setAnimationLoop( animate ); |
| 115 | + renderer.toneMapping = THREE.AgXToneMapping; |
| 116 | + renderer.toneMappingExposure = 10; |
90 | 117 | document.body.appendChild( renderer.domElement ); |
91 | 118 |
|
92 | 119 | // loader |
93 | 120 | const dracoLoader = new DRACOLoader(); |
94 | | - dracoLoader.setDecoderPath( 'jsm/libs/draco/' ); |
| 121 | + dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' ); |
95 | 122 | dracoLoader.setDecoderConfig( { type: 'js' } ); |
96 | 123 |
|
| 124 | + const DEG2RAD = Math.PI / 180; |
| 125 | + |
97 | 126 | // tiles |
| 127 | + class TileCreasedNormalsPlugin { |
| 128 | + |
| 129 | + processTileModel( scene ) { |
| 130 | + |
| 131 | + scene.traverse( ( mesh ) => { |
| 132 | + |
| 133 | + if ( mesh.geometry ) { |
| 134 | + |
| 135 | + mesh.geometry = toCreasedNormals( mesh.geometry, 30 * DEG2RAD ); |
| 136 | + |
| 137 | + } |
| 138 | + |
| 139 | + } ); |
| 140 | + |
| 141 | + } |
| 142 | + |
| 143 | + } |
| 144 | + |
98 | 145 | tiles = new TilesRenderer(); |
99 | 146 | tiles.registerPlugin( new CesiumIonAuthPlugin( { apiToken: ION_KEY, assetId: '2275207', autoRefreshToken: true } ) ); |
100 | 147 | tiles.registerPlugin( new GLTFExtensionsPlugin( { dracoLoader } ) ); |
| 148 | + tiles.registerPlugin( new TileCreasedNormalsPlugin() ); |
101 | 149 | tiles.registerPlugin( new TilesFadePlugin() ); |
102 | 150 | tiles.registerPlugin( new UpdateOnChangePlugin() ); |
103 | 151 | tiles.setCamera( camera ); |
104 | 152 | tiles.setResolutionFromRenderer( camera, renderer ); |
| 153 | + |
105 | 154 | scene.add( tiles.group ); |
106 | 155 |
|
107 | | - // rotate the globe so the north pole is up |
108 | | - tiles.group.rotation.x = - Math.PI / 2; |
| 156 | + // position camera above Tokyo |
| 157 | + tiles.ellipsoid.getObjectFrame( |
| 158 | + 35.6812 * DEG2RAD, 139.80 * DEG2RAD, 500, |
| 159 | + - 90 * DEG2RAD, - 10 * DEG2RAD, 0, |
| 160 | + camera.matrix, CAMERA_FRAME |
| 161 | + ); |
| 162 | + camera.matrix.decompose( camera.position, camera.quaternion, camera.scale ); |
109 | 163 |
|
110 | 164 | // controls |
111 | | - controls = new GlobeControls( scene, camera, renderer.domElement, tiles ); |
| 165 | + controls = new GlobeControls( scene, camera, renderer.domElement ); |
| 166 | + controls.setEllipsoid( tiles.ellipsoid, tiles.group ); |
112 | 167 | controls.enableDamping = true; |
113 | 168 |
|
| 169 | + // Workaround: adjustHeight causes camera drift as tiles load. |
| 170 | + // Disable until first user interaction. |
| 171 | + controls.adjustHeight = false; |
| 172 | + |
| 173 | + function enableAdjustHeight() { |
| 174 | + |
| 175 | + controls.adjustHeight = true; |
| 176 | + renderer.domElement.removeEventListener( 'pointerdown', enableAdjustHeight ); |
| 177 | + renderer.domElement.removeEventListener( 'wheel', enableAdjustHeight ); |
| 178 | + |
| 179 | + } |
| 180 | + |
| 181 | + renderer.domElement.addEventListener( 'pointerdown', enableAdjustHeight ); |
| 182 | + renderer.domElement.addEventListener( 'wheel', enableAdjustHeight ); |
| 183 | + |
| 184 | + // sun direction |
| 185 | + const sunDirection = new THREE.Vector3(); |
| 186 | + const params = { hourUTC: 0 }; // 0:00 UTC = 9:00 AM Tokyo |
| 187 | + |
| 188 | + function updateSunDirection() { |
| 189 | + |
| 190 | + const ms = params.hourUTC * 3600000; |
| 191 | + const date = new Date( Date.UTC( 2024, 2, 1 ) + ms ); |
| 192 | + getSunDirectionECEF( date, sunDirection ); |
| 193 | + aerialPerspective.sunDirection.copy( sunDirection ); |
| 194 | + clouds.sunDirection.copy( sunDirection ); |
| 195 | + |
| 196 | + } |
| 197 | + |
| 198 | + // aerial perspective (sky + atmosphere + deferred lighting) |
| 199 | + aerialPerspective = new AerialPerspectiveEffect( camera ); |
| 200 | + aerialPerspective.sky = true; |
| 201 | + aerialPerspective.sunLight = true; |
| 202 | + aerialPerspective.skyLight = true; |
| 203 | + |
| 204 | + const normalPass = new NormalPass( scene, camera ); |
| 205 | + aerialPerspective.normalBuffer = normalPass.texture; |
| 206 | + |
| 207 | + // clouds |
| 208 | + clouds = new CloudsEffect( camera ); |
| 209 | + clouds.coverage = 0.3; |
| 210 | + clouds.localWeatherVelocity.set( 0.001, 0 ); |
| 211 | + clouds.shadow.farScale = 0.25; |
| 212 | + clouds.shadow.maxFar = 1e5; |
| 213 | + clouds.shadow.cascadeCount = 2; |
| 214 | + clouds.shadow.mapSize.set( 512, 512 ); |
| 215 | + clouds.shadow.splitMode = 'practical'; |
| 216 | + clouds.shadow.splitLambda = 0.71; |
| 217 | + |
| 218 | + // sync cloud shadows to atmosphere |
| 219 | + clouds.events.addEventListener( 'change', ( event ) => { |
| 220 | + |
| 221 | + if ( event.property === 'atmosphereOverlay' ) aerialPerspective.overlay = clouds.atmosphereOverlay; |
| 222 | + if ( event.property === 'atmosphereShadow' ) aerialPerspective.shadow = clouds.atmosphereShadow; |
| 223 | + if ( event.property === 'atmosphereShadowLength' ) aerialPerspective.shadowLength = clouds.atmosphereShadowLength; |
| 224 | + |
| 225 | + } ); |
| 226 | + |
| 227 | + // adapter: bridges pmndrs postprocessing passes to renderer.setEffects() |
| 228 | + |
| 229 | + class EffectPassAdapter { |
| 230 | + |
| 231 | + constructor( pass ) { |
| 232 | + |
| 233 | + this.pass = pass; |
| 234 | + this.needsSwap = pass.needsSwap !== false; |
| 235 | + this.enabled = true; |
| 236 | + this._initialized = false; |
| 237 | + |
| 238 | + } |
| 239 | + |
| 240 | + render( renderer, writeBuffer, readBuffer ) { |
| 241 | + |
| 242 | + if ( ! this._initialized ) { |
| 243 | + |
| 244 | + this.pass.initialize( renderer, false, THREE.HalfFloatType ); |
| 245 | + this.pass.setSize( readBuffer.width, readBuffer.height ); |
| 246 | + |
| 247 | + if ( readBuffer.depthTexture && this.pass.setDepthTexture ) { |
| 248 | + |
| 249 | + this.pass.setDepthTexture( readBuffer.depthTexture ); |
| 250 | + |
| 251 | + } |
| 252 | + |
| 253 | + this._initialized = true; |
| 254 | + |
| 255 | + } |
| 256 | + |
| 257 | + if ( this.pass.fullscreenMaterial instanceof EffectMaterial ) { |
| 258 | + |
| 259 | + this.pass.fullscreenMaterial.adoptCameraSettings( camera ); |
| 260 | + |
| 261 | + } |
| 262 | + |
| 263 | + this.pass.render( renderer, readBuffer, writeBuffer, _deltaTime ); |
| 264 | + |
| 265 | + } |
| 266 | + |
| 267 | + setSize( width, height ) { |
| 268 | + |
| 269 | + if ( this._initialized ) this.pass.setSize( width, height ); |
| 270 | + |
| 271 | + } |
| 272 | + |
| 273 | + } |
| 274 | + |
| 275 | + // postprocessing |
| 276 | + renderer.setEffects( [ |
| 277 | + new EffectPassAdapter( normalPass ), |
| 278 | + new EffectPassAdapter( new EffectPass( camera, clouds, aerialPerspective ) ), |
| 279 | + new EffectPassAdapter( new EffectPass( camera, new LensFlareEffect() ) ), |
| 280 | + new EffectPassAdapter( new EffectPass( camera, new SMAAEffect() ) ), |
| 281 | + new EffectPassAdapter( new EffectPass( camera, new DitheringEffect() ) ), |
| 282 | + ] ); |
| 283 | + |
| 284 | + // generate precomputed atmosphere textures on GPU |
| 285 | + const texturesGenerator = new PrecomputedTexturesGenerator( renderer ); |
| 286 | + const textures = await texturesGenerator.update(); |
| 287 | + Object.assign( aerialPerspective, textures ); |
| 288 | + Object.assign( clouds, textures ); |
| 289 | + |
| 290 | + // load cloud textures |
| 291 | + const textureLoader = new THREE.TextureLoader(); |
| 292 | + |
| 293 | + function loadCloudTexture( url, property ) { |
| 294 | + |
| 295 | + textureLoader.load( url, ( texture ) => { |
| 296 | + |
| 297 | + texture.minFilter = THREE.LinearMipMapLinearFilter; |
| 298 | + texture.magFilter = THREE.LinearFilter; |
| 299 | + texture.wrapS = THREE.RepeatWrapping; |
| 300 | + texture.wrapT = THREE.RepeatWrapping; |
| 301 | + texture.colorSpace = THREE.NoColorSpace; |
| 302 | + texture.needsUpdate = true; |
| 303 | + clouds[ property ] = texture; |
| 304 | + |
| 305 | + } ); |
| 306 | + |
| 307 | + } |
| 308 | + |
| 309 | + loadCloudTexture( DEFAULT_LOCAL_WEATHER_URL, 'localWeatherTexture' ); |
| 310 | + loadCloudTexture( DEFAULT_TURBULENCE_URL, 'turbulenceTexture' ); |
| 311 | + |
| 312 | + function loadData3DTexture( url, size, property ) { |
| 313 | + |
| 314 | + fetch( url ).then( res => res.arrayBuffer() ).then( buffer => { |
| 315 | + |
| 316 | + const data = new Uint8Array( buffer ); |
| 317 | + const texture = new THREE.Data3DTexture( data, size, size, size ); |
| 318 | + texture.format = THREE.RedFormat; |
| 319 | + texture.minFilter = THREE.LinearFilter; |
| 320 | + texture.magFilter = THREE.LinearFilter; |
| 321 | + texture.wrapS = THREE.RepeatWrapping; |
| 322 | + texture.wrapT = THREE.RepeatWrapping; |
| 323 | + texture.wrapR = THREE.RepeatWrapping; |
| 324 | + texture.colorSpace = THREE.NoColorSpace; |
| 325 | + texture.needsUpdate = true; |
| 326 | + clouds[ property ] = texture; |
| 327 | + |
| 328 | + } ); |
| 329 | + |
| 330 | + } |
| 331 | + |
| 332 | + loadData3DTexture( DEFAULT_SHAPE_URL, CLOUD_SHAPE_TEXTURE_SIZE, 'shapeTexture' ); |
| 333 | + loadData3DTexture( DEFAULT_SHAPE_DETAIL_URL, CLOUD_SHAPE_DETAIL_TEXTURE_SIZE, 'shapeDetailTexture' ); |
| 334 | + |
| 335 | + new STBNLoader().load( DEFAULT_STBN_URL, ( texture ) => { |
| 336 | + |
| 337 | + clouds.stbnTexture = texture; |
| 338 | + aerialPerspective.stbnTexture = texture; |
| 339 | + |
| 340 | + } ); |
| 341 | + |
| 342 | + // initial sun position |
| 343 | + updateSunDirection(); |
| 344 | + |
| 345 | + document.getElementById( 'hour' ).addEventListener( 'input', ( e ) => { |
| 346 | + |
| 347 | + params.hourUTC = parseFloat( e.target.value ); |
| 348 | + updateSunDirection(); |
| 349 | + |
| 350 | + } ); |
| 351 | + |
| 352 | + // start rendering |
| 353 | + renderer.setAnimationLoop( animate ); |
| 354 | + |
114 | 355 | window.addEventListener( 'resize', onWindowResize ); |
115 | 356 | onWindowResize(); |
116 | 357 |
|
|
127 | 368 |
|
128 | 369 | } |
129 | 370 |
|
130 | | - function animate() { |
| 371 | + function animate( time ) { |
| 372 | + |
| 373 | + _deltaTime = ( time - _prevTime ) / 1000; |
| 374 | + _prevTime = time; |
131 | 375 |
|
132 | 376 | controls.update(); |
133 | 377 | tiles.update(); |
|
0 commit comments