| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <title>three.js webgpu - city generator</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 - city generator">
- <meta property="og:type" content="website">
- <meta property="og:url" content="https://threejs.org/examples/webgpu_generator_city.html">
- <meta property="og:image" content="https://threejs.org/examples/screenshots/webgpu_generator_city.jpg">
- <link type="text/css" rel="stylesheet" href="example.css">
- </head>
- <body>
- <div id="info">
- <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>City Generator</span>
- </div>
- <small>
- A few procedurally generated city blocks of Neo-Gothic terracotta skyscrapers at sunset.
- </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 { pass } from 'three/tsl';
- import { Inspector } from 'three/addons/inspector/Inspector.js';
- import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
- import { SkyMesh } from 'three/addons/objects/SkyMesh.js';
- import { bloom } from 'three/addons/tsl/display/BloomNode.js';
- import { CityGenerator, createBuildingMaterial, createRoadMaterial } from 'three/addons/generators/CityGenerator.js';
- let camera, scene, renderer, controls, timer;
- let cityGroup, materials, city, renderPipeline;
- let sky, sun, sunLight, pmremGenerator, envScene;
- const parameters = {
- seed: 94,
- timeOfDay: 6.4 // hours: 6 sunrise, 12 noon, 18 sunset
- };
- 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.45;
- 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 );
- scene = new THREE.Scene();
- scene.environmentIntensity = 0.2; // sky as a soft fill; the sun is the key
- // a physical sky drives both the backdrop and the image-based
- // lighting; the time-of-day slider walks the sun along its arc
- sky = new SkyMesh();
- sky.scale.setScalar( 10000 );
- sky.turbidity.value = 8;
- sky.rayleigh.value = 3;
- sky.mieCoefficient.value = 0.008;
- sky.mieDirectionalG.value = 0.88;
- sun = new THREE.Vector3();
- envScene = new THREE.Scene();
- camera = new THREE.PerspectiveCamera( 55, window.innerWidth / window.innerHeight, 1, 20000 );
- camera.position.set( - 35, 55, - 100 );
- controls = new FirstPersonControls( camera, renderer.domElement );
- controls.movementSpeed = 15;
- controls.lookSpeed = 0.25;
- controls.lookAt( 35, 55, 0 );
- timer = new THREE.Timer();
- city = new CityGenerator( { seed: parameters.seed } );
- // road: wet asphalt with sidewalks, lane lines and crosswalks, all
- // aligned to the city grid via TSL. sized to the city footprint plus
- // one street ( a crosswalk's width ) of margin all round
- const floorW = city.layout.cityW + 2 * city.layout.street;
- const floorD = city.layout.cityD + 2 * city.layout.street;
- const ground = new THREE.Mesh( new THREE.PlaneGeometry( floorW, floorD ).rotateX( - Math.PI / 2 ), createRoadMaterial( city.layout ) );
- ground.receiveShadow = true;
- scene.add( ground );
- // a single directional key aligned with the sky's sun, for the crisp
- // relief shadows the IBL 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 = - 280;
- sunLight.shadow.camera.right = 280;
- sunLight.shadow.camera.top = 360;
- sunLight.shadow.camera.bottom = - 40;
- sunLight.shadow.camera.far = 2400;
- sunLight.shadow.mapSize.set( 4096, 4096 );
- sunLight.shadow.bias = - 0.0004;
- scene.add( sunLight );
- updateSun();
- materials = { building: createBuildingMaterial( city.layout, parameters.seed ) };
- generateCity();
- // bloom: a soft glow on the brightest areas, run on the linear HDR scene
- // colour before tone mapping folds it back to display range
- renderPipeline = new THREE.RenderPipeline( renderer );
- const scenePassColor = pass( scene, camera ).getTextureNode( 'output' );
- const bloomPass = bloom( scenePassColor, 0.05, 0.0, 0.0 );
- renderPipeline.outputNode = scenePassColor.add( bloomPass );
- // parameters
- const gui = renderer.inspector.createParameters( 'City' );
- gui.add( parameters, 'seed', 0, 100, 1 ).onChange( generateCity );
- gui.add( parameters, 'timeOfDay', 6, 18, 0.1 ).name( 'time of day' ).onChange( updateSun );
- gui.add( renderer, 'toneMappingExposure', 0.01, 1 ).name( 'exposure' );
- gui.add( bloomPass.strength, 'value', 0, 2 ).name( 'bloom' );
- window.addEventListener( 'resize', onWindowResize );
- }
- function updateSun() {
- const t = parameters.timeOfDay;
- const u = ( t - 12 ) / 6; // -1 at sunrise, 0 at noon, +1 at sunset
- const elevation = Math.max( 0, 1 - u * u ) * 72; // degrees above the horizon, peaks at noon
- const azimuth = 90 - u * 55; // the sun swings east → west across the day
- sun.setFromSphericalCoords(
- 1,
- THREE.MathUtils.degToRad( 90 - elevation ),
- THREE.MathUtils.degToRad( azimuth )
- );
- sky.sunPosition.value.copy( sun );
- // physically-motivated key light: real sunlight is far brighter than the
- // sky, so the sun stays the dominant source and cast shadows read clearly
- // against the soft sky fill. the longer air path near the horizon dims and
- // warms it ( the fill dims with it, as the environment is re-baked below ).
- const sinElevation = Math.sin( THREE.MathUtils.degToRad( elevation ) );
- const transmittance = Math.sqrt( Math.max( sinElevation, 0 ) ); // 0 at the horizon → 1 at the zenith
- sunLight.color.set( 0xffb072 ).lerp( new THREE.Color( 0xfff4e8 ), transmittance );
- sunLight.intensity = 6 * transmittance; // illuminance perpendicular to the rays; the renderer applies N·L per surface
- sunLight.position.copy( sun ).multiplyScalar( 600 );
- // re-bake the sky ( without the sun disc ) into the environment map for IBL
- sky.showSunDisc.value = false;
- envScene.add( sky );
- const env = pmremGenerator.fromScene( envScene ).texture;
- if ( scene.environment ) scene.environment.dispose();
- scene.environment = env;
- sky.showSunDisc.value = true;
- scene.add( sky );
- }
- function generateCity() {
- if ( cityGroup ) scene.remove( cityGroup );
- city.parameters.seed = parameters.seed;
- cityGroup = city.build( materials );
- scene.add( cityGroup );
- }
- function onWindowResize() {
- camera.aspect = window.innerWidth / window.innerHeight;
- camera.updateProjectionMatrix();
- renderer.setSize( window.innerWidth, window.innerHeight );
- }
- function animate() {
- timer.update();
- controls.update( timer.getDelta() );
- renderPipeline.render();
- }
- </script>
- </body>
- </html>
|