Skip to content

Commit e48228f

Browse files
mrdoobclaude
andauthored
Examples: Add volumetric clouds and atmosphere to webgl_loader_3dtiles (#33292)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 85f77d7 commit e48228f

2 files changed

Lines changed: 267 additions & 23 deletions

File tree

17.5 KB
Loading

examples/webgl_loader_3dtiles.html

Lines changed: 267 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4-
<title>three.js loader - 3d tiles</title>
4+
<title>three.js loader - 3d tiles + clouds</title>
55
<meta charset="utf-8">
66
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
77
<link type="text/css" rel="stylesheet" href="main.css">
88
<style>
99
body {
10-
background-color: #111;
10+
background-color: #000;
1111
color: #eee;
1212
}
1313

@@ -33,84 +33,325 @@
3333

3434
<body>
3535
<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>
4041
</div>
4142

4243
<div id="credits">
4344
<img src="./textures/google_on_non_white_hdpi.png" />
4445
<a href="https://ion.cesium.com/" target="_blank" rel="noopener"><img src="./textures/cesiumion.png" /></a>
4546
</div>
4647

48+
<!-- "three/examples/" import map entry required by 3d-tiles-renderer -->
4749
<script type="importmap">
4850
{
4951
"imports": {
5052
"three": "../build/three.module.js",
53+
"three/addons/": "./jsm/",
5154
"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"
5465
}
5566
}
5667
</script>
5768

5869
<script type="module">
5970

6071
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';
6491

6592
// Ion key provided by Cesium for use on threejs.org
6693
// A personal Cesium Ion key can be used for development.
6794
const ION_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJiMTFiZTRmZS1mMWIxLTQ5YzYtYjA4Zi0xYTE0MjFmYzQ5OGYiLCJpZCI6MjY3NzgzLCJpYXQiOjE3MzY0NzQxMDh9.ppGPgpse1lq7QeNyljX7THUyK5w1x_4HksSHSlhe5sY';
6895

6996
let camera, scene, renderer;
7097
let tiles, controls;
98+
let clouds, aerialPerspective;
99+
let _prevTime = 0, _deltaTime = 0;
71100

72101
init();
73102

74-
function init() {
103+
async function init() {
75104

76105
// 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 );
81107

82108
// scene
83109
scene = new THREE.Scene();
84110

85111
// renderer
86-
renderer = new THREE.WebGLRenderer( { antialias: true, alpha: true } );
112+
renderer = new THREE.WebGLRenderer( { outputBufferType: THREE.HalfFloatType } );
87113
renderer.setPixelRatio( window.devicePixelRatio );
88114
renderer.setSize( window.innerWidth, window.innerHeight );
89-
renderer.setAnimationLoop( animate );
115+
renderer.toneMapping = THREE.AgXToneMapping;
116+
renderer.toneMappingExposure = 10;
90117
document.body.appendChild( renderer.domElement );
91118

92119
// loader
93120
const dracoLoader = new DRACOLoader();
94-
dracoLoader.setDecoderPath( 'jsm/libs/draco/' );
121+
dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );
95122
dracoLoader.setDecoderConfig( { type: 'js' } );
96123

124+
const DEG2RAD = Math.PI / 180;
125+
97126
// 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+
98145
tiles = new TilesRenderer();
99146
tiles.registerPlugin( new CesiumIonAuthPlugin( { apiToken: ION_KEY, assetId: '2275207', autoRefreshToken: true } ) );
100147
tiles.registerPlugin( new GLTFExtensionsPlugin( { dracoLoader } ) );
148+
tiles.registerPlugin( new TileCreasedNormalsPlugin() );
101149
tiles.registerPlugin( new TilesFadePlugin() );
102150
tiles.registerPlugin( new UpdateOnChangePlugin() );
103151
tiles.setCamera( camera );
104152
tiles.setResolutionFromRenderer( camera, renderer );
153+
105154
scene.add( tiles.group );
106155

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 );
109163

110164
// 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 );
112167
controls.enableDamping = true;
113168

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+
114355
window.addEventListener( 'resize', onWindowResize );
115356
onWindowResize();
116357

@@ -127,7 +368,10 @@
127368

128369
}
129370

130-
function animate() {
371+
function animate( time ) {
372+
373+
_deltaTime = ( time - _prevTime ) / 1000;
374+
_prevTime = time;
131375

132376
controls.update();
133377
tiles.update();

0 commit comments

Comments
 (0)