1
0

CityGenerator.js 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673
  1. import {
  2. BoxGeometry,
  3. Group,
  4. InstancedMesh,
  5. Matrix4,
  6. Quaternion,
  7. Vector3
  8. } from 'three';
  9. import { MeshStandardNodeMaterial } from 'three/webgpu';
  10. 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';
  11. import { SkyscraperGenerator, createSkyscraperMaterial, buildingPalette } from './city/SkyscraperGenerator.js';
  12. import { SidewalkGenerator } from './city/SidewalkGenerator.js';
  13. import { StreetlightGenerator } from './city/StreetlightGenerator.js';
  14. import { TrafficlightGenerator } from './city/TrafficlightGenerator.js';
  15. import { TrashcanGenerator } from './city/TrashcanGenerator.js';
  16. import { BenchGenerator } from './city/BenchGenerator.js';
  17. import { HydrantGenerator } from './city/HydrantGenerator.js';
  18. import { StreetTreeGenerator } from './city/StreetTreeGenerator.js';
  19. import { CarGenerator } from './city/CarGenerator.js';
  20. import { PersonGenerator } from './city/PersonGenerator.js';
  21. /**
  22. * Lays out a grid of city blocks and fills each lot with a {@link SkyscraperGenerator}
  23. * tower of its own seed, height and footprint, optionally on raised sidewalk
  24. * slabs (curbs). Returns a `THREE.Group` ready to add to a scene.
  25. *
  26. * Pass a building material to dress the towers; the sidewalks dress themselves
  27. * via {@link SidewalkGenerator}. The layout is exposed as
  28. * {@link CityGenerator#layout} so the surrounding scene (road markings, etc.)
  29. * can align to the same grid.
  30. *
  31. * ```js
  32. * const city = new CityGenerator( { seed: 1 } );
  33. * scene.add( city.build( materials ) );
  34. * ```
  35. */
  36. class CityGenerator {
  37. constructor( parameters = {} ) {
  38. this.parameters = Object.assign( {}, CityGenerator.defaults, parameters );
  39. this.layout = cityLayout( this.parameters );
  40. this.generators = [];
  41. // per-tower box specs recorded during build(), consumed by buildProxy()
  42. this.towers = [];
  43. this.sidewalk = new SidewalkGenerator( {
  44. width: this.layout.blockW,
  45. depth: this.layout.blockD,
  46. height: this.parameters.curbHeight,
  47. radius: this.parameters.curbRadius
  48. } );
  49. // the street-furniture generators, each instanced across placements the
  50. // layout hands them ( see buildFurniture )
  51. this.furniture = {
  52. streetlight: new StreetlightGenerator(),
  53. trafficlight: new TrafficlightGenerator(),
  54. trashcan: new TrashcanGenerator(),
  55. bench: new BenchGenerator(),
  56. hydrant: new HydrantGenerator(),
  57. tree: new StreetTreeGenerator(),
  58. car: new CarGenerator(),
  59. person: new PersonGenerator()
  60. };
  61. this.group = null;
  62. }
  63. build( materials = {} ) {
  64. this.dispose();
  65. const group = new Group();
  66. group.name = 'City';
  67. this.towers = [];
  68. const L = this.layout;
  69. const random = createRandom( this.parameters.seed );
  70. // raise the lots onto rounded sidewalk slabs ( curbs ) when curbHeight > 0
  71. const curb = this.parameters.curbHeight;
  72. const sw = this.parameters.sidewalkWidth;
  73. const slabs = [];
  74. for ( let bx = 0; bx < L.blocksX; bx ++ ) {
  75. for ( let bz = 0; bz < L.blocksZ; bz ++ ) {
  76. const blockX = - L.cityW / 2 + bx * ( L.blockW + L.street );
  77. const blockZ = - L.cityD / 2 + bz * ( L.blockD + L.street );
  78. if ( curb > 0 ) {
  79. slabs.push( new Matrix4().makeTranslation( blockX + L.blockW / 2, 0, blockZ + L.blockD / 2 ) );
  80. }
  81. // the lots sit in an inner zone set back from the block edge by the
  82. // sidewalk width, so a real walking strip is left between the street
  83. // wall and the curb. buildings front onto that building line.
  84. const zoneX = blockX + sw, zoneZ = blockZ + sw;
  85. for ( let lx = 0; lx < L.lotsX; lx ++ ) {
  86. for ( let lz = 0; lz < L.lotsZ; lz ++ ) {
  87. // a chamfered corner only reads as architecture when it faces the
  88. // block's corner ( the street intersection ), so only the four corner
  89. // lots are cut, each toward its own outward corner; the rest stay square
  90. const cornerX = lx === 0 ? - 1 : ( lx === L.lotsX - 1 ? 1 : 0 );
  91. const cornerZ = lz === 0 ? - 1 : ( lz === L.lotsZ - 1 ? 1 : 0 );
  92. const onCorner = cornerX !== 0 && cornerZ !== 0;
  93. const tall = random();
  94. // nearly fill the lot so neighbours abut into a continuous street
  95. // wall; the small slack goes to the interior side ( a light well )
  96. const fw = L.innerLotX - ( 0.4 + random() * 1 );
  97. const fd = L.innerLotZ - ( 0.4 + random() * 1 );
  98. const totalHeight = 38 + tall * tall * 114; // a few tall towers, mostly mid-rise
  99. const generator = new SkyscraperGenerator( {
  100. seed: Math.floor( random() * 100000 ),
  101. totalHeight,
  102. footprint: { width: fw, depth: fd },
  103. floorHeight: 3.4 + random() * 1.8,
  104. bayWidth: 1.9 + random() * 2.1,
  105. pierWidth: 0.4 + random() * 0.5,
  106. pierDepth: 0.3 + random() * 0.4,
  107. chamferWidth: onCorner ? 3 + random() * 4 : 0,
  108. chamferCornerX: cornerX,
  109. chamferCornerZ: cornerZ,
  110. setbackDepth: random() < 0.4 ? 0.8 + random() * 2 : 0, // only some towers step back at the crown; the rest rise flat
  111. stringCourseEvery: random() < 0.85 ? 3 + Math.floor( random() * 6 ) : 0
  112. }, materials.building );
  113. const building = generator.build();
  114. // place within the lot, fronted toward the streets it borders so its
  115. // outer faces land on the building line; interior columns stay centred
  116. const lotLeft = zoneX + lx * L.innerLotX, lotNear = zoneZ + lz * L.innerLotZ;
  117. const cx = cornerX === - 1 ? lotLeft + fw / 2 : ( cornerX === 1 ? lotLeft + L.innerLotX - fw / 2 : lotLeft + L.innerLotX / 2 );
  118. const cz = cornerZ === - 1 ? lotNear + fd / 2 : ( cornerZ === 1 ? lotNear + L.innerLotZ - fd / 2 : lotNear + L.innerLotZ / 2 );
  119. building.position.set( cx, curb, cz );
  120. building.castShadow = building.receiveShadow = true;
  121. group.add( building );
  122. this.generators.push( generator );
  123. // record a plain box matching this tower for the GI proxy
  124. this.towers.push( { x: cx, y: curb + totalHeight / 2, z: cz, w: fw, h: totalHeight, d: fd } );
  125. }
  126. }
  127. }
  128. }
  129. if ( slabs.length > 0 ) group.add( this.sidewalk.build( slabs ) );
  130. group.add( this.buildFurniture( random ) );
  131. this.group = group;
  132. return group;
  133. }
  134. /**
  135. * Builds a lightweight stand-in for the city: one instanced box per tower,
  136. * sized to match, in a single draw call. Intended for cheap global-illumination
  137. * bakes, where the detailed facades and street furniture are unnecessary and the
  138. * boxes still cast the same street shadows and bounce the same warm fill.
  139. *
  140. * Call after {@link CityGenerator#build}, which records the tower boxes.
  141. *
  142. * @return {InstancedMesh} The proxy mesh.
  143. */
  144. buildProxy() {
  145. const towers = this.towers;
  146. // each box takes its tower's own masonry colour ( same node, keyed off world
  147. // position ), so the GI bleeds the real per-building tints. mid roughness / metalness
  148. // give the boxes a soft, part-glazed sky reflection.
  149. const material = new MeshStandardNodeMaterial( { roughness: 0.4, metalness: 0.4 } );
  150. material.colorNode = buildingColorNode( this.layout, this.parameters.seed );
  151. const mesh = new InstancedMesh( new BoxGeometry( 1, 1, 1 ), material, towers.length );
  152. mesh.name = 'CityProxy';
  153. const matrix = new Matrix4();
  154. for ( let i = 0; i < towers.length; i ++ ) {
  155. const t = towers[ i ];
  156. matrix.makeScale( t.w, t.h, t.d );
  157. matrix.setPosition( t.x, t.y, t.z );
  158. mesh.setMatrixAt( i, matrix );
  159. }
  160. mesh.castShadow = true;
  161. mesh.receiveShadow = true;
  162. return mesh;
  163. }
  164. // dresses the sidewalks and roadway with instanced street furniture, walking the
  165. // curb edge of every block so poles, baskets, trees, pedestrians and parked cars
  166. // all align to the same grid the buildings and road markings use
  167. buildFurniture( random ) {
  168. const L = this.layout;
  169. const top = this.parameters.curbHeight; // sidewalk surface the furniture stands on
  170. const sw = this.parameters.sidewalkWidth;
  171. const group = new Group();
  172. group.name = 'StreetFurniture';
  173. const lights = [], signals = [], cans = [], benches = [], hydrants = [], trees = [], people = [], cars = [];
  174. // the kerbside mix of a New York street: yellow cabs over an almost
  175. // entirely grayscale fleet ( black livery cars, whites, silvers,
  176. // graphites ), with a dark navy, a burgundy or a bronze only here and
  177. // there. cumulative weights, so common colours repeat like they do kerb
  178. // to kerb
  179. const carColor = () => {
  180. const r = random();
  181. for ( const [ threshold, color ] of CAR_COLORS ) if ( r < threshold ) return color;
  182. return CAR_COLORS[ CAR_COLORS.length - 1 ][ 1 ];
  183. };
  184. for ( let bx = 0; bx < L.blocksX; bx ++ ) {
  185. for ( let bz = 0; bz < L.blocksZ; bz ++ ) {
  186. const blockX = - L.cityW / 2 + bx * ( L.blockW + L.street );
  187. const blockZ = - L.cityD / 2 + bz * ( L.blockD + L.street );
  188. const edges = blockEdges( blockX, blockZ, L.blockW, L.blockD );
  189. for ( const e of edges ) {
  190. const len = e.length;
  191. // cars on opposite kerbs of a street face opposite ways ( with the
  192. // traffic on their side ), keyed off which way the kerb faces
  193. const fdir = Math.sign( e.nx + e.nz ) || 1;
  194. // a point on the sidewalk, `lateral` metres in from the curb
  195. const onWalk = ( t, lateral ) => place( e.x0 + e.dx * t - e.nx * lateral, top, e.z0 + e.dz * t - e.nz * lateral, e.nx, e.nz );
  196. // streetlights spaced along the curb, facing the road so the arm reaches over it
  197. const lc = Math.max( 1, Math.round( len / 30 ) );
  198. for ( let i = 0; i < lc; i ++ ) lights.push( onWalk( len * ( i + 0.5 ) / lc, 0.8 ) );
  199. // street trees in curbside pits, offset from the lights, clear of the corners
  200. const tc = Math.max( 1, Math.round( len / 16 ) );
  201. for ( let i = 0; i < tc; i ++ ) {
  202. const t = len * ( i + 0.5 ) / tc + 5;
  203. if ( t > 7 && t < len - 7 && random() < 0.85 ) {
  204. // random yaw and height so no two street trees read as copies
  205. trees.push( placeYawScale( e.x0 + e.dx * t - e.nx * 1.5, top, e.z0 + e.dz * t - e.nz * 1.5, random() * Math.PI * 2, 0.8 + random() * 0.5 ) );
  206. }
  207. }
  208. // a single hydrant on the kerb, its no-parking zone honoured below
  209. const hydT = len * ( 0.25 + random() * 0.5 );
  210. hydrants.push( onWalk( hydT, 0.7 ) );
  211. // a bench facing the street on some edges
  212. if ( random() < 0.4 ) benches.push( onWalk( len * ( 0.3 + random() * 0.4 ), 1.7 ) );
  213. // pedestrians scattered across the walking strip, facing any way
  214. const pc = Math.max( 2, Math.round( len / 9 ) );
  215. for ( let i = 0; i < pc; i ++ ) {
  216. if ( random() < 0.7 ) {
  217. const t = len * ( i + random() ) / pc;
  218. const lateral = 0.9 + random() * ( sw - 2 );
  219. people.push( placeYawScale( e.x0 + e.dx * t - e.nx * lateral, top, e.z0 + e.dz * t - e.nz * lateral, random() * Math.PI * 2, 0.92 + random() * 0.16 ) );
  220. }
  221. }
  222. // parked cars in the kerb lane, nose-to-tail with gaps, clear of the
  223. // corners ( daylighting ) and the hydrant
  224. for ( let t = 9; t < len - 9; t += 5.8 ) {
  225. if ( Math.abs( t - hydT ) > 3 && random() < 0.72 ) {
  226. cars.push( { matrix: place( e.x0 + e.dx * t + e.nx * 1.5, 0, e.z0 + e.dz * t + e.nz * 1.5, e.dx * fdir, e.dz * fdir ), color: carColor() } );
  227. }
  228. }
  229. // the occasional vehicle out in a travel lane
  230. for ( let t = 14; t < len - 14; t += 13 ) {
  231. if ( random() < 0.4 ) cars.push( { matrix: place( e.x0 + e.dx * t + e.nx * 5.5, 0, e.z0 + e.dz * t + e.nz * 5.5, e.dx * fdir, e.dz * fdir ), color: carColor() } );
  232. }
  233. }
  234. // at two opposite corners: a mast-arm signal reaching over the crossing,
  235. // and a litter basket on every corner just along the kerb
  236. const corners = [[ 0, 0 ], [ 1, 0 ], [ 0, 1 ], [ 1, 1 ]];
  237. for ( const [ cx, cz ] of corners ) {
  238. const x = blockX + cx * L.blockW, z = blockZ + cz * L.blockD;
  239. const ix = cx ? - 1 : 1, iz = cz ? - 1 : 1; // inward, toward the block centre
  240. if ( cx === cz ) signals.push( place( x + ix * 1.4, top, z + iz * 1.4, - ix, - iz ) );
  241. cans.push( place( x + ix * 2.2, top, z + iz * 2.2, ix, iz ) );
  242. }
  243. }
  244. }
  245. group.add( this.furniture.streetlight.build( lights ) );
  246. group.add( this.furniture.trafficlight.build( signals ) );
  247. group.add( this.furniture.trashcan.build( cans ) );
  248. if ( benches.length ) group.add( this.furniture.bench.build( benches ) );
  249. if ( hydrants.length ) group.add( this.furniture.hydrant.build( hydrants ) );
  250. if ( trees.length ) group.add( this.furniture.tree.build( trees ) );
  251. if ( people.length ) group.add( this.furniture.person.build( people ) );
  252. if ( cars.length ) group.add( this.furniture.car.build( cars ) );
  253. // the instanced furniture spans the whole city, so give each a bounding sphere
  254. // over its instances ( instead of the canonical model at the origin ) to cull by
  255. group.traverse( ( o ) => o.isInstancedMesh && o.computeBoundingSphere() );
  256. return group;
  257. }
  258. dispose() {
  259. for ( const generator of this.generators ) generator.dispose();
  260. this.generators.length = 0;
  261. this.sidewalk.dispose();
  262. for ( const key in this.furniture ) this.furniture[ key ].dispose();
  263. this.group = null;
  264. }
  265. }
  266. CityGenerator.defaults = {
  267. seed: 1,
  268. street: 22,
  269. lot: 30,
  270. lotsX: 3,
  271. lotsZ: 2,
  272. blocksX: 2,
  273. blocksZ: 2,
  274. curbHeight: 0.15, // ~6 in standard curb reveal / sidewalk height above the road
  275. curbRadius: 5,
  276. sidewalkWidth: 5 // walkable strip between the street wall and the curb ( NYC ~15 ft )
  277. };
  278. // kerbside paint mix sampled from Manhattan street photos: cumulative
  279. // probability thresholds and the paint they select
  280. const CAR_COLORS = [
  281. [ 0.22, 0xf5c518 ], // yellow cab
  282. [ 0.42, 0x111216 ], // black ( livery sedans and SUVs )
  283. [ 0.59, 0xe9e8e3 ], // white
  284. [ 0.72, 0xb2b5b8 ], // silver
  285. [ 0.84, 0x3e4247 ], // graphite
  286. [ 0.90, 0x74787c ], // mid grey
  287. [ 0.95, 0x1c2a3f ], // dark navy
  288. [ 0.98, 0x571f1f ], // burgundy
  289. [ 1.01, 0x5c4834 ] // bronze
  290. ];
  291. // derives the block / street dimensions from the parameters
  292. function cityLayout( parameters ) {
  293. const { street, lot, lotsX, lotsZ, blocksX, blocksZ, sidewalkWidth } = parameters;
  294. const blockW = lotsX * lot;
  295. const blockD = lotsZ * lot;
  296. // the lots tile the inner zone left after the sidewalk strip is taken from
  297. // every edge; buildings front onto its perimeter ( the building line )
  298. const innerLotX = ( blockW - 2 * sidewalkWidth ) / lotsX;
  299. const innerLotZ = ( blockD - 2 * sidewalkWidth ) / lotsZ;
  300. return {
  301. street, lot, lotsX, lotsZ, blocksX, blocksZ, blockW, blockD, sidewalkWidth, innerLotX, innerLotZ,
  302. cityW: blocksX * blockW + ( blocksX - 1 ) * street,
  303. cityD: blocksZ * blockD + ( blocksZ - 1 ) * street
  304. };
  305. }
  306. // deterministic PRNG (mulberry32) so a seed always lays out the same city
  307. function createRandom( seed ) {
  308. let s = ( seed >>> 0 ) || 1;
  309. return function () {
  310. s = ( s + 0x6D2B79F5 ) | 0;
  311. let t = Math.imul( s ^ ( s >>> 15 ), 1 | s );
  312. t = ( t + Math.imul( t ^ ( t >>> 7 ), 61 | t ) ) ^ t;
  313. return ( ( t ^ ( t >>> 14 ) ) >>> 0 ) / 4294967296;
  314. };
  315. }
  316. // --- furniture placement -------------------------------------------------
  317. // the four curb edges of a block: each carries a start corner, a unit direction
  318. // along the edge, the outward normal ( toward the road ) and a length. furniture
  319. // walks these, insetting against the normal to sit on the sidewalk.
  320. function blockEdges( x, z, w, d ) {
  321. return [
  322. { x0: x, z0: z, dx: 1, dz: 0, nx: 0, nz: - 1, length: w },
  323. { x0: x, z0: z + d, dx: 1, dz: 0, nx: 0, nz: 1, length: w },
  324. { x0: x, z0: z, dx: 0, dz: 1, nx: - 1, nz: 0, length: d },
  325. { x0: x + w, z0: z, dx: 0, dz: 1, nx: 1, nz: 0, length: d }
  326. ];
  327. }
  328. const _f = /*@__PURE__*/ new Vector3();
  329. const _r = /*@__PURE__*/ new Vector3();
  330. const _up = /*@__PURE__*/ new Vector3( 0, 1, 0 );
  331. // a placement matrix at ( x, y, z ) whose local +Z faces ( faceX, faceZ ) in the
  332. // XZ plane, so a canonical model authored facing +Z turns to face that way
  333. function place( x, y, z, faceX, faceZ ) {
  334. _f.set( faceX, 0, faceZ ).normalize();
  335. _r.crossVectors( _up, _f ).normalize();
  336. return new Matrix4().makeBasis( _r, _up, _f ).setPosition( x, y, z );
  337. }
  338. const _q = /*@__PURE__*/ new Quaternion();
  339. const _pos = /*@__PURE__*/ new Vector3();
  340. const _sca = /*@__PURE__*/ new Vector3();
  341. // a placement with a free yaw and a uniform scale, so repeated instances ( trees,
  342. // pedestrians ) read as individuals rather than copies
  343. function placeYawScale( x, y, z, yaw, scale ) {
  344. _q.setFromAxisAngle( _up, yaw );
  345. return new Matrix4().compose( _pos.set( x, y, z ), _q, _sca.set( scale, scale, scale ) );
  346. }
  347. // --- road material -------------------------------------------------------
  348. // derivative-based bump for a procedural, world-space height field. the built-in bumpMap
  349. // offsets the UV to read its height, so it returns a zero gradient for a height keyed off
  350. // world position; this feeds the hardware screen-space derivatives of the height into
  351. // Mikkelsen's surface-gradient method so the relief actually perturbs the normal.
  352. function bumpNormal( height ) {
  353. const dpdx = positionView.dFdx();
  354. const dpdy = positionView.dFdy();
  355. const r1 = dpdy.cross( normalView );
  356. const r2 = normalView.cross( dpdx );
  357. const det = dpdx.dot( r1 );
  358. const grad = det.sign().mul( height.dFdx().mul( r1 ).add( height.dFdy().mul( r2 ) ) );
  359. return det.abs().mul( normalView ).sub( grad ).normalize();
  360. }
  361. // antialiased filled band: 1 where |coord| < halfWidth, edge sized to the
  362. // pixel footprint ( fwidth ) so thin road paint stays crisp and doesn't shimmer
  363. function lineAA( coord, halfWidth ) {
  364. const aa = fwidth( coord ).max( 0.0001 );
  365. return smoothstep( float( halfWidth ).add( aa ), float( halfWidth ).sub( aa ), coord.abs() );
  366. }
  367. // the same, repeated at every multiple of `period` ( stripes, joints )
  368. function gridLine( coord, period, halfWidth ) {
  369. const g = coord.div( period );
  370. const d = float( 0.5 ).sub( fract( g ).sub( 0.5 ).abs() ); // distance to nearest line, in periods
  371. const aa = fwidth( g ).max( 0.0001 );
  372. const hw = halfWidth / period;
  373. return smoothstep( float( hw ).add( aa ), float( hw ).sub( aa ), d );
  374. }
  375. /**
  376. * The per-tower flat masonry colour: one palette entry per lot, picked by hashing the
  377. * lot's grid cell from world position. Keyed off `positionWorld`, so it colours anything
  378. * standing on the city grid identically, the towers and their {@link CityGenerator#buildProxy}
  379. * boxes alike.
  380. *
  381. * @param {Object} layout - The city layout.
  382. * @param {number} [seed] - The city seed.
  383. * @return {Node<vec3>} The tower colour.
  384. */
  385. function buildingColorNode( layout, seed = 0 ) {
  386. // every tower takes one flat colour, picked by hashing its lot; common tones repeat
  387. // so the equal-probability pick feels real
  388. const palette = buildingPalette.map( hex => color( hex ) );
  389. const periodX = layout.blockW + layout.street;
  390. const periodZ = layout.blockD + layout.street;
  391. const gx = positionWorld.x.add( layout.cityW / 2 );
  392. const gz = positionWorld.z.add( layout.cityD / 2 );
  393. const blockIX = floor( gx.div( periodX ) );
  394. const blockIZ = floor( gz.div( periodZ ) );
  395. // cells align to the inner-lot grid each tower sits in, so an inset, fronted
  396. // tower maps wholly to its own cell and reads as one flat colour
  397. const lotIX = floor( gx.sub( blockIX.mul( periodX ) ).sub( layout.sidewalkWidth ).div( layout.innerLotX ) ).clamp( 0, layout.lotsX - 1 );
  398. const lotIZ = floor( gz.sub( blockIZ.mul( periodZ ) ).sub( layout.sidewalkWidth ).div( layout.innerLotZ ) ).clamp( 0, layout.lotsZ - 1 );
  399. const cellX = blockIX.mul( layout.lotsX ).add( lotIX );
  400. const cellZ = blockIZ.mul( layout.lotsZ ).add( lotIZ );
  401. const cellKey = uint( cellX.add( 4096 ) ).mul( uint( 73856093 ) ).bitXor( uint( cellZ.add( 4096 ) ).mul( uint( 19349663 ) ) ).bitXor( uint( ( seed * 2654435761 ) >>> 0 ) ).toVar();
  402. const cellHash = ( a, b ) => hash( cellKey.add( uint( Math.round( ( a + b * 7 ) * 100 ) ) ) );
  403. const pick = cellHash( 127.1, 311.7 );
  404. let buildingBase = palette[ 0 ];
  405. for ( let i = 1; i < palette.length; i ++ ) buildingBase = mix( buildingBase, palette[ i ], step( i / palette.length, pick ) );
  406. buildingBase = buildingBase.mul( cellHash( 269.5, 183.3 ).mul( 0.12 ).add( 0.94 ) ); // subtle per-building brightness
  407. return buildingBase;
  408. }
  409. /**
  410. * The shared material every tower in a {@link CityGenerator} is dressed with: the per-lot
  411. * {@link buildingColorNode} resolved once per vertex on a skyscraper material.
  412. */
  413. function createBuildingMaterial( layout, seed = 0 ) {
  414. // the pick is constant across a tower, so resolve it once per vertex ( varying )
  415. return createSkyscraperMaterial( varying( buildingColorNode( layout, seed ) ) );
  416. }
  417. /**
  418. * The road surface: wet asphalt with lane lines and crosswalks aligned to a
  419. * {@link CityGenerator} layout. Apply it to a ground plane sized to the city.
  420. */
  421. function createRoadMaterial( layout ) {
  422. // wet asphalt: a warm-grey base in patchwork pours, two-scale aggregate
  423. // grit, oily wear stains and low-frequency wet patches that turn glossy and
  424. // mirror the sky. detail fades in as the camera nears.
  425. const p = positionWorld;
  426. const dist = p.distance( cameraPosition );
  427. const detail = smoothstep( 240, 25, dist );
  428. const microFade = smoothstep( 22, 4, dist ); // the finest grit only resolves right under the camera, so fade it out fast to avoid shimmer
  429. const blotch = mx_fractal_noise_float( p.mul( 0.2 ), 3 ).mul( 0.5 ).add( 0.5 );
  430. // close-range detail — aggregate grit, oily wear pools and worn paint — only
  431. // resolves near the camera, so its noise is sampled ( inside the branch ) only
  432. // where detail is non-zero and skipped across the far majority of the road
  433. const near = Fn( () => {
  434. const grit = float( 0 ).toVar(); // two scales of aggregate, -1..1
  435. const stain = float( 0 ).toVar(); // oily wear pools
  436. const worn = float( 1 ).toVar(); // paint rubbed thin and patchy, more so where tyres cross it
  437. const micro = float( 0 ).toVar(); // fine, high-frequency grain for the close-up bump
  438. If( detail.greaterThan( 0 ), () => {
  439. grit.assign( mx_noise_float( p.mul( 7 ) ).add( mx_noise_float( p.mul( 23 ) ) ).mul( 0.5 ) );
  440. stain.assign( smoothstep( 0.5, 0.85, mx_fractal_noise_float( p.mul( 0.45 ), 3 ).mul( 0.5 ).add( 0.5 ) ) );
  441. 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 ) );
  442. } );
  443. If( microFade.greaterThan( 0 ), () => {
  444. micro.assign( mx_noise_float( p.mul( 45 ) ).mul( 0.6 ).add( mx_noise_float( p.mul( 80 ) ).mul( 0.4 ) ) );
  445. } );
  446. return vec4( grit, stain, worn, micro );
  447. } )();
  448. const grit = near.x;
  449. const stain = near.y;
  450. const worn = near.z;
  451. const micro = near.w;
  452. const base = mix( color( 0x24262b ), color( 0x3b3f46 ), blotch );
  453. const gritty = base.mul( grit.mul( 0.22 ).mul( detail ).add( 1 ) );
  454. const asphalt = mix( gritty, gritty.mul( 0.5 ), stain.mul( 0.5 ).mul( detail ) );
  455. const wet = smoothstep( 0.6, 0.85, mx_fractal_noise_float( p.mul( 0.14 ), 2 ).mul( 0.5 ).add( 0.5 ) );
  456. // markings, aligned to the block / street grid. fx, fz are the position
  457. // within one block+street period; the street is the [ blockW, period ) part.
  458. const periodX = layout.blockW + layout.street;
  459. const periodZ = layout.blockD + layout.street;
  460. const fx = mod( p.x.add( layout.cityW / 2 ), periodX );
  461. const fz = mod( p.z.add( layout.cityD / 2 ), periodZ );
  462. const inStreetX = step( layout.blockW, fx ); // in a vertical street ( gap in X )
  463. const inStreetZ = step( layout.blockD, fz ); // in a horizontal street ( gap in Z )
  464. const su = fx.sub( layout.blockW ); // across the vertical street
  465. const sv = fz.sub( layout.blockD ); // across the horizontal street
  466. // lane markings down each street ( not through intersections ): a solid
  467. // centre line splitting the two directions, with a dashed divider in each
  468. // half, so every street carries four lanes
  469. const dashV = step( fract( p.z.div( 7 ) ), 0.5 );
  470. const dashH = step( fract( p.x.div( 7 ) ), 0.5 );
  471. const centreV = lineAA( su.sub( layout.street / 2 ), 0.12 );
  472. const dividerV = lineAA( su.sub( layout.street / 4 ), 0.1 ).max( lineAA( su.sub( layout.street * 3 / 4 ), 0.1 ) ).mul( dashV );
  473. const laneV = centreV.max( dividerV ).mul( inStreetX ).mul( inStreetZ.oneMinus() );
  474. const centreH = lineAA( sv.sub( layout.street / 2 ), 0.12 );
  475. const dividerH = lineAA( sv.sub( layout.street / 4 ), 0.1 ).max( lineAA( sv.sub( layout.street * 3 / 4 ), 0.1 ) ).mul( dashH );
  476. const laneH = centreH.max( dividerH ).mul( inStreetZ ).mul( inStreetX.oneMinus() );
  477. // continental crosswalk bars ( long in the travel direction ) in each
  478. // street arm, near the block edges it meets
  479. const cw = 5;
  480. const nearZ = step( fz, cw ).max( step( layout.blockD - cw, fz ) );
  481. const nearX = step( fx, cw ).max( step( layout.blockW - cw, fx ) );
  482. const crossV = gridLine( su, 1.2, 0.38 ).mul( inStreetX ).mul( inStreetZ.oneMinus() ).mul( nearZ );
  483. const crossH = gridLine( sv, 1.2, 0.38 ).mul( inStreetZ ).mul( inStreetX.oneMinus() ).mul( nearX );
  484. const paint = laneV.max( laneH ).max( crossV ).max( crossH ).mul( detail ).mul( worn );
  485. const material = new MeshStandardNodeMaterial();
  486. const surface = mix( asphalt, asphalt.mul( 0.6 ), wet );
  487. material.colorNode = mix( surface, color( 0xd0ccc0 ), paint ); // worn white paint
  488. material.roughnessNode = mix( float( 0.95 ).sub( paint.mul( 0.2 ) ), float( 0.32 ), wet );
  489. material.normalNode = bumpNormal( grit.mul( 0.003 ).mul( detail ).add( micro.mul( 0.0016 ).mul( microFade ) ) ); // world units: ~3 mm aggregate, ~1.6 mm fine grain up close
  490. return material;
  491. }
  492. export { CityGenerator, createBuildingMaterial, createRoadMaterial };
粤ICP备19079148号