| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376 |
- <!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.<br />
- Use WASD to move around.
- </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';
- import { LightProbeGrid } from 'three/addons/lighting/LightProbeGrid.js';
- import { LightProbeGridHelper } from 'three/addons/helpers/LightProbeGridHelper.js';
- let camera, scene, renderer, controls, timer;
- let cityGroup, cityProxy, materials, city, renderPipeline;
- let sky, sun, sunLight, pmremGenerator, envScene, envRenderTarget, cityBounds;
- let probes, probesHelper, rebakeTimer = null;
- // the irradiance grid spans the whole footprint plus the streets, rising to
- // the mid-rise rooftops. diffuse GI is low frequency, so a coarse grid still
- // reads the urban canyon: streets sit in shade, walls glow with terracotta bounce
- const GRID_SIZE = new THREE.Vector3( 240, 120, 170 );
- const GRID_PROBES = new THREE.Vector3( 10, 7, 8 );
- const parameters = {
- seed: 94,
- timeOfDay: 6.4, // hours: 6 sunrise, 12 noon, 18 sunset
- gi: true,
- showProbes: false
- };
- 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.13;
- renderer.shadowMap.enabled = true;
- renderer.inspector = new Inspector();
- document.body.appendChild( renderer.domElement );
- await renderer.init();
- pmremGenerator = new THREE.PMREMGenerator( renderer );
- scene = new THREE.Scene();
- scene.environmentIntensity = 0.05; // a hint of sky in the reflections; the diffuse sky fill comes occlusion-aware from the probe grid
- // 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 );
- // everything a shadow can touch fits this box: the ground plane up to just
- // above the tallest tower crown. updateSun() wraps the shadow camera around it
- cityBounds = new THREE.Box3(
- new THREE.Vector3( - floorW / 2, 0, - floorD / 2 ),
- new THREE.Vector3( floorW / 2, 160, floorD / 2 )
- );
- // 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.mapSize.set( 4096, 4096 );
- // acne is suppressed along the surface normal ( in world units ) rather
- // than with a depth bias: a depth bias scales with the shadow range, and
- // even a small one erases short casters, like the shadow beneath a car
- sunLight.shadow.normalBias = 0.05;
- scene.add( sunLight );
- updateSun();
- materials = { building: createBuildingMaterial( city.layout, parameters.seed ) };
- generateCity();
- // bake the irradiance grid once the city and sun are in place
- bakeProbes();
- renderPipeline = new THREE.RenderPipeline( renderer );
- const scenePass = pass( scene, camera );
- const scenePassColor = scenePass.getTextureNode();
- // bloom: a soft glow on the brightest areas, at quarter-res since the
- // result is heavily blurred anyway
- const bloomPass = bloom( scenePassColor, 0.05, 0.0, 0.0 );
- bloomPass.setResolutionScale( 0.25 );
- renderPipeline.outputNode = scenePassColor.add( bloomPass );
- // parameters
- const gui = renderer.inspector.createParameters( 'City' );
- gui.add( parameters, 'seed', 0, 100, 1 ).onChange( () => {
- generateCity();
- scheduleRebake();
- } );
- gui.add( parameters, 'timeOfDay', 6, 18, 0.1 ).name( 'time of day' ).onChange( () => {
- updateSun();
- scheduleRebake();
- } );
- gui.add( renderer, 'toneMappingExposure', 0.01, 1 ).name( 'exposure' );
- gui.add( parameters, 'gi' ).name( 'global illumination' ).onChange( ( value ) => {
- if ( probes ) probes.visible = value;
- } );
- gui.add( parameters, 'showProbes' ).name( 'show probes' ).onChange( toggleProbesHelper );
- 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: on a real hazy midday the sun delivers
- // about three times the sky's irradiance onto a horizontal surface, so it
- // is anchored to ~3x the sky dome the probes capture. the longer air path
- // near the horizon dims and warms it ( the fill dims with it, as the
- // environment and probes are re-baked ).
- 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 = 100 * transmittance; // illuminance perpendicular to the rays; the renderer applies N·L per surface
- sunLight.position.copy( sun ).multiplyScalar( 600 );
- // shrink-wrap the shadow camera around the city for this sun angle: project
- // the scene box into light space and fit the ortho bounds and depth range to
- // it, so the map spends its texels only where shadows can actually fall
- const shadowCamera = sunLight.shadow.camera;
- const lightSpace = new THREE.Quaternion().setFromRotationMatrix( new THREE.Matrix4().lookAt( sunLight.position, scene.position, THREE.Object3D.DEFAULT_UP ) ).invert();
- const fit = new THREE.Box3();
- const corner = new THREE.Vector3();
- for ( let i = 0; i < 8; i ++ ) {
- corner.set(
- i & 1 ? cityBounds.max.x : cityBounds.min.x,
- i & 2 ? cityBounds.max.y : cityBounds.min.y,
- i & 4 ? cityBounds.max.z : cityBounds.min.z
- ).sub( sunLight.position ).applyQuaternion( lightSpace );
- fit.expandByPoint( corner );
- }
- const pad = 2;
- shadowCamera.left = fit.min.x - pad;
- shadowCamera.right = fit.max.x + pad;
- shadowCamera.bottom = fit.min.y - pad;
- shadowCamera.top = fit.max.y + pad;
- shadowCamera.near = - fit.max.z - pad; // the camera looks down its negative z axis, so depth flips sign
- shadowCamera.far = - fit.min.z + pad;
- shadowCamera.updateProjectionMatrix();
- // re-bake the sky ( without the sun disc ) into the environment map for IBL.
- // reuse one render target so the texture keeps its identity: a fresh texture
- // would force every material to recompile its shader
- sky.showSunDisc.value = false;
- envScene.add( sky );
- envRenderTarget = pmremGenerator.fromScene( envScene, 0, 0.1, 100, { renderTarget: envRenderTarget } );
- scene.environment = envRenderTarget.texture;
- sky.showSunDisc.value = true;
- scene.add( sky );
- }
- function generateCity() {
- if ( cityGroup ) scene.remove( cityGroup );
- if ( cityProxy ) {
- scene.remove( cityProxy );
- cityProxy.geometry.dispose();
- cityProxy.material.dispose();
- }
- city.parameters.seed = parameters.seed;
- cityGroup = city.build( materials );
- scene.add( cityGroup );
- // the box proxy shares the towers just built; it stands in only during the bake
- cityProxy = city.buildProxy();
- cityProxy.visible = false;
- scene.add( cityProxy );
- }
- function bakeProbes() {
- if ( probes === undefined ) {
- probes = new LightProbeGrid( GRID_SIZE.x, GRID_SIZE.y, GRID_SIZE.z, GRID_PROBES.x, GRID_PROBES.y, GRID_PROBES.z );
- probes.position.set( 0, GRID_SIZE.y / 2 + 1, 0 ); // bottom probe layer floats just above the road, never in its plane
- scene.add( probes );
- }
- // swap the detailed city for its box proxy, and hide the probe spheres and the
- // sun disc, so each cubemap captures only the boxes, ground and sky fill. the
- // DirectionalLight supplies the direct sun; a single bounce pass folds the warm
- // stone back into the shade. the proxy is a single draw, so the bake stays cheap
- if ( probesHelper ) probesHelper.visible = false;
- cityGroup.visible = false;
- cityProxy.visible = true;
- sky.showSunDisc.value = false;
- probes.bake( renderer, scene, { cubemapSize: 16, near: 0.1, far: 20000, bounces: 1 } );
- sky.showSunDisc.value = true;
- cityProxy.visible = false;
- cityGroup.visible = true;
- probes.visible = parameters.gi;
- if ( probesHelper ) {
- probesHelper.update();
- probesHelper.visible = parameters.showProbes;
- }
- }
- // re-baking walks the whole city cubemap by cubemap, so coalesce the rapid
- // onChange calls from dragging a slider into a single bake once it settles
- function scheduleRebake() {
- if ( rebakeTimer !== null ) clearTimeout( rebakeTimer );
- rebakeTimer = setTimeout( () => {
- rebakeTimer = null;
- bakeProbes();
- }, 300 );
- }
- function toggleProbesHelper( value ) {
- if ( probesHelper === undefined ) {
- probesHelper = new LightProbeGridHelper( probes, 3 );
- scene.add( probesHelper );
- }
- probesHelper.visible = value;
- }
- 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>
|