webgpu_custom_fog.html 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  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.shadowMap.type = THREE.PCFSoftShadowMap;
  61. renderer.inspector = new Inspector();
  62. document.body.appendChild( renderer.domElement );
  63. await renderer.init();
  64. pmremGenerator = new THREE.PMREMGenerator( renderer );
  65. camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 20000 );
  66. camera.position.set( - 50, 88, 230 );
  67. scene = new THREE.Scene();
  68. scene.environmentIntensity = 0.16; // a dim, cool sky fill so the warm sun stays the key and shadows read
  69. // a physical sky, used only to bake the image-based lighting ( it is not
  70. // the visible background — the fog gradient below is ); elevation / azimuth move the sun
  71. sky = new SkyMesh();
  72. sky.scale.setScalar( 10000 );
  73. sky.turbidity.value = 12;
  74. sky.rayleigh.value = 2;
  75. sky.mieCoefficient.value = 0.005;
  76. sky.mieDirectionalG.value = 0.88;
  77. sun = new THREE.Vector3();
  78. envScene = new THREE.Scene();
  79. // custom fog. an animated, two-octave triNoise3D haze that settles into
  80. // the valley as a level band: solid below `fogBase` ( down under the
  81. // mountains ) and fading out by `fogTop` ( around mid-height ), so the
  82. // peaks rise clear above it. the band sits at a fixed altitude whatever
  83. // the distance, and the noise wobbles its top edge and drifts it through
  84. // world space, so it reads as slow-moving cloud. ( the peaks reach ~130 )
  85. const skyColorValue = 0xf0f5f5;
  86. const groundColorValue = 0xd0dee7;
  87. const skyColor = color( skyColorValue );
  88. const groundColor = color( groundColorValue );
  89. const fogBase = uniform( - 20 ); // world-y the fog is solid below ( the valley floor )
  90. const fogTop = uniform( 55 ); // world-y the fog fades out by ( mid-mountain )
  91. const haze = uniform( 0.0012 ); // distance haze, so the far peaks dissolve into the same grey
  92. // a alternative way to create a TimerNode
  93. const time = uniform( 0 ).onFrameUpdate( ( frame ) => frame.time );
  94. const fogNoiseA = triNoise3D( positionWorld.mul( .005 ), 0.2, time );
  95. const fogNoiseB = triNoise3D( positionWorld.mul( .01 ), 0.2, time.mul( 1.2 ) );
  96. const fogNoise = fogNoiseA.add( fogNoiseB );
  97. // the noise lifts and drops the top of the band so it breaks into wisps
  98. const top = fogTop.add( fogNoise.sub( 0.7 ).mul( 22 ) );
  99. const groundFogArea = top.sub( positionWorld.y ).div( top.sub( fogBase ) ).saturate().mul( .98 );
  100. // the valley band plus a distance haze, so the far peaks dissolve into the grey too
  101. const fogArea = groundFogArea.oneMinus().mul( densityFogFactor( haze ).oneMinus() ).oneMinus();
  102. scene.fogNode = fog( groundColor, fogArea );
  103. scene.backgroundNode = normalWorld.y.max( 0 ).mix( groundColor, skyColor );
  104. // terrain + forest
  105. terrain = new TerrainGenerator( {
  106. seed: 1,
  107. size: 900,
  108. segments: 512,
  109. frequency: 0.0065,
  110. heightScale: 150,
  111. erosion: 0.7,
  112. valleyBias: 1.2
  113. } );
  114. forest = new ForestGenerator( { count: 500000, castShadow: true } );
  115. generate();
  116. // a single directional key aligned with the sky's sun, for the crisp relief
  117. // shadows the sky fill alone can't give. its colour, intensity and position —
  118. // and the baked environment — are set by updateSun()
  119. sunLight = new THREE.DirectionalLight();
  120. sunLight.castShadow = true;
  121. sunLight.shadow.camera.left = - 420;
  122. sunLight.shadow.camera.right = 420;
  123. sunLight.shadow.camera.top = 420;
  124. sunLight.shadow.camera.bottom = - 420;
  125. sunLight.shadow.camera.near = 200;
  126. sunLight.shadow.camera.far = 1800;
  127. sunLight.shadow.mapSize.set( 4096, 4096 );
  128. sunLight.shadow.bias = - 0.0004;
  129. sunLight.shadow.normalBias = 0.15;
  130. sunLight.shadow.autoUpdate = false; // the scene is static — re-render the shadow map only when the sun moves ( see updateSun ), not every frame
  131. scene.add( sunLight );
  132. updateSun();
  133. // gui
  134. const gui = renderer.inspector.createParameters( 'Settings' );
  135. const skyFolder = gui.addFolder( 'Sun' );
  136. skyFolder.add( parameters, 'elevation', 1, 40 ).step( 0.5 ).name( 'elevation' ).onChange( updateSun );
  137. skyFolder.add( parameters, 'azimuth', 0, 360 ).step( 1 ).name( 'azimuth' ).onChange( updateSun );
  138. const fogFolder = gui.addFolder( 'Fog' );
  139. fogFolder.add( fogBase, 'value', - 40, 20 ).step( 1 ).name( 'base' );
  140. fogFolder.add( fogTop, 'value', 0, 130 ).step( 1 ).name( 'top' );
  141. fogFolder.add( haze, 'value', 0, 0.005 ).step( 0.0001 ).name( 'haze' );
  142. const forestFolder = gui.addFolder( 'Forest' );
  143. forestFolder.add( forest.from, 'value', 50, 1000 ).step( 10 ).name( 'cull from' );
  144. forestFolder.add( forest.to, 'value', 100, 1400 ).step( 10 ).name( 'cull to' );
  145. const terrainFolder = gui.addFolder( 'Terrain' );
  146. terrainFolder.add( terrain.parameters, 'erosion', 0, 1.5 ).step( 0.05 ).name( 'erosion' );
  147. terrainFolder.add( terrain.parameters, 'valleyBias', 1, 3 ).step( 0.1 ).name( 'valley bias' );
  148. terrainFolder.add( { newSeed }, 'newSeed' ).name( 'regenerate' );
  149. // controls
  150. timer = new THREE.Timer();
  151. controls = new FirstPersonControls( camera, renderer.domElement );
  152. controls.movementSpeed = 20;
  153. controls.lookSpeed = 0.1;
  154. controls.lookAt( 0, 5, - 120 ); // face across the valley
  155. window.addEventListener( 'resize', resize );
  156. }
  157. // walks the sun: aligns the sky and the key light ( warm and dim near the
  158. // horizon ), then re-bakes the sky into the IBL environment
  159. function updateSun() {
  160. const elevation = parameters.elevation;
  161. sun.setFromSphericalCoords(
  162. 1,
  163. THREE.MathUtils.degToRad( 90 - elevation ),
  164. THREE.MathUtils.degToRad( parameters.azimuth )
  165. );
  166. sky.sunPosition.value.copy( sun );
  167. // the longer air path near the horizon dims and warms the sun. it stays
  168. // far brighter than the sky fill, so it reads as the key and casts firm shadows
  169. const transmittance = Math.sqrt( Math.max( Math.sin( THREE.MathUtils.degToRad( elevation ) ), 0 ) );
  170. sunLight.color.set( 0xff7a2f ).lerp( new THREE.Color( 0xfff2e0 ), transmittance ); // deep orange → warm white
  171. sunLight.intensity = 11 * transmittance + 0.3;
  172. sunLight.position.copy( sun ).multiplyScalar( 900 );
  173. sunLight.shadow.needsUpdate = true; // the sun moved, so the on-demand shadow map needs one refresh
  174. // re-bake the sky ( without the sun disc ) into the environment map for IBL.
  175. // the sky lives only in envScene; it is never added to the visible scene
  176. sky.showSunDisc.value = false;
  177. envScene.add( sky );
  178. const env = pmremGenerator.fromScene( envScene ).texture;
  179. if ( scene.environment ) scene.environment.dispose();
  180. scene.environment = env;
  181. }
  182. // a new seed ( and whatever erosion / valley bias is dialled in ) rebuilds
  183. // the terrain, and with it the forest that sits on top
  184. function newSeed() {
  185. terrain.parameters.seed ++;
  186. generate();
  187. }
  188. // the forest sits on the terrain, so a new terrain means a new forest
  189. function generate() {
  190. if ( terrainGroup ) scene.remove( terrainGroup );
  191. if ( forestGroup ) scene.remove( forestGroup );
  192. terrainGroup = terrain.build();
  193. forestGroup = forest.build( terrain );
  194. scene.add( terrainGroup );
  195. scene.add( forestGroup );
  196. 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 )
  197. }
  198. function resize() {
  199. camera.aspect = window.innerWidth / window.innerHeight;
  200. camera.updateProjectionMatrix();
  201. renderer.setSize( window.innerWidth, window.innerHeight );
  202. }
  203. function animate() {
  204. timer.update();
  205. controls.update( timer.getDelta() );
  206. forest.setCameraPosition( camera.position ); // drives the forest cull
  207. renderer.render( scene, camera );
  208. }
  209. </script>
  210. </body>
  211. </html>
粤ICP备19079148号