webgpu_custom_fog.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>three.js webgpu - custom fog</title>
  5. <meta charset="utf-8">
  6. <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
  7. <meta property="og:title" content="three.js webgpu - custom fog">
  8. <meta property="og:type" content="website">
  9. <meta property="og:url" content="https://threejs.org/examples/webgpu_custom_fog.html">
  10. <meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_custom_fog.jpg">
  11. <link type="text/css" rel="stylesheet" href="example.css">
  12. <style>
  13. body { background-color: #d0dee7; } /* match the scene's background grey */
  14. </style>
  15. </head>
  16. <body>
  17. <div id="info" class="invert">
  18. <a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
  19. <div class="title-wrapper">
  20. <a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>Custom Fog</span>
  21. </div>
  22. <small>
  23. Custom height fog via TSL, pooling in a procedural alpine valley forested with 500,000 instanced trees.
  24. </small>
  25. </div>
  26. <script type="importmap">
  27. {
  28. "imports": {
  29. "three": "../build/three.webgpu.js",
  30. "three/webgpu": "../build/three.webgpu.js",
  31. "three/tsl": "../build/three.tsl.js",
  32. "three/addons/": "./jsm/"
  33. }
  34. }
  35. </script>
  36. <script type="module">
  37. import * as THREE from 'three/webgpu';
  38. import { color, fog, positionWorld, triNoise3D, normalWorld, uniform, densityFogFactor } from 'three/tsl';
  39. import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
  40. import { Inspector } from 'three/addons/inspector/Inspector.js';
  41. import { SkyMesh } from 'three/addons/objects/SkyMesh.js';
  42. import { TerrainGenerator } from 'three/addons/generators/TerrainGenerator.js';
  43. import { ForestGenerator } from 'three/addons/generators/ForestGenerator.js';
  44. let camera, scene, renderer, controls, timer;
  45. let terrain, forest, terrainGroup, forestGroup;
  46. let sky, sun, sunLight, pmremGenerator, envScene;
  47. const parameters = {
  48. elevation: 11, // sun height above the horizon, in degrees ( low = golden hour )
  49. azimuth: 150 // sun compass direction, in degrees
  50. };
  51. init();
  52. async function init() {
  53. renderer = new THREE.WebGPURenderer( { antialias: true } );
  54. renderer.setPixelRatio( window.devicePixelRatio );
  55. renderer.setSize( window.innerWidth, window.innerHeight );
  56. renderer.setAnimationLoop( animate );
  57. renderer.toneMapping = THREE.ACESFilmicToneMapping;
  58. renderer.toneMappingExposure = 0.62;
  59. renderer.shadowMap.enabled = true;
  60. renderer.inspector = new Inspector();
  61. document.body.appendChild( renderer.domElement );
  62. await renderer.init();
  63. pmremGenerator = new THREE.PMREMGenerator( renderer );
  64. camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 20000 );
  65. camera.position.set( - 50, 88, 230 );
  66. scene = new THREE.Scene();
  67. scene.environmentIntensity = 0.16; // a dim, cool sky fill so the warm sun stays the key and shadows read
  68. // a physical sky, used only to bake the image-based lighting ( it is not
  69. // the visible background — the fog gradient below is ); elevation / azimuth move the sun
  70. sky = new SkyMesh();
  71. sky.scale.setScalar( 10000 );
  72. sky.turbidity.value = 12;
  73. sky.rayleigh.value = 2;
  74. sky.mieCoefficient.value = 0.005;
  75. sky.mieDirectionalG.value = 0.88;
  76. sun = new THREE.Vector3();
  77. envScene = new THREE.Scene();
  78. // custom fog. an animated, two-octave triNoise3D haze that settles into
  79. // the valley as a level band: solid below `fogBase` ( down under the
  80. // mountains ) and fading out by `fogTop` ( around mid-height ), so the
  81. // peaks rise clear above it. the band sits at a fixed altitude whatever
  82. // the distance, and the noise wobbles its top edge and drifts it through
  83. // world space, so it reads as slow-moving cloud. ( the peaks reach ~130 )
  84. const skyColorValue = 0xf0f5f5;
  85. const groundColorValue = 0xd0dee7;
  86. const skyColor = color( skyColorValue );
  87. const groundColor = color( groundColorValue );
  88. const fogBase = uniform( - 20 ); // world-y the fog is solid below ( the valley floor )
  89. const fogTop = uniform( 55 ); // world-y the fog fades out by ( mid-mountain )
  90. const haze = uniform( 0.0012 ); // distance haze, so the far peaks dissolve into the same grey
  91. // a alternative way to create a TimerNode
  92. const time = uniform( 0 ).onFrameUpdate( ( frame ) => frame.time );
  93. const fogNoiseA = triNoise3D( positionWorld.mul( .005 ), 0.2, time );
  94. const fogNoiseB = triNoise3D( positionWorld.mul( .01 ), 0.2, time.mul( 1.2 ) );
  95. const fogNoise = fogNoiseA.add( fogNoiseB );
  96. // the noise lifts and drops the top of the band so it breaks into wisps
  97. const top = fogTop.add( fogNoise.sub( 0.7 ).mul( 22 ) );
  98. const groundFogArea = top.sub( positionWorld.y ).div( top.sub( fogBase ) ).saturate().mul( .98 );
  99. // the valley band plus a distance haze, so the far peaks dissolve into the grey too
  100. const fogArea = groundFogArea.oneMinus().mul( densityFogFactor( haze ).oneMinus() ).oneMinus();
  101. scene.fogNode = fog( groundColor, fogArea );
  102. scene.backgroundNode = normalWorld.y.max( 0 ).mix( groundColor, skyColor );
  103. // terrain + forest
  104. terrain = new TerrainGenerator( {
  105. seed: 1,
  106. size: 900,
  107. segments: 512,
  108. frequency: 0.0065,
  109. heightScale: 150,
  110. erosion: 0.7,
  111. valleyBias: 1.2
  112. } );
  113. forest = new ForestGenerator( { count: 500000, castShadow: true } );
  114. generate();
  115. // a single directional key aligned with the sky's sun, for the crisp relief
  116. // shadows the sky fill alone can't give. its colour, intensity and position —
  117. // and the baked environment — are set by updateSun()
  118. sunLight = new THREE.DirectionalLight();
  119. sunLight.castShadow = true;
  120. sunLight.shadow.camera.left = - 420;
  121. sunLight.shadow.camera.right = 420;
  122. sunLight.shadow.camera.top = 420;
  123. sunLight.shadow.camera.bottom = - 420;
  124. sunLight.shadow.camera.near = 200;
  125. sunLight.shadow.camera.far = 1800;
  126. sunLight.shadow.mapSize.set( 4096, 4096 );
  127. sunLight.shadow.bias = - 0.0004;
  128. sunLight.shadow.normalBias = 0.15;
  129. sunLight.shadow.autoUpdate = false; // the scene is static — re-render the shadow map only when the sun moves ( see updateSun ), not every frame
  130. scene.add( sunLight );
  131. updateSun();
  132. // gui
  133. const gui = renderer.inspector.createParameters( 'Settings' );
  134. const skyFolder = gui.addFolder( 'Sun' );
  135. skyFolder.add( parameters, 'elevation', 1, 40 ).step( 0.5 ).name( 'elevation' ).onChange( updateSun );
  136. skyFolder.add( parameters, 'azimuth', 0, 360 ).step( 1 ).name( 'azimuth' ).onChange( updateSun );
  137. const fogFolder = gui.addFolder( 'Fog' );
  138. fogFolder.add( fogBase, 'value', - 40, 20 ).step( 1 ).name( 'base' );
  139. fogFolder.add( fogTop, 'value', 0, 130 ).step( 1 ).name( 'top' );
  140. fogFolder.add( haze, 'value', 0, 0.005 ).step( 0.0001 ).name( 'haze' );
  141. const forestFolder = gui.addFolder( 'Forest' );
  142. forestFolder.add( forest.from, 'value', 50, 1000 ).step( 10 ).name( 'cull from' );
  143. forestFolder.add( forest.to, 'value', 100, 1400 ).step( 10 ).name( 'cull to' );
  144. const terrainFolder = gui.addFolder( 'Terrain' );
  145. terrainFolder.add( terrain.parameters, 'erosion', 0, 1.5 ).step( 0.05 ).name( 'erosion' );
  146. terrainFolder.add( terrain.parameters, 'valleyBias', 1, 3 ).step( 0.1 ).name( 'valley bias' );
  147. terrainFolder.add( { newSeed }, 'newSeed' ).name( 'regenerate' );
  148. // controls
  149. timer = new THREE.Timer();
  150. controls = new FirstPersonControls( camera, renderer.domElement );
  151. controls.movementSpeed = 20;
  152. controls.lookSpeed = 0.1;
  153. controls.lookAt( 0, 5, - 120 ); // face across the valley
  154. window.addEventListener( 'resize', resize );
  155. }
  156. // walks the sun: aligns the sky and the key light ( warm and dim near the
  157. // horizon ), then re-bakes the sky into the IBL environment
  158. function updateSun() {
  159. const elevation = parameters.elevation;
  160. sun.setFromSphericalCoords(
  161. 1,
  162. THREE.MathUtils.degToRad( 90 - elevation ),
  163. THREE.MathUtils.degToRad( parameters.azimuth )
  164. );
  165. sky.sunPosition.value.copy( sun );
  166. // the longer air path near the horizon dims and warms the sun. it stays
  167. // far brighter than the sky fill, so it reads as the key and casts firm shadows
  168. const transmittance = Math.sqrt( Math.max( Math.sin( THREE.MathUtils.degToRad( elevation ) ), 0 ) );
  169. sunLight.color.set( 0xff7a2f ).lerp( new THREE.Color( 0xfff2e0 ), transmittance ); // deep orange → warm white
  170. sunLight.intensity = 11 * transmittance + 0.3;
  171. sunLight.position.copy( sun ).multiplyScalar( 900 );
  172. sunLight.shadow.needsUpdate = true; // the sun moved, so the on-demand shadow map needs one refresh
  173. // re-bake the sky ( without the sun disc ) into the environment map for IBL.
  174. // the sky lives only in envScene; it is never added to the visible scene
  175. sky.showSunDisc.value = false;
  176. envScene.add( sky );
  177. const env = pmremGenerator.fromScene( envScene ).texture;
  178. if ( scene.environment ) scene.environment.dispose();
  179. scene.environment = env;
  180. }
  181. // a new seed ( and whatever erosion / valley bias is dialled in ) rebuilds
  182. // the terrain, and with it the forest that sits on top
  183. function newSeed() {
  184. terrain.parameters.seed ++;
  185. generate();
  186. }
  187. // the forest sits on the terrain, so a new terrain means a new forest
  188. function generate() {
  189. if ( terrainGroup ) scene.remove( terrainGroup );
  190. if ( forestGroup ) scene.remove( forestGroup );
  191. terrainGroup = terrain.build();
  192. forestGroup = forest.build( terrain );
  193. scene.add( terrainGroup );
  194. scene.add( forestGroup );
  195. 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 )
  196. }
  197. function resize() {
  198. camera.aspect = window.innerWidth / window.innerHeight;
  199. camera.updateProjectionMatrix();
  200. renderer.setSize( window.innerWidth, window.innerHeight );
  201. }
  202. function animate() {
  203. timer.update();
  204. controls.update( timer.getDelta() );
  205. forest.setCameraPosition( camera.position ); // drives the forest cull
  206. renderer.render( scene, camera );
  207. }
  208. </script>
  209. </body>
  210. </html>
粤ICP备19079148号