CityGenerator.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. import {
  2. Group,
  3. Matrix4
  4. } from 'three';
  5. import { MeshStandardNodeMaterial } from 'three/webgpu';
  6. import { cameraPosition, color, float, floor, Fn, fract, fwidth, hash, If, mix, mod, mx_fractal_noise_float, mx_noise_float, normalView, positionView, positionWorld, smoothstep, step, uint, varying, vec4 } from 'three/tsl';
  7. import { SkyscraperGenerator, createSkyscraperMaterial, buildingPalette } from './city/SkyscraperGenerator.js';
  8. import { SidewalkGenerator } from './city/SidewalkGenerator.js';
  9. /**
  10. * Lays out a grid of city blocks and fills each lot with a {@link SkyscraperGenerator}
  11. * tower of its own seed, height and footprint, optionally on raised sidewalk
  12. * slabs (curbs). Returns a `THREE.Group` ready to add to a scene.
  13. *
  14. * Pass a building material to dress the towers; the sidewalks dress themselves
  15. * via {@link SidewalkGenerator}. The layout is exposed as
  16. * {@link CityGenerator#layout} so the surrounding scene (road markings, etc.)
  17. * can align to the same grid.
  18. *
  19. * ```js
  20. * const city = new CityGenerator( { seed: 1 } );
  21. * scene.add( city.build( materials ) );
  22. * ```
  23. */
  24. class CityGenerator {
  25. constructor( parameters = {} ) {
  26. this.parameters = Object.assign( {}, CityGenerator.defaults, parameters );
  27. this.layout = cityLayout( this.parameters );
  28. this.generators = [];
  29. this.sidewalk = new SidewalkGenerator( {
  30. width: this.layout.blockW,
  31. depth: this.layout.blockD,
  32. height: this.parameters.curbHeight,
  33. radius: this.parameters.curbRadius
  34. } );
  35. this.group = null;
  36. }
  37. build( materials = {} ) {
  38. this.dispose();
  39. const group = new Group();
  40. group.name = 'City';
  41. const L = this.layout;
  42. const random = createRandom( this.parameters.seed );
  43. // raise the lots onto rounded sidewalk slabs ( curbs ) when curbHeight > 0
  44. const curb = this.parameters.curbHeight;
  45. const slabs = [];
  46. for ( let bx = 0; bx < L.blocksX; bx ++ ) {
  47. for ( let bz = 0; bz < L.blocksZ; bz ++ ) {
  48. const blockX = - L.cityW / 2 + bx * ( L.blockW + L.street );
  49. const blockZ = - L.cityD / 2 + bz * ( L.blockD + L.street );
  50. if ( curb > 0 ) {
  51. slabs.push( new Matrix4().makeTranslation( blockX + L.blockW / 2, 0, blockZ + L.blockD / 2 ) );
  52. }
  53. for ( let lx = 0; lx < L.lotsX; lx ++ ) {
  54. for ( let lz = 0; lz < L.lotsZ; lz ++ ) {
  55. // a chamfered corner only reads as architecture when it faces the
  56. // block's corner ( the street intersection ), so only the four corner
  57. // lots are cut, each toward its own outward corner; the rest stay square
  58. const cornerX = lx === 0 ? - 1 : ( lx === L.lotsX - 1 ? 1 : 0 );
  59. const cornerZ = lz === 0 ? - 1 : ( lz === L.lotsZ - 1 ? 1 : 0 );
  60. const onCorner = cornerX !== 0 && cornerZ !== 0;
  61. const tall = random();
  62. const generator = new SkyscraperGenerator( {
  63. seed: Math.floor( random() * 100000 ),
  64. totalHeight: 38 + tall * tall * 114, // a few tall towers, mostly mid-rise
  65. footprint: { width: L.lot - 1 - random() * 4, depth: L.lot - 1 - random() * 4 }, // nearly fill the lot so neighbours sit close; width and depth vary independently
  66. floorHeight: 3.4 + random() * 1.8,
  67. bayWidth: 1.9 + random() * 2.1,
  68. pierWidth: 0.4 + random() * 0.5,
  69. pierDepth: 0.3 + random() * 0.4,
  70. chamferWidth: onCorner ? 3 + random() * 4 : 0,
  71. chamferCornerX: cornerX,
  72. chamferCornerZ: cornerZ,
  73. setbackDepth: random() < 0.4 ? 0.8 + random() * 2 : 0, // only some towers step back at the crown; the rest rise flat
  74. stringCourseEvery: random() < 0.85 ? 3 + Math.floor( random() * 6 ) : 0
  75. }, materials.building );
  76. const building = generator.build();
  77. building.position.set( blockX + ( lx + 0.5 ) * L.lot, curb, blockZ + ( lz + 0.5 ) * L.lot );
  78. building.castShadow = building.receiveShadow = true;
  79. group.add( building );
  80. this.generators.push( generator );
  81. }
  82. }
  83. }
  84. }
  85. if ( slabs.length > 0 ) group.add( this.sidewalk.build( slabs ) );
  86. this.group = group;
  87. return group;
  88. }
  89. dispose() {
  90. for ( const generator of this.generators ) generator.dispose();
  91. this.generators.length = 0;
  92. this.sidewalk.dispose();
  93. this.group = null;
  94. }
  95. }
  96. CityGenerator.defaults = {
  97. seed: 1,
  98. street: 22,
  99. lot: 30,
  100. lotsX: 3,
  101. lotsZ: 2,
  102. blocksX: 2,
  103. blocksZ: 2,
  104. curbHeight: 0.15, // ~6 in standard curb reveal / sidewalk height above the road
  105. curbRadius: 5
  106. };
  107. // derives the block / street dimensions from the parameters
  108. function cityLayout( parameters ) {
  109. const { street, lot, lotsX, lotsZ, blocksX, blocksZ } = parameters;
  110. const blockW = lotsX * lot;
  111. const blockD = lotsZ * lot;
  112. return {
  113. street, lot, lotsX, lotsZ, blocksX, blocksZ, blockW, blockD,
  114. cityW: blocksX * blockW + ( blocksX - 1 ) * street,
  115. cityD: blocksZ * blockD + ( blocksZ - 1 ) * street
  116. };
  117. }
  118. // deterministic PRNG (mulberry32) so a seed always lays out the same city
  119. function createRandom( seed ) {
  120. let s = ( seed >>> 0 ) || 1;
  121. return function () {
  122. s = ( s + 0x6D2B79F5 ) | 0;
  123. let t = Math.imul( s ^ ( s >>> 15 ), 1 | s );
  124. t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t;
  125. return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296;
  126. };
  127. }
  128. // --- road material -------------------------------------------------------
  129. // derivative-based bump for a procedural, world-space height field. the built-in bumpMap
  130. // offsets the UV to read its height, so it returns a zero gradient for a height keyed off
  131. // world position; this feeds the hardware screen-space derivatives of the height into
  132. // Mikkelsen's surface-gradient method so the relief actually perturbs the normal.
  133. function bumpNormal( height ) {
  134. const dpdx = positionView.dFdx();
  135. const dpdy = positionView.dFdy();
  136. const r1 = dpdy.cross( normalView );
  137. const r2 = normalView.cross( dpdx );
  138. const det = dpdx.dot( r1 );
  139. const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) );
  140. return det.abs().mul( normalView ).sub( grad ).normalize();
  141. }
  142. // antialiased filled band: 1 where |coord| < halfWidth, edge sized to the
  143. // pixel footprint ( fwidth ) so thin road paint stays crisp and doesn't shimmer
  144. function lineAA( coord, halfWidth ) {
  145. const aa = fwidth( coord ).max( 0.0001 );
  146. return smoothstep( float( halfWidth ).add( aa ), float( halfWidth ).sub( aa ), coord.abs() );
  147. }
  148. // the same, repeated at every multiple of `period` ( stripes, joints )
  149. function gridLine( coord, period, halfWidth ) {
  150. const g = coord.div( period );
  151. const d = float( 0.5 ).sub( fract( g ).sub( 0.5 ).abs() ); // distance to nearest line, in periods
  152. const aa = fwidth( g ).max( 0.0001 );
  153. const hw = halfWidth / period;
  154. return smoothstep( float( hw ).add( aa ), float( hw ).sub( aa ), d );
  155. }
  156. /**
  157. * The shared material every tower in a {@link CityGenerator} is dressed with: one flat
  158. * masonry colour per lot, picked from a palette by hashing the lot's grid cell.
  159. */
  160. function createBuildingMaterial( layout, seed = 0 ) {
  161. // every tower takes one flat colour, picked by hashing its lot — one shared material
  162. // dresses the whole skyline; common tones repeat so the equal-probability pick feels real
  163. const palette = buildingPalette.map( hex => color( hex ) );
  164. const periodX = layout.blockW + layout.street;
  165. const periodZ = layout.blockD + layout.street;
  166. const gx = positionWorld.x.add( layout.cityW / 2 );
  167. const gz = positionWorld.z.add( layout.cityD / 2 );
  168. const blockIX = floor( gx.div( periodX ) );
  169. const blockIZ = floor( gz.div( periodZ ) );
  170. const cellX = blockIX.mul( layout.lotsX ).add( floor( gx.sub( blockIX.mul( periodX ) ).div( layout.lot ) ) );
  171. const cellZ = blockIZ.mul( layout.lotsZ ).add( floor( gz.sub( blockIZ.mul( periodZ ) ).div( layout.lot ) ) );
  172. const cellKey = uint( cellX.add( 4096 ) ).mul( uint( 73856093 ) ).bitXor( uint( cellZ.add( 4096 ) ).mul( uint( 19349663 ) ) ).bitXor( uint( ( seed * 2654435761 ) >>> 0 ) ).toVar();
  173. const cellHash = ( a, b ) => hash( cellKey.add( uint( Math.round( ( a + b * 7 ) * 100 ) ) ) );
  174. const pick = cellHash( 127.1, 311.7 );
  175. let buildingBase = palette[ 0 ];
  176. for ( let i = 1; i < palette.length; i ++ ) buildingBase = mix( buildingBase, palette[ i ], step( i / palette.length, pick ) );
  177. buildingBase = buildingBase.mul( cellHash( 269.5, 183.3 ).mul( 0.12 ).add( 0.94 ) ); // subtle per-building brightness
  178. // the pick is constant across a tower, so resolve it once per vertex ( varying )
  179. return createSkyscraperMaterial( varying( buildingBase ) );
  180. }
  181. /**
  182. * The road surface: wet asphalt with lane lines and crosswalks aligned to a
  183. * {@link CityGenerator} layout. Apply it to a ground plane sized to the city.
  184. */
  185. function createRoadMaterial( layout ) {
  186. // wet asphalt: a warm-grey base in patchwork pours, two-scale aggregate
  187. // grit, oily wear stains, hairline cracks and low-frequency wet patches
  188. // that turn glossy and mirror the sky. detail fades in as the camera nears.
  189. const p = positionWorld;
  190. const detail = smoothstep( 240, 25, p.distance( cameraPosition ) );
  191. const blotch = mx_fractal_noise_float( p.mul( 0.2 ), 3 ).mul( 0.5 ).add( 0.5 );
  192. // close-range detail — aggregate grit, oily wear pools, hairline cracks and worn
  193. // paint — only resolves near the camera, so its noise is sampled ( inside the branch )
  194. // only where detail is non-zero and skipped across the far majority of the road
  195. const near = Fn( () => {
  196. const grit = float( 0 ).toVar(); // two scales of aggregate, -1..1
  197. const stain = float( 0 ).toVar(); // oily wear pools
  198. const crack = float( 0 ).toVar();
  199. const worn = float( 1 ).toVar(); // paint rubbed thin and patchy, more so where tyres cross it
  200. If( detail.greaterThan( 0 ), () => {
  201. grit.assign( mx_noise_float( p.mul( 7 ) ).add( mx_noise_float( p.mul( 23 ) ) ).mul( 0.5 ) );
  202. stain.assign( smoothstep( 0.5, 0.85, mx_fractal_noise_float( p.mul( 0.45 ), 3 ).mul( 0.5 ).add( 0.5 ) ) );
  203. crack.assign( smoothstep( 0.88, 1, mx_fractal_noise_float( p.mul( 1.1 ), 4 ).abs().oneMinus() ).mul( detail ) );
  204. worn.assign( smoothstep( 0.25, 0.7, mx_fractal_noise_float( p.mul( 0.7 ), 3 ).mul( 0.5 ).add( 0.5 ) ).mul( 0.55 ).add( 0.35 ) );
  205. } );
  206. return vec4( grit, stain, crack, worn );
  207. } )();
  208. const grit = near.x;
  209. const stain = near.y;
  210. const crack = near.z;
  211. const worn = near.w;
  212. const base = mix( color( 0x24262b ), color( 0x3b3f46 ), blotch );
  213. const gritty = base.mul( grit.mul( 0.22 ).mul( detail ).add( 1 ) );
  214. const asphalt = mix( gritty, gritty.mul( 0.5 ), stain.mul( 0.5 ).mul( detail ) );
  215. const wet = smoothstep( 0.6, 0.85, mx_fractal_noise_float( p.mul( 0.14 ), 2 ).mul( 0.5 ).add( 0.5 ) );
  216. // markings, aligned to the block / street grid. fx, fz are the position
  217. // within one block+street period; the street is the [ blockW, period ) part.
  218. const periodX = layout.blockW + layout.street;
  219. const periodZ = layout.blockD + layout.street;
  220. const fx = mod( p.x.add( layout.cityW / 2 ), periodX );
  221. const fz = mod( p.z.add( layout.cityD / 2 ), periodZ );
  222. const inStreetX = step( layout.blockW, fx ); // in a vertical street ( gap in X )
  223. const inStreetZ = step( layout.blockD, fz ); // in a horizontal street ( gap in Z )
  224. const su = fx.sub( layout.blockW ); // across the vertical street
  225. const sv = fz.sub( layout.blockD ); // across the horizontal street
  226. // lane markings down each street ( not through intersections ): a solid
  227. // centre line splitting the two directions, with a dashed divider in each
  228. // half, so every street carries four lanes
  229. const dashV = step( fract( p.z.div( 7 ) ), 0.5 );
  230. const dashH = step( fract( p.x.div( 7 ) ), 0.5 );
  231. const centreV = lineAA( su.sub( layout.street / 2 ), 0.12 );
  232. const dividerV = lineAA( su.sub( layout.street / 4 ), 0.1 ).max( lineAA( su.sub( layout.street * 3 / 4 ), 0.1 ) ).mul( dashV );
  233. const laneV = centreV.max( dividerV ).mul( inStreetX ).mul( inStreetZ.oneMinus() );
  234. const centreH = lineAA( sv.sub( layout.street / 2 ), 0.12 );
  235. const dividerH = lineAA( sv.sub( layout.street / 4 ), 0.1 ).max( lineAA( sv.sub( layout.street * 3 / 4 ), 0.1 ) ).mul( dashH );
  236. const laneH = centreH.max( dividerH ).mul( inStreetZ ).mul( inStreetX.oneMinus() );
  237. // continental crosswalk bars ( long in the travel direction ) in each
  238. // street arm, near the block edges it meets
  239. const cw = 5;
  240. const nearZ = step( fz, cw ).max( step( layout.blockD - cw, fz ) );
  241. const nearX = step( fx, cw ).max( step( layout.blockW - cw, fx ) );
  242. const crossV = gridLine( su, 1.2, 0.38 ).mul( inStreetX ).mul( inStreetZ.oneMinus() ).mul( nearZ );
  243. const crossH = gridLine( sv, 1.2, 0.38 ).mul( inStreetZ ).mul( inStreetX.oneMinus() ).mul( nearX );
  244. const paint = laneV.max( laneH ).max( crossV ).max( crossH ).mul( detail ).mul( worn );
  245. const material = new MeshStandardNodeMaterial();
  246. const surface = mix( asphalt, asphalt.mul( 0.6 ), wet ).mul( crack.mul( 0.5 ).oneMinus() );
  247. material.colorNode = mix( surface, color( 0xd0ccc0 ), paint ); // worn white paint
  248. material.roughnessNode = mix( float( 0.95 ).sub( paint.mul( 0.2 ) ), float( 0.32 ), wet );
  249. material.normalNode = bumpNormal( grit.mul( 0.003 ).sub( crack.mul( 0.01 ) ).mul( detail ) ); // world units: ~3 mm aggregate, ~10 mm cracks
  250. return material;
  251. }
  252. export { CityGenerator, createBuildingMaterial, createRoadMaterial };
粤ICP备19079148号