| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <title>three.js webgpu - custom fog</title>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
- <meta property="og:title" content="three.js webgpu - custom fog">
- <meta property="og:type" content="website">
- <meta property="og:url" content="https://threejs.org/examples/webgpu_custom_fog.html">
- <meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_custom_fog.jpg">
- <link type="text/css" rel="stylesheet" href="example.css">
- <style>
- body { background-color: #d0dee7; } /* match the scene's background grey */
- </style>
- </head>
- <body>
- <div id="info" class="invert">
- <a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
- <div class="title-wrapper">
- <a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Custom Fog</span>
- </div>
- <small>
- Custom height fog via TSL, pooling in a procedural alpine valley forested with 500,000 instanced trees.
- </small>
- </div>
- <script type="importmap">
- {
- "imports": {
- "three": "../build/three.webgpu.js",
- "three/webgpu": "../build/three.webgpu.js",
- "three/tsl": "../build/three.tsl.js",
- "three/addons/": "./jsm/"
- }
- }
- </script>
- <script type="module">
- import * as THREE from 'three/webgpu';
- import { color, fog, positionWorld, triNoise3D, normalWorld, uniform, densityFogFactor } from 'three/tsl';
- import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
- import { Inspector } from 'three/addons/inspector/Inspector.js';
- import { SkyMesh } from 'three/addons/objects/SkyMesh.js';
- import { TerrainGenerator } from 'three/addons/generators/TerrainGenerator.js';
- import { ForestGenerator } from 'three/addons/generators/ForestGenerator.js';
- let camera, scene, renderer, controls, timer;
- let terrain, forest, terrainGroup, forestGroup;
- let sky, sun, sunLight, pmremGenerator, envScene;
- const parameters = {
- elevation: 11, // sun height above the horizon, in degrees ( low = golden hour )
- azimuth: 150 // sun compass direction, in degrees
- };
- init();
- async function init() {
- renderer = new THREE.WebGPURenderer( { antialias: true } );
- renderer.setPixelRatio( window.devicePixelRatio );
- renderer.setSize( window.innerWidth, window.innerHeight );
- renderer.setAnimationLoop( animate );
- renderer.toneMapping = THREE.ACESFilmicToneMapping;
- renderer.toneMappingExposure = 0.62;
- renderer.shadowMap.enabled = true;
- renderer.shadowMap.type = THREE.PCFSoftShadowMap;
- renderer.inspector = new Inspector();
- document.body.appendChild( renderer.domElement );
- await renderer.init();
- pmremGenerator = new THREE.PMREMGenerator( renderer );
- camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 20000 );
- camera.position.set( - 50, 88, 230 );
- scene = new THREE.Scene();
- scene.environmentIntensity = 0.16; // a dim, cool sky fill so the warm sun stays the key and shadows read
- // a physical sky, used only to bake the image-based lighting ( it is not
- // the visible background — the fog gradient below is ); elevation / azimuth move the sun
- sky = new SkyMesh();
- sky.scale.setScalar( 10000 );
- sky.turbidity.value = 12;
- sky.rayleigh.value = 2;
- sky.mieCoefficient.value = 0.005;
- sky.mieDirectionalG.value = 0.88;
- sun = new THREE.Vector3();
- envScene = new THREE.Scene();
- // custom fog. an animated, two-octave triNoise3D haze that settles into
- // the valley as a level band: solid below `fogBase` ( down under the
- // mountains ) and fading out by `fogTop` ( around mid-height ), so the
- // peaks rise clear above it. the band sits at a fixed altitude whatever
- // the distance, and the noise wobbles its top edge and drifts it through
- // world space, so it reads as slow-moving cloud. ( the peaks reach ~130 )
- const skyColorValue = 0xf0f5f5;
- const groundColorValue = 0xd0dee7;
- const skyColor = color( skyColorValue );
- const groundColor = color( groundColorValue );
- const fogBase = uniform( - 20 ); // world-y the fog is solid below ( the valley floor )
- const fogTop = uniform( 55 ); // world-y the fog fades out by ( mid-mountain )
- const haze = uniform( 0.0012 ); // distance haze, so the far peaks dissolve into the same grey
- // a alternative way to create a TimerNode
- const time = uniform( 0 ).onFrameUpdate( ( frame ) => frame.time );
- const fogNoiseA = triNoise3D( positionWorld.mul( .005 ), 0.2, time );
- const fogNoiseB = triNoise3D( positionWorld.mul( .01 ), 0.2, time.mul( 1.2 ) );
- const fogNoise = fogNoiseA.add( fogNoiseB );
- // the noise lifts and drops the top of the band so it breaks into wisps
- const top = fogTop.add( fogNoise.sub( 0.7 ).mul( 22 ) );
- const groundFogArea = top.sub( positionWorld.y ).div( top.sub( fogBase ) ).saturate().mul( .98 );
- // the valley band plus a distance haze, so the far peaks dissolve into the grey too
- const fogArea = groundFogArea.oneMinus().mul( densityFogFactor( haze ).oneMinus() ).oneMinus();
- scene.fogNode = fog( groundColor, fogArea );
- scene.backgroundNode = normalWorld.y.max( 0 ).mix( groundColor, skyColor );
- // terrain + forest
- terrain = new TerrainGenerator( {
- seed: 1,
- size: 900,
- segments: 512,
- frequency: 0.0065,
- heightScale: 150,
- erosion: 0.7,
- valleyBias: 1.2
- } );
- forest = new ForestGenerator( { count: 500000, castShadow: true } );
- generate();
- // a single directional key aligned with the sky's sun, for the crisp relief
- // shadows the sky fill alone can't give. its colour, intensity and position —
- // and the baked environment — are set by updateSun()
- sunLight = new THREE.DirectionalLight();
- sunLight.castShadow = true;
- sunLight.shadow.camera.left = - 420;
- sunLight.shadow.camera.right = 420;
- sunLight.shadow.camera.top = 420;
- sunLight.shadow.camera.bottom = - 420;
- sunLight.shadow.camera.near = 200;
- sunLight.shadow.camera.far = 1800;
- sunLight.shadow.mapSize.set( 4096, 4096 );
- sunLight.shadow.bias = - 0.0004;
- sunLight.shadow.normalBias = 0.15;
- sunLight.shadow.autoUpdate = false; // the scene is static — re-render the shadow map only when the sun moves ( see updateSun ), not every frame
- scene.add( sunLight );
- updateSun();
- // gui
- const gui = renderer.inspector.createParameters( 'Settings' );
- const skyFolder = gui.addFolder( 'Sun' );
- skyFolder.add( parameters, 'elevation', 1, 40 ).step( 0.5 ).name( 'elevation' ).onChange( updateSun );
- skyFolder.add( parameters, 'azimuth', 0, 360 ).step( 1 ).name( 'azimuth' ).onChange( updateSun );
- const fogFolder = gui.addFolder( 'Fog' );
- fogFolder.add( fogBase, 'value', - 40, 20 ).step( 1 ).name( 'base' );
- fogFolder.add( fogTop, 'value', 0, 130 ).step( 1 ).name( 'top' );
- fogFolder.add( haze, 'value', 0, 0.005 ).step( 0.0001 ).name( 'haze' );
- const forestFolder = gui.addFolder( 'Forest' );
- forestFolder.add( forest.from, 'value', 50, 1000 ).step( 10 ).name( 'cull from' );
- forestFolder.add( forest.to, 'value', 100, 1400 ).step( 10 ).name( 'cull to' );
- const terrainFolder = gui.addFolder( 'Terrain' );
- terrainFolder.add( terrain.parameters, 'erosion', 0, 1.5 ).step( 0.05 ).name( 'erosion' );
- terrainFolder.add( terrain.parameters, 'valleyBias', 1, 3 ).step( 0.1 ).name( 'valley bias' );
- terrainFolder.add( { newSeed }, 'newSeed' ).name( 'regenerate' );
- // controls
- timer = new THREE.Timer();
- controls = new FirstPersonControls( camera, renderer.domElement );
- controls.movementSpeed = 20;
- controls.lookSpeed = 0.1;
- controls.lookAt( 0, 5, - 120 ); // face across the valley
- window.addEventListener( 'resize', resize );
- }
- // walks the sun: aligns the sky and the key light ( warm and dim near the
- // horizon ), then re-bakes the sky into the IBL environment
- function updateSun() {
- const elevation = parameters.elevation;
- sun.setFromSphericalCoords(
- 1,
- THREE.MathUtils.degToRad( 90 - elevation ),
- THREE.MathUtils.degToRad( parameters.azimuth )
- );
- sky.sunPosition.value.copy( sun );
- // the longer air path near the horizon dims and warms the sun. it stays
- // far brighter than the sky fill, so it reads as the key and casts firm shadows
- const transmittance = Math.sqrt( Math.max( Math.sin( THREE.MathUtils.degToRad( elevation ) ), 0 ) );
- sunLight.color.set( 0xff7a2f ).lerp( new THREE.Color( 0xfff2e0 ), transmittance ); // deep orange → warm white
- sunLight.intensity = 11 * transmittance + 0.3;
- sunLight.position.copy( sun ).multiplyScalar( 900 );
- sunLight.shadow.needsUpdate = true; // the sun moved, so the on-demand shadow map needs one refresh
- // re-bake the sky ( without the sun disc ) into the environment map for IBL.
- // the sky lives only in envScene; it is never added to the visible scene
- sky.showSunDisc.value = false;
- envScene.add( sky );
- const env = pmremGenerator.fromScene( envScene ).texture;
- if ( scene.environment ) scene.environment.dispose();
- scene.environment = env;
- }
- // a new seed ( and whatever erosion / valley bias is dialled in ) rebuilds
- // the terrain, and with it the forest that sits on top
- function newSeed() {
- terrain.parameters.seed ++;
- generate();
- }
- // the forest sits on the terrain, so a new terrain means a new forest
- function generate() {
- if ( terrainGroup ) scene.remove( terrainGroup );
- if ( forestGroup ) scene.remove( forestGroup );
- terrainGroup = terrain.build();
- forestGroup = forest.build( terrain );
- scene.add( terrainGroup );
- scene.add( forestGroup );
- if ( sunLight ) sunLight.shadow.needsUpdate = true; // rebuilt geometry ⇒ re-render the shadow map ( skipped on the first build, before the light exists; updateSun covers it )
- }
- function resize() {
- camera.aspect = window.innerWidth / window.innerHeight;
- camera.updateProjectionMatrix();
- renderer.setSize( window.innerWidth, window.innerHeight );
- }
- function animate() {
- timer.update();
- controls.update( timer.getDelta() );
- forest.setCameraPosition( camera.position ); // drives the forest cull
- renderer.render( scene, camera );
- }
- </script>
- </body>
- </html>
|