webgpu_generator_city.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <title>three.js webgpu - city generator</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 - city generator">
  8. <meta property="og:type" content="website">
  9. <meta property="og:url" content="https://threejs.org/examples/webgpu_generator_city.html">
  10. <meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_generator_city.jpg">
  11. <link type="text/css" rel="stylesheet" href="example.css">
  12. </head>
  13. <body>
  14. <div id="info">
  15. <a href="https://threejs.org/" target="_blank" rel="noopener" class="logo-link"></a>
  16. <div class="title-wrapper">
  17. <a href="https://threejs.org/" target="_blank" rel="noopener">three.js</a><span>City Generator</span>
  18. </div>
  19. <small>
  20. A few procedurally generated city blocks of Neo-Gothic terracotta skyscrapers.<br />
  21. Use WASD to move around.
  22. </small>
  23. </div>
  24. <script type="importmap">
  25. {
  26. "imports": {
  27. "three": "../build/three.webgpu.js",
  28. "three/webgpu": "../build/three.webgpu.js",
  29. "three/tsl": "../build/three.tsl.js",
  30. "three/addons/": "./jsm/"
  31. }
  32. }
  33. </script>
  34. <script type="module">
  35. import * as THREE from 'three/webgpu';
  36. import { pass } from 'three/tsl';
  37. import { Inspector } from 'three/addons/inspector/Inspector.js';
  38. import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
  39. import { SkyMesh } from 'three/addons/objects/SkyMesh.js';
  40. import { bloom } from 'three/addons/tsl/display/BloomNode.js';
  41. import { CityGenerator, createBuildingMaterial, createRoadMaterial } from 'three/addons/generators/CityGenerator.js';
  42. import { LightProbeGrid } from 'three/addons/lighting/LightProbeGrid.js';
  43. import { LightProbeGridHelper } from 'three/addons/helpers/LightProbeGridHelper.js';
  44. let camera, scene, renderer, controls, timer;
  45. let cityGroup, cityProxy, materials, city, renderPipeline;
  46. let sky, sun, sunLight, pmremGenerator, envScene, envRenderTarget, cityBounds;
  47. let probes, probesHelper, rebakeTimer = null;
  48. // the irradiance grid spans the whole footprint plus the streets, rising to
  49. // the mid-rise rooftops. diffuse GI is low frequency, so a coarse grid still
  50. // reads the urban canyon: streets sit in shade, walls glow with terracotta bounce
  51. const GRID_SIZE = new THREE.Vector3( 240, 120, 170 );
  52. const GRID_PROBES = new THREE.Vector3( 10, 7, 8 );
  53. const parameters = {
  54. seed: 94,
  55. timeOfDay: 6.4, // hours: 6 sunrise, 12 noon, 18 sunset
  56. gi: true,
  57. showProbes: false
  58. };
  59. init();
  60. async function init() {
  61. renderer = new THREE.WebGPURenderer( { antialias: true } );
  62. renderer.setPixelRatio( window.devicePixelRatio );
  63. renderer.setSize( window.innerWidth, window.innerHeight );
  64. renderer.setAnimationLoop( animate );
  65. renderer.toneMapping = THREE.ACESFilmicToneMapping;
  66. renderer.toneMappingExposure = 0.13;
  67. renderer.shadowMap.enabled = true;
  68. renderer.inspector = new Inspector();
  69. document.body.appendChild( renderer.domElement );
  70. await renderer.init();
  71. pmremGenerator = new THREE.PMREMGenerator( renderer );
  72. scene = new THREE.Scene();
  73. scene.environmentIntensity = 0.05; // a hint of sky in the reflections; the diffuse sky fill comes occlusion-aware from the probe grid
  74. // a physical sky drives both the backdrop and the image-based
  75. // lighting; the time-of-day slider walks the sun along its arc
  76. sky = new SkyMesh();
  77. sky.scale.setScalar( 10000 );
  78. sky.turbidity.value = 8;
  79. sky.rayleigh.value = 3;
  80. sky.mieCoefficient.value = 0.008;
  81. sky.mieDirectionalG.value = 0.88;
  82. sun = new THREE.Vector3();
  83. envScene = new THREE.Scene();
  84. camera = new THREE.PerspectiveCamera( 55, window.innerWidth / window.innerHeight, 1, 20000 );
  85. camera.position.set( - 35, 55, - 100 );
  86. controls = new FirstPersonControls( camera, renderer.domElement );
  87. controls.movementSpeed = 15;
  88. controls.lookSpeed = 0.25;
  89. controls.lookAt( 35, 55, 0 );
  90. timer = new THREE.Timer();
  91. city = new CityGenerator( { seed: parameters.seed } );
  92. // road: wet asphalt with sidewalks, lane lines and crosswalks, all
  93. // aligned to the city grid via TSL. sized to the city footprint plus
  94. // one street ( a crosswalk's width ) of margin all round
  95. const floorW = city.layout.cityW + 2 * city.layout.street;
  96. const floorD = city.layout.cityD + 2 * city.layout.street;
  97. const ground = new THREE.Mesh( new THREE.PlaneGeometry( floorW, floorD ).rotateX( - Math.PI / 2 ), createRoadMaterial( city.layout ) );
  98. ground.receiveShadow = true;
  99. scene.add( ground );
  100. // everything a shadow can touch fits this box: the ground plane up to just
  101. // above the tallest tower crown. updateSun() wraps the shadow camera around it
  102. cityBounds = new THREE.Box3(
  103. new THREE.Vector3( - floorW / 2, 0, - floorD / 2 ),
  104. new THREE.Vector3( floorW / 2, 160, floorD / 2 )
  105. );
  106. // a single directional key aligned with the sky's sun, for the crisp
  107. // relief shadows the IBL alone can't give. its colour, intensity and
  108. // position — and the baked environment — are set by updateSun()
  109. sunLight = new THREE.DirectionalLight();
  110. sunLight.castShadow = true;
  111. sunLight.shadow.mapSize.set( 4096, 4096 );
  112. // acne is suppressed along the surface normal ( in world units ) rather
  113. // than with a depth bias: a depth bias scales with the shadow range, and
  114. // even a small one erases short casters, like the shadow beneath a car
  115. sunLight.shadow.normalBias = 0.05;
  116. scene.add( sunLight );
  117. updateSun();
  118. materials = { building: createBuildingMaterial( city.layout, parameters.seed ) };
  119. generateCity();
  120. // bake the irradiance grid once the city and sun are in place
  121. bakeProbes();
  122. renderPipeline = new THREE.RenderPipeline( renderer );
  123. const scenePass = pass( scene, camera );
  124. const scenePassColor = scenePass.getTextureNode();
  125. // bloom: a soft glow on the brightest areas, at quarter-res since the
  126. // result is heavily blurred anyway
  127. const bloomPass = bloom( scenePassColor, 0.05, 0.0, 0.0 );
  128. bloomPass.setResolutionScale( 0.25 );
  129. renderPipeline.outputNode = scenePassColor.add( bloomPass );
  130. // parameters
  131. const gui = renderer.inspector.createParameters( 'City' );
  132. gui.add( parameters, 'seed', 0, 100, 1 ).onChange( () => {
  133. generateCity();
  134. scheduleRebake();
  135. } );
  136. gui.add( parameters, 'timeOfDay', 6, 18, 0.1 ).name( 'time of day' ).onChange( () => {
  137. updateSun();
  138. scheduleRebake();
  139. } );
  140. gui.add( renderer, 'toneMappingExposure', 0.01, 1 ).name( 'exposure' );
  141. gui.add( parameters, 'gi' ).name( 'global illumination' ).onChange( ( value ) => {
  142. if ( probes ) probes.visible = value;
  143. } );
  144. gui.add( parameters, 'showProbes' ).name( 'show probes' ).onChange( toggleProbesHelper );
  145. window.addEventListener( 'resize', onWindowResize );
  146. }
  147. function updateSun() {
  148. const t = parameters.timeOfDay;
  149. const u = ( t - 12 ) / 6; // -1 at sunrise, 0 at noon, +1 at sunset
  150. const elevation = Math.max( 0, 1 - u * u ) * 72; // degrees above the horizon, peaks at noon
  151. const azimuth = 90 - u * 55; // the sun swings east → west across the day
  152. sun.setFromSphericalCoords(
  153. 1,
  154. THREE.MathUtils.degToRad( 90 - elevation ),
  155. THREE.MathUtils.degToRad( azimuth )
  156. );
  157. sky.sunPosition.value.copy( sun );
  158. // physically-motivated key light: on a real hazy midday the sun delivers
  159. // about three times the sky's irradiance onto a horizontal surface, so it
  160. // is anchored to ~3x the sky dome the probes capture. the longer air path
  161. // near the horizon dims and warms it ( the fill dims with it, as the
  162. // environment and probes are re-baked ).
  163. const sinElevation = Math.sin( THREE.MathUtils.degToRad( elevation ) );
  164. const transmittance = Math.sqrt( Math.max( sinElevation, 0 ) ); // 0 at the horizon → 1 at the zenith
  165. sunLight.color.set( 0xffb072 ).lerp( new THREE.Color( 0xfff4e8 ), transmittance );
  166. sunLight.intensity = 100 * transmittance; // illuminance perpendicular to the rays; the renderer applies N·L per surface
  167. sunLight.position.copy( sun ).multiplyScalar( 600 );
  168. // shrink-wrap the shadow camera around the city for this sun angle: project
  169. // the scene box into light space and fit the ortho bounds and depth range to
  170. // it, so the map spends its texels only where shadows can actually fall
  171. const shadowCamera = sunLight.shadow.camera;
  172. const lightSpace = new THREE.Quaternion().setFromRotationMatrix( new THREE.Matrix4().lookAt( sunLight.position, scene.position, THREE.Object3D.DEFAULT_UP ) ).invert();
  173. const fit = new THREE.Box3();
  174. const corner = new THREE.Vector3();
  175. for ( let i = 0; i < 8; i ++ ) {
  176. corner.set(
  177. i & 1 ? cityBounds.max.x : cityBounds.min.x,
  178. i & 2 ? cityBounds.max.y : cityBounds.min.y,
  179. i & 4 ? cityBounds.max.z : cityBounds.min.z
  180. ).sub( sunLight.position ).applyQuaternion( lightSpace );
  181. fit.expandByPoint( corner );
  182. }
  183. const pad = 2;
  184. shadowCamera.left = fit.min.x - pad;
  185. shadowCamera.right = fit.max.x + pad;
  186. shadowCamera.bottom = fit.min.y - pad;
  187. shadowCamera.top = fit.max.y + pad;
  188. shadowCamera.near = - fit.max.z - pad; // the camera looks down its negative z axis, so depth flips sign
  189. shadowCamera.far = - fit.min.z + pad;
  190. shadowCamera.updateProjectionMatrix();
  191. // re-bake the sky ( without the sun disc ) into the environment map for IBL.
  192. // reuse one render target so the texture keeps its identity: a fresh texture
  193. // would force every material to recompile its shader
  194. sky.showSunDisc.value = false;
  195. envScene.add( sky );
  196. envRenderTarget = pmremGenerator.fromScene( envScene, 0, 0.1, 100, { renderTarget: envRenderTarget } );
  197. scene.environment = envRenderTarget.texture;
  198. sky.showSunDisc.value = true;
  199. scene.add( sky );
  200. }
  201. function generateCity() {
  202. if ( cityGroup ) scene.remove( cityGroup );
  203. if ( cityProxy ) {
  204. scene.remove( cityProxy );
  205. cityProxy.geometry.dispose();
  206. cityProxy.material.dispose();
  207. }
  208. city.parameters.seed = parameters.seed;
  209. cityGroup = city.build( materials );
  210. scene.add( cityGroup );
  211. // the box proxy shares the towers just built; it stands in only during the bake
  212. cityProxy = city.buildProxy();
  213. cityProxy.visible = false;
  214. scene.add( cityProxy );
  215. }
  216. function bakeProbes() {
  217. if ( probes === undefined ) {
  218. probes = new LightProbeGrid( GRID_SIZE.x, GRID_SIZE.y, GRID_SIZE.z, GRID_PROBES.x, GRID_PROBES.y, GRID_PROBES.z );
  219. probes.position.set( 0, GRID_SIZE.y / 2 + 1, 0 ); // bottom probe layer floats just above the road, never in its plane
  220. scene.add( probes );
  221. }
  222. // swap the detailed city for its box proxy, and hide the probe spheres and the
  223. // sun disc, so each cubemap captures only the boxes, ground and sky fill. the
  224. // DirectionalLight supplies the direct sun; a single bounce pass folds the warm
  225. // stone back into the shade. the proxy is a single draw, so the bake stays cheap
  226. if ( probesHelper ) probesHelper.visible = false;
  227. cityGroup.visible = false;
  228. cityProxy.visible = true;
  229. sky.showSunDisc.value = false;
  230. probes.bake( renderer, scene, { cubemapSize: 16, near: 0.1, far: 20000, bounces: 1 } );
  231. sky.showSunDisc.value = true;
  232. cityProxy.visible = false;
  233. cityGroup.visible = true;
  234. probes.visible = parameters.gi;
  235. if ( probesHelper ) {
  236. probesHelper.update();
  237. probesHelper.visible = parameters.showProbes;
  238. }
  239. }
  240. // re-baking walks the whole city cubemap by cubemap, so coalesce the rapid
  241. // onChange calls from dragging a slider into a single bake once it settles
  242. function scheduleRebake() {
  243. if ( rebakeTimer !== null ) clearTimeout( rebakeTimer );
  244. rebakeTimer = setTimeout( () => {
  245. rebakeTimer = null;
  246. bakeProbes();
  247. }, 300 );
  248. }
  249. function toggleProbesHelper( value ) {
  250. if ( probesHelper === undefined ) {
  251. probesHelper = new LightProbeGridHelper( probes, 3 );
  252. scene.add( probesHelper );
  253. }
  254. probesHelper.visible = value;
  255. }
  256. function onWindowResize() {
  257. camera.aspect = window.innerWidth / window.innerHeight;
  258. camera.updateProjectionMatrix();
  259. renderer.setSize( window.innerWidth, window.innerHeight );
  260. }
  261. function animate() {
  262. timer.update();
  263. controls.update( timer.getDelta() );
  264. renderPipeline.render();
  265. }
  266. </script>
  267. </body>
  268. </html>
粤ICP备19079148号